fix: complete presigned capability and upload transport contracts

BT-PRE-02: add the top-level PRESIGNED_TRANSFER_V1 protocol literal to the
capability request and response. A missing, V0 or V2 envelope is closed as
POLICY_REJECTED before the vault registers anything. The server negotiates by
request shape; fields are never dual-emitted into a strict decoder, and the
nested PRESIGNED_MULTIPART_V1 binding protocol is unchanged.

BT-PRE-03: aborting a controller does not settle a fetch that ignores its
signal, so both presigned scopes now race the task, cancel a late response body
and survive a throwing scheduler without leaking the external abort listener.

BT-PRE-04: the vault owns its registration invariants, re-checking method,
href/origin/path agreement, embedded credentials, byte bounds, digest shape and
expiry, so a second issuer cannot register a weaker capability of the same type.

BT-PRE-05: decode each path segment once and require it to round-trip through
the canonical uppercase percent encoder, closing %2f, %5c, %252e%252e, mixed-case
escapes and encoded NUL while still admitting valid opaque UTF-8 segments.

BT-UP-02: inject and snapshot the upload transport clock and scheduler, so
Retry-After delta-seconds and HTTP-date resolve against the same captured now
and a clock rollback clamps to zero instead of producing a negative delay.

BT-IMG-01: make the image resolve() lifetime signal required, replacing the
hidden PRIMARY_REQUIRED preset precondition with a type-level one, and add the
negative typecheck fixture and gate that prove it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-08-14 02:19:00 +09:00
co-authored by Claude Opus 5
parent 976c8a8da4
commit 000a2581af
13 changed files with 683 additions and 43 deletions
@@ -67,6 +67,56 @@ function createTransport(
}
describe("resumable upload fetch transport", () => {
it.each([
{ label: "delta-seconds", header: "2", now: 1_000, expected: 2_000 },
{ label: "HTTP-date ahead", header: "Thu, 01 Jan 1970 00:00:03 GMT", now: 1_000, expected: 2_000 },
{ label: "HTTP-date behind (clock rollback)", header: "Thu, 01 Jan 1970 00:00:01 GMT", now: 9_000, expected: 0 },
])(
"resolves Retry-After against the injected clock ($label)",
async ({ header, now, expected }) => {
// BT-UP-02. Both branches use the same captured `now`, so boundaries and
// clock rollback are deterministic.
const transport = createTransport(
(async () =>
responseAt(ENDPOINTS.GET_STATUS, null, {
status: 429,
headers: { "retry-after": header },
})) as unknown as typeof fetch,
{ nowEpochMs: () => now },
);
const result = await transport.execute({
operation: "GET_STATUS",
body: { sessionId: "session_01" },
signal: new AbortController().signal,
});
expect(result).toMatchObject({
ok: false,
error: { code: "UNAVAILABLE", retryAfterMs: expected },
});
},
);
it("ignores an invalid Retry-After date instead of guessing", async () => {
const transport = createTransport(
(async () =>
responseAt(ENDPOINTS.GET_STATUS, null, {
status: 429,
headers: { "retry-after": "not-a-date" },
})) as unknown as typeof fetch,
{ nowEpochMs: () => 1_000 },
);
const result = await transport.execute({
operation: "GET_STATUS",
body: { sessionId: "session_01" },
signal: new AbortController().signal,
});
expect(result).toMatchObject({ ok: false });
if (result.ok) return;
expect(result.error.retryAfterMs).toBeUndefined();
});
it("uses a closed operation map and fixed production fetch policy", async () => {
let receivedUrl = "";
let receivedInit: RequestInit | undefined;