fix: hold transfer inputs and raw transfer work to what was verified

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>
This commit is contained in:
DongHyeonka
2026-08-15 01:26:01 +09:00
co-authored by Claude Opus 5
parent aa8ac35600
commit 39a4a973a8
8 changed files with 972 additions and 225 deletions
+372
View File
@@ -914,6 +914,186 @@ describe("presigned transfer", () => {
},
);
/**
* TR-01. The vault validated the issuer's own object and then read it again
* to copy it. Between those reads a stateful issuer could show an allowed
* header set to the forbidden-header check and hand `Authorization` to the
* stored binding, so the executor sent a credential no rule had approved.
*/
describe("TR-01 the stored capability is the one that was validated", () => {
const baseRegistration = () => ({
protocol: PRESIGNED_TRANSFER_PROTOCOL,
capabilityReceipt: "capability-snapshot-1",
method: "GET" as const,
binding: { kind: "DOWNLOAD" as const, resourceId: "resource-1" },
href: `${DATA_ORIGIN}${DOWNLOAD_PATH}`,
origin: DATA_ORIGIN,
path: DOWNLOAD_PATH,
allowedQueryParameters: [],
requestHeaders: [{ name: "x-safe", value: "1" }],
requiredResponseHeaders: [],
digestRequestHeader: null,
digestResponseHeader: null,
receiptResponseHeader: null,
expectedStatus: 200,
expectedResponseByteLength: 3,
mediaType: "application/octet-stream",
byteLength: 3,
maxBytes: 3,
expectedSha256: "a".repeat(64),
expiresAtEpochMs: NOW + 60_000,
});
const freshVault = () =>
createPresignedCapabilityVault({
now: () => NOW,
maxActiveCapabilities: 4,
});
it("refuses a header row that answers differently on a second read", () => {
const vault = freshVault();
let nameReads = 0;
const header = new Proxy(
{ name: "x-safe", value: "1" },
{
getOwnPropertyDescriptor(target, key) {
if (key === "name") {
nameReads += 1;
return {
configurable: true,
enumerable: true,
value: nameReads > 1 ? "authorization" : "x-safe",
};
}
return Reflect.getOwnPropertyDescriptor(target, key);
},
},
);
const registered = vault.register({
...baseRegistration(),
requestHeaders: [header],
} as never);
if (registered.ok) {
// A single read means the value that was checked is the value stored.
const resolved = vault.resolve(registered.value);
expect(resolved.ok).toBe(true);
if (resolved.ok) {
expect(resolved.value.requestHeaders.map((row) => row.name)).toEqual([
"x-safe",
]);
}
}
vault.dispose();
});
const hostileRegistrations: readonly (readonly [string, () => unknown])[] = [
[
"an accessor field",
() =>
Object.defineProperty(baseRegistration(), "href", {
enumerable: true,
get: () => `${DATA_ORIGIN}${DOWNLOAD_PATH}`,
}),
],
[
"an inherited field",
() => Object.assign(Object.create({ injected: true }), baseRegistration()),
],
[
"a symbol field",
() => ({ ...baseRegistration(), [Symbol.for("injected")]: true }),
],
[
"a non-enumerable own field",
() =>
Object.defineProperty(baseRegistration(), "injected", {
enumerable: false,
value: true,
}),
],
[
"a throwing ownKeys trap",
() =>
new Proxy(baseRegistration(), {
ownKeys() {
throw new TypeError("hostile ownKeys trap");
},
}),
],
[
"a null header array",
() => ({ ...baseRegistration(), requestHeaders: null }),
],
[
"a non-iterable header array",
() => ({ ...baseRegistration(), requestHeaders: { length: 1 } }),
],
[
"a header row with an extra field",
() => ({
...baseRegistration(),
requestHeaders: [{ name: "x-safe", value: "1", injected: true }],
}),
],
[
"an accessor header name",
() => ({
...baseRegistration(),
requestHeaders: [
Object.defineProperty({ value: "1" }, "name", {
enumerable: true,
get: () => "x-safe",
}),
],
}),
],
[
"a binding with an extra field",
() => ({
...baseRegistration(),
binding: { kind: "DOWNLOAD", resourceId: "r", injected: true },
}),
],
[
"a null binding",
() => ({ ...baseRegistration(), binding: null }),
],
];
for (const [label, build] of hostileRegistrations) {
it(`rejects ${label} as POLICY_REJECTED`, () => {
const vault = freshVault();
expect(vault.register(build() as never)).toMatchObject({
ok: false,
error: { code: "POLICY_REJECTED" },
});
vault.dispose();
});
}
it("does not observe a mutation of the issuer's object after registration", () => {
const vault = freshVault();
const registration = baseRegistration();
const registered = vault.register(registration as never);
expect(registered.ok).toBe(true);
if (!registered.ok) return;
registration.requestHeaders[0]!.name = "authorization";
registration.expiresAtEpochMs = NOW + 999_999;
const resolved = vault.resolve(registered.value);
expect(resolved.ok).toBe(true);
if (!resolved.ok) return;
expect(resolved.value.requestHeaders.map((row) => row.name)).toEqual([
"x-safe",
]);
expect(resolved.value.expiresAtEpochMs).toBe(NOW + 60_000);
vault.dispose();
});
});
it("does not fetch a presigned download until stream consumption", async () => {
const bytes = new Uint8Array([1, 2, 3]);
const responsePayload = downloadCapabilityPayload(bytes);
@@ -2083,6 +2263,198 @@ describe("presigned transfer", () => {
void deliveryResult;
expect(closes).toBe(1);
});
/**
* TR-02. A lease that resolved after the abort already ended the delivery
* never reached the holder, so nothing closed it: the fetch reader and the
* capability lease outlived the terminal result.
*/
it("closes a source lease that arrives after the delivery was aborted", async () => {
const bytes = new Uint8Array([1, 2, 3]);
let closes = 0;
const controller = new AbortController();
let releaseOpen:
| ((value: { ok: true; value: unknown }) => void)
| undefined;
const source = {
byteLength: bytes.byteLength,
integrity: "VERIFIED_ON_SUCCESSFUL_EXHAUSTION" as const,
capability: undefined as never,
close() {
closes += 1;
},
async *stream() {
yield { ok: true as const, value: bytes };
},
};
const capability = Object.freeze({
capabilityReceipt: "capability-late-1",
method: "GET" as const,
binding: { kind: "DOWNLOAD" as const, resourceId: "resource-1" },
mediaType: "application/octet-stream",
byteLength: bytes.byteLength,
maxBytes: bytes.byteLength,
expectedSha256: "a".repeat(64),
expiresAtEpochMs: NOW + 60_000,
});
source.capability = capability as never;
const latePolicy = browserFilePolicyReference("download", "presigned-late");
const policies = new BrowserFilePolicyRegistry({
profiles: [
{
reference: latePolicy,
download: {
strategy: "PROMPT_AND_STREAM",
mediaType: "application/octet-stream",
safeExtension: ".bin",
maxTransferBytes: 64,
maxBufferedBytes: 8,
integrity: "REQUIRED",
},
},
],
hardLimits: {
maxInspectionBytes: 64,
maxRetainedFileBytes: 64,
maxPreviewBytes: 64,
maxObjectUrlBytes: 64,
maxTransferBytes: 64,
},
});
const downloads = createDownloadDeliveryAdapter({
host: { handoff() {} },
policies,
hardMaxObjectUrlBytes: 64,
hardMaxTransferBytes: 64,
browserManagedCapabilities: {
resolve() {
throw new TypeError("not used");
},
},
// Ignores the signal entirely and resolves only when the test says so.
openAuthorizedSource: () =>
new Promise((resolve) => {
releaseOpen = resolve as never;
}) as never,
showSaveFilePicker: async () => ({
async createWritable() {
return new WritableStream<Uint8Array>({ write() {} });
},
}),
userActivation: { isActive: true },
now: () => NOW,
});
const delivering = downloads.deliver({
policy: latePolicy,
source: {
kind: "AUTHORIZED_STREAM_RESOURCE",
resourceId: "resource-1",
capability: capability as never,
},
suggestedFileName: "artifact.bin",
signal: controller.signal,
onProgress() {},
});
await new Promise((resolve) => setTimeout(resolve, 0));
controller.abort();
const delivered = await delivering;
expect(delivered.ok).toBe(false);
// The lease arrives only now, long after the terminal result.
releaseOpen?.({ ok: true, value: source });
await new Promise((resolve) => setTimeout(resolve, 0));
expect(closes).toBe(1);
});
it("does not leave a late rejection unhandled after an abort", async () => {
const unhandled: unknown[] = [];
const onUnhandled = (reason: unknown) => unhandled.push(reason);
process.on("unhandledRejection", onUnhandled);
try {
const controller = new AbortController();
let rejectOpen: ((reason: unknown) => void) | undefined;
const rejectPolicy = browserFilePolicyReference(
"download",
"presigned-late-reject",
);
const policies = new BrowserFilePolicyRegistry({
profiles: [
{
reference: rejectPolicy,
download: {
strategy: "PROMPT_AND_STREAM",
mediaType: "application/octet-stream",
safeExtension: ".bin",
maxTransferBytes: 64,
maxBufferedBytes: 8,
integrity: "REQUIRED",
},
},
],
hardLimits: {
maxInspectionBytes: 64,
maxRetainedFileBytes: 64,
maxPreviewBytes: 64,
maxObjectUrlBytes: 64,
maxTransferBytes: 64,
},
});
const downloads = createDownloadDeliveryAdapter({
host: { handoff() {} },
policies,
hardMaxObjectUrlBytes: 64,
hardMaxTransferBytes: 64,
browserManagedCapabilities: {
resolve() {
throw new TypeError("not used");
},
},
openAuthorizedSource: () =>
new Promise((_resolve, reject) => {
rejectOpen = reject;
}) as never,
showSaveFilePicker: async () => ({
async createWritable() {
return new WritableStream<Uint8Array>({ write() {} });
},
}),
userActivation: { isActive: true },
now: () => NOW,
});
const delivering = downloads.deliver({
policy: rejectPolicy,
source: {
kind: "AUTHORIZED_STREAM_RESOURCE",
resourceId: "resource-1",
capability: Object.freeze({
capabilityReceipt: "capability-late-2",
method: "GET" as const,
binding: { kind: "DOWNLOAD" as const, resourceId: "resource-1" },
mediaType: "application/octet-stream",
byteLength: 3,
maxBytes: 3,
expectedSha256: "a".repeat(64),
expiresAtEpochMs: NOW + 60_000,
}) as never,
},
suggestedFileName: "artifact.bin",
signal: controller.signal,
onProgress() {},
});
await new Promise((resolve) => setTimeout(resolve, 0));
controller.abort();
await delivering;
rejectOpen?.(new Error("late open failure"));
await new Promise((resolve) => setTimeout(resolve, 10));
expect(unhandled).toEqual([]);
} finally {
process.off("unhandledRejection", onUnhandled);
}
});
});
/**