import { z } from "zod"; const version = z.string().regex(/^\d+(?:\.\d+){0,2}$/); export const releaseManifestSchema = z .object({ schemaVersion: z.literal(1), appVersion: z.string().min(1), buildId: z.string().min(1), commitSha: z.string().min(1), configSchemaVersion: version, apiContractVersion: version, assetManifestHash: z.string().min(1), releaseId: z.string().min(1), builtAt: z.string().min(1), routeChunks: z.record(z.string().min(1), z.string().min(1)), }) .strict(); export class ReleaseManifestError extends Error { /** @param {string} code @param {{buildId: string, releaseId?: string}} safe */ constructor(code, safe) { super("Release manifest could not be loaded"); this.name = "ReleaseManifestError"; this.kind = { MANIFEST_BUILD_MISMATCH: "BUILD_MISMATCH", MANIFEST_CONFIG_SCHEMA_MISMATCH: "CONFIG_MISMATCH", MANIFEST_API_CONTRACT_MISMATCH: "API_CONTRACT_MISMATCH", MANIFEST_RELEASE_MISMATCH: "RELEASE_MISMATCH", MANIFEST_ASSET_MISMATCH: "ASSET_MISMATCH", }[code] ?? "RELEASE_MANIFEST_FAILURE"; this.code = code; this.safe = Object.freeze({ kind: this.kind, code, buildId: safe.buildId, releaseId: safe.releaseId, supportReference: `${safe.buildId}:${code}`, }); } } /** * Fetches and validates the active manifest without imposing the current * build tuple. Chunk recovery uses this no-store view to detect a new release. * * @param {string} url * @param {{ * fetcher?: typeof fetch, * buildId: string, * releaseId?: string * }} options */ export async function fetchReleaseManifest(url, options) { const fetcher = options.fetcher ?? fetch; let response; try { response = await fetcher(url, { cache: "no-store", headers: { Accept: "application/json" }, }); } catch { throw new ReleaseManifestError("MANIFEST_FETCH_FAILED", options); } if (!response.ok) { throw new ReleaseManifestError("MANIFEST_HTTP_FAILED", options); } let raw; try { raw = await response.json(); } catch { throw new ReleaseManifestError("MANIFEST_JSON_INVALID", options); } const parsed = releaseManifestSchema.safeParse(raw); if (!parsed.success) { throw new ReleaseManifestError("MANIFEST_SCHEMA_INVALID", options); } return Object.freeze(structuredClone(parsed.data)); } /** * @param {Awaited>} runtime * @param {{fetcher?: typeof fetch, expectedAssetManifestHash?: string}} [options] */ export async function loadReleaseManifest(runtime, options = {}) { const manifest = await fetchReleaseManifest( runtime.config.RELEASE_MANIFEST_URL, { fetcher: options.fetcher, buildId: runtime.build.buildId, releaseId: runtime.config.RELEASE_ID, }, ); let mismatchCode = null; if (manifest.buildId !== runtime.build.buildId) { mismatchCode = "MANIFEST_BUILD_MISMATCH"; } if ( !mismatchCode && runtime.config.BUILD_ID && manifest.buildId !== runtime.config.BUILD_ID ) { mismatchCode = "MANIFEST_BUILD_MISMATCH"; } if ( !mismatchCode && manifest.configSchemaVersion !== runtime.config.CONFIG_SCHEMA_VERSION ) { mismatchCode = "MANIFEST_CONFIG_SCHEMA_MISMATCH"; } if ( !mismatchCode && manifest.apiContractVersion !== runtime.config.API_CONTRACT_VERSION ) { mismatchCode = "MANIFEST_API_CONTRACT_MISMATCH"; } if ( !mismatchCode && runtime.config.RELEASE_ID && manifest.releaseId !== runtime.config.RELEASE_ID ) { mismatchCode = "MANIFEST_RELEASE_MISMATCH"; } if ( !mismatchCode && options.expectedAssetManifestHash && manifest.assetManifestHash !== options.expectedAssetManifestHash ) { mismatchCode = "MANIFEST_ASSET_MISMATCH"; } if (mismatchCode) { throw new ReleaseManifestError(mismatchCode, { buildId: runtime.build.buildId, releaseId: runtime.config.RELEASE_ID, }); } return manifest; }