chore: initialize from frontend template 4dc033c
This commit is contained in:
@@ -0,0 +1,294 @@
|
||||
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<Record<string, string>>;
|
||||
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<BootLoadFailure, ReleaseManifestErrorCode>
|
||||
> = 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<ReleaseManifest> {
|
||||
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<ReleaseManifest> {
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user