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:
co-authored by
Claude Opus 5
parent
002ba3624e
commit
4bff9ca151
@@ -1,3 +1,6 @@
|
||||
import {
|
||||
PRESIGNED_TRANSFER_PROTOCOL,
|
||||
} from "../../../application/ports/browser-transfer/presigned-transfer.ts";
|
||||
import type {
|
||||
PresignedDownloadCapability,
|
||||
PresignedTransferBinding,
|
||||
@@ -7,6 +10,11 @@ import type {
|
||||
PresignedUploadPartCapability,
|
||||
PresignedUploadPartCapabilityProvider,
|
||||
} from "../../../application/ports/browser-transfer/presigned-transfer.ts";
|
||||
import {
|
||||
createAbortableOperation,
|
||||
snapshotAbortTimers,
|
||||
type AbortTimerSnapshot,
|
||||
} from "../../platform/abortable-operation.ts";
|
||||
import { RESUMABLE_UPLOAD_PROTOCOL } from "../../../application/ports/browser-transfer/resumable-upload.ts";
|
||||
import type {
|
||||
BrowserDataFailureCode,
|
||||
@@ -141,16 +149,20 @@ export function createPresignedCapabilityHttpProvider(
|
||||
);
|
||||
const fetcher = (options.fetcher ?? fetch).bind(globalThis);
|
||||
const now = options.now ?? Date.now;
|
||||
const scheduler =
|
||||
// X-AUDIT-02. The timer callables are captured once, bound to their
|
||||
// receiver, so replacing a scheduler method after composition cannot change
|
||||
// how work already in flight is bounded.
|
||||
const timers = snapshotAbortTimers(
|
||||
options.scheduler ??
|
||||
({
|
||||
setTimeout: (callback, milliseconds) =>
|
||||
globalThis.setTimeout(callback, milliseconds),
|
||||
clearTimeout: (handle) =>
|
||||
globalThis.clearTimeout(
|
||||
handle as ReturnType<typeof globalThis.setTimeout>,
|
||||
),
|
||||
} satisfies Scheduler);
|
||||
({
|
||||
setTimeout: (callback, milliseconds) =>
|
||||
globalThis.setTimeout(callback, milliseconds),
|
||||
clearTimeout: (handle) =>
|
||||
globalThis.clearTimeout(
|
||||
handle as ReturnType<typeof globalThis.setTimeout>,
|
||||
),
|
||||
} satisfies Scheduler),
|
||||
);
|
||||
const credentials = options.controlPlaneCredentials ?? "same-origin";
|
||||
const observer = options.observer;
|
||||
|
||||
@@ -183,9 +195,10 @@ export function createPresignedCapabilityHttpProvider(
|
||||
if (signal.aborted) {
|
||||
return browserDataFailure("ABORTED", "PRESIGNED_TRANSFER");
|
||||
}
|
||||
const scope = createAbortScope(signal, timeoutMs, scheduler);
|
||||
const scope = createAbortScope(signal, timeoutMs, timers);
|
||||
try {
|
||||
const response = await fetcher(endpoint, {
|
||||
const raced = await scope.race(
|
||||
fetcher(endpoint, {
|
||||
method: "POST",
|
||||
credentials,
|
||||
redirect: "error",
|
||||
@@ -196,6 +209,10 @@ export function createPresignedCapabilityHttpProvider(
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
// BT-PRE-02. The server negotiates by request shape: a legacy request
|
||||
// gets a legacy response and a V1 request gets a V1 response. Fields
|
||||
// are never dual-emitted into a strict decoder.
|
||||
protocol: PRESIGNED_TRANSFER_PROTOCOL,
|
||||
method: expected.method,
|
||||
binding: expected.binding,
|
||||
...(expected.mediaType !== undefined
|
||||
@@ -208,8 +225,13 @@ export function createPresignedCapabilityHttpProvider(
|
||||
? { expectedSha256: expected.expectedSha256 }
|
||||
: {}),
|
||||
}),
|
||||
signal: scope.signal,
|
||||
});
|
||||
signal: scope.signal,
|
||||
}),
|
||||
);
|
||||
if (raced === SCOPE_ENDED) {
|
||||
return transferFailure(signal, scope.timedOut());
|
||||
}
|
||||
const response = raced;
|
||||
if (
|
||||
response.redirected ||
|
||||
response.type === "opaqueredirect" ||
|
||||
@@ -440,6 +462,7 @@ function validateCapabilityPayload(
|
||||
try {
|
||||
const payload = strictRecord(value, [
|
||||
"allowedQueryParameters",
|
||||
"protocol",
|
||||
"binding",
|
||||
"byteLength",
|
||||
"capabilityReceipt",
|
||||
@@ -460,6 +483,11 @@ function validateCapabilityPayload(
|
||||
"requiredResponseHeaders",
|
||||
"singleUse",
|
||||
]);
|
||||
if (payload.protocol !== PRESIGNED_TRANSFER_PROTOCOL) {
|
||||
// Missing, V0 and V2 all close the same way: this envelope is not one we
|
||||
// can interpret. No new failure code is introduced.
|
||||
throw new TypeError("Capability transfer protocol is unsupported.");
|
||||
}
|
||||
if (payload.singleUse !== true || payload.method !== context.expected.method) {
|
||||
throw new TypeError("Capability method or replay policy is invalid.");
|
||||
}
|
||||
@@ -618,6 +646,9 @@ function validateCapabilityPayload(
|
||||
|
||||
return browserDataSuccess(
|
||||
Object.freeze({
|
||||
// TR-RR-03. The registration carries the negotiated protocol version so
|
||||
// the vault validates the same shape the executor later reads.
|
||||
protocol: PRESIGNED_TRANSFER_PROTOCOL,
|
||||
capabilityReceipt,
|
||||
method: context.expected.method,
|
||||
binding,
|
||||
@@ -854,30 +885,104 @@ function statusFailure(
|
||||
});
|
||||
}
|
||||
|
||||
/** BT-PRE-03. The scope ended before the task settled. */
|
||||
const SCOPE_ENDED = Symbol("presigned-capability-scope-ended");
|
||||
|
||||
/**
|
||||
* TR-RR-01 / TR-RR-05. The abort scope is a thin projection of the shared
|
||||
* `createAbortableOperation` primitive into this subsystem's vocabulary.
|
||||
*
|
||||
* Two things changed with the migration. `abort()` exists, so `close()` on a
|
||||
* download can actually stop a pending fetch instead of only dropping
|
||||
* listeners; and additional ownership signals — a consumer's stream signal —
|
||||
* are composed into the same operation *before* any I/O starts, so an
|
||||
* already-aborted consumer never causes a fetch.
|
||||
*/
|
||||
function createAbortScope(
|
||||
external: AbortSignal,
|
||||
timeoutMs: number,
|
||||
scheduler: Scheduler,
|
||||
timers: AbortTimerSnapshot,
|
||||
additionalSignals: readonly AbortSignal[] = [],
|
||||
) {
|
||||
const controller = new AbortController();
|
||||
let timedOut = false;
|
||||
const onAbort = () => controller.abort(external.reason);
|
||||
external.addEventListener("abort", onAbort, { once: true });
|
||||
if (external.aborted) onAbort();
|
||||
const timer = scheduler.setTimeout(() => {
|
||||
timedOut = true;
|
||||
controller.abort("timeout");
|
||||
}, timeoutMs);
|
||||
const operation = createAbortableOperation({
|
||||
signal: external,
|
||||
timeoutMs,
|
||||
setTimer: timers.setTimer,
|
||||
clearTimer: timers.clearTimer,
|
||||
});
|
||||
const releases: (() => void)[] = [];
|
||||
for (const extra of additionalSignals) {
|
||||
if (!extra) continue;
|
||||
if (extra.aborted) {
|
||||
operation.close();
|
||||
continue;
|
||||
}
|
||||
const onAbort = () => operation.close();
|
||||
try {
|
||||
extra.addEventListener("abort", onAbort, { once: true });
|
||||
releases.push(() => {
|
||||
try {
|
||||
extra.removeEventListener("abort", onAbort);
|
||||
} catch {
|
||||
// A hostile signal facade cannot block the rest of cleanup.
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
// A signal that refuses a listener cannot bound this operation, so the
|
||||
// operation closes rather than running unbounded.
|
||||
operation.close();
|
||||
}
|
||||
}
|
||||
const releaseExtras = () => {
|
||||
for (const release of releases.splice(0)) release();
|
||||
};
|
||||
return Object.freeze({
|
||||
signal: controller.signal,
|
||||
timedOut: () => timedOut,
|
||||
signal: operation.signal,
|
||||
timedOut: () => operation.terminal() === "DEADLINE",
|
||||
/** Bounds a fetch that ignores its signal. */
|
||||
async race<Value>(task: Promise<Value>): Promise<Value | typeof SCOPE_ENDED> {
|
||||
const outcome = await operation.race(
|
||||
task,
|
||||
(value) => compensateLateResponseValue(value),
|
||||
);
|
||||
if (outcome.kind === "VALUE") return outcome.value;
|
||||
// A collaborator's own rejection stays a rejection: the caller's existing
|
||||
// catch classifies it, and it is never forged into a cancellation.
|
||||
if (outcome.kind === "REJECTED") throw outcome.reason;
|
||||
return SCOPE_ENDED;
|
||||
},
|
||||
/** TR-RR-01. Ends the physical work, not just the bookkeeping. */
|
||||
abort() {
|
||||
operation.close();
|
||||
releaseExtras();
|
||||
},
|
||||
release() {
|
||||
scheduler.clearTimeout(timer);
|
||||
external.removeEventListener("abort", onAbort);
|
||||
operation.close();
|
||||
releaseExtras();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function compensateLateResponseValue(value: unknown): void {
|
||||
const body = (value as { body?: { cancel(): Promise<void> } | null } | null)
|
||||
?.body;
|
||||
void body?.cancel().catch(() => undefined);
|
||||
}
|
||||
|
||||
function transferFailure(
|
||||
signal: AbortSignal,
|
||||
timedOut: boolean,
|
||||
): BrowserDataResult<never> {
|
||||
if (signal.aborted) {
|
||||
return browserDataFailure("ABORTED", "PRESIGNED_TRANSFER");
|
||||
}
|
||||
return browserDataFailure(
|
||||
timedOut ? "UNAVAILABLE" : "NOT_READABLE",
|
||||
"PRESIGNED_TRANSFER",
|
||||
{ retryable: true, recovery: "REISSUE_CAPABILITY" },
|
||||
);
|
||||
}
|
||||
|
||||
function isAbortSignal(value: unknown): value is AbortSignal {
|
||||
try {
|
||||
if (!value || typeof value !== "object") return false;
|
||||
@@ -1083,18 +1188,64 @@ function validatePathPrefix(value: string): string {
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* BT-PRE-05. Object stores and CDNs may percent-decode a path one more time
|
||||
* than this client does, so rejecting only literal `.`, `..` and backslash is
|
||||
* not enough: `%2f`, `%5c` and `%252e%252e` can still become separators or dot
|
||||
* segments downstream.
|
||||
*
|
||||
* Each raw segment is strictly percent-decoded once. The decoded value may not
|
||||
* contain a separator, NUL, a dot segment or a further percent-escape, and
|
||||
* re-encoding it canonically must reproduce the raw segment exactly. That
|
||||
* closes double encoding and mixed-case variants while still allowing any valid
|
||||
* opaque UTF-8 segment.
|
||||
*/
|
||||
function validateExactPath(value: unknown): string {
|
||||
const path = requiredString(value, 2_048);
|
||||
if (
|
||||
!path.startsWith("/") ||
|
||||
path.includes("\\") ||
|
||||
/[\0\r\n]/.test(path) ||
|
||||
path.split("/").some((segment) => segment === "." || segment === "..")
|
||||
/[\0\r\n]/.test(path)
|
||||
) {
|
||||
throw new TypeError("Path is invalid.");
|
||||
}
|
||||
for (const segment of path.split("/")) {
|
||||
if (segment === "") continue;
|
||||
if (segment === "." || segment === "..") {
|
||||
throw new TypeError("Path is invalid.");
|
||||
}
|
||||
let decoded: string;
|
||||
try {
|
||||
decoded = decodeURIComponent(segment);
|
||||
} catch {
|
||||
// Malformed or non-UTF-8 percent-escapes are rejected outright.
|
||||
throw new TypeError("Path is invalid.");
|
||||
}
|
||||
if (
|
||||
decoded === "." ||
|
||||
decoded === ".." ||
|
||||
decoded.includes("/") ||
|
||||
decoded.includes("\\") ||
|
||||
decoded.includes("\0") ||
|
||||
// A decoded value that still carries a percent-escape would decode again.
|
||||
/%[0-9A-Fa-f]{2}/u.test(decoded)
|
||||
) {
|
||||
throw new TypeError("Path is invalid.");
|
||||
}
|
||||
if (canonicalPathSegment(decoded) !== segment) {
|
||||
throw new TypeError("Path is invalid.");
|
||||
}
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/** Uppercase percent-hex canonical form, matching the provider fixtures. */
|
||||
function canonicalPathSegment(decoded: string): string {
|
||||
return encodeURIComponent(decoded).replace(
|
||||
/%[0-9a-f]{2}/gu,
|
||||
(escape) => escape.toUpperCase(),
|
||||
);
|
||||
}
|
||||
|
||||
class ResponseLimitError extends Error {}
|
||||
class ResponseIntegrityError extends Error {}
|
||||
|
||||
Reference in New Issue
Block a user