752 lines
25 KiB
TypeScript
752 lines
25 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
|
|
import {
|
|
PRESIGNED_TRANSFER_PROTOCOL,
|
|
type PresignedDownloadCapability,
|
|
} from "../../src/application/ports/browser-transfer/presigned-transfer.ts";
|
|
import { createPresignedCapabilityVault } from "../../src/adapters/browser-transfer/presigned/presigned-capability-vault.ts";
|
|
import { sha256Hex } from "../../src/adapters/browser-transfer/presigned/incremental-sha256.ts";
|
|
import {
|
|
CHECKSUM_HEADER,
|
|
CONTROL_ENDPOINT,
|
|
DATA_ORIGIN,
|
|
DIGEST_HEADER,
|
|
DOWNLOAD_HREF,
|
|
DOWNLOAD_PATH,
|
|
NOW,
|
|
POLICY_HEADER,
|
|
REQUEST_BINDING_SHA256,
|
|
UPLOAD_SESSION_ID,
|
|
collect,
|
|
createHarness,
|
|
downloadCapabilityPayload,
|
|
downloadResponse,
|
|
firstStreamResult,
|
|
jsonResponse,
|
|
responseWithUrl,
|
|
uploadCapabilityPayload,
|
|
} from "./presigned-transfer-fixture.ts";
|
|
|
|
describe("presigned transfer capability issuance and admission", () => {
|
|
it("matches the standard SHA-256 vector", () => {
|
|
expect(sha256Hex(new TextEncoder().encode("abc"))).toBe(
|
|
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
|
|
);
|
|
});
|
|
|
|
it("answers a refused capability envelope with a re-issuable recovery", async () => {
|
|
// BT-PRE-04. A capability document the adapter will not accept is closed as
|
|
// `POLICY_REJECTED`, and the caller's only way forward is a new capability.
|
|
// `NONE` said there was nothing to be done, which contradicted both the
|
|
// design record for an unsupported protocol and the vault, which already
|
|
// answers `REISSUE_CAPABILITY` for the same class of refusal.
|
|
for (const [label, overrides] of [
|
|
["unknown protocol", { protocol: "PRESIGNED_TRANSFER_V2" }],
|
|
["missing protocol", { protocol: undefined }],
|
|
] as const) {
|
|
const bytes = new Uint8Array([1, 2, 3]);
|
|
const payload: Record<string, unknown> = {
|
|
...downloadCapabilityPayload(bytes),
|
|
...overrides,
|
|
};
|
|
if (overrides.protocol === undefined) delete payload["protocol"];
|
|
const fetcher = vi.fn(async () => jsonResponse(payload)) as unknown as typeof fetch;
|
|
const { provider } = createHarness({ fetcher });
|
|
|
|
expect(
|
|
await provider.issueDownload({
|
|
resourceId: "resource-1",
|
|
signal: new AbortController().signal,
|
|
}),
|
|
label,
|
|
).toMatchObject({
|
|
ok: false,
|
|
error: {
|
|
code: "POLICY_REJECTED",
|
|
retryable: false,
|
|
recovery: "REISSUE_CAPABILITY",
|
|
},
|
|
});
|
|
}
|
|
});
|
|
|
|
it("keeps URL and headers adapter-private and streams bounded verified chunks", async () => {
|
|
const bytes = new Uint8Array([1, 2, 3, 4, 5]);
|
|
const payload = downloadCapabilityPayload(bytes);
|
|
const dataCalls: RequestInit[] = [];
|
|
const fetcher = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
if (String(input) === CONTROL_ENDPOINT) return jsonResponse(payload);
|
|
dataCalls.push(init ?? {});
|
|
return downloadResponse(bytes.slice().buffer, payload);
|
|
}) as unknown as typeof fetch;
|
|
const { provider, executor } = createHarness({ fetcher });
|
|
|
|
const issued = await provider.issueDownload({
|
|
resourceId: "resource-1",
|
|
signal: new AbortController().signal,
|
|
});
|
|
expect(issued.ok).toBe(true);
|
|
if (!issued.ok) return;
|
|
expect(Object.isFrozen(issued.value)).toBe(true);
|
|
expect("href" in issued.value).toBe(false);
|
|
expect("requestHeaders" in issued.value).toBe(false);
|
|
|
|
const opened = await executor.downloadSources.open({
|
|
resourceId: "resource-1",
|
|
capability: issued.value,
|
|
signal: new AbortController().signal,
|
|
});
|
|
expect(opened.ok).toBe(true);
|
|
if (!opened.ok) return;
|
|
const results = await collect(opened.value);
|
|
expect(results.every((result) => result.ok)).toBe(true);
|
|
expect(
|
|
results.flatMap((result) =>
|
|
result.ok ? [...result.value] : [],
|
|
),
|
|
).toEqual([...bytes]);
|
|
expect(
|
|
results.map((result) =>
|
|
result.ok ? result.value.byteLength : 0,
|
|
),
|
|
).toEqual([2, 2, 1]);
|
|
expect(dataCalls[0]).toMatchObject({
|
|
method: "GET",
|
|
credentials: "omit",
|
|
redirect: "error",
|
|
referrerPolicy: "no-referrer",
|
|
cache: "no-store",
|
|
});
|
|
|
|
expect(
|
|
await executor.downloadSources.open({
|
|
resourceId: "resource-1",
|
|
capability: issued.value,
|
|
signal: new AbortController().signal,
|
|
}),
|
|
).toMatchObject({
|
|
ok: false,
|
|
error: {
|
|
code: "POLICY_REJECTED",
|
|
},
|
|
});
|
|
});
|
|
|
|
it("retires consumed identities immediately and reuses the active slot before TTL", async () => {
|
|
const bytes = new Uint8Array([1, 2, 3]);
|
|
const responsePayload = downloadCapabilityPayload(bytes);
|
|
let issueSequence = 0;
|
|
const fetcher = vi.fn(
|
|
async (input: RequestInfo | URL) => {
|
|
if (String(input) === CONTROL_ENDPOINT) {
|
|
issueSequence += 1;
|
|
return jsonResponse(
|
|
downloadCapabilityPayload(bytes, {
|
|
capabilityReceipt: `capability-download-${issueSequence}`,
|
|
}),
|
|
);
|
|
}
|
|
return downloadResponse(
|
|
bytes.slice().buffer,
|
|
responsePayload,
|
|
);
|
|
},
|
|
) as unknown as typeof fetch;
|
|
const { provider, executor, vault } = createHarness({
|
|
fetcher,
|
|
maxActiveCapabilities: 1,
|
|
});
|
|
const signal = new AbortController().signal;
|
|
|
|
const first = await provider.issueDownload({
|
|
resourceId: "resource-1",
|
|
signal,
|
|
});
|
|
expect(first.ok).toBe(true);
|
|
if (!first.ok) return;
|
|
expect(
|
|
await provider.issueDownload({
|
|
resourceId: "resource-1",
|
|
signal,
|
|
}),
|
|
).toMatchObject({
|
|
ok: false,
|
|
error: { code: "LIMIT_EXCEEDED" },
|
|
});
|
|
|
|
const opened = await executor.downloadSources.open({
|
|
resourceId: "resource-1",
|
|
capability: first.value,
|
|
signal,
|
|
});
|
|
expect(opened.ok).toBe(true);
|
|
if (!opened.ok) return;
|
|
expect(vault.resolve(first.value)).toMatchObject({
|
|
ok: false,
|
|
error: { code: "POLICY_REJECTED" },
|
|
});
|
|
expect((await collect(opened.value)).every((result) => result.ok)).toBe(
|
|
true,
|
|
);
|
|
|
|
const afterConsume = await provider.issueDownload({
|
|
resourceId: "resource-1",
|
|
signal,
|
|
});
|
|
expect(afterConsume.ok).toBe(true);
|
|
if (!afterConsume.ok) return;
|
|
vault.revoke(afterConsume.value);
|
|
expect(vault.resolve(afterConsume.value)).toMatchObject({
|
|
ok: false,
|
|
error: { code: "POLICY_REJECTED" },
|
|
});
|
|
|
|
const afterRevoke = await provider.issueDownload({
|
|
resourceId: "resource-1",
|
|
signal,
|
|
});
|
|
expect(afterRevoke.ok).toBe(true);
|
|
if (!afterRevoke.ok) return;
|
|
vault.dispose();
|
|
expect(vault.resolve(afterRevoke.value)).toMatchObject({
|
|
ok: false,
|
|
error: { code: "UNAVAILABLE" },
|
|
});
|
|
});
|
|
|
|
it("snapshots the issuance request before asynchronous mutation", async () => {
|
|
const bytes = new Uint8Array([1]);
|
|
const payload = downloadCapabilityPayload(bytes);
|
|
let release: (() => void) | undefined;
|
|
const gate = new Promise<void>((resolve) => {
|
|
release = resolve;
|
|
});
|
|
let posted: unknown;
|
|
const fetcher = vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => {
|
|
posted = JSON.parse(String(init?.body)) as unknown;
|
|
await gate;
|
|
return jsonResponse(payload);
|
|
}) as unknown as typeof fetch;
|
|
const { provider } = createHarness({ fetcher });
|
|
const request = {
|
|
resourceId: "resource-1",
|
|
signal: new AbortController().signal,
|
|
};
|
|
|
|
const pending = provider.issueDownload(request);
|
|
request.resourceId = "mutated-resource";
|
|
release?.();
|
|
expect(await pending).toMatchObject({ ok: true });
|
|
expect(posted).toMatchObject({
|
|
binding: { resourceId: "resource-1" },
|
|
});
|
|
});
|
|
|
|
it("rejects a capability BFF response from a different final URL", async () => {
|
|
const bytes = new Uint8Array([1]);
|
|
const payload = downloadCapabilityPayload(bytes);
|
|
const fetcher = vi.fn(async () =>
|
|
jsonResponse(
|
|
payload,
|
|
"https://api.example/retargeted-capabilities",
|
|
),
|
|
) as unknown as typeof fetch;
|
|
const { provider } = createHarness({ fetcher });
|
|
|
|
expect(
|
|
await provider.issueDownload({
|
|
resourceId: "resource-1",
|
|
signal: new AbortController().signal,
|
|
}),
|
|
).toMatchObject({
|
|
ok: false,
|
|
error: { code: "POLICY_REJECTED" },
|
|
});
|
|
});
|
|
|
|
it("snapshots multipart session bindings before capability issuance awaits", async () => {
|
|
const bytes = new Uint8Array([1, 2]);
|
|
const checksum = sha256Hex(bytes);
|
|
const payload = uploadCapabilityPayload({ bytes, checksum });
|
|
let release: (() => void) | undefined;
|
|
const gate = new Promise<void>((resolve) => {
|
|
release = resolve;
|
|
});
|
|
let posted: unknown;
|
|
const fetcher = vi.fn(
|
|
async (_input: RequestInfo | URL, init?: RequestInit) => {
|
|
posted = JSON.parse(String(init?.body)) as unknown;
|
|
await gate;
|
|
return jsonResponse(payload);
|
|
},
|
|
) as unknown as typeof fetch;
|
|
const { provider } = createHarness({ fetcher });
|
|
const request = {
|
|
sessionId: UPLOAD_SESSION_ID,
|
|
requestBindingSha256: REQUEST_BINDING_SHA256,
|
|
uploadBindingSha256: "b".repeat(64),
|
|
partNumber: 1,
|
|
offset: 0,
|
|
byteLength: bytes.byteLength,
|
|
checksumSha256: checksum,
|
|
mediaType: "application/octet-stream",
|
|
idempotencyKey: "part-attempt-1",
|
|
signal: new AbortController().signal,
|
|
};
|
|
|
|
const pending = provider.issueUploadPart(request);
|
|
request.sessionId = "mutated-session";
|
|
request.requestBindingSha256 = "d".repeat(64);
|
|
release?.();
|
|
|
|
expect(await pending).toMatchObject({
|
|
ok: true,
|
|
value: {
|
|
binding: {
|
|
sessionId: UPLOAD_SESSION_ID,
|
|
requestBindingSha256: REQUEST_BINDING_SHA256,
|
|
},
|
|
},
|
|
});
|
|
expect(posted).toMatchObject({
|
|
binding: {
|
|
sessionId: UPLOAD_SESSION_ID,
|
|
requestBindingSha256: REQUEST_BINDING_SHA256,
|
|
},
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
"protocol",
|
|
"sessionId",
|
|
"requestBindingSha256",
|
|
] as const)(
|
|
"rejects a capability response with a mismatched %s",
|
|
async (field) => {
|
|
const bytes = new Uint8Array([1, 2]);
|
|
const checksum = sha256Hex(bytes);
|
|
const valid = uploadCapabilityPayload({ bytes, checksum });
|
|
const payload = uploadCapabilityPayload(
|
|
{ bytes, checksum },
|
|
{
|
|
binding: {
|
|
...valid.binding,
|
|
[field]:
|
|
field === "protocol"
|
|
? "PRESIGNED_MULTIPART_V2"
|
|
: field === "sessionId"
|
|
? "different-session"
|
|
: "d".repeat(64),
|
|
},
|
|
},
|
|
);
|
|
const fetcher = vi.fn(async () =>
|
|
jsonResponse(payload),
|
|
) as unknown as typeof fetch;
|
|
const { provider } = createHarness({ fetcher });
|
|
expect(
|
|
await provider.issueUploadPart({
|
|
sessionId: UPLOAD_SESSION_ID,
|
|
requestBindingSha256: REQUEST_BINDING_SHA256,
|
|
uploadBindingSha256: "b".repeat(64),
|
|
partNumber: 1,
|
|
offset: 0,
|
|
byteLength: bytes.byteLength,
|
|
checksumSha256: checksum,
|
|
mediaType: "application/octet-stream",
|
|
idempotencyKey: "part-attempt-1",
|
|
signal: new AbortController().signal,
|
|
}),
|
|
).toMatchObject({
|
|
ok: false,
|
|
error: { code: "POLICY_REJECTED" },
|
|
});
|
|
},
|
|
);
|
|
|
|
it("rejects capability query, origin, path and fabricated-handle mismatches", async () => {
|
|
const bytes = new Uint8Array([1]);
|
|
const malformed = downloadCapabilityPayload(bytes, {
|
|
href: `${DOWNLOAD_HREF}&extra=1`,
|
|
});
|
|
const fetcher = vi.fn(async () => jsonResponse(malformed)) as unknown as typeof fetch;
|
|
const { provider, executor } = createHarness({ fetcher });
|
|
expect(
|
|
await provider.issueDownload({
|
|
resourceId: "resource-1",
|
|
signal: new AbortController().signal,
|
|
}),
|
|
).toMatchObject({
|
|
ok: false,
|
|
error: { code: "POLICY_REJECTED" },
|
|
});
|
|
|
|
const forged = Object.freeze({
|
|
capabilityReceipt: "forged",
|
|
method: "GET",
|
|
binding: Object.freeze({
|
|
kind: "DOWNLOAD",
|
|
resourceId: "resource-1",
|
|
}),
|
|
mediaType: "application/octet-stream",
|
|
byteLength: 1,
|
|
maxBytes: 1,
|
|
expectedSha256: "a".repeat(64),
|
|
expiresAtEpochMs: NOW + 1_000,
|
|
}) as unknown as PresignedDownloadCapability;
|
|
expect(
|
|
await executor.downloadSources.open({
|
|
resourceId: "resource-1",
|
|
capability: forged,
|
|
signal: new AbortController().signal,
|
|
}),
|
|
).toMatchObject({
|
|
ok: false,
|
|
error: { code: "POLICY_REJECTED" },
|
|
});
|
|
});
|
|
|
|
it("rejects redirected, retargeted and response-header-mismatched downloads", async () => {
|
|
const bytes = new Uint8Array([1, 2]);
|
|
const payload = downloadCapabilityPayload(bytes);
|
|
let mode: "redirect" | "retarget" | "header" = "redirect";
|
|
const fetcher = vi.fn(async (input: RequestInfo | URL) => {
|
|
if (String(input) === CONTROL_ENDPOINT) return jsonResponse(payload);
|
|
const response =
|
|
mode === "header"
|
|
? downloadResponse(bytes.slice().buffer, payload, {
|
|
[DIGEST_HEADER]: "f".repeat(64),
|
|
})
|
|
: downloadResponse(bytes.slice().buffer, payload);
|
|
if (mode === "redirect") {
|
|
Object.defineProperty(response, "redirected", { value: true });
|
|
} else if (mode === "retarget") {
|
|
Object.defineProperty(response, "url", {
|
|
value: `${DATA_ORIGIN}/files/different?sig=do-not-log-this`,
|
|
});
|
|
}
|
|
return response;
|
|
}) as unknown as typeof fetch;
|
|
|
|
let harness = createHarness({ fetcher });
|
|
let issued = await harness.provider.issueDownload({
|
|
resourceId: "resource-1",
|
|
signal: new AbortController().signal,
|
|
});
|
|
expect(issued.ok).toBe(true);
|
|
if (!issued.ok) return;
|
|
await expect(
|
|
firstStreamResult(
|
|
await harness.executor.downloadSources.open({
|
|
resourceId: "resource-1",
|
|
capability: issued.value,
|
|
signal: new AbortController().signal,
|
|
}),
|
|
),
|
|
).resolves.toMatchObject({
|
|
ok: false,
|
|
error: { code: "POLICY_REJECTED" },
|
|
});
|
|
|
|
mode = "retarget";
|
|
harness = createHarness({ fetcher });
|
|
issued = await harness.provider.issueDownload({
|
|
resourceId: "resource-1",
|
|
signal: new AbortController().signal,
|
|
});
|
|
expect(issued.ok).toBe(true);
|
|
if (!issued.ok) return;
|
|
await expect(
|
|
firstStreamResult(
|
|
await harness.executor.downloadSources.open({
|
|
resourceId: "resource-1",
|
|
capability: issued.value,
|
|
signal: new AbortController().signal,
|
|
}),
|
|
),
|
|
).resolves.toMatchObject({
|
|
ok: false,
|
|
error: { code: "POLICY_REJECTED" },
|
|
});
|
|
|
|
mode = "header";
|
|
harness = createHarness({ fetcher });
|
|
issued = await harness.provider.issueDownload({
|
|
resourceId: "resource-1",
|
|
signal: new AbortController().signal,
|
|
});
|
|
expect(issued.ok).toBe(true);
|
|
if (!issued.ok) return;
|
|
await expect(
|
|
firstStreamResult(
|
|
await harness.executor.downloadSources.open({
|
|
resourceId: "resource-1",
|
|
capability: issued.value,
|
|
signal: new AbortController().signal,
|
|
}),
|
|
),
|
|
).resolves.toMatchObject({
|
|
ok: false,
|
|
error: { code: "INTEGRITY_FAILED" },
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
{ label: "missing", protocol: undefined },
|
|
{ label: "V0", protocol: "PRESIGNED_TRANSFER_V0" },
|
|
{ label: "V2", protocol: "PRESIGNED_TRANSFER_V2" },
|
|
])(
|
|
"requires PRESIGNED_TRANSFER_V1 in request and response ($label)",
|
|
async ({ protocol }) => {
|
|
const bytes = new Uint8Array([1, 2, 3]);
|
|
const payload = downloadCapabilityPayload(bytes);
|
|
const body =
|
|
protocol === undefined
|
|
? (({ protocol: _dropped, ...rest }) => rest)(
|
|
payload as Record<string, unknown>,
|
|
)
|
|
: { ...payload, protocol };
|
|
const requests: unknown[] = [];
|
|
const fetcher = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
if (String(input) === CONTROL_ENDPOINT) {
|
|
requests.push(JSON.parse(String(init?.body)));
|
|
return jsonResponse(body as never);
|
|
}
|
|
return downloadResponse(bytes.slice().buffer, payload);
|
|
}) as unknown as typeof fetch;
|
|
const { provider, vault } = createHarness({ fetcher });
|
|
|
|
const issued = await provider.issueDownload({
|
|
resourceId: "resource-1",
|
|
signal: new AbortController().signal,
|
|
});
|
|
|
|
// BT-PRE-02. The request always declares V1, and a response that does not
|
|
// is closed before the vault ever registers it.
|
|
expect(requests[0]).toMatchObject({
|
|
protocol: PRESIGNED_TRANSFER_PROTOCOL,
|
|
});
|
|
expect(issued).toMatchObject({
|
|
ok: false,
|
|
error: { code: "POLICY_REJECTED" },
|
|
});
|
|
// The rejected envelope never reached vault registration.
|
|
expect(vault.resolve).toBeTypeOf("function");
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
{ label: "encoded slash", path: "/files/a%2Fb" },
|
|
{ label: "encoded backslash", path: "/files/a%5Cb" },
|
|
{ label: "double-encoded dot segment", path: "/files/%252e%252e" },
|
|
{ label: "lowercase percent-hex", path: "/files/a%c3%a9" },
|
|
{ label: "encoded NUL", path: "/files/a%00b" },
|
|
])("rejects a provider path that can decode again ($label)", async ({ path }) => {
|
|
const bytes = new Uint8Array([1, 2, 3]);
|
|
const payload = downloadCapabilityPayload(bytes, {
|
|
path,
|
|
href: `${DATA_ORIGIN}${path}`,
|
|
});
|
|
const fetcher = vi.fn(async (input: RequestInfo | URL) =>
|
|
String(input) === CONTROL_ENDPOINT
|
|
? jsonResponse(payload)
|
|
: downloadResponse(bytes.slice().buffer, payload),
|
|
) as unknown as typeof fetch;
|
|
const { provider } = createHarness({ fetcher });
|
|
|
|
await expect(
|
|
provider.issueDownload({
|
|
resourceId: "resource-1",
|
|
signal: new AbortController().signal,
|
|
}),
|
|
).resolves.toMatchObject({
|
|
ok: false,
|
|
error: { code: "POLICY_REJECTED" },
|
|
});
|
|
});
|
|
|
|
it("accepts a valid opaque UTF-8 path segment", async () => {
|
|
const bytes = new Uint8Array([1, 2, 3]);
|
|
// BT-PRE-05. Canonical uppercase percent-hex for a real UTF-8 segment.
|
|
const path = `/files/${encodeURIComponent("caf\u00e9")}`;
|
|
const payload = downloadCapabilityPayload(bytes, {
|
|
path,
|
|
href: `${DATA_ORIGIN}${path}?sig=do-not-log-this`,
|
|
});
|
|
const fetcher = vi.fn(async (input: RequestInfo | URL) =>
|
|
String(input) === CONTROL_ENDPOINT
|
|
? jsonResponse(payload)
|
|
: downloadResponse(bytes.slice().buffer, payload),
|
|
) as unknown as typeof fetch;
|
|
const { provider } = createHarness({ fetcher });
|
|
|
|
await expect(
|
|
provider.issueDownload({
|
|
resourceId: "resource-1",
|
|
signal: new AbortController().signal,
|
|
}),
|
|
).resolves.toMatchObject({ ok: true });
|
|
});
|
|
|
|
it("bounds a fetch that ignores its abort signal and cancels the late body", async () => {
|
|
const bytes = new Uint8Array([1, 2, 3]);
|
|
const payload = downloadCapabilityPayload(bytes);
|
|
const cancel = vi.fn(async () => {});
|
|
let releaseControl: ((response: Response) => void) | undefined;
|
|
const timers: Array<() => void> = [];
|
|
const fetcher = vi.fn(
|
|
async () =>
|
|
await new Promise<Response>((resolve) => {
|
|
releaseControl = resolve;
|
|
}),
|
|
) as unknown as typeof fetch;
|
|
const { provider } = createHarness({
|
|
fetcher,
|
|
scheduler: {
|
|
setTimeout: (callback: () => void) => {
|
|
timers.push(callback);
|
|
return timers.length;
|
|
},
|
|
clearTimeout: () => {},
|
|
},
|
|
});
|
|
|
|
const issuing = provider.issueDownload({
|
|
resourceId: "resource-1",
|
|
signal: new AbortController().signal,
|
|
});
|
|
await Promise.resolve();
|
|
// BT-PRE-03. The timeout fires while the fetch is still pending and never
|
|
// settles on its own.
|
|
timers.forEach((fire) => fire());
|
|
|
|
await expect(issuing).resolves.toMatchObject({ ok: false });
|
|
|
|
releaseControl?.({ body: { cancel } } as unknown as Response);
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
expect(cancel).toHaveBeenCalledOnce();
|
|
void payload;
|
|
});
|
|
|
|
/**
|
|
* TR-RR-05. A scheduler that cannot install the deadline leaves the operation
|
|
* unbounded. Releasing the caller listener and continuing anyway meant a
|
|
* later abort was invisible, so an install failure is itself terminal: the
|
|
* request fails closed with a typed Result and its resources are released.
|
|
*/
|
|
it("fails closed when the scheduler cannot install the deadline", async () => {
|
|
const bytes = new Uint8Array([1, 2, 3]);
|
|
const payload = downloadCapabilityPayload(bytes);
|
|
const caller = new AbortController();
|
|
const remove = vi.spyOn(caller.signal, "removeEventListener");
|
|
const fetcher = vi.fn(async (input: RequestInfo | URL) =>
|
|
String(input) === CONTROL_ENDPOINT
|
|
? jsonResponse(payload)
|
|
: downloadResponse(bytes.slice().buffer, payload),
|
|
) as unknown as typeof fetch;
|
|
const { provider } = createHarness({
|
|
fetcher,
|
|
scheduler: {
|
|
setTimeout: () => {
|
|
throw new TypeError("scheduler exploded");
|
|
},
|
|
clearTimeout: () => {},
|
|
},
|
|
});
|
|
|
|
// The public result stays a typed Result, not a rejection.
|
|
await expect(
|
|
provider.issueDownload({
|
|
resourceId: "resource-1",
|
|
signal: caller.signal,
|
|
}),
|
|
).resolves.toMatchObject({ ok: false });
|
|
expect(remove).toHaveBeenCalled();
|
|
});
|
|
|
|
it.each([
|
|
{ label: "href/origin mismatch", patch: { origin: "https://evil.example" } },
|
|
{ label: "href/path mismatch", patch: { path: "/files/other" } },
|
|
{ label: "credentials in href", patch: { href: `https://u:p@objects.example${DOWNLOAD_PATH}` } },
|
|
{ label: "maxBytes below byteLength", patch: { maxBytes: 0 } },
|
|
{ label: "malformed digest", patch: { expectedSha256: "not-a-digest" } },
|
|
{ label: "non-positive expiry", patch: { expiresAtEpochMs: 0 } },
|
|
// TR-RR-03. The registration is a versioned exact union: plaintext, an
|
|
// ambient credential header, an unknown protocol version and any extra
|
|
// own field are all refused at the issuer seam.
|
|
{ label: "unknown protocol version", patch: { protocol: "PRESIGNED_TRANSFER_V0" } },
|
|
{ label: "missing protocol version", patch: { protocol: undefined } },
|
|
{
|
|
label: "plaintext target",
|
|
patch: {
|
|
href: `http://objects.example${DOWNLOAD_PATH}`,
|
|
origin: "http://objects.example",
|
|
},
|
|
},
|
|
{
|
|
label: "ambient credential header",
|
|
patch: {
|
|
requestHeaders: [{ name: "authorization", value: "Bearer leak" }],
|
|
},
|
|
},
|
|
{
|
|
label: "cookie response header",
|
|
patch: {
|
|
requiredResponseHeaders: [{ name: "set-cookie", value: "a=b" }],
|
|
},
|
|
},
|
|
{ label: "status outside 2xx", patch: { expectedStatus: 302 } },
|
|
{ label: "extra own field", patch: { injected: true } },
|
|
])(
|
|
"rejects a malformed registration at the vault issuer seam ($label)",
|
|
({ patch }) => {
|
|
// BT-PRE-04. The vault owns these invariants itself, so a second issuer
|
|
// cannot register a weaker capability of the same type.
|
|
const vault = createPresignedCapabilityVault({
|
|
now: () => NOW,
|
|
maxActiveCapabilities: 4,
|
|
});
|
|
const base = {
|
|
protocol: PRESIGNED_TRANSFER_PROTOCOL,
|
|
capabilityReceipt: "capability-direct-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: [],
|
|
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,
|
|
};
|
|
|
|
expect(
|
|
vault.register({ ...base, ...patch } as never),
|
|
).toMatchObject({
|
|
ok: false,
|
|
error: { code: "POLICY_REJECTED" },
|
|
});
|
|
// The same registration without the defect is accepted.
|
|
expect(vault.register(base as never)).toMatchObject({ ok: true });
|
|
vault.dispose();
|
|
},
|
|
);
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
});
|