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, ), } satisfies Scheduler), ); const credentials = options.controlPlaneCredentials ?? "same-origin"; const observer = options.observer; function observeIssue( result: BrowserDataResult, byteLength?: number, ): BrowserDataResult { 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( expected: Readonly<{ binding: PresignedTransferBinding; method: "GET" | "PUT"; mediaType?: string; byteLength?: number; expectedSha256?: string; }>, signal: AbortSignal, ): Promise> { 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; } 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( { 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( { 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; allowedDataPathPrefixes: readonly string[]; allowedQueryParameters: ReadonlySet; allowedRequestHeaders: ReadonlySet; allowedResponseHeaders: ReadonlySet; allowInsecureLocalhost: boolean; }>; function validateCapabilityPayload( value: unknown, context: ValidationContext, ): BrowserDataResult { 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, request: boolean, ): readonly PresignedHeaderBinding[] { if (!Array.isArray(value) || value.length > 32) { throw new TypeError("Capability headers are invalid."); } const seen = new Set(); 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 { 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, signal: AbortSignal, ): Promise> { 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 { 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(task: Promise): Promise { 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 } | null } | null) ?.body; void body?.cancel().catch(() => undefined); } function transferFailure( signal: AbortSignal, timedOut: boolean, ): BrowserDataResult { 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 { if (!value || typeof value !== "object" || Array.isArray(value)) { throw new TypeError("Expected an object."); } const record = value as Record; 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 { 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 | 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 {}