/** * §6.4–§6.5. The only boot-time JSON reader. * * `response.json()` and unbounded `response.text()` are prohibited: a hostile or * misconfigured origin must not be able to amplify boot memory, and an HTML * error page must not reach `JSON.parse` as if it were configuration. */ export type BootJsonOperation = "RUNTIME_CONFIG" | "RELEASE_MANIFEST"; export interface BootJsonPolicy { readonly operation: BootJsonOperation; readonly maximumBytes: number; readonly totalDeadlineMs: 5_000; } export const BOOT_JSON_POLICIES = Object.freeze({ RUNTIME_CONFIG: Object.freeze({ operation: "RUNTIME_CONFIG" as const, maximumBytes: 65_536, totalDeadlineMs: 5_000 as const, }), RELEASE_MANIFEST: Object.freeze({ operation: "RELEASE_MANIFEST" as const, maximumBytes: 1_048_576, totalDeadlineMs: 5_000 as const, }), } satisfies Readonly>); export type BootLoadFailure = | "FETCH_FAILED" | "TIMEOUT" | "HTTP_STATUS_INVALID" | "CONTENT_TYPE_INVALID" | "BODY_TOO_LARGE" | "UTF8_INVALID" | "JSON_INVALID" | "SHAPE_INVALID" | "SECRET_NAME_REJECTED" | "SCHEMA_INVALID" | "BUILD_MISMATCH" | "RELEASE_MISMATCH" | "ASSET_MISMATCH" | "CONTRACT_SET_MISMATCH"; export type BootJsonOutcome = | Readonly<{ ok: true; value: Readonly> }> | Readonly<{ ok: false; failure: BootLoadFailure }>; export type ReadBoundedBootJsonOptions = Readonly<{ fetcher?: typeof fetch; signal?: AbortSignal; }>; function isJsonMediaType(headerValue: string | null): boolean { if (!headerValue) return false; const essence = headerValue.split(";", 1)[0]?.trim().toLowerCase() ?? ""; return essence === "application/json" || essence.endsWith("+json"); } function isRecord(value: unknown): value is Record { return Boolean(value) && typeof value === "object" && !Array.isArray(value); } export async function readBoundedBootJson( url: string, policy: BootJsonPolicy, options: ReadBoundedBootJsonOptions = {}, ): Promise { if (options.signal?.aborted) return fail("FETCH_FAILED"); const fetcher = options.fetcher ?? fetch; const controller = new AbortController(); let timedOut = false; const timer = setTimeout(() => { timedOut = true; controller.abort(); }, policy.totalDeadlineMs); const forwardAbort = () => controller.abort(); options.signal?.addEventListener("abort", forwardAbort, { once: true }); try { let response: Response; try { response = await fetcher(url, { method: "GET", cache: "no-store", credentials: "same-origin", redirect: "error", referrerPolicy: "no-referrer", headers: { Accept: "application/json" }, signal: controller.signal, }); } catch { return fail(timedOut ? "TIMEOUT" : "FETCH_FAILED"); } if (response.status !== 200 || response.redirected) { await discard(response); return fail("HTTP_STATUS_INVALID"); } if (!isJsonMediaType(response.headers.get("content-type"))) { await discard(response); return fail("CONTENT_TYPE_INVALID"); } const declared = Number(response.headers.get("content-length")); if (Number.isFinite(declared) && declared > policy.maximumBytes) { await discard(response); return fail("BODY_TOO_LARGE"); } const bytes = await readBoundedBytes(response, policy.maximumBytes, controller); if (bytes === "TOO_LARGE") return fail("BODY_TOO_LARGE"); if (bytes === "STREAM_FAILED") return fail(timedOut ? "TIMEOUT" : "FETCH_FAILED"); let text: string; try { text = new TextDecoder("utf-8", { fatal: true }).decode(bytes); } catch { return fail("UTF8_INVALID"); } let parsed: unknown; try { parsed = JSON.parse(text); } catch { return fail("JSON_INVALID"); } if (!isRecord(parsed)) return fail("SHAPE_INVALID"); return Object.freeze({ ok: true as const, value: parsed }); } finally { clearTimeout(timer); options.signal?.removeEventListener("abort", forwardAbort); } } async function readBoundedBytes( response: Response, maximumBytes: number, controller: AbortController, ): Promise { const body = response.body; if (!body) { // A body-less 200 cannot satisfy any boot document. return new Uint8Array(0); } const reader = body.getReader(); const chunks: Uint8Array[] = []; let total = 0; try { for (;;) { const { done, value } = await reader.read(); if (done) break; if (!value) continue; total += value.byteLength; if (total > maximumBytes) { await reader.cancel().catch(() => {}); controller.abort(); return "TOO_LARGE"; } chunks.push(value); } } catch { await reader.cancel().catch(() => {}); return "STREAM_FAILED"; } const output = new Uint8Array(total); let offset = 0; for (const chunk of chunks) { output.set(chunk, offset); offset += chunk.byteLength; } return output; } async function discard(response: Response): Promise { try { await response.body?.cancel(); } catch { // Cancelling an already-settled body is not a boot failure. } } function fail(failure: BootLoadFailure): BootJsonOutcome { return Object.freeze({ ok: false as const, failure }); }