import { verifyContractSet, type ContractSet, type ContractSetFailureCode, } from "../contracts/contract-set.ts"; import type { ContractSetPackage } from "../contracts/contract-set-canonical.ts"; import { releaseManifestV1ArtifactSchema, releaseManifestV2ArtifactSchema, type ReleaseManifestV1Artifact, type ReleaseManifestV2Artifact, } from "../contracts/release-artifacts.ts"; import { EXPECTED_CONTRACT_SET_PACKAGES } from "../features/installed-contract-contributions.ts"; import { BOOT_JSON_POLICIES, readBoundedBootJson, type BootLoadFailure, } from "./read-bounded-boot-json.ts"; import type { RuntimeConfigLoadResult } from "./load-runtime-config.ts"; /** §5.8. Retained for one compatibility window; carries the removed scalar. */ export const releaseManifestV1Schema = releaseManifestV1ArtifactSchema; /** §5.2. The frontend build's compiled external contract package set. */ export const releaseManifestV2Schema = releaseManifestV2ArtifactSchema; export type ReleaseManifestV1 = ReleaseManifestV1Artifact; export type ReleaseManifestV2 = ReleaseManifestV2Artifact; /** * The composition-facing manifest. A V1 document is normalized onto it with a * null contract set so downstream runtime never branches on schema version. */ export type ReleaseManifest = Readonly<{ schemaVersion: 1 | 2; appVersion: string; buildId: string; commitSha: string; configSchemaVersion: string; assetManifestHash: string; releaseId: string; builtAt: string; routeChunks: Readonly>; contractSet: ContractSet | null; legacyApiContractVersion?: string; }>; export type ReleaseManifestErrorCode = | "MANIFEST_BUILD_MISMATCH" | "MANIFEST_PROTOCOL_PAIR_MISMATCH" | "MANIFEST_CONFIG_SCHEMA_MISMATCH" | "MANIFEST_API_CONTRACT_MISMATCH" | "MANIFEST_RELEASE_MISMATCH" | "MANIFEST_ASSET_MISMATCH" | "MANIFEST_FETCH_FAILED" | "MANIFEST_TIMEOUT" | "MANIFEST_HTTP_FAILED" | "MANIFEST_CONTENT_TYPE_INVALID" | "MANIFEST_BODY_TOO_LARGE" | "MANIFEST_UTF8_INVALID" | "MANIFEST_JSON_INVALID" | "MANIFEST_SCHEMA_INVALID" | ContractSetFailureCode; export type ReleaseManifestFailureKind = | "BUILD_MISMATCH" | "PROTOCOL_PAIR_MISMATCH" | "CONFIG_MISMATCH" | "API_CONTRACT_MISMATCH" | "RELEASE_MISMATCH" | "ASSET_MISMATCH" | "CONTRACT_SET_MISMATCH" | "RELEASE_MANIFEST_FAILURE"; export type ReleaseManifestSafe = Readonly<{ kind: ReleaseManifestFailureKind; code: ReleaseManifestErrorCode; buildId: string; releaseId?: string; supportReference: string; }>; type ReleaseManifestSafeInput = Readonly<{ buildId: string; releaseId?: string; }>; function failureKindFor( code: ReleaseManifestErrorCode, ): ReleaseManifestFailureKind { switch (code) { case "MANIFEST_BUILD_MISMATCH": return "BUILD_MISMATCH"; case "MANIFEST_PROTOCOL_PAIR_MISMATCH": return "PROTOCOL_PAIR_MISMATCH"; case "MANIFEST_CONFIG_SCHEMA_MISMATCH": return "CONFIG_MISMATCH"; case "MANIFEST_API_CONTRACT_MISMATCH": return "API_CONTRACT_MISMATCH"; case "MANIFEST_RELEASE_MISMATCH": return "RELEASE_MISMATCH"; case "MANIFEST_ASSET_MISMATCH": return "ASSET_MISMATCH"; default: return code.startsWith("CONTRACT_") ? "CONTRACT_SET_MISMATCH" : "RELEASE_MANIFEST_FAILURE"; } } export class ReleaseManifestError extends Error { readonly kind: ReleaseManifestFailureKind; readonly code: ReleaseManifestErrorCode; readonly safe: ReleaseManifestSafe; constructor(code: ReleaseManifestErrorCode, safe: ReleaseManifestSafeInput) { super("Release manifest could not be loaded"); this.name = "ReleaseManifestError"; this.kind = failureKindFor(code); this.code = code; this.safe = Object.freeze({ kind: this.kind, code, buildId: safe.buildId, releaseId: safe.releaseId, supportReference: `${safe.buildId}:${code}`, }); } } const READ_FAILURE_CODE: Readonly< Record > = Object.freeze({ FETCH_FAILED: "MANIFEST_FETCH_FAILED", TIMEOUT: "MANIFEST_TIMEOUT", HTTP_STATUS_INVALID: "MANIFEST_HTTP_FAILED", CONTENT_TYPE_INVALID: "MANIFEST_CONTENT_TYPE_INVALID", BODY_TOO_LARGE: "MANIFEST_BODY_TOO_LARGE", UTF8_INVALID: "MANIFEST_UTF8_INVALID", JSON_INVALID: "MANIFEST_JSON_INVALID", SHAPE_INVALID: "MANIFEST_SCHEMA_INVALID", SECRET_NAME_REJECTED: "MANIFEST_SCHEMA_INVALID", SCHEMA_INVALID: "MANIFEST_SCHEMA_INVALID", BUILD_MISMATCH: "MANIFEST_BUILD_MISMATCH", RELEASE_MISMATCH: "MANIFEST_RELEASE_MISMATCH", ASSET_MISMATCH: "MANIFEST_ASSET_MISMATCH", CONTRACT_SET_MISMATCH: "CONTRACT_SET_DIGEST_MISMATCH", }); export type FetchReleaseManifestOptions = Readonly<{ fetcher?: typeof fetch; buildId: string; releaseId?: string; signal?: AbortSignal; }>; /** * Fetches and validates the active manifest without imposing the current build * tuple. Chunk recovery uses this no-store view to detect a new release. */ export async function fetchReleaseManifest( url: string, options: FetchReleaseManifestOptions, ): Promise { const outcome = await readBoundedBootJson( url, BOOT_JSON_POLICIES.RELEASE_MANIFEST, { ...(options.fetcher ? { fetcher: options.fetcher } : {}), ...(options.signal ? { signal: options.signal } : {}), }, ); if (!outcome.ok) { throw new ReleaseManifestError(READ_FAILURE_CODE[outcome.failure], options); } if (outcome.value.schemaVersion === 2) { const parsed = releaseManifestV2Schema.safeParse(outcome.value); if (!parsed.success) { throw new ReleaseManifestError("MANIFEST_SCHEMA_INVALID", options); } return Object.freeze(structuredClone(parsed.data)); } const parsed = releaseManifestV1Schema.safeParse(outcome.value); if (!parsed.success) { throw new ReleaseManifestError("MANIFEST_SCHEMA_INVALID", options); } const { apiContractVersion, ...rest } = structuredClone(parsed.data); return Object.freeze({ ...rest, contractSet: null, legacyApiContractVersion: apiContractVersion, }); } export type LoadReleaseManifestOptions = Readonly<{ fetcher?: typeof fetch; expectedAssetManifestHash?: string; expectedContractSetPackages?: readonly ContractSetPackage[]; signal?: AbortSignal; }>; export async function loadReleaseManifest( runtime: RuntimeConfigLoadResult, options: LoadReleaseManifestOptions = {}, ): Promise { const identity: ReleaseManifestSafeInput = { buildId: runtime.build.buildId, ...(runtime.config.RELEASE_ID ? { releaseId: runtime.config.RELEASE_ID } : {}), }; const manifest = await fetchReleaseManifest( runtime.config.RELEASE_MANIFEST_URL, { ...(options.fetcher ? { fetcher: options.fetcher } : {}), ...(options.signal ? { signal: options.signal } : {}), buildId: runtime.build.buildId, ...(runtime.config.RELEASE_ID ? { releaseId: runtime.config.RELEASE_ID } : {}), }, ); const expectedManifestVersion = runtime.configSchema === "V1" ? 1 : 2; if (manifest.schemaVersion !== expectedManifestVersion) { throw new ReleaseManifestError( "MANIFEST_PROTOCOL_PAIR_MISMATCH", identity, ); } // §6.7 steps 5-6, in order: build, config, release, assets, then contractSet. let mismatchCode: ReleaseManifestErrorCode | null = 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 && runtime.configSchema === "V1" && (manifest.legacyApiContractVersion === undefined || runtime.config.LEGACY_API_CONTRACT_VERSION === undefined || manifest.legacyApiContractVersion !== runtime.config.LEGACY_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, identity); } if (manifest.schemaVersion === 2 && manifest.contractSet) { const verification = await verifyContractSet({ expected: options.expectedContractSetPackages ?? (EXPECTED_CONTRACT_SET_PACKAGES as readonly ContractSetPackage[]), manifest: manifest.contractSet, }); if (!verification.ok) { throw new ReleaseManifestError(verification.code, identity); } } return manifest; }