Files
clean-architecture-frontend…/src/adapters/browser-transfer/presigned/presigned-capability-http-provider.ts
T
DongHyeonkaandClaude Opus 5 cc91fc6ae0 fix: settle a shared abort operation by observation, not by drain count
The primitive decided a raced outcome by draining a hard-coded four
microtasks and then asking whether the task had landed. That made the
answer depend on scheduling rather than on what was observed: a caller
abort could fix the terminal owner synchronously and a rejection later in
the same call stack still won the public result, so `race()` disagreed
with `terminal()` and the failure taxonomy a caller received depended on
microtask ordering.

Task settlement and the terminal event now share one settle-once state
machine. Whichever callback actually runs first owns the outcome; a value
that loses is compensated exactly once and a rejection that loses is
absorbed, so neither can surface late.

The three consumers that kept their own copies of these mechanics move
onto it. The Image probe and the Resumable fetch transport attached their
caller listener before installing the timer, so a scheduler that threw
rejected the public `probe()`/`execute()` promise natively and left the
listener on the caller's signal; both now close atomically inside their
own Result vocabulary and start no fetch. `snapshotAbortTimers` binds the
scheduler callables once at construction, so replacing a method after
composition can no longer change how work already in flight is bounded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 01:24:53 +09:00

1252 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 {
return browserDataFailure("POLICY_REJECTED", "PRESIGNED_TRANSFER");
}
}
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 {}