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:
co-authored by
Claude Opus 5
parent
976c8a8da4
commit
000a2581af
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user