import { z } from "zod"; const version = z.string().regex(/^\d+(?:\.\d+){0,2}$/); 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), }) .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 = "RELEASE_MANIFEST_FAILURE"; this.code = code; this.safe = Object.freeze({ kind: this.kind, code, buildId: safe.buildId, releaseId: safe.releaseId, supportReference: `${safe.buildId}:${code}`, }); } } /** * @param {Awaited>} runtime * @param {{fetcher?: typeof fetch}} [options] */ export async function loadReleaseManifest(runtime, options = {}) { const fetcher = options.fetcher ?? fetch; let response; try { response = await fetcher(runtime.config.RELEASE_MANIFEST_URL, { cache: "no-store", headers: { Accept: "application/json" }, }); } catch { throw new ReleaseManifestError("MANIFEST_FETCH_FAILED", { buildId: runtime.build.buildId, releaseId: runtime.config.RELEASE_ID, }); } if (!response.ok) { throw new ReleaseManifestError("MANIFEST_HTTP_FAILED", { buildId: runtime.build.buildId, releaseId: runtime.config.RELEASE_ID, }); } let raw; try { raw = await response.json(); } catch { throw new ReleaseManifestError("MANIFEST_JSON_INVALID", { buildId: runtime.build.buildId, releaseId: runtime.config.RELEASE_ID, }); } const parsed = releaseManifestSchema.safeParse(raw); if (!parsed.success) { throw new ReleaseManifestError("MANIFEST_SCHEMA_INVALID", { buildId: runtime.build.buildId, releaseId: runtime.config.RELEASE_ID, }); } const manifest = parsed.data; const mismatches = []; if (manifest.buildId !== runtime.build.buildId) mismatches.push("buildId"); if ( runtime.config.BUILD_ID && manifest.buildId !== runtime.config.BUILD_ID ) { mismatches.push("runtimeBuildId"); } if ( manifest.configSchemaVersion !== runtime.config.CONFIG_SCHEMA_VERSION ) { mismatches.push("configSchemaVersion"); } if (manifest.apiContractVersion !== runtime.config.API_CONTRACT_VERSION) { mismatches.push("apiContractVersion"); } if ( runtime.config.RELEASE_ID && manifest.releaseId !== runtime.config.RELEASE_ID ) { mismatches.push("releaseId"); } if (mismatches.length > 0) { throw new ReleaseManifestError("MANIFEST_RUNTIME_MISMATCH", { buildId: runtime.build.buildId, releaseId: runtime.config.RELEASE_ID, }); } return Object.freeze(structuredClone(manifest)); }