Files
tech-log-frontend/src/adapters/browser-transfer/presigned/presigned-capability-http-provider.ts
T
DongHyeonkaandClaude Opus 5 bdee07a93b chore: sync the frontend template from a0fbafb to 5434760
Carries eight template commits: the provider sandbox actually running, release
admission to a named environment, the product feature manifest with its runtime
kill switch, architecture and documentation rules that match what is enforced,
the removability fixtures, and the browser, visual and performance evidence.

Product identity is unchanged. `package.json` keeps `tech-log-frontend` and the
catalog keeps the Tech Log naming; the home page was not in the delta. The
visual baselines are this product's own — the template's were excluded from the
transplant and these were regenerated here, where the only difference is the
platform overview's new product-feature section.

What this repository gains operationally: `config/runtime/{local,development,
staging,production}.json` with `FE-GATE-027` refusing an artifact whose runtime
document does not match the environment it is being admitted to, and
`FEATURE_OVERRIDES` for taking an installed feature out of service without a
rebuild.

Verified here: eight gates green, build green, visual 5/5, and 1,858 of 1,859
tests in the suites that do not need a sandbox — the one failure passes in
isolation and is a jsdom lazy-chunk timeout under parallel load. The provider
suites cannot run on this machine at all: `kernel.apparmor_restrict_unprivileged
_userns=1` makes `bwrap --unshare-net` fail, reproducible without any code from
either repository.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 21:34:19 +09:00

1259 lines
39 KiB
TypeScript

import {
PRESIGNED_TRANSFER_PROTOCOL,
} from "../../../application/ports/browser-transfer/presigned-transfer.ts";
import type {
PresignedDownloadCapability,
PresignedTransferBinding,
PresignedTransferCapability,
PresignedTransferCapabilityProvider,
PresignedTransferCapabilityReceipt,
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,
BrowserDataObserver,
BrowserDataRecovery,
BrowserDataResult,
} from "../../../application/ports/browser-file-storage/shared.ts";
import {
browserDataFailure,
browserDataSuccess,
observeBrowserData,
} from "../../browser-file-storage/result.ts";
import type {
PresignedCapabilityRegistration,
PresignedCapabilityVault,
PresignedHeaderBinding,
} from "./presigned-capability-vault.ts";
const SHA256 = /^[a-f0-9]{64}$/;
const MEDIA_TYPE = /^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/;
const OPAQUE_ID = /^[a-z0-9][a-z0-9._:-]{0,255}$/i;
const HEADER_NAME = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
const QUERY_NAME = /^[A-Za-z0-9_.~-]{1,128}$/;
const FORBIDDEN_REQUEST_HEADERS = new Set([
"authorization",
"connection",
"content-length",
"cookie",
"host",
"origin",
"proxy-authorization",
"range",
"referer",
"set-cookie",
"transfer-encoding",
]);
type Scheduler = Readonly<{
setTimeout(callback: () => void, milliseconds: number): unknown;
clearTimeout(handle: unknown): void;
}>;
export type PresignedCapabilityHttpProviderOptions = Readonly<{
/**
* Composition-owned BFF endpoint. It is captured once and never accepted
* from issueDownload/issueUploadPart callers.
*/
endpoint: string;
vault: PresignedCapabilityVault;
allowedDataOrigins: readonly string[];
allowedDataPathPrefixes: readonly string[];
allowedQueryParameters: readonly string[];
allowedRequestHeaders: readonly string[];
allowedResponseHeaders?: readonly string[];
hardMaxTransferBytes: number;
hardMaxUploadResponseBytes: number;
maxCapabilityTtlMs: number;
/**
* A capability with less time remaining is treated as expired. Keep this
* aligned with the upload runtime refresh skew so a transfer is not started
* with a token that is predictably going to expire in flight.
*/
minimumRemainingLifetimeMs: number;
timeoutMs: number;
maxCapabilityResponseBytes?: number;
fetcher?: typeof fetch;
now?: () => number;
scheduler?: Scheduler;
controlPlaneCredentials?: "same-origin" | "include";
allowInsecureLocalhost?: boolean;
observer?: BrowserDataObserver;
}>;
export type PresignedCapabilityHttpProvider =
PresignedTransferCapabilityProvider &
PresignedUploadPartCapabilityProvider;
export function createPresignedCapabilityHttpProvider(
options: PresignedCapabilityHttpProviderOptions,
): PresignedCapabilityHttpProvider {
const endpoint = validateEndpoint(
options.endpoint,
options.allowInsecureLocalhost ?? false,
);
const vault = options.vault;
const register = vault.register.bind(vault);
const allowedDataOrigins = new Set(
options.allowedDataOrigins.map((origin) =>
normalizedOrigin(origin, options.allowInsecureLocalhost ?? false),
),
);
const allowedDataPathPrefixes = Object.freeze(
options.allowedDataPathPrefixes.map(validatePathPrefix),
);
const allowedQueryParameters = new Set(
options.allowedQueryParameters.map(validateQueryName),
);
const allowedRequestHeaders = normalizedHeaderSet(
options.allowedRequestHeaders,
);
const allowedResponseHeaders = normalizedHeaderSet(
options.allowedResponseHeaders ?? [],
);
const hardMaxTransferBytes = positiveSafeInteger(
options.hardMaxTransferBytes,
"Presigned transfer hard byte limit",
);
const hardMaxUploadResponseBytes = positiveSafeInteger(
options.hardMaxUploadResponseBytes,
"Presigned upload response hard byte limit",
);
const maxCapabilityTtlMs = positiveSafeInteger(
options.maxCapabilityTtlMs,
"Presigned capability TTL",
);
const minimumRemainingLifetimeMs = positiveSafeInteger(
options.minimumRemainingLifetimeMs,
"Presigned capability minimum remaining lifetime",
);
if (minimumRemainingLifetimeMs > maxCapabilityTtlMs) {
throw new TypeError(
"Presigned capability minimum remaining lifetime exceeds its TTL.",
);
}
const timeoutMs = positiveSafeInteger(
options.timeoutMs,
"Presigned capability timeout",
);
const maxCapabilityResponseBytes = positiveSafeInteger(
options.maxCapabilityResponseBytes ?? 64 * 1024,
"Presigned capability response limit",
);
const fetcher = (options.fetcher ?? fetch).bind(globalThis);
const now = options.now ?? Date.now;
// 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),
);
const credentials = options.controlPlaneCredentials ?? "same-origin";
const observer = options.observer;
function observeIssue<Capability extends PresignedTransferCapability>(
result: BrowserDataResult<Capability>,
byteLength?: number,
): BrowserDataResult<Capability> {
const bucket = byteBucket(
result.ok ? result.value.byteLength : byteLength,
);
observeBrowserData(observer, {
operation: "PRESIGNED_TRANSFER",
outcome: result.ok ? "SUCCEEDED" : "FAILED",
...(!result.ok ? { failureCode: result.error.code } : {}),
...(bucket !== undefined ? { byteBucket: bucket } : {}),
});
return result;
}
async function issue<Capability extends PresignedTransferCapability>(
expected: Readonly<{
binding: PresignedTransferBinding;
method: "GET" | "PUT";
mediaType?: string;
byteLength?: number;
expectedSha256?: string;
}>,
signal: AbortSignal,
): Promise<BrowserDataResult<Capability>> {
if (signal.aborted) {
return browserDataFailure("ABORTED", "PRESIGNED_TRANSFER");
}
const scope = createAbortScope(signal, timeoutMs, timers);
try {
const raced = await scope.race(
fetcher(endpoint, {
method: "POST",
credentials,
redirect: "error",
referrerPolicy: "no-referrer",
cache: "no-store",
headers: {
Accept: "application/json",
"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
? { mediaType: expected.mediaType }
: {}),
...(expected.byteLength !== undefined
? { byteLength: expected.byteLength }
: {}),
...(expected.expectedSha256 !== undefined
? { expectedSha256: expected.expectedSha256 }
: {}),
}),
signal: scope.signal,
}),
);
if (raced === SCOPE_ENDED) {
return transferFailure(signal, scope.timedOut());
}
const response = raced;
if (
response.redirected ||
response.type === "opaqueredirect" ||
response.url !== endpoint
) {
cancelResponseBody(response);
return browserDataFailure("POLICY_REJECTED", "PRESIGNED_TRANSFER");
}
if (!response.ok || response.status !== 200) {
cancelResponseBody(response);
return statusFailure(response.status);
}
const contentType = response.headers.get("content-type") ?? "";
if (
contentType.split(";", 1)[0]?.trim().toLowerCase() !==
"application/json"
) {
cancelResponseBody(response);
return browserDataFailure("POLICY_REJECTED", "PRESIGNED_TRANSFER");
}
const payload = await readBoundedJson(
response,
maxCapabilityResponseBytes,
scope.signal,
);
const validated = validateCapabilityPayload(payload, {
expected,
hardMaxTransferBytes,
hardMaxUploadResponseBytes,
maxCapabilityTtlMs,
minimumRemainingLifetimeMs,
nowEpochMs: now(),
allowedDataOrigins,
allowedDataPathPrefixes,
allowedQueryParameters,
allowedRequestHeaders,
allowedResponseHeaders,
allowInsecureLocalhost: options.allowInsecureLocalhost ?? false,
});
if (!validated.ok) return validated;
return register(validated.value) as BrowserDataResult<Capability>;
} catch (error) {
if (signal.aborted) {
return browserDataFailure("ABORTED", "PRESIGNED_TRANSFER");
}
if (scope.timedOut()) {
return browserDataFailure("UNAVAILABLE", "PRESIGNED_TRANSFER", {
retryable: true,
recovery: "RETRY",
});
}
if (error instanceof ResponseLimitError) {
return browserDataFailure("LIMIT_EXCEEDED", "PRESIGNED_TRANSFER");
}
if (error instanceof ResponseIntegrityError) {
return browserDataFailure("CORRUPT_DATA", "PRESIGNED_TRANSFER");
}
return browserDataFailure("UNAVAILABLE", "PRESIGNED_TRANSFER", {
retryable: true,
recovery: "RETRY",
});
} finally {
scope.release();
}
}
return Object.freeze({
issueDownload(
input: Parameters<
PresignedTransferCapabilityProvider["issueDownload"]
>[0],
) {
let resourceId: string;
let signal: AbortSignal;
try {
resourceId = snapshotOpaqueId(input.resourceId, "resource ID");
signal = input.signal;
if (!isAbortSignal(signal)) {
throw new TypeError("Abort signal is invalid.");
}
} catch {
return Promise.resolve(
observeIssue(
browserDataFailure("INVALID_INPUT", "PRESIGNED_TRANSFER"),
),
);
}
return issue<PresignedDownloadCapability>(
{
method: "GET",
binding: Object.freeze({
kind: "DOWNLOAD",
resourceId,
}),
},
signal,
).then(
(result) => observeIssue(result),
() =>
observeIssue(
browserDataFailure(
"UNAVAILABLE",
"PRESIGNED_TRANSFER",
{ retryable: true, recovery: "RETRY" },
),
),
);
},
issueUploadPart(
input: Parameters<
PresignedUploadPartCapabilityProvider["issueUploadPart"]
>[0],
) {
let request: Readonly<{
binding: Extract<
PresignedTransferBinding,
Readonly<{ kind: "UPLOAD_PART" }>
>;
mediaType: string;
byteLength: number;
expectedSha256: string;
signal: AbortSignal;
}>;
try {
const mediaType = normalizedMediaType(input.mediaType);
const byteLength = positiveSafeInteger(
input.byteLength,
"part byte length",
);
if (byteLength > hardMaxTransferBytes) {
return Promise.resolve(
observeIssue(
browserDataFailure("LIMIT_EXCEEDED", "PRESIGNED_TRANSFER"),
byteLength,
),
);
}
if (!isAbortSignal(input.signal)) {
throw new TypeError("Abort signal is invalid.");
}
request = Object.freeze({
binding: Object.freeze({
kind: "UPLOAD_PART" as const,
protocol: RESUMABLE_UPLOAD_PROTOCOL,
sessionId: snapshotOpaqueId(
input.sessionId,
"upload session ID",
),
requestBindingSha256: normalizedSha256(
input.requestBindingSha256,
),
uploadBindingSha256: normalizedSha256(
input.uploadBindingSha256,
),
partNumber: positiveSafeInteger(
input.partNumber,
"part number",
),
offset: nonNegativeSafeInteger(input.offset, "part offset"),
idempotencyKey: snapshotOpaqueId(
input.idempotencyKey,
"idempotency key",
),
}),
mediaType,
byteLength,
expectedSha256: normalizedSha256(input.checksumSha256),
signal: input.signal,
});
} catch {
return Promise.resolve(
observeIssue(
browserDataFailure("INVALID_INPUT", "PRESIGNED_TRANSFER"),
),
);
}
return issue<PresignedUploadPartCapability>(
{
method: "PUT",
binding: request.binding,
mediaType: request.mediaType,
byteLength: request.byteLength,
expectedSha256: request.expectedSha256,
},
request.signal,
).then(
(result) => observeIssue(result, request.byteLength),
() =>
observeIssue(
browserDataFailure(
"UNAVAILABLE",
"PRESIGNED_TRANSFER",
{ retryable: true, recovery: "RETRY" },
),
request.byteLength,
),
);
},
});
}
type ValidationContext = Readonly<{
expected: Readonly<{
binding: PresignedTransferBinding;
method: "GET" | "PUT";
mediaType?: string;
byteLength?: number;
expectedSha256?: string;
}>;
hardMaxTransferBytes: number;
hardMaxUploadResponseBytes: number;
maxCapabilityTtlMs: number;
minimumRemainingLifetimeMs: number;
nowEpochMs: number;
allowedDataOrigins: ReadonlySet<string>;
allowedDataPathPrefixes: readonly string[];
allowedQueryParameters: ReadonlySet<string>;
allowedRequestHeaders: ReadonlySet<string>;
allowedResponseHeaders: ReadonlySet<string>;
allowInsecureLocalhost: boolean;
}>;
function validateCapabilityPayload(
value: unknown,
context: ValidationContext,
): BrowserDataResult<PresignedCapabilityRegistration> {
try {
const payload = strictRecord(value, [
"allowedQueryParameters",
"protocol",
"binding",
"byteLength",
"capabilityReceipt",
"digestRequestHeader",
"digestResponseHeader",
"expectedSha256",
"expectedResponseByteLength",
"expectedStatus",
"expiresAtEpochMs",
"href",
"maxBytes",
"mediaType",
"method",
"origin",
"path",
"requestHeaders",
"receiptResponseHeader",
"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.");
}
const capabilityReceipt = snapshotOpaqueId(
payload.capabilityReceipt,
"capability receipt",
) as PresignedTransferCapabilityReceipt;
const binding = validateBinding(payload.binding, context.expected.binding);
const mediaType = normalizedMediaType(payload.mediaType);
const byteLength = nonNegativeSafeInteger(
payload.byteLength,
"capability byte length",
);
const maxBytes = positiveSafeInteger(
payload.maxBytes,
"capability maximum bytes",
);
const expectedSha256 = normalizedSha256(payload.expectedSha256);
if (
byteLength > maxBytes ||
(context.expected.method === "PUT" && byteLength === 0) ||
maxBytes > context.hardMaxTransferBytes ||
(context.expected.mediaType !== undefined &&
mediaType !== context.expected.mediaType) ||
(context.expected.byteLength !== undefined &&
byteLength !== context.expected.byteLength) ||
(context.expected.expectedSha256 !== undefined &&
expectedSha256 !== context.expected.expectedSha256)
) {
throw new TypeError("Capability transfer binding is invalid.");
}
const expiresAtEpochMs = positiveSafeInteger(
payload.expiresAtEpochMs,
"capability expiry",
);
const remainingLifetimeMs =
expiresAtEpochMs - context.nowEpochMs;
if (
!Number.isSafeInteger(context.nowEpochMs) ||
context.nowEpochMs < 0 ||
!Number.isSafeInteger(remainingLifetimeMs) ||
remainingLifetimeMs < context.minimumRemainingLifetimeMs ||
remainingLifetimeMs > context.maxCapabilityTtlMs
) {
return browserDataFailure(
Number.isSafeInteger(remainingLifetimeMs) &&
remainingLifetimeMs <
context.minimumRemainingLifetimeMs
? "EXPIRED_RESOURCE"
: "POLICY_REJECTED",
"PRESIGNED_TRANSFER",
{ recovery: "REISSUE_CAPABILITY" },
);
}
const href = requiredString(payload.href, 8_192);
const target = new URL(href);
assertSafeUrl(target, context.allowInsecureLocalhost);
const origin = normalizedOrigin(
payload.origin,
context.allowInsecureLocalhost,
);
const path = validateExactPath(payload.path);
if (
target.origin !== origin ||
target.pathname !== path ||
!context.allowedDataOrigins.has(origin) ||
!context.allowedDataPathPrefixes.some((prefix) =>
path.startsWith(prefix),
)
) {
throw new TypeError("Capability origin or path is invalid.");
}
const queryParameters = uniqueStringArray(
payload.allowedQueryParameters,
validateQueryName,
32,
);
const actualQueryParameters = [...target.searchParams.keys()];
if (
new Set(actualQueryParameters).size !== actualQueryParameters.length ||
!sameStringSet(queryParameters, actualQueryParameters) ||
queryParameters.some(
(parameter) => !context.allowedQueryParameters.has(parameter),
)
) {
throw new TypeError("Capability query binding is invalid.");
}
const requestHeaders = validateHeaders(
payload.requestHeaders,
context.allowedRequestHeaders,
true,
);
const requiredResponseHeaders = validateHeaders(
payload.requiredResponseHeaders,
context.allowedResponseHeaders,
false,
);
const digestRequestHeader = nullableBoundHeaderName(
payload.digestRequestHeader,
context.allowedRequestHeaders,
);
const digestResponseHeader = nullableBoundHeaderName(
payload.digestResponseHeader,
context.allowedResponseHeaders,
);
const receiptResponseHeader = nullableBoundHeaderName(
payload.receiptResponseHeader,
context.allowedResponseHeaders,
);
if (context.expected.method === "GET") {
if (
digestRequestHeader !== null ||
digestResponseHeader === null ||
receiptResponseHeader !== null ||
payload.expectedResponseByteLength !== null
) {
throw new TypeError("Download digest header binding is invalid.");
}
} else {
if (
digestRequestHeader === null ||
receiptResponseHeader === null ||
requestHeaders.find(
(header) => header.name === digestRequestHeader,
)?.value.toLowerCase() !== expectedSha256 ||
requestHeaders.find(
(header) => header.name === "content-type",
)?.value.toLowerCase() !== mediaType
) {
throw new TypeError("Upload acknowledgement binding is invalid.");
}
}
const expectedStatus = positiveSafeInteger(
payload.expectedStatus,
"expected response status",
);
const expectedResponseByteLength =
context.expected.method === "PUT"
? nonNegativeSafeInteger(
payload.expectedResponseByteLength,
"expected upload response byte length",
)
: null;
if (
(context.expected.method === "GET" && expectedStatus !== 200) ||
(context.expected.method === "PUT" &&
(![200, 201, 204].includes(expectedStatus) ||
expectedResponseByteLength === null ||
expectedResponseByteLength >
context.hardMaxUploadResponseBytes ||
(expectedStatus === 204 &&
expectedResponseByteLength !== 0)))
) {
throw new TypeError("Expected response status is invalid.");
}
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,
href,
origin,
path,
allowedQueryParameters: queryParameters,
requestHeaders,
requiredResponseHeaders,
digestRequestHeader,
digestResponseHeader,
receiptResponseHeader,
expectedStatus,
expectedResponseByteLength,
mediaType,
byteLength,
maxBytes,
expectedSha256,
expiresAtEpochMs,
}),
);
} catch {
// BT-PRE-04. A capability document this adapter refuses is not a dead end
// for the caller: the only way forward is to ask the issuer for a new one.
// `NONE` said the opposite — that nothing could be done — and disagreed
// with both the design record for an unsupported protocol and the vault,
// which already answers `REISSUE_CAPABILITY` for the same class of refusal.
return browserDataFailure("POLICY_REJECTED", "PRESIGNED_TRANSFER", {
recovery: "REISSUE_CAPABILITY",
});
}
}
function validateBinding(
value: unknown,
expected: PresignedTransferBinding,
): PresignedTransferBinding {
if (expected.kind === "DOWNLOAD") {
const binding = strictRecord(value, ["kind", "resourceId"]);
if (
binding.kind !== "DOWNLOAD" ||
binding.resourceId !== expected.resourceId
) {
throw new TypeError("Download binding is invalid.");
}
return Object.freeze({
kind: "DOWNLOAD",
resourceId: expected.resourceId,
});
}
const binding = strictRecord(value, [
"idempotencyKey",
"kind",
"offset",
"partNumber",
"protocol",
"requestBindingSha256",
"sessionId",
"uploadBindingSha256",
]);
if (
binding.kind !== "UPLOAD_PART" ||
binding.protocol !== expected.protocol ||
binding.sessionId !== expected.sessionId ||
binding.requestBindingSha256 !== expected.requestBindingSha256 ||
binding.uploadBindingSha256 !== expected.uploadBindingSha256 ||
binding.partNumber !== expected.partNumber ||
binding.offset !== expected.offset ||
binding.idempotencyKey !== expected.idempotencyKey
) {
throw new TypeError("Upload binding is invalid.");
}
return Object.freeze({ ...expected });
}
function validateHeaders(
value: unknown,
allowedNames: ReadonlySet<string>,
request: boolean,
): readonly PresignedHeaderBinding[] {
if (!Array.isArray(value) || value.length > 32) {
throw new TypeError("Capability headers are invalid.");
}
const seen = new Set<string>();
return Object.freeze(
value.map((entry) => {
const header = strictRecord(entry, ["name", "value"]);
const name = normalizedHeaderName(header.name);
const headerValue = requiredString(header.value, 4_096);
if (
seen.has(name) ||
!allowedNames.has(name) ||
(request && FORBIDDEN_REQUEST_HEADERS.has(name)) ||
/[\r\n\0]/.test(headerValue)
) {
throw new TypeError("Capability header binding is invalid.");
}
seen.add(name);
return Object.freeze({ name, value: headerValue });
}),
);
}
async function readBoundedJson(
response: Response,
maxBytes: number,
signal: AbortSignal,
): Promise<unknown> {
const declared = response.headers.get("content-length");
let declaredLength: number | null = null;
if (declared !== null) {
declaredLength = normalizedContentLength(declared);
if (declaredLength === null) {
cancelResponseBody(response);
throw new ResponseIntegrityError();
}
if (declaredLength > maxBytes) {
cancelResponseBody(response);
throw new ResponseLimitError();
}
}
if (!response.body) throw new TypeError("Capability response body is absent.");
const reader = response.body.getReader();
const decoder = new TextDecoder("utf-8", { fatal: true });
let total = 0;
let text = "";
let completed = false;
try {
while (true) {
if (signal.aborted) throw new DOMException("Aborted", "AbortError");
const result = await readWithSignal(reader, signal);
if (result.done) break;
if (!(result.value instanceof Uint8Array)) {
throw new TypeError("Capability response chunk is invalid.");
}
total += result.value.byteLength;
if (!Number.isSafeInteger(total) || total > maxBytes) {
throw new ResponseLimitError();
}
text += decoder.decode(result.value, { stream: true });
}
completed = true;
if (declaredLength !== null && total !== declaredLength) {
throw new ResponseIntegrityError();
}
text += decoder.decode();
return JSON.parse(text) as unknown;
} finally {
if (!completed) {
try {
void reader.cancel().catch(() => {
// Response cancellation is best effort after classification.
});
} catch {
// Response cleanup cannot alter the closed result.
}
}
try {
reader.releaseLock();
} catch {
// Reader cleanup cannot alter the already classified result.
}
}
}
function cancelResponseBody(response: Response): void {
try {
void response.body?.cancel().catch(() => {
// Cancellation is best effort after the response is classified.
});
} catch {
// Response cleanup cannot alter the closed result.
}
}
function normalizedContentLength(value: string): number | null {
const trimmed = value.trim();
if (!/^(?:0|[1-9][0-9]{0,15})$/.test(trimmed)) return null;
const length = Number(trimmed);
return Number.isSafeInteger(length) ? length : null;
}
function readWithSignal(
reader: ReadableStreamDefaultReader<Uint8Array>,
signal: AbortSignal,
): Promise<ReadableStreamReadResult<Uint8Array>> {
if (signal.aborted) {
return Promise.reject(new DOMException("Aborted", "AbortError"));
}
return new Promise((resolve, reject) => {
const onAbort = () =>
reject(new DOMException("Aborted", "AbortError"));
signal.addEventListener("abort", onAbort, { once: true });
reader.read().then(
(result) => {
signal.removeEventListener("abort", onAbort);
resolve(result);
},
(error: unknown) => {
signal.removeEventListener("abort", onAbort);
reject(error);
},
);
});
}
function statusFailure(
status: number,
): BrowserDataResult<never> {
let code: BrowserDataFailureCode = "UNAVAILABLE";
let recovery: BrowserDataRecovery = "RETRY";
let retryable = status === 429 || status >= 500;
if (status === 401 || status === 403) {
code = "PERMISSION_DENIED";
recovery = "NONE";
retryable = false;
} else if (status === 404) {
code = "NOT_FOUND";
recovery = "NONE";
retryable = false;
} else if (status === 409) {
code = "CONFLICT";
recovery = "REISSUE_CAPABILITY";
retryable = false;
} else if (status === 410) {
code = "EXPIRED_RESOURCE";
recovery = "REISSUE_CAPABILITY";
retryable = false;
} else if (status === 413) {
code = "LIMIT_EXCEEDED";
recovery = "NONE";
retryable = false;
} else if (status >= 400 && status < 500 && status !== 429) {
code = "POLICY_REJECTED";
recovery = "NONE";
retryable = false;
}
return browserDataFailure(code, "PRESIGNED_TRANSFER", {
retryable,
recovery,
});
}
/** 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,
timers: AbortTimerSnapshot,
additionalSignals: readonly AbortSignal[] = [],
) {
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: 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() {
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;
const abortedGetter = Object.getOwnPropertyDescriptor(
AbortSignal.prototype,
"aborted",
)?.get;
return Boolean(
abortedGetter &&
typeof abortedGetter.call(value) === "boolean" &&
typeof (value as AbortSignal).addEventListener === "function" &&
typeof (value as AbortSignal).removeEventListener === "function",
);
} catch {
return false;
}
}
function byteBucket(
byteLength: number | undefined,
):
| "ZERO"
| "LT1MIB"
| "1_TO_9MIB"
| "10_TO_99MIB"
| "GTE100MIB"
| undefined {
if (
byteLength === undefined ||
!Number.isSafeInteger(byteLength) ||
byteLength < 0
) {
return undefined;
}
if (byteLength === 0) return "ZERO";
if (byteLength < 1_048_576) return "LT1MIB";
if (byteLength < 10_485_760) return "1_TO_9MIB";
if (byteLength < 104_857_600) return "10_TO_99MIB";
return "GTE100MIB";
}
function strictRecord(
value: unknown,
keys: readonly string[],
): Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new TypeError("Expected an object.");
}
const record = value as Record<string, unknown>;
const actual = Object.keys(record).sort();
const expected = [...keys].sort();
if (
actual.length !== expected.length ||
actual.some((key, index) => key !== expected[index])
) {
throw new TypeError("Object keys are invalid.");
}
return record;
}
function uniqueStringArray(
value: unknown,
validate: (item: unknown) => string,
maxCount: number,
): readonly string[] {
if (!Array.isArray(value) || value.length > maxCount) {
throw new TypeError("Expected a bounded string array.");
}
const items = value.map(validate);
if (new Set(items).size !== items.length) {
throw new TypeError("Duplicate string value.");
}
return Object.freeze(items);
}
function sameStringSet(left: readonly string[], right: readonly string[]) {
if (left.length !== right.length) return false;
const rightSet = new Set(right);
return left.every((value) => rightSet.has(value));
}
function normalizedHeaderSet(values: readonly string[]): ReadonlySet<string> {
const normalized = values.map(normalizedHeaderName);
if (new Set(normalized).size !== normalized.length) {
throw new TypeError("Allowed header names contain duplicates.");
}
return new Set(normalized);
}
function normalizedHeaderName(value: unknown): string {
const name = requiredString(value, 128).toLowerCase();
if (!HEADER_NAME.test(name)) throw new TypeError("Header name is invalid.");
return name;
}
function nullableBoundHeaderName(
value: unknown,
allowedNames: ReadonlySet<string>,
): string | null {
if (value === null) return null;
const name = normalizedHeaderName(value);
if (!allowedNames.has(name)) {
throw new TypeError("Bound header name is not allowlisted.");
}
return name;
}
function validateQueryName(value: unknown): string {
const name = requiredString(value, 128);
if (!QUERY_NAME.test(name)) throw new TypeError("Query name is invalid.");
return name;
}
function normalizedMediaType(value: unknown): string {
const mediaType = requiredString(value, 127).trim().toLowerCase();
if (!MEDIA_TYPE.test(mediaType)) throw new TypeError("Media type is invalid.");
return mediaType;
}
function normalizedSha256(value: unknown): string {
const digest = requiredString(value, 64).toLowerCase();
if (!SHA256.test(digest)) throw new TypeError("SHA-256 is invalid.");
return digest;
}
function snapshotOpaqueId(value: unknown, name: string): string {
const id = requiredString(value, 256);
if (!OPAQUE_ID.test(id)) throw new TypeError(`${name} is invalid.`);
return id;
}
function requiredString(value: unknown, maxLength: number): string {
if (
typeof value !== "string" ||
value.length === 0 ||
value.length > maxLength
) {
throw new TypeError("String is invalid.");
}
return value;
}
function positiveSafeInteger(value: unknown, name: string): number {
if (!Number.isSafeInteger(value) || (value as number) <= 0) {
throw new TypeError(`${name} is invalid.`);
}
return value as number;
}
function nonNegativeSafeInteger(value: unknown, name: string): number {
if (!Number.isSafeInteger(value) || (value as number) < 0) {
throw new TypeError(`${name} is invalid.`);
}
return value as number;
}
function validateEndpoint(value: string, allowInsecureLocalhost: boolean) {
const endpoint = new URL(value);
assertSafeUrl(endpoint, allowInsecureLocalhost);
if (endpoint.search || endpoint.hash) {
throw new TypeError("Capability endpoint cannot contain query or fragment.");
}
return endpoint.href;
}
function normalizedOrigin(
value: unknown,
allowInsecureLocalhost: boolean,
): string {
const originUrl = new URL(requiredString(value, 2_048));
assertSafeUrl(originUrl, allowInsecureLocalhost);
if (
originUrl.origin !== originUrl.href.replace(/\/$/, "") &&
originUrl.pathname !== "/"
) {
throw new TypeError("Origin must not contain a path.");
}
if (originUrl.search || originUrl.hash) {
throw new TypeError("Origin must not contain query or fragment.");
}
return originUrl.origin;
}
function assertSafeUrl(url: URL, allowInsecureLocalhost: boolean): void {
const local =
["localhost", "127.0.0.1", "[::1]"].includes(url.hostname) &&
allowInsecureLocalhost;
if (
(url.protocol !== "https:" && !(local && url.protocol === "http:")) ||
url.username ||
url.password ||
url.hash
) {
throw new TypeError("URL is not allowed.");
}
}
function validatePathPrefix(value: string): string {
const path = validateExactPath(value);
if (!path.endsWith("/")) {
throw new TypeError("Allowed data path prefix must end with a slash.");
}
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)
) {
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 {}