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>
This commit is contained in:
DongHyeonka
2026-08-15 12:04:58 +09:00
co-authored by Claude Opus 5
parent 002ba3624e
commit 4bff9ca151
142 changed files with 23010 additions and 1544 deletions
@@ -40,7 +40,8 @@ const scope: WorkerScopeLike = {
open: (name) => caches.open(name),
keys: () => caches.keys(),
delete: (name) => caches.delete(name),
match: (request) => caches.match(request),
// SW-01. No CacheStorage-wide match: only the current release cache may
// answer a verified static request.
},
clients: {
matchAll: (options) =>
@@ -33,7 +33,6 @@ export type WorkerScopeLike = Readonly<{
open(cacheName: string): Promise<Cache>;
keys(): Promise<readonly string[]>;
delete(cacheName: string): Promise<boolean>;
match(request: string): Promise<Response | undefined>;
}>;
clients: Readonly<{
matchAll(
@@ -59,6 +58,34 @@ export type WorkerRuntimeConfig = Readonly<{
releaseManifestUrl: string;
}>;
/**
* SW-URL-01. Root-relative generated URLs become absolute same-origin URLs
* exactly once. Anything that escapes the scope origin is dropped rather than
* silently classified.
*/
function canonicalManifestUrls(
assets: readonly Readonly<{ url: string }>[],
scopeHref: string,
): readonly string[] {
let base: URL;
try {
base = new URL(scopeHref);
} catch {
return [];
}
const canonical: string[] = [];
for (const asset of assets) {
try {
const absolute = new URL(asset.url, base);
if (absolute.origin !== base.origin) continue;
canonical.push(absolute.href);
} catch {
// A manifest URL that cannot be canonicalized is never classified.
}
}
return canonical;
}
const ACTIVATION_MARKER_URL =
"https://clean-architecture.invalid/__service-worker-activation-v1__";
const ACTIVATION_MARKER_MAX_BYTES = 256;
@@ -73,8 +100,22 @@ export function createServiceWorkerRuntime(
config: WorkerRuntimeConfig,
) {
const staticEnabled = config.handlers.includes("PWA_STATIC_ASSETS");
const manifestUrls = new Set(
staticEnabled ? (config.manifest?.assets ?? []).map((asset) => asset.url) : [],
/**
* SW-URL-01. The generated manifest stores root-relative URLs while `Request`
* exposes absolute ones, so comparing the two directly classified every
* verified asset as a network fallback. Canonicalize once against the
* registration scope, re-check same-origin, and share that identity across
* install cache keys, fetch classification and cache lookup or delete.
*/
const manifestUrls: ReadonlySet<string> = Object.freeze(
new Set(
staticEnabled
? canonicalManifestUrls(
config.manifest?.assets ?? [],
scope.registrationScope,
)
: [],
),
);
const consumedNonces = new Set<string>();
type PendingActivation = Readonly<{
@@ -176,17 +217,30 @@ export function createServiceWorkerRuntime(
});
if (classification !== "VERIFIED_CACHE_FIRST") return null;
const cached = await scope.caches.match(request.url);
// SW-01. Only the current release cache may answer. A CacheStorage-wide
// match could return a previous release's response for the same URL, and
// the subsequent delete would then target a cache that was never read.
const currentCacheName = config.manifest
? staticCacheName(config.manifest.setDigest)
: null;
if (!currentCacheName) return null;
// SW-RR-04. Both `open` and `match` are storage calls that can throw
// synchronously or reject. Either one escaping here rejects `respondWith`
// itself, so the entry never reaches its network fallback and the page
// gets a network error instead of the live response.
let cached: Response | undefined;
let currentCache: Cache;
try {
currentCache = await scope.caches.open(currentCacheName);
cached = await currentCache.match(request.url);
} catch {
return null;
}
if (!cached) return null;
if (cached.status !== 200 || cached.type === "opaque") {
// §18.6. An invalid hit is deleted and treated as a release mismatch.
const current = config.manifest
? staticCacheName(config.manifest.setDigest)
: null;
if (current) {
const cache = await scope.caches.open(current);
await cache.delete(request.url).catch(() => false);
}
// §18.6. An invalid hit is deleted from the cache it was read from and
// treated as a release mismatch.
await currentCache.delete(request.url).catch(() => false);
return null;
}
return cached;
@@ -228,49 +282,64 @@ export function createServiceWorkerRuntime(
parsed.message.sourceBuildId,
);
if (!drained) {
for (const client of clients) {
client.postMessage(
createServiceWorkerMessage({
kind: "ACTIVATE_REJECTED",
sourceBuildId: config.identity.buildId,
targetBuildId: parsed.message.sourceBuildId,
nonce,
}),
);
}
notifyClients(clients, "ACTIVATE_REJECTED", parsed.message.sourceBuildId, nonce);
return "REJECTED";
}
for (const client of clients) {
client.postMessage(
createServiceWorkerMessage({
kind: "ACTIVATE_ACCEPTED",
sourceBuildId: config.identity.buildId,
targetBuildId: parsed.message.sourceBuildId,
nonce,
}),
);
}
await scope.skipWaiting();
for (const client of clients) {
client.postMessage(
createServiceWorkerMessage({
kind: "ACTIVATED_RELOAD_REQUIRED",
sourceBuildId: config.identity.buildId,
targetBuildId: parsed.message.sourceBuildId,
nonce,
}),
);
// SW-08. `skipWaiting()` is the activation commit. It must succeed before
// any client is told the activation was accepted, and its failure is a
// rejection rather than an accepted-then-failed activation.
try {
await scope.skipWaiting();
} catch {
notifyClients(clients, "ACTIVATE_REJECTED", parsed.message.sourceBuildId, nonce);
return "REJECTED";
}
// Post-commit notifications are per-client best effort.
notifyClients(clients, "ACTIVATE_ACCEPTED", parsed.message.sourceBuildId, nonce);
notifyClients(
clients,
"ACTIVATED_RELOAD_REQUIRED",
parsed.message.sourceBuildId,
nonce,
);
return "ACCEPTED";
}
/**
* SW-08. One client's `postMessage()` throwing must not break the whole
* activation event; delivery is isolated per client.
*/
function notifyClients(
clients: readonly WorkerClientLike[],
kind: "ACTIVATE_REJECTED" | "ACTIVATE_ACCEPTED" | "ACTIVATED_RELOAD_REQUIRED",
targetBuildId: string,
nonce: string,
): void {
for (const client of clients) {
try {
client.postMessage(
createServiceWorkerMessage({
kind,
sourceBuildId: config.identity.buildId,
targetBuildId,
nonce,
}),
);
} catch {
// A dead client cannot change the already committed activation.
}
}
}
async function drainClients(
clients: readonly WorkerClientLike[],
nonce: string,
requesterBuildId: string,
): Promise<boolean> {
if (clients.length === 0) return false;
// SW-07. No in-scope client means nothing dirty to drain, so the set is
// vacuously drained. A `clients.matchAll()` failure still rejects upstream.
if (clients.length === 0) return true;
const drained = new Promise<boolean>((resolve) => {
const timer = setTimeout(() => {
pendingActivations.delete(nonce);
@@ -287,15 +356,24 @@ export function createServiceWorkerRuntime(
}),
);
});
// SW-08. A client that cannot receive the drain request can never
// acknowledge it, so it fails immediately instead of holding the pending
// state until the timeout.
for (const client of clients) {
client.postMessage(
createServiceWorkerMessage({
kind: "CLIENT_DRAIN_REQUEST",
sourceBuildId: config.identity.buildId,
targetBuildId: requesterBuildId,
nonce,
}),
);
try {
client.postMessage(
createServiceWorkerMessage({
kind: "CLIENT_DRAIN_REQUEST",
sourceBuildId: config.identity.buildId,
targetBuildId: requesterBuildId,
nonce,
}),
);
} catch {
const pending = pendingActivations.get(nonce);
if (pending) settlePendingActivation(nonce, pending, false);
return await drained;
}
}
return drained;
}
@@ -347,7 +425,9 @@ export function createServiceWorkerRuntime(
let cachesDeleted = 0;
const names = await scope.caches.keys();
for (const name of names) {
if (!name.startsWith("ca-static-v1-")) continue;
// SW-02. Exact ownership only: a prefix match would also delete
// `ca-static-v1-not-owned` and any longer-suffixed foreign cache.
if (!isOwnedStaticCacheName(name)) continue;
try {
if (await scope.caches.delete(name)) cachesDeleted += 1;
} catch {
@@ -399,6 +479,141 @@ function isClientWithinRegistrationScope(
}
}
/**
* SW-RR-01. Reads at most one byte beyond the marker ceiling, cancels the
* reader as soon as that byte arrives, and fails closed on invalid UTF-8. The
* reader is also raced against a deadline so a stream that never produces a
* chunk cannot hold `activate` open.
*/
const ACTIVATION_MARKER_READ_DEADLINE_MS = 5_000;
async function readBoundedMarkerText(
response: Response,
): Promise<string | null> {
if (!response.body) {
try {
const text = await response.text();
return new TextEncoder().encode(text).byteLength >
ACTIVATION_MARKER_MAX_BYTES
? null
: text;
} catch {
return null;
}
}
// SW-01. The allowance is `limit + 1` bytes: one byte past the ceiling is
// enough to prove the marker is oversized, and nothing beyond it is ever
// retained. A BYOB reader asks the source for exactly the remaining
// allowance, so a corrupt body cannot answer a 1 MiB chunk to a 257-byte
// request. Without BYOB the first oversized chunk is refused outright rather
// than copied and then measured.
const allowance = ACTIVATION_MARKER_MAX_BYTES + 1;
const reader = byobReader(response.body) ?? response.body.getReader();
const byob = "read" in reader && isByobReader(reader);
const bytes = new Uint8Array(allowance);
let total = 0;
let timer: ReturnType<typeof setTimeout> | undefined;
let cancelled = false;
const deadline = new Promise<"DEADLINE">((resolve) => {
try {
timer = setTimeout(
() => resolve("DEADLINE"),
ACTIVATION_MARKER_READ_DEADLINE_MS,
);
} catch {
// An unschedulable deadline leaves the read unbounded, so it ends now.
resolve("DEADLINE");
}
});
const cancelOnce = (): void => {
if (cancelled) return;
cancelled = true;
// Never awaited: cancelling a stream whose source ignores cancellation can
// itself hang, and the marker read already has its answer.
try {
void reader.cancel().catch(() => {});
} catch {
// A hostile reader cannot block the release below.
}
};
try {
for (;;) {
const remaining = allowance - total;
if (remaining <= 0) {
// More than the ceiling has already arrived.
return null;
}
const pending = byob
? (reader as ReadableStreamBYOBReader).read(
new Uint8Array(remaining),
)
: (reader as ReadableStreamDefaultReader<Uint8Array>).read();
const next = await Promise.race([pending, deadline]);
if (next === "DEADLINE") {
cancelOnce();
return null;
}
if (next.done) break;
const chunk = next.value;
if (!chunk) continue;
if (chunk.byteLength > remaining) {
// Refused before it is retained: the oversized chunk is not copied.
cancelOnce();
return null;
}
bytes.set(chunk, total);
total += chunk.byteLength;
}
} catch {
cancelOnce();
return null;
} finally {
if (timer !== undefined) {
try {
clearTimeout(timer);
} catch {
// Releasing the timer is best effort.
}
}
cancelOnce();
try {
reader.releaseLock();
} catch {
// A cancelled reader has already released its lock.
}
}
if (total > ACTIVATION_MARKER_MAX_BYTES) return null;
try {
return new TextDecoder("utf-8", { fatal: true }).decode(
bytes.subarray(0, total),
);
} catch {
return null;
}
}
/** A byte stream can hand out a BYOB reader; a regular one cannot. */
function byobReader(
body: ReadableStream<Uint8Array>,
): ReadableStreamBYOBReader | null {
try {
return (
body as ReadableStream<Uint8Array> & {
getReader(options: { mode: "byob" }): ReadableStreamBYOBReader;
}
).getReader({ mode: "byob" });
} catch {
return null;
}
}
function isByobReader(reader: unknown): reader is ReadableStreamBYOBReader {
return (
typeof ReadableStreamBYOBReader === "function" &&
reader instanceof ReadableStreamBYOBReader
);
}
async function readActivationMarker(
cache: Cache,
expectedCacheName: string,
@@ -412,12 +627,21 @@ async function readActivationMarker(
(!/^\d+$/u.test(declaredLength) ||
Number(declaredLength) > ACTIVATION_MARKER_MAX_BYTES)
) {
// SW-01. A declared oversize ends the read, but the body it declared is
// still an open stream: returning without cancelling it left the source
// holding the connection for the rest of the worker's life.
try {
void response.body?.cancel().catch(() => {});
} catch {
// Cancelling is best effort and cannot change the closed outcome.
}
return null;
}
const text = await response.text();
if (new TextEncoder().encode(text).byteLength > ACTIVATION_MARKER_MAX_BYTES) {
return null;
}
// SW-RR-01. A Content-Length is a claim, not a bound. Without one the
// previous `response.text()` read the whole body, so a large or
// non-terminating stream could consume the activation step indefinitely.
const text = await readBoundedMarkerText(response);
if (text === null) return null;
const value: unknown = JSON.parse(text);
if (
value === null ||
@@ -2,6 +2,7 @@ import {
SERVICE_WORKER_BOUNDS,
type InstalledServiceWorkerSelection,
type ServiceWorkerActivationOutcome,
type ServiceWorkerRemovalOutcome,
type ServiceWorkerResetOutcome,
type ServiceWorkerRuntimeHost,
type ServiceWorkerStartOutcome,
@@ -57,6 +58,9 @@ export function createServiceWorkerPageController(
let registration: ServiceWorkerRegistration | null = null;
let messageListener: ((event: MessageEvent) => void) | null = null;
let updateTimer: ReturnType<typeof setInterval> | null = null;
/** SW-06. Single-flight command state. */
let activationInFlight: Promise<ServiceWorkerActivationOutcome> | null = null;
let resetInFlight: Promise<ServiceWorkerResetOutcome> | null = null;
let stopped = false;
const pendingStops = new Set<() => void>();
@@ -75,6 +79,29 @@ export function createServiceWorkerPageController(
return false;
}
/**
* SW-04. Staged removal reports what actually happened.
*
* Returning DISABLED for every outcome let a later release delete the worker
* source and handlers while a registration or an owned cache was still
* present, or while the registration belonged to someone else.
*/
function removalStartOutcome(
outcome: ServiceWorkerRemovalOutcome,
failureReason: string,
): ServiceWorkerStartOutcome {
switch (outcome.kind) {
case "ABSENT":
case "UNREGISTERED":
case "PURGED":
return Object.freeze({ kind: "DISABLED" as const });
case "OWNERSHIP_MISMATCH":
return Object.freeze({ kind: "INCOMPATIBLE" as const });
case "FAILED":
return failed(failureReason);
}
}
async function start(): Promise<ServiceWorkerStartOutcome> {
if (stopped) return failed("STOPPED");
const container = dependencies.container;
@@ -90,8 +117,7 @@ export function createServiceWorkerPageController(
origin: dependencies.origin,
});
observe("disable_cleanup", outcome.kind);
if (outcome.kind === "FAILED") return failed("DISABLE_CLEANUP_FAILED");
return Object.freeze({ kind: "DISABLED" as const });
return removalStartOutcome(outcome, "DISABLE_CLEANUP_FAILED");
}
const selection = dependencies.selection;
@@ -108,7 +134,7 @@ export function createServiceWorkerPageController(
origin: dependencies.origin,
});
observe("remove_registration", outcome.kind);
return Object.freeze({ kind: "DISABLED" as const });
return removalStartOutcome(outcome, "REMOVE_FAILED");
}
if (selection.mode === "PURGE_OWNED_RESOURCES") {
const outcome = await purgeOwnedResources({
@@ -118,7 +144,7 @@ export function createServiceWorkerPageController(
origin: dependencies.origin,
});
observe("purge_owned_resources", outcome.kind);
return Object.freeze({ kind: "DISABLED" as const });
return removalStartOutcome(outcome, "PURGE_FAILED");
}
// §17.5. StrictMode's repeated effect returns the same in-flight promise
@@ -194,6 +220,13 @@ export function createServiceWorkerPageController(
observe("client_drain", "MALFORMED");
return;
}
// SW-06. An arbitrary same-origin source must not be able to close this
// page's admission. The request has to come from the worker we are
// actually waiting on or the one currently controlling us.
if (!isExpectedWorkerSource(source)) {
observe("client_drain", "SOURCE_MISMATCH");
return;
}
const rejected = isBlocked();
source.postMessage(
createServiceWorkerMessage({
@@ -211,6 +244,23 @@ export function createServiceWorkerPageController(
container.addEventListener("message", messageListener);
}
/**
* SW-06. Source identity is checked by object identity against the
* registration's waiting/installing/active worker and the container's
* controller. An empty `event.origin` is never used as a trust signal.
*/
function isExpectedWorkerSource(source: unknown): boolean {
const expected = [
registration?.waiting,
registration?.installing,
registration?.active,
dependencies.container?.controller,
];
return expected.some(
(candidate) => candidate != null && candidate === source,
);
}
function scheduleUpdateChecks(): void {
// §17.14. At most one check per 6 hours, and none while the page is hidden.
if (updateTimer) return;
@@ -228,7 +278,16 @@ export function createServiceWorkerPageController(
* §17.11. Activation is a handshake: every controlled client must close new
* admission and acknowledge within 30s. One missing client rejects it.
*/
async function requestActivation(): Promise<ServiceWorkerActivationOutcome> {
function requestActivation(): Promise<ServiceWorkerActivationOutcome> {
// SW-06. Concurrent callers share one command: a second call must not issue
// a second nonce, a second listener or a second postMessage.
activationInFlight ??= runActivation().finally(() => {
activationInFlight = null;
});
return activationInFlight;
}
async function runActivation(): Promise<ServiceWorkerActivationOutcome> {
const waiting = registration?.waiting;
if (!waiting) return Object.freeze({ kind: "NO_WAITING_WORKER" as const });
if (isBlocked()) {
@@ -264,6 +323,18 @@ export function createServiceWorkerPageController(
) {
return;
}
// SW-06 / SW-RR-02. The reply must come from the exact worker this
// request was sent to. A matching nonce is not identity: `null`
// source means the sender cannot be established, so it is refused
// like any other mismatch rather than accepted as this worker.
if (event.source !== waiting) {
finish(Object.freeze({ kind: "PROTOCOL_MISMATCH" as const }));
return;
}
if (registration?.waiting !== waiting) {
finish(Object.freeze({ kind: "PROTOCOL_MISMATCH" as const }));
return;
}
if (
parsed.message.kind === "ACTIVATE_REJECTED" &&
parsed.message.nonce === nonce
@@ -306,11 +377,21 @@ export function createServiceWorkerPageController(
}
/** §18.10. Static caches only; the registration itself is left in place. */
async function resetOwnedCaches(): Promise<ServiceWorkerResetOutcome> {
function resetOwnedCaches(): Promise<ServiceWorkerResetOutcome> {
// SW-06. Single-flight, like activation.
resetInFlight ??= runReset().finally(() => {
resetInFlight = null;
});
return resetInFlight;
}
async function runReset(): Promise<ServiceWorkerResetOutcome> {
const container = dependencies.container;
if (!container?.controller) {
return Object.freeze({ kind: "NOT_CONTROLLED" as const });
}
// SW-06. The reply must come from the controller this request was sent to.
const requestedController = container.controller;
const nonce = nonces.issue();
return new Promise<ServiceWorkerResetOutcome>((resolve) => {
let settled = false;
@@ -334,6 +415,20 @@ export function createServiceWorkerPageController(
) {
return;
}
// SW-RR-02. An unattributable reset result is never proof this
// controller performed the reset.
if (
event.source !== requestedController ||
container.controller !== requestedController
) {
finish(
Object.freeze({
kind: "FAILED" as const,
code: "PROTOCOL_MISMATCH",
}),
);
return;
}
finish(
Object.freeze({
kind: "RESET" as const,
@@ -109,14 +109,24 @@ export async function removeOwnedRegistration(
) {
return Object.freeze({ kind: "OWNERSHIP_MISMATCH" as const });
}
let unregistered: boolean;
try {
await registration.unregister();
unregistered = await registration.unregister();
} catch {
return Object.freeze({
kind: "FAILED" as const,
operation: "UNREGISTER" as const,
});
}
// SW-03. `unregister()` resolving is not success: `false` means the
// registration is still installed, so reporting UNREGISTERED would let a
// later release delete the worker source while it is still controlling.
if (!unregistered) {
return Object.freeze({
kind: "FAILED" as const,
operation: "UNREGISTER" as const,
});
}
return Object.freeze({ kind: "UNREGISTERED" as const });
}
@@ -149,6 +149,19 @@ export async function installStaticAssets(
if (outcome.kind === "REJECTED") {
await dependencies.caches.delete(cacheName).catch(() => false);
// SW-09. A non-cooperative fetch, digest or `cache.put` started before the
// deadline cannot be cancelled, so it may recreate the candidate cache
// after that delete. The public result already closed at the deadline; a
// second exact delete is registered once the abandoned work settles. It is
// deliberately not awaited, so the public bound is not extended.
if (deadlineExceeded) {
void installation
.catch(() => undefined)
.then(async () => {
await dependencies.caches.delete(cacheName).catch(() => false);
})
.catch(() => undefined);
}
}
return outcome;
}
@@ -173,6 +186,11 @@ async function installCandidate(
const worker = async (): Promise<void> => {
for (;;) {
if (failure) return;
// SW-09. Once fenced, no new candidate work is started.
if (signal.aborted) {
failure ??= rejected("INSTALL_DEADLINE_EXCEEDED");
return;
}
const asset = queue.shift();
if (!asset) return;
const outcome = await storeAsset(asset, cache, dependencies, signal);
@@ -205,15 +223,22 @@ async function storeAsset(
if (signal.aborted) return rejected("FETCH_FAILED");
let response: Response;
try {
const fetched = await abortable(
dependencies.fetcher(asset.url, {
cache: "no-store",
credentials: "omit",
redirect: "error",
signal,
}),
// SW-09. A non-cooperative fetch that ignores the signal still settles
// later; its body is compensated so an abandoned response is not left open.
const pending = dependencies.fetcher(asset.url, {
cache: "no-store",
credentials: "omit",
redirect: "error",
signal,
);
});
const fetched = await abortable(pending, signal);
if (fetched === ABORTED) {
void pending
.then(async (late) => {
await late.body?.cancel();
})
.catch(() => undefined);
}
if (fetched === ABORTED) return rejected("FETCH_FAILED");
response = fetched;
} catch {
@@ -234,7 +259,17 @@ async function storeAsset(
if (!body.ok) return rejected(body.code);
const bytes = body.bytes;
const digest = await abortable(dependencies.digest(bytes), signal);
// SW-09. A digest dependency that throws becomes a closed typed outcome
// rather than an escaping rejection.
let digest: string | typeof ABORTED;
try {
digest = await abortable(
Promise.resolve(dependencies.digest(bytes)),
signal,
);
} catch {
return rejected("INTEGRITY_MISMATCH");
}
if (digest === ABORTED) return rejected("FETCH_FAILED");
if (digest !== asset.sha256) return rejected("INTEGRITY_MISMATCH");