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
@@ -232,6 +232,30 @@ describe("resumable upload HTTP control plane", () => {
},
}),
],
[
"a nested fingerprint with an extra field",
() => ({
...validSession(),
fingerprint: { ...fingerprint, injected: true },
}),
],
[
"a nested fingerprint behind an accessor",
() => {
const value = validSession() as Record<string, unknown>;
Object.defineProperty(value, "fingerprint", {
configurable: true,
enumerable: true,
get: () => fingerprint,
});
return value;
},
],
[
"a custom prototype",
() =>
Object.assign(Object.create({ injected: true }), validSession()),
],
];
for (const [label, build] of hostile) {
@@ -261,6 +285,53 @@ describe("resumable upload HTTP control plane", () => {
});
expect(JSON.stringify(result)).not.toContain("signature=leak");
}
/**
* TR-05. The decoder checked the sender's object and then read it again to
* build the result, so a stateful answer could show a safe `sessionId` to
* the regex and hand an unvalidated one to the receipt. Reading once means
* the value that was validated is the value that is returned.
*/
let sessionIdReads = 0;
const statefulControl = createResumableUploadHttpControlPlane({
transport: {
async execute() {
return browserDataSuccess(
new Proxy(validSession() as Record<string, unknown>, {
getOwnPropertyDescriptor(target, key) {
if (key === "sessionId") {
sessionIdReads += 1;
return {
configurable: true,
enumerable: true,
value: sessionIdReads > 1 ? "../../unsafe" : "session_01",
};
}
return Reflect.getOwnPropertyDescriptor(target, key);
},
}),
);
},
},
partCapabilities: { issueUploadPart: vi.fn() },
});
const stateful = await statefulControl.createSession({
protocol: RESUMABLE_UPLOAD_PROTOCOL,
uploadKey: "upload_key_strict",
purpose: "attachment",
mediaType: "application/octet-stream",
requestBindingSha256: "b".repeat(64),
fingerprint,
requestedPartSizeBytes: 4,
requestedMaxConcurrency: 1,
idempotencyKey: "upload-create-idempotency-02",
signal: activeSignal,
});
expect(sessionIdReads).toBe(1);
expect(JSON.stringify(stateful)).not.toContain("../../unsafe");
if (stateful.ok) {
expect(stateful.value.sessionId).toBe("session_01");
}
});
it("rejects unknown response fields so URLs cannot cross the DTO boundary", async () => {