diff --git a/src/index.ts b/src/index.ts index 586dc7e..8dac7c1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,6 @@ import { handleRequest } from './router'; import { StorageService } from './services/storage'; import { applyCors, jsonResponse } from './utils/response'; import { runScheduledBackupIfDue } from './handlers/backup'; -import { buildWebBootstrapResponse } from './router-public'; let dbInitialized = false; let dbInitError: string | null = null; @@ -23,41 +22,13 @@ function isWorkerHandledPath(path: string): boolean { ); } -function injectBootstrapIntoHtml(html: string, env: Env): string { - const payload = JSON.stringify(buildWebBootstrapResponse(env)).replace(/window.__NW_BOOT__=${payload};`; - if (html.includes('')) { - return html.replace('', `${script}`); - } - return `${script}${html}`; -} - -function responseStatusCannotHaveBody(status: number): boolean { - return status === 101 || status === 204 || status === 205 || status === 304; -} - async function maybeServeAsset(request: Request, env: Env): Promise { if (!env.ASSETS) return null; if (request.method !== 'GET' && request.method !== 'HEAD') return null; const url = new URL(request.url); if (isWorkerHandledPath(url.pathname)) return null; - const assetResponse = await env.ASSETS.fetch(request); - const contentType = String(assetResponse.headers.get('Content-Type') || '').toLowerCase(); - if ( - request.method === 'GET' && - contentType.includes('text/html') && - !responseStatusCannotHaveBody(assetResponse.status) - ) { - const html = await assetResponse.text(); - const injected = injectBootstrapIntoHtml(html, env); - return new Response(injected, { - status: assetResponse.status, - statusText: assetResponse.statusText, - headers: assetResponse.headers, - }); - } - return assetResponse; + return env.ASSETS.fetch(request); } async function ensureDatabaseInitialized(env: Env): Promise { diff --git a/src/router-public.ts b/src/router-public.ts index 031deed..5a2baa7 100644 --- a/src/router-public.ts +++ b/src/router-public.ts @@ -175,6 +175,12 @@ export async function handlePublicRoute( }); } + if ((path === '/api/web-bootstrap' || path === '/web-bootstrap') && method === 'GET') { + const blocked = await enforcePublicRateLimit('public-read', LIMITS.rateLimit.publicReadRequestsPerMinute); + if (blocked) return blocked; + return jsonResponse(buildWebBootstrapResponse(env)); + } + const iconMatch = path.match(/^\/icons\/([^/]+)\/icon\.png$/i); if (iconMatch && method === 'GET') { return handleWebsiteIcon(iconMatch[1]); diff --git a/webapp/src/lib/app-auth.ts b/webapp/src/lib/app-auth.ts index e20d5ac..4098310 100644 --- a/webapp/src/lib/app-auth.ts +++ b/webapp/src/lib/app-auth.ts @@ -92,6 +92,35 @@ function readWindowBootstrap(): WebBootstrapResponse { return raw && typeof raw === 'object' ? raw : {}; } +function normalizeBootstrapResponse(boot: WebBootstrapResponse): Pick { + const defaultKdfIterations = Number(boot.defaultKdfIterations || 600000); + const jwtUnsafeReason = boot.jwtUnsafeReason || null; + const jwtWarning = jwtUnsafeReason + ? { + reason: jwtUnsafeReason, + minLength: Number(boot.jwtSecretMinLength || 32), + } + : null; + + return { + defaultKdfIterations, + jwtWarning, + }; +} + +async function fetchBootstrapConfig(): Promise { + try { + const resp = await fetch('/api/web-bootstrap', { + method: 'GET', + headers: { Accept: 'application/json' }, + }); + if (!resp.ok) return {}; + return ((await resp.json()) as WebBootstrapResponse) || {}; + } catch { + return {}; + } +} + interface AccessTokenClaims { sub?: string; email?: string; @@ -129,15 +158,7 @@ function buildTransientProfile(token: TokenSuccess, email: string): Profile { } export function readInitialAppBootstrapState(): InitialAppBootstrapState { - const boot = readWindowBootstrap(); - const defaultKdfIterations = Number(boot.defaultKdfIterations || 600000); - const jwtUnsafeReason = boot.jwtUnsafeReason || null; - const jwtWarning = jwtUnsafeReason - ? { - reason: jwtUnsafeReason, - minLength: Number(boot.jwtSecretMinLength || 32), - } - : null; + const { defaultKdfIterations, jwtWarning } = normalizeBootstrapResponse(readWindowBootstrap()); const session = loadSession(); const hasInviteCode = !!readInviteCodeFromUrl(); @@ -150,8 +171,10 @@ export function readInitialAppBootstrapState(): InitialAppBootstrapState { } export async function bootstrapAppSession(initial: InitialAppBootstrapState = readInitialAppBootstrapState()): Promise { - const defaultKdfIterations = initial.defaultKdfIterations; - const jwtWarning = initial.jwtWarning; + const remoteBoot = await fetchBootstrapConfig(); + const normalizedBoot = normalizeBootstrapResponse(remoteBoot); + const defaultKdfIterations = normalizedBoot.defaultKdfIterations || initial.defaultKdfIterations; + const jwtWarning = normalizedBoot.jwtWarning ?? initial.jwtWarning; if (jwtWarning) { return { diff --git a/wrangler.kv.toml b/wrangler.kv.toml index 1e5dd1e..1e17497 100644 --- a/wrangler.kv.toml +++ b/wrangler.kv.toml @@ -18,7 +18,7 @@ enabled = false binding = "ASSETS" directory = "./dist" not_found_handling = "single-page-application" -run_worker_first = true +run_worker_first = false [build] command = "npm run build" diff --git a/wrangler.toml b/wrangler.toml index b0216e0..56b9099 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -18,7 +18,7 @@ enabled = false binding = "ASSETS" directory = "./dist" not_found_handling = "single-page-application" -run_worker_first = true +run_worker_first = false [build] command = "npm run build"