Files
tech-log-frontend/scripts/lib/service-worker-build-input.ts
T
DongHyeonkaandClaude Opus 5 4bff9ca151 chore: sync the frontend template from 4dc033c to 8157ad4
The product was materialized from the template at `4dc033c` and has stayed
on it through 43 template commits, so it was missing all three rounds of
adapter remediation — including files it never had, such as the shared
`abortable-operation` primitive and the `exact-snapshot` decoder that
later fixes are written against. Taking only the newest round was not
possible for that reason: the delta is coherent only as a whole.

The product had not touched `src/adapters` at all since materialization,
so the 140-file delta applied with a three-way merge and no conflicts.
`package.json` was the single overlap and merged cleanly: the product owns
`name`, the template contributed `check:adapter-inventory`,
`check:remediation-ledger` and the image-resolve-signal type fixture.
All 24 product-owned files — README, index.html, CI workflow, i18n
catalog, home page, generated schemas, evidence scripts, component and
visual snapshots — are byte-identical to `main`.

`template.lock.json` now pins the synced revision and tree.

Verified in this repository, not inherited from the template: six type
projects, lint, nine gates (adapter inventory, remediation ledger,
registries, diagnostics, realtime boundaries, architecture, browser
file/storage boundaries, optional recipes, documentation), the production
build, and 2,054 of 2,073 tests. The 19 failures are all in
`tests/unit/ci-artifact-contract.test.ts` and are the same pre-existing
sandbox RLIMIT, EMFILE, umask and `/tmp` permission behaviour the template
records; four suites that failed once under parallel load pass in
isolation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 12:04:58 +09:00

118 lines
3.9 KiB
TypeScript

import { createHash } from "node:crypto";
import {
canonicalStaticManifestBytes,
decodeStaticAssetManifest,
} from "../../src/contracts/service-worker-static-manifest.ts";
import type {
InstalledServiceWorkerSelection,
ServiceWorkerHandlerId,
StaticAssetManifestV1,
} from "../../src/contracts/service-worker.ts";
const DIGEST = /^sha256:[0-9a-f]{64}$/u;
export type ServiceWorkerBuildInput = Readonly<{
assets: StaticAssetManifestV1;
handlers: readonly ServiceWorkerHandlerId[];
contractSetDigest: string;
releaseManifestUrl: string;
}>;
/**
* ACTIVE worker compilation is a release-integrity boundary. Missing generated
* modules, stale identities and placeholder digests are fatal build defects;
* they must never be converted into a worker that merely degrades at runtime.
*/
export function resolveServiceWorkerBuildInput(input: Readonly<{
selection: InstalledServiceWorkerSelection | null;
assets: unknown;
contractSet: unknown;
runtimeConfig: unknown;
buildId: string;
releaseId: string;
}>): ServiceWorkerBuildInput {
if (input.selection?.mode !== "ACTIVE") {
throw new TypeError(
"Service Worker build requires an ACTIVE static selection.",
);
}
if (!Array.isArray(input.selection.handlers)) {
throw new TypeError("Service Worker handlers must be an array.");
}
const handlers = new Set<ServiceWorkerHandlerId>();
for (const handler of input.selection.handlers) {
if (
handler !== "PWA_STATIC_ASSETS" &&
handler !== "OFFLINE_SYNC_WAKEUP" &&
handler !== "WEB_PUSH"
) {
throw new TypeError(`Unknown Service Worker handler: ${String(handler)}.`);
}
if (handlers.has(handler)) {
throw new TypeError(`Duplicate Service Worker handler: ${handler}.`);
}
handlers.add(handler);
}
if (handlers.has("WEB_PUSH")) {
throw new TypeError(
"WEB_PUSH requires an installed product-owned worker contribution.",
);
}
const assets = parseAssets(input.assets);
if (assets.buildId !== input.buildId || assets.releaseId !== input.releaseId) {
throw new TypeError("Generated Service Worker asset identity is stale.");
}
const contractSet = record(input.contractSet);
const contractSetDigest = contractSet?.setDigest;
if (typeof contractSetDigest !== "string" || !DIGEST.test(contractSetDigest)) {
throw new TypeError("Generated contract set digest is invalid.");
}
const runtimeConfig = record(input.runtimeConfig);
const releaseManifestUrl = runtimeConfig?.RELEASE_MANIFEST_URL;
if (
typeof releaseManifestUrl !== "string" ||
releaseManifestUrl.length === 0 ||
releaseManifestUrl.length > 2_048
) {
throw new TypeError("Runtime release manifest URL is invalid.");
}
return Object.freeze({
assets,
handlers: Object.freeze([...handlers]),
contractSetDigest,
releaseManifestUrl,
});
}
/**
* SW-05. The build gate no longer type-casts the manifest. It decodes every row
* through the shared runtime-neutral codec and recomputes the set digest from
* the same canonical bytes the generator hashed, so a tampered row, a reordered
* set or a stale digest fails admission instead of shipping.
*/
function parseAssets(value: unknown): StaticAssetManifestV1 {
const decoded = decodeStaticAssetManifest(value);
if (!decoded.ok) {
throw new TypeError(
`Generated Service Worker asset manifest is invalid: ${decoded.error.reason}`,
);
}
const expected = `sha256:${createHash("sha256")
.update(canonicalStaticManifestBytes(decoded.manifest.assets))
.digest("hex")}`;
if (expected !== decoded.manifest.setDigest) {
throw new TypeError(
"Generated Service Worker asset manifest set digest does not match its assets.",
);
}
return decoded.manifest as unknown as StaticAssetManifestV1;
}
function record(value: unknown): Record<string, unknown> | null {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: null;
}