import { PRESIGNED_TRANSFER_PROTOCOL, type PresignedTransferBinding, type PresignedTransferCapability, type PresignedTransferCapabilityReceipt, type PresignedTransferMethod, type PresignedTransferProtocol, type PresignedTransferReplayGuard, } from "../../../application/ports/browser-transfer/presigned-transfer.ts"; import type { BrowserDataResult } from "../../../application/ports/browser-file-storage/shared.ts"; import { browserDataFailure, browserDataSuccess, } from "../../browser-file-storage/result.ts"; import { ownDataValue, snapshotExactArray, snapshotExactObject, } from "../../../contracts/exact-snapshot.ts"; export type PresignedHeaderBinding = Readonly<{ name: string; value: string; }>; /** * TR-RR-03. A registration is a versioned exact union. Without the version an * issuer from another release could register a shape this vault validates under * different rules than the executor later applies to it. */ /** TR-RR-03. The exact own-data key set a registration may carry. */ const REGISTRATION_KEYS: readonly string[] = Object.freeze( [ "allowedQueryParameters", "binding", "byteLength", "capabilityReceipt", "digestRequestHeader", "digestResponseHeader", "expectedResponseByteLength", "expectedSha256", "expectedStatus", "expiresAtEpochMs", "href", "maxBytes", "mediaType", "method", "origin", "path", "protocol", "receiptResponseHeader", "requestHeaders", "requiredResponseHeaders", ].sort(), ); /** * TR-RR-03. Ambient credential and cookie headers are forbidden on a presigned * capability: the signature is the authorization. */ const FORBIDDEN_CAPABILITY_HEADERS: ReadonlySet = new Set([ "authorization", "cookie", "cookie2", "proxy-authorization", "set-cookie", ]); export type PresignedCapabilityRegistration = Readonly<{ protocol: PresignedTransferProtocol; capabilityReceipt: PresignedTransferCapabilityReceipt; method: PresignedTransferMethod; binding: PresignedTransferBinding; href: string; origin: string; path: string; allowedQueryParameters: readonly string[]; requestHeaders: readonly PresignedHeaderBinding[]; requiredResponseHeaders: readonly PresignedHeaderBinding[]; digestRequestHeader: string | null; digestResponseHeader: string | null; receiptResponseHeader: string | null; expectedStatus: number; expectedResponseByteLength: number | null; mediaType: string; byteLength: number; maxBytes: number; expectedSha256: string; expiresAtEpochMs: number; }>; export type PresignedCapabilityBinding = Readonly< PresignedCapabilityRegistration & { capability: PresignedTransferCapability; } >; export interface PresignedCapabilityVault { register( registration: PresignedCapabilityRegistration, ): BrowserDataResult; resolve( capability: PresignedTransferCapability, ): BrowserDataResult; /** * Atomically retires an exact identity after its single-use replay claim. * The caller may keep the already-resolved binding on its stack for the * in-flight request, but the vault must no longer retain or resolve it. */ consume( capability: PresignedTransferCapability, ): BrowserDataResult; /** * Best-effort, idempotent retirement for an unused or abandoned identity. */ revoke(capability: PresignedTransferCapability): void; dispose(): void; } export function createPresignedCapabilityVault(options: Readonly<{ maxActiveCapabilities: number; now?: () => number; }>): PresignedCapabilityVault { if ( !Number.isSafeInteger(options.maxActiveCapabilities) || options.maxActiveCapabilities < 1 ) { throw new TypeError("Presigned capability vault limit is invalid."); } const maxActiveCapabilities = options.maxActiveCapabilities; const now = options.now ?? Date.now; const byIdentity = new WeakMap(); const byReceipt = new Map(); let disposed = false; function pruneExpired(): void { const current = now(); if (!Number.isSafeInteger(current)) return; for (const [receipt, capability] of byReceipt) { if (capability.expiresAtEpochMs <= current) { byReceipt.delete(receipt); byIdentity.delete(capability); } } } function revoke(capability: PresignedTransferCapability): boolean { try { const binding = byIdentity.get(capability); if (!binding) return false; byIdentity.delete(capability); if (byReceipt.get(binding.capabilityReceipt) === capability) { byReceipt.delete(binding.capabilityReceipt); } return true; } catch { return false; } } /** * BT-PRE-04 / TR-RR-03. Runtime invariants every registration must satisfy, * regardless of which issuer produced it. */ function validatePresignedCapabilityRegistration( registration: PresignedCapabilityRegistration, ): BrowserDataResult | null { const invalid = () => browserDataFailure("POLICY_REJECTED", "PRESIGNED_TRANSFER", { recovery: "REISSUE_CAPABILITY", }); if (!registration || typeof registration !== "object") return invalid(); if (registration.protocol !== PRESIGNED_TRANSFER_PROTOCOL) { return invalid(); } if ( typeof registration.capabilityReceipt !== "string" || registration.capabilityReceipt.length === 0 || (registration.method !== "GET" && registration.method !== "PUT") ) { return invalid(); } let target: URL; let origin: URL; try { target = new URL(registration.href); origin = new URL(registration.origin); } catch { return invalid(); } if ( // TR-RR-03. A presigned capability travels the network as a bearer of its // own authority, so plaintext is never acceptable. target.protocol !== "https:" || origin.protocol !== "https:" || target.origin !== origin.origin || origin.href.replace(/\/$/u, "") !== registration.origin.replace(/\/$/u, "") || target.pathname !== registration.path || target.username.length > 0 || target.password.length > 0 || target.hash.length > 0 ) { return invalid(); } // TR-RR-03. A presigned URL already carries its authorization. An ambient // credential header alongside it would send the user's session to an // origin the capability alone was meant to reach. for (const header of [ ...registration.requestHeaders, ...registration.requiredResponseHeaders, ]) { if ( FORBIDDEN_CAPABILITY_HEADERS.has(header.name.toLowerCase()) ) { return invalid(); } } if ( registration.allowedQueryParameters.some( (parameter) => typeof parameter !== "string" || parameter.length === 0, ) ) { return invalid(); } if ( !Number.isSafeInteger(registration.expectedStatus) || registration.expectedStatus < 200 || registration.expectedStatus > 299 || (registration.expectedResponseByteLength !== null && (!Number.isSafeInteger(registration.expectedResponseByteLength) || registration.expectedResponseByteLength < 0)) ) { return invalid(); } if ( !Number.isSafeInteger(registration.byteLength) || registration.byteLength < 0 || !Number.isSafeInteger(registration.maxBytes) || registration.maxBytes < registration.byteLength || !Number.isSafeInteger(registration.expiresAtEpochMs) || registration.expiresAtEpochMs <= 0 || !/^[a-f0-9]{64}$/u.test(registration.expectedSha256) || typeof registration.mediaType !== "string" || registration.mediaType.length === 0 ) { return invalid(); } return null; } return Object.freeze({ register( registration: PresignedCapabilityRegistration, ): BrowserDataResult { if (disposed) { return browserDataFailure("UNAVAILABLE", "PRESIGNED_TRANSFER"); } // TR-01. One owned snapshot first, then validate and store only that // snapshot. Validating the issuer's own object and reading it again to // copy it let a stateful answer show an allowed header set to the // forbidden-header check and hand `Authorization` to the copy, so the // vault stored a capability no rule had ever seen. const snapshot = snapshotRegistration(registration); if (!snapshot) { return browserDataFailure("POLICY_REJECTED", "PRESIGNED_TRANSFER", { recovery: "REISSUE_CAPABILITY", }); } // BT-PRE-04. The vault owns its own registration invariants so a second // issuer adapter, a test seam or composition code cannot register a // weaker capability of the same type. The HTTP decoder still owns the // wire shape; this only re-checks runtime invariants. const invalid = validatePresignedCapabilityRegistration(snapshot); if (invalid) return invalid; pruneExpired(); if ( byReceipt.has(registration.capabilityReceipt) || byReceipt.size >= maxActiveCapabilities ) { return browserDataFailure( byReceipt.has(registration.capabilityReceipt) ? "CONFLICT" : "LIMIT_EXCEEDED", "PRESIGNED_TRANSFER", byReceipt.has(registration.capabilityReceipt) ? { recovery: "REISSUE_CAPABILITY" } : undefined, ); } const capability = Object.freeze({ capabilityReceipt: snapshot.capabilityReceipt, method: snapshot.method, binding: freezeBinding(snapshot.binding), mediaType: snapshot.mediaType, byteLength: snapshot.byteLength, maxBytes: snapshot.maxBytes, expectedSha256: snapshot.expectedSha256, expiresAtEpochMs: snapshot.expiresAtEpochMs, }) as PresignedTransferCapability; const binding: PresignedCapabilityBinding = Object.freeze({ capability, protocol: PRESIGNED_TRANSFER_PROTOCOL, capabilityReceipt: capability.capabilityReceipt, method: capability.method, binding: capability.binding, href: snapshot.href, origin: snapshot.origin, path: snapshot.path, allowedQueryParameters: Object.freeze([ ...snapshot.allowedQueryParameters, ]), requestHeaders: freezeHeaders(snapshot.requestHeaders), requiredResponseHeaders: freezeHeaders( snapshot.requiredResponseHeaders, ), digestRequestHeader: snapshot.digestRequestHeader, digestResponseHeader: snapshot.digestResponseHeader, receiptResponseHeader: snapshot.receiptResponseHeader, expectedStatus: snapshot.expectedStatus, expectedResponseByteLength: snapshot.expectedResponseByteLength, mediaType: capability.mediaType, byteLength: capability.byteLength, maxBytes: capability.maxBytes, expectedSha256: capability.expectedSha256, expiresAtEpochMs: capability.expiresAtEpochMs, }); byIdentity.set(capability, binding); byReceipt.set(capability.capabilityReceipt, capability); return browserDataSuccess(capability); }, resolve( capability: PresignedTransferCapability, ): BrowserDataResult { if (disposed) { return browserDataFailure("UNAVAILABLE", "PRESIGNED_TRANSFER"); } try { const binding = byIdentity.get(capability); return binding ? browserDataSuccess(binding) : browserDataFailure("POLICY_REJECTED", "PRESIGNED_TRANSFER"); } catch { return browserDataFailure("POLICY_REJECTED", "PRESIGNED_TRANSFER"); } }, consume( capability: PresignedTransferCapability, ): BrowserDataResult { if (disposed) { return browserDataFailure("UNAVAILABLE", "PRESIGNED_TRANSFER"); } return revoke(capability) ? browserDataSuccess(true as const) : browserDataFailure( "POLICY_REJECTED", "PRESIGNED_TRANSFER", ); }, revoke(capability: PresignedTransferCapability): void { if (disposed) return; revoke(capability); }, dispose() { if (disposed) return; disposed = true; for (const capability of byReceipt.values()) { byIdentity.delete(capability); } byReceipt.clear(); }, }); } export function createSingleUsePresignedReplayGuard(): PresignedTransferReplayGuard { const claimed = new WeakSet(); return Object.freeze({ claim( capability: PresignedTransferCapability, ): BrowserDataResult { try { if (claimed.has(capability)) { return browserDataFailure("CONFLICT", "PRESIGNED_TRANSFER", { recovery: "REISSUE_CAPABILITY", }); } claimed.add(capability); return browserDataSuccess(true as const); } catch { return browserDataFailure("POLICY_REJECTED", "PRESIGNED_TRANSFER"); } }, }); } const DOWNLOAD_BINDING_KEYS = Object.freeze(["kind", "resourceId"]); const UPLOAD_BINDING_KEYS = Object.freeze([ "kind", "protocol", "sessionId", "requestBindingSha256", "uploadBindingSha256", "partNumber", "offset", "idempotencyKey", ]); /** * TR-01. Copies a registration and everything nested inside it into owned, * frozen values, reading each property exactly once. Only this snapshot is * validated and stored, so a stateful issuer cannot show one value to the * forbidden-header and HTTPS checks and hand another to the vault. A hostile * trap, an accessor, an inherited or extra field and a non-iterable header * array all resolve to `null` — a typed `POLICY_REJECTED` — rather than * escaping as a native exception. */ function snapshotRegistration( source: unknown, ): PresignedCapabilityRegistration | null { const outer = snapshotExactObject(source, { allowed: REGISTRATION_KEYS, required: REGISTRATION_KEYS, }); if (outer === null) return null; const binding = snapshotBinding(outer["binding"]); if (binding === null) return null; const allowedQueryParameters = snapshotExactArray( outer["allowedQueryParameters"], ); if (allowedQueryParameters === null) return null; const requestHeaders = snapshotHeaderBindings(outer["requestHeaders"]); const requiredResponseHeaders = snapshotHeaderBindings( outer["requiredResponseHeaders"], ); if (requestHeaders === null || requiredResponseHeaders === null) return null; return Object.freeze({ ...outer, binding, allowedQueryParameters: Object.freeze([...allowedQueryParameters]), requestHeaders, requiredResponseHeaders, }) as PresignedCapabilityRegistration; } function snapshotBinding(source: unknown): PresignedTransferBinding | null { const kind = ownDataValue(source, "kind"); if (kind !== "DOWNLOAD" && kind !== "UPLOAD_PART") return null; const keys = kind === "DOWNLOAD" ? DOWNLOAD_BINDING_KEYS : UPLOAD_BINDING_KEYS; const binding = snapshotExactObject(source, { allowed: keys, required: keys, }); return binding === null ? null : (binding as unknown as PresignedTransferBinding); } function snapshotHeaderBindings( source: unknown, ): readonly PresignedHeaderBinding[] | null { const rows = snapshotExactArray(source); if (rows === null) return null; const headers: PresignedHeaderBinding[] = []; for (const row of rows) { const header = snapshotExactObject(row, { allowed: ["name", "value"], required: ["name", "value"], }); if ( header === null || typeof header["name"] !== "string" || header["name"].length === 0 || typeof header["value"] !== "string" ) { return null; } headers.push(header as unknown as PresignedHeaderBinding); } return Object.freeze(headers); } function freezeBinding( binding: PresignedTransferBinding, ): PresignedTransferBinding { return binding.kind === "DOWNLOAD" ? Object.freeze({ kind: "DOWNLOAD" as const, resourceId: binding.resourceId, }) : Object.freeze({ kind: "UPLOAD_PART" as const, protocol: binding.protocol, sessionId: binding.sessionId, requestBindingSha256: binding.requestBindingSha256, uploadBindingSha256: binding.uploadBindingSha256, partNumber: binding.partNumber, offset: binding.offset, idempotencyKey: binding.idempotencyKey, }); } function freezeHeaders( headers: readonly PresignedHeaderBinding[], ): readonly PresignedHeaderBinding[] { return Object.freeze( headers.map((header) => Object.freeze({ name: header.name, value: header.value }), ), ); }