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
@@ -109,6 +109,59 @@ export function createPresignedCapabilityVault(options: Readonly<{
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* BT-PRE-04. 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 (
|
||||
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 (
|
||||
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();
|
||||
}
|
||||
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,
|
||||
@@ -116,6 +169,12 @@ export function createPresignedCapabilityVault(options: Readonly<{
|
||||
if (disposed) {
|
||||
return browserDataFailure("UNAVAILABLE", "PRESIGNED_TRANSFER");
|
||||
}
|
||||
// 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(registration);
|
||||
if (invalid) return invalid;
|
||||
pruneExpired();
|
||||
if (
|
||||
byReceipt.has(registration.capabilityReceipt) ||
|
||||
|
||||
Reference in New Issue
Block a user