fix: bound the activation marker in bytes and give a click one observer
The marker read added a whole chunk to a running total and compared the total afterwards, so a corrupt body could hand activation a 1 MiB chunk against a 257-byte ceiling. It now reads at most the remaining allowance — through a BYOB reader where the source offers one, and by refusing an oversized chunk before copying it otherwise. A declared oversize cancels the body it refuses instead of leaving the stream open, and the reader lock is released on every path. The build generator and the runtime decoder shared only the extension table, not the path grammar. The generator happily emitted `/assets/bad@name-abcdefgh.js`, which the decoder then refused — a correct build failing at install time. Both now use one exported canonical path predicate and the generator decodes its own output before returning it. The notification click handler emitted its terminal record from inside `process` and again from the `waitUntil` wrapper, so an ordinary click was counted twice. Worse, a late rejection downgraded `MAYBE_APPLIED` to `NOT_APPLIED` — telling operators the click had definitely not been applied when nobody knew that — and the late observation ran outside `waitUntil`, so a worker shutdown lost the evidence. There is one observation authority per click now, certainty is monotone, only an explicit null window confirms `NOT_APPLIED`, and the bounded tail is owned by `waitUntil` without extending the public deadline. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
39a4a973a8
commit
d7b35cfca3
@@ -3,6 +3,8 @@ import { createHash } from "node:crypto";
|
||||
import {
|
||||
CACHEABLE_ASSET_CONTENT_TYPES,
|
||||
canonicalStaticManifestBytes,
|
||||
decodeStaticAssetManifest,
|
||||
isCanonicalStaticAssetUrl,
|
||||
} from "../src/contracts/service-worker-static-manifest.ts";
|
||||
import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
@@ -57,8 +59,17 @@ export async function collectStaticAssets(
|
||||
if (bytes.byteLength > SERVICE_WORKER_BOUNDS.singleAssetBytes) {
|
||||
throw new Error(`Static asset exceeds its byte bound: ${relative}`);
|
||||
}
|
||||
// SW-02. The URL is checked against the same predicate the runtime decoder
|
||||
// applies. Emitting a path the decoder will refuse turned a correct build
|
||||
// into a runtime contract failure discovered only at install time.
|
||||
const url = `/${relative.split(path.sep).join("/")}`;
|
||||
if (!isCanonicalStaticAssetUrl(url)) {
|
||||
throw new Error(
|
||||
`Static asset path is not canonical for the service worker manifest: ${relative}`,
|
||||
);
|
||||
}
|
||||
assets.push({
|
||||
url: `/${relative.split(path.sep).join("/")}`,
|
||||
url,
|
||||
sha256: `sha256:${createHash("sha256").update(bytes).digest("hex")}`,
|
||||
bytes: bytes.byteLength,
|
||||
contentType,
|
||||
@@ -79,13 +90,23 @@ export async function collectStaticAssets(
|
||||
.update(canonicalStaticManifestBytes(assets))
|
||||
.digest("hex")}`;
|
||||
|
||||
return {
|
||||
const manifest: StaticAssetManifestV1 = {
|
||||
schemaVersion: 1,
|
||||
buildId,
|
||||
releaseId,
|
||||
setDigest,
|
||||
assets,
|
||||
};
|
||||
// SW-02. Every manifest this generator returns has already passed the exact
|
||||
// decoder the runtime will apply to it, so the build stops here rather than
|
||||
// at install time.
|
||||
const decoded = decodeStaticAssetManifest(manifest);
|
||||
if (!decoded.ok) {
|
||||
throw new Error(
|
||||
`Generated service worker manifest is not decodable: ${decoded.error.reason}`,
|
||||
);
|
||||
}
|
||||
return manifest;
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user