The capability vault checked an issuer's registration and then read it again to store it, including its nested header rows. A stateful issuer could show an allowed header set to the forbidden-header check and hand `Authorization` to the copy, so the vault stored — and the executor sent — a credential no rule had ever seen. The registration and everything nested in it is now snapshotted once, and only that snapshot is validated, frozen and stored. The upload control plane had the same shape one level down: a `sessionId` that answered `session_01` to the regex and `../../unsafe` to the result snapshot reached a success receipt. Two lifetimes were also unowned. A download source lease that resolved after the caller's abort never reached the holder, so nothing closed it and its fetch reader and capability lease outlived the terminal result; a compensator sharing the holder's close-once latch now closes it exactly once. And `dispose()` proved quiescence from the wrapper registry alone, so a provider that ignored its attempt deadline let teardown report a drained runtime and close the checkpoint store while the provider was still running. Raw provider promises are now their own registry and the drain must prove both. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
522 lines
17 KiB
TypeScript
522 lines
17 KiB
TypeScript
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<string> = 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<PresignedTransferCapability>;
|
|
resolve(
|
|
capability: PresignedTransferCapability,
|
|
): BrowserDataResult<PresignedCapabilityBinding>;
|
|
/**
|
|
* 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<true>;
|
|
/**
|
|
* 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<PresignedTransferCapability, PresignedCapabilityBinding>();
|
|
const byReceipt =
|
|
new Map<PresignedTransferCapabilityReceipt, PresignedTransferCapability>();
|
|
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<never> | 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<PresignedTransferCapability> {
|
|
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<PresignedCapabilityBinding> {
|
|
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<true> {
|
|
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<PresignedTransferCapability>();
|
|
return Object.freeze({
|
|
claim(
|
|
capability: PresignedTransferCapability,
|
|
): BrowserDataResult<true> {
|
|
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 }),
|
|
),
|
|
);
|
|
}
|