chore: sync the frontend template from 4dc033c to 8157ad4
The product was materialized from the template at `4dc033c` and has stayed on it through 43 template commits, so it was missing all three rounds of adapter remediation — including files it never had, such as the shared `abortable-operation` primitive and the `exact-snapshot` decoder that later fixes are written against. Taking only the newest round was not possible for that reason: the delta is coherent only as a whole. The product had not touched `src/adapters` at all since materialization, so the 140-file delta applied with a three-way merge and no conflicts. `package.json` was the single overlap and merged cleanly: the product owns `name`, the template contributed `check:adapter-inventory`, `check:remediation-ledger` and the image-resolve-signal type fixture. All 24 product-owned files — README, index.html, CI workflow, i18n catalog, home page, generated schemas, evidence scripts, component and visual snapshots — are byte-identical to `main`. `template.lock.json` now pins the synced revision and tree. Verified in this repository, not inherited from the template: six type projects, lint, nine gates (adapter inventory, remediation ledger, registries, diagnostics, realtime boundaries, architecture, browser file/storage boundaries, optional recipes, documentation), the production build, and 2,054 of 2,073 tests. The 19 failures are all in `tests/unit/ci-artifact-contract.test.ts` and are the same pre-existing sandbox RLIMIT, EMFILE, umask and `/tmp` permission behaviour the template records; four suites that failed once under parallel load pass in isolation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
002ba3624e
commit
4bff9ca151
@@ -1,5 +1,7 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { PRESIGNED_TRANSFER_PROTOCOL } from "../../src/application/ports/browser-transfer/presigned-transfer.ts";
|
||||
|
||||
import type {
|
||||
PresignedDownloadCapability,
|
||||
PresignedDownloadByteSource,
|
||||
@@ -43,6 +45,8 @@ function downloadCapabilityPayload(
|
||||
) {
|
||||
const digest = sha256Hex(bytes);
|
||||
return {
|
||||
// BT-PRE-02. Every capability envelope carries the top-level protocol.
|
||||
protocol: PRESIGNED_TRANSFER_PROTOCOL,
|
||||
capabilityReceipt: "capability-download-1",
|
||||
method: "GET",
|
||||
binding: {
|
||||
@@ -81,6 +85,7 @@ function uploadCapabilityPayload(input: Readonly<{
|
||||
checksum: string;
|
||||
}>, overrides: Readonly<Record<string, unknown>> = {}) {
|
||||
return {
|
||||
protocol: PRESIGNED_TRANSFER_PROTOCOL,
|
||||
capabilityReceipt: "capability-upload-1",
|
||||
method: "PUT",
|
||||
binding: {
|
||||
@@ -602,13 +607,15 @@ describe("presigned transfer", () => {
|
||||
});
|
||||
expect(issued.ok).toBe(true);
|
||||
if (!issued.ok) return;
|
||||
expect(
|
||||
await harness.executor.downloadSources.open({
|
||||
resourceId: "resource-1",
|
||||
capability: issued.value,
|
||||
signal: new AbortController().signal,
|
||||
}),
|
||||
).toMatchObject({
|
||||
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" },
|
||||
});
|
||||
@@ -621,13 +628,15 @@ describe("presigned transfer", () => {
|
||||
});
|
||||
expect(issued.ok).toBe(true);
|
||||
if (!issued.ok) return;
|
||||
expect(
|
||||
await harness.executor.downloadSources.open({
|
||||
resourceId: "resource-1",
|
||||
capability: issued.value,
|
||||
signal: new AbortController().signal,
|
||||
}),
|
||||
).toMatchObject({
|
||||
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" },
|
||||
});
|
||||
@@ -640,18 +649,532 @@ describe("presigned transfer", () => {
|
||||
});
|
||||
expect(issued.ok).toBe(true);
|
||||
if (!issued.ok) return;
|
||||
expect(
|
||||
await harness.executor.downloadSources.open({
|
||||
resourceId: "resource-1",
|
||||
capability: issued.value,
|
||||
signal: new AbortController().signal,
|
||||
}),
|
||||
).toMatchObject({
|
||||
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.
|
||||
*/
|
||||
describe("TR-01 the stored capability is the one that was validated", () => {
|
||||
const baseRegistration = () => ({
|
||||
protocol: PRESIGNED_TRANSFER_PROTOCOL,
|
||||
capabilityReceipt: "capability-snapshot-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: [{ name: "x-safe", value: "1" }],
|
||||
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,
|
||||
});
|
||||
|
||||
const freshVault = () =>
|
||||
createPresignedCapabilityVault({
|
||||
now: () => NOW,
|
||||
maxActiveCapabilities: 4,
|
||||
});
|
||||
|
||||
it("refuses a header row that answers differently on a second read", () => {
|
||||
const vault = freshVault();
|
||||
let nameReads = 0;
|
||||
const header = new Proxy(
|
||||
{ name: "x-safe", value: "1" },
|
||||
{
|
||||
getOwnPropertyDescriptor(target, key) {
|
||||
if (key === "name") {
|
||||
nameReads += 1;
|
||||
return {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
value: nameReads > 1 ? "authorization" : "x-safe",
|
||||
};
|
||||
}
|
||||
return Reflect.getOwnPropertyDescriptor(target, key);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const registered = vault.register({
|
||||
...baseRegistration(),
|
||||
requestHeaders: [header],
|
||||
} as never);
|
||||
|
||||
if (registered.ok) {
|
||||
// A single read means the value that was checked is the value stored.
|
||||
const resolved = vault.resolve(registered.value);
|
||||
expect(resolved.ok).toBe(true);
|
||||
if (resolved.ok) {
|
||||
expect(resolved.value.requestHeaders.map((row) => row.name)).toEqual([
|
||||
"x-safe",
|
||||
]);
|
||||
}
|
||||
}
|
||||
vault.dispose();
|
||||
});
|
||||
|
||||
const hostileRegistrations: readonly (readonly [string, () => unknown])[] = [
|
||||
[
|
||||
"an accessor field",
|
||||
() =>
|
||||
Object.defineProperty(baseRegistration(), "href", {
|
||||
enumerable: true,
|
||||
get: () => `${DATA_ORIGIN}${DOWNLOAD_PATH}`,
|
||||
}),
|
||||
],
|
||||
[
|
||||
"an inherited field",
|
||||
() => Object.assign(Object.create({ injected: true }), baseRegistration()),
|
||||
],
|
||||
[
|
||||
"a symbol field",
|
||||
() => ({ ...baseRegistration(), [Symbol.for("injected")]: true }),
|
||||
],
|
||||
[
|
||||
"a non-enumerable own field",
|
||||
() =>
|
||||
Object.defineProperty(baseRegistration(), "injected", {
|
||||
enumerable: false,
|
||||
value: true,
|
||||
}),
|
||||
],
|
||||
[
|
||||
"a throwing ownKeys trap",
|
||||
() =>
|
||||
new Proxy(baseRegistration(), {
|
||||
ownKeys() {
|
||||
throw new TypeError("hostile ownKeys trap");
|
||||
},
|
||||
}),
|
||||
],
|
||||
[
|
||||
"a null header array",
|
||||
() => ({ ...baseRegistration(), requestHeaders: null }),
|
||||
],
|
||||
[
|
||||
"a non-iterable header array",
|
||||
() => ({ ...baseRegistration(), requestHeaders: { length: 1 } }),
|
||||
],
|
||||
[
|
||||
"a header row with an extra field",
|
||||
() => ({
|
||||
...baseRegistration(),
|
||||
requestHeaders: [{ name: "x-safe", value: "1", injected: true }],
|
||||
}),
|
||||
],
|
||||
[
|
||||
"an accessor header name",
|
||||
() => ({
|
||||
...baseRegistration(),
|
||||
requestHeaders: [
|
||||
Object.defineProperty({ value: "1" }, "name", {
|
||||
enumerable: true,
|
||||
get: () => "x-safe",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
[
|
||||
"a binding with an extra field",
|
||||
() => ({
|
||||
...baseRegistration(),
|
||||
binding: { kind: "DOWNLOAD", resourceId: "r", injected: true },
|
||||
}),
|
||||
],
|
||||
[
|
||||
"a null binding",
|
||||
() => ({ ...baseRegistration(), binding: null }),
|
||||
],
|
||||
];
|
||||
|
||||
for (const [label, build] of hostileRegistrations) {
|
||||
it(`rejects ${label} as POLICY_REJECTED`, () => {
|
||||
const vault = freshVault();
|
||||
expect(vault.register(build() as never)).toMatchObject({
|
||||
ok: false,
|
||||
error: { code: "POLICY_REJECTED" },
|
||||
});
|
||||
vault.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
it("does not observe a mutation of the issuer's object after registration", () => {
|
||||
const vault = freshVault();
|
||||
const registration = baseRegistration();
|
||||
const registered = vault.register(registration as never);
|
||||
expect(registered.ok).toBe(true);
|
||||
if (!registered.ok) return;
|
||||
|
||||
registration.requestHeaders[0]!.name = "authorization";
|
||||
registration.expiresAtEpochMs = NOW + 999_999;
|
||||
|
||||
const resolved = vault.resolve(registered.value);
|
||||
expect(resolved.ok).toBe(true);
|
||||
if (!resolved.ok) return;
|
||||
expect(resolved.value.requestHeaders.map((row) => row.name)).toEqual([
|
||||
"x-safe",
|
||||
]);
|
||||
expect(resolved.value.expiresAtEpochMs).toBe(NOW + 60_000);
|
||||
vault.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
it("does not fetch a presigned download until stream consumption", async () => {
|
||||
const bytes = new Uint8Array([1, 2, 3]);
|
||||
const responsePayload = downloadCapabilityPayload(bytes);
|
||||
let downloadFetches = 0;
|
||||
const fetcher = vi.fn(async (input: RequestInfo | URL) => {
|
||||
if (String(input) === CONTROL_ENDPOINT) {
|
||||
return jsonResponse(responsePayload);
|
||||
}
|
||||
downloadFetches += 1;
|
||||
return downloadResponse(bytes.slice().buffer, responsePayload);
|
||||
}) as unknown as typeof fetch;
|
||||
const { provider, executor } = createHarness({ fetcher });
|
||||
const signal = new AbortController().signal;
|
||||
const issued = await provider.issueDownload({
|
||||
resourceId: "resource-1",
|
||||
signal,
|
||||
});
|
||||
expect(issued.ok).toBe(true);
|
||||
if (!issued.ok) return;
|
||||
|
||||
const opened = await executor.downloadSources.open({
|
||||
resourceId: "resource-1",
|
||||
capability: issued.value,
|
||||
signal,
|
||||
});
|
||||
expect(opened.ok).toBe(true);
|
||||
if (!opened.ok) return;
|
||||
// BT-PRE-01. open() performs no network I/O.
|
||||
expect(downloadFetches).toBe(0);
|
||||
|
||||
for await (const chunk of opened.value.stream(signal)) {
|
||||
expect(chunk.ok).toBe(true);
|
||||
}
|
||||
expect(downloadFetches).toBe(1);
|
||||
opened.value.close();
|
||||
});
|
||||
|
||||
it("closes an unused download source without network I/O", async () => {
|
||||
const bytes = new Uint8Array([1, 2, 3]);
|
||||
const responsePayload = downloadCapabilityPayload(bytes);
|
||||
let downloadFetches = 0;
|
||||
const fetcher = vi.fn(async (input: RequestInfo | URL) => {
|
||||
if (String(input) === CONTROL_ENDPOINT) {
|
||||
return jsonResponse(responsePayload);
|
||||
}
|
||||
downloadFetches += 1;
|
||||
return downloadResponse(bytes.slice().buffer, responsePayload);
|
||||
}) as unknown as typeof fetch;
|
||||
const { provider, executor } = createHarness({ fetcher });
|
||||
const signal = new AbortController().signal;
|
||||
const issued = await provider.issueDownload({
|
||||
resourceId: "resource-1",
|
||||
signal,
|
||||
});
|
||||
expect(issued.ok).toBe(true);
|
||||
if (!issued.ok) return;
|
||||
|
||||
const opened = await executor.downloadSources.open({
|
||||
resourceId: "resource-1",
|
||||
capability: issued.value,
|
||||
signal,
|
||||
});
|
||||
expect(opened.ok).toBe(true);
|
||||
if (!opened.ok) return;
|
||||
|
||||
opened.value.close();
|
||||
// close() is idempotent and never starts the transfer.
|
||||
opened.value.close();
|
||||
expect(downloadFetches).toBe(0);
|
||||
|
||||
// A stream after close is one terminal conflict, still without fetching.
|
||||
const results = [];
|
||||
for await (const chunk of opened.value.stream(signal)) {
|
||||
results.push(chunk);
|
||||
}
|
||||
expect(results).toMatchObject([
|
||||
{ ok: false, error: { code: "CONFLICT" } },
|
||||
]);
|
||||
expect(downloadFetches).toBe(0);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "truncation",
|
||||
@@ -1632,4 +2155,328 @@ describe("presigned transfer", () => {
|
||||
});
|
||||
expect(written).toEqual([...bytes]);
|
||||
});
|
||||
|
||||
/**
|
||||
* TR-RR-04. A presigned byte source owns a fetch reader and a capability
|
||||
* lease, and its port requires `close()`. The delivery consumer never called
|
||||
* it, so every outcome — success, validation failure, writer failure and
|
||||
* abort — leaked both.
|
||||
*/
|
||||
it.each([
|
||||
{ label: "success", mode: "SUCCESS" as const },
|
||||
{ label: "writer failure", mode: "WRITER_FAILURE" as const },
|
||||
{ label: "abort", mode: "ABORT" as const },
|
||||
])("closes the presigned source exactly once on $label", async ({ mode }) => {
|
||||
const bytes = new Uint8Array([1, 2, 3]);
|
||||
let closes = 0;
|
||||
const controller = new AbortController();
|
||||
const source = {
|
||||
byteLength: bytes.byteLength,
|
||||
integrity: "VERIFIED_ON_SUCCESSFUL_EXHAUSTION" as const,
|
||||
capability: undefined as never,
|
||||
close() {
|
||||
closes += 1;
|
||||
},
|
||||
async *stream() {
|
||||
if (mode === "ABORT") controller.abort();
|
||||
yield { ok: true as const, value: bytes };
|
||||
},
|
||||
};
|
||||
const capability = Object.freeze({
|
||||
capabilityReceipt: "capability-close-1",
|
||||
method: "GET" as const,
|
||||
binding: { kind: "DOWNLOAD" as const, resourceId: "resource-1" },
|
||||
mediaType: "application/octet-stream",
|
||||
byteLength: bytes.byteLength,
|
||||
maxBytes: bytes.byteLength,
|
||||
expectedSha256: "a".repeat(64),
|
||||
expiresAtEpochMs: NOW + 60_000,
|
||||
});
|
||||
source.capability = capability as never;
|
||||
|
||||
const closePolicy = browserFilePolicyReference(
|
||||
"download",
|
||||
"presigned-close",
|
||||
);
|
||||
const policies = new BrowserFilePolicyRegistry({
|
||||
profiles: [
|
||||
{
|
||||
reference: closePolicy,
|
||||
download: {
|
||||
strategy: "PROMPT_AND_STREAM",
|
||||
mediaType: "application/octet-stream",
|
||||
safeExtension: ".bin",
|
||||
maxTransferBytes: 64,
|
||||
maxBufferedBytes: 8,
|
||||
integrity: "REQUIRED",
|
||||
},
|
||||
},
|
||||
],
|
||||
hardLimits: {
|
||||
maxInspectionBytes: 64,
|
||||
maxRetainedFileBytes: 64,
|
||||
maxPreviewBytes: 64,
|
||||
maxObjectUrlBytes: 64,
|
||||
maxTransferBytes: 64,
|
||||
},
|
||||
});
|
||||
const handle: SaveFileHandle = {
|
||||
async createWritable() {
|
||||
return new WritableStream<Uint8Array>({
|
||||
write() {
|
||||
if (mode === "WRITER_FAILURE") {
|
||||
throw new TypeError("writer exploded");
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
const downloads = createDownloadDeliveryAdapter({
|
||||
host: { handoff() {} },
|
||||
policies,
|
||||
hardMaxObjectUrlBytes: 64,
|
||||
hardMaxTransferBytes: 64,
|
||||
browserManagedCapabilities: {
|
||||
resolve() {
|
||||
throw new TypeError("not used");
|
||||
},
|
||||
},
|
||||
openAuthorizedSource: async () =>
|
||||
({ ok: true, value: source }) as never,
|
||||
showSaveFilePicker: async () => handle,
|
||||
userActivation: { isActive: true },
|
||||
now: () => NOW,
|
||||
});
|
||||
|
||||
const deliveryResult = await downloads.deliver({
|
||||
policy: closePolicy,
|
||||
source: {
|
||||
kind: "AUTHORIZED_STREAM_RESOURCE",
|
||||
resourceId: "resource-1",
|
||||
capability: capability as never,
|
||||
},
|
||||
suggestedFileName: "artifact.bin",
|
||||
signal: controller.signal,
|
||||
onProgress() {},
|
||||
});
|
||||
|
||||
void deliveryResult;
|
||||
expect(closes).toBe(1);
|
||||
});
|
||||
|
||||
/**
|
||||
* TR-02. A lease that resolved after the abort already ended the delivery
|
||||
* never reached the holder, so nothing closed it: the fetch reader and the
|
||||
* capability lease outlived the terminal result.
|
||||
*/
|
||||
it("closes a source lease that arrives after the delivery was aborted", async () => {
|
||||
const bytes = new Uint8Array([1, 2, 3]);
|
||||
let closes = 0;
|
||||
const controller = new AbortController();
|
||||
let releaseOpen:
|
||||
| ((value: { ok: true; value: unknown }) => void)
|
||||
| undefined;
|
||||
const source = {
|
||||
byteLength: bytes.byteLength,
|
||||
integrity: "VERIFIED_ON_SUCCESSFUL_EXHAUSTION" as const,
|
||||
capability: undefined as never,
|
||||
close() {
|
||||
closes += 1;
|
||||
},
|
||||
async *stream() {
|
||||
yield { ok: true as const, value: bytes };
|
||||
},
|
||||
};
|
||||
const capability = Object.freeze({
|
||||
capabilityReceipt: "capability-late-1",
|
||||
method: "GET" as const,
|
||||
binding: { kind: "DOWNLOAD" as const, resourceId: "resource-1" },
|
||||
mediaType: "application/octet-stream",
|
||||
byteLength: bytes.byteLength,
|
||||
maxBytes: bytes.byteLength,
|
||||
expectedSha256: "a".repeat(64),
|
||||
expiresAtEpochMs: NOW + 60_000,
|
||||
});
|
||||
source.capability = capability as never;
|
||||
|
||||
const latePolicy = browserFilePolicyReference("download", "presigned-late");
|
||||
const policies = new BrowserFilePolicyRegistry({
|
||||
profiles: [
|
||||
{
|
||||
reference: latePolicy,
|
||||
download: {
|
||||
strategy: "PROMPT_AND_STREAM",
|
||||
mediaType: "application/octet-stream",
|
||||
safeExtension: ".bin",
|
||||
maxTransferBytes: 64,
|
||||
maxBufferedBytes: 8,
|
||||
integrity: "REQUIRED",
|
||||
},
|
||||
},
|
||||
],
|
||||
hardLimits: {
|
||||
maxInspectionBytes: 64,
|
||||
maxRetainedFileBytes: 64,
|
||||
maxPreviewBytes: 64,
|
||||
maxObjectUrlBytes: 64,
|
||||
maxTransferBytes: 64,
|
||||
},
|
||||
});
|
||||
const downloads = createDownloadDeliveryAdapter({
|
||||
host: { handoff() {} },
|
||||
policies,
|
||||
hardMaxObjectUrlBytes: 64,
|
||||
hardMaxTransferBytes: 64,
|
||||
browserManagedCapabilities: {
|
||||
resolve() {
|
||||
throw new TypeError("not used");
|
||||
},
|
||||
},
|
||||
// Ignores the signal entirely and resolves only when the test says so.
|
||||
openAuthorizedSource: () =>
|
||||
new Promise((resolve) => {
|
||||
releaseOpen = resolve as never;
|
||||
}) as never,
|
||||
showSaveFilePicker: async () => ({
|
||||
async createWritable() {
|
||||
return new WritableStream<Uint8Array>({ write() {} });
|
||||
},
|
||||
}),
|
||||
userActivation: { isActive: true },
|
||||
now: () => NOW,
|
||||
});
|
||||
|
||||
const delivering = downloads.deliver({
|
||||
policy: latePolicy,
|
||||
source: {
|
||||
kind: "AUTHORIZED_STREAM_RESOURCE",
|
||||
resourceId: "resource-1",
|
||||
capability: capability as never,
|
||||
},
|
||||
suggestedFileName: "artifact.bin",
|
||||
signal: controller.signal,
|
||||
onProgress() {},
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
controller.abort();
|
||||
const delivered = await delivering;
|
||||
expect(delivered.ok).toBe(false);
|
||||
|
||||
// The lease arrives only now, long after the terminal result.
|
||||
releaseOpen?.({ ok: true, value: source });
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
expect(closes).toBe(1);
|
||||
});
|
||||
|
||||
it("does not leave a late rejection unhandled after an abort", async () => {
|
||||
const unhandled: unknown[] = [];
|
||||
const onUnhandled = (reason: unknown) => unhandled.push(reason);
|
||||
process.on("unhandledRejection", onUnhandled);
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
let rejectOpen: ((reason: unknown) => void) | undefined;
|
||||
const rejectPolicy = browserFilePolicyReference(
|
||||
"download",
|
||||
"presigned-late-reject",
|
||||
);
|
||||
const policies = new BrowserFilePolicyRegistry({
|
||||
profiles: [
|
||||
{
|
||||
reference: rejectPolicy,
|
||||
download: {
|
||||
strategy: "PROMPT_AND_STREAM",
|
||||
mediaType: "application/octet-stream",
|
||||
safeExtension: ".bin",
|
||||
maxTransferBytes: 64,
|
||||
maxBufferedBytes: 8,
|
||||
integrity: "REQUIRED",
|
||||
},
|
||||
},
|
||||
],
|
||||
hardLimits: {
|
||||
maxInspectionBytes: 64,
|
||||
maxRetainedFileBytes: 64,
|
||||
maxPreviewBytes: 64,
|
||||
maxObjectUrlBytes: 64,
|
||||
maxTransferBytes: 64,
|
||||
},
|
||||
});
|
||||
const downloads = createDownloadDeliveryAdapter({
|
||||
host: { handoff() {} },
|
||||
policies,
|
||||
hardMaxObjectUrlBytes: 64,
|
||||
hardMaxTransferBytes: 64,
|
||||
browserManagedCapabilities: {
|
||||
resolve() {
|
||||
throw new TypeError("not used");
|
||||
},
|
||||
},
|
||||
openAuthorizedSource: () =>
|
||||
new Promise((_resolve, reject) => {
|
||||
rejectOpen = reject;
|
||||
}) as never,
|
||||
showSaveFilePicker: async () => ({
|
||||
async createWritable() {
|
||||
return new WritableStream<Uint8Array>({ write() {} });
|
||||
},
|
||||
}),
|
||||
userActivation: { isActive: true },
|
||||
now: () => NOW,
|
||||
});
|
||||
|
||||
const delivering = downloads.deliver({
|
||||
policy: rejectPolicy,
|
||||
source: {
|
||||
kind: "AUTHORIZED_STREAM_RESOURCE",
|
||||
resourceId: "resource-1",
|
||||
capability: Object.freeze({
|
||||
capabilityReceipt: "capability-late-2",
|
||||
method: "GET" as const,
|
||||
binding: { kind: "DOWNLOAD" as const, resourceId: "resource-1" },
|
||||
mediaType: "application/octet-stream",
|
||||
byteLength: 3,
|
||||
maxBytes: 3,
|
||||
expectedSha256: "a".repeat(64),
|
||||
expiresAtEpochMs: NOW + 60_000,
|
||||
}) as never,
|
||||
},
|
||||
suggestedFileName: "artifact.bin",
|
||||
signal: controller.signal,
|
||||
onProgress() {},
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
controller.abort();
|
||||
await delivering;
|
||||
|
||||
rejectOpen?.(new Error("late open failure"));
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
expect(unhandled).toEqual([]);
|
||||
} finally {
|
||||
process.off("unhandledRejection", onUnhandled);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* BT-PRE-01. The download lease is lazy, so a response-shape rejection is
|
||||
* observed on first consumption rather than at `open()`.
|
||||
*/
|
||||
async function firstStreamResult(
|
||||
opened: Awaited<
|
||||
ReturnType<
|
||||
ReturnType<typeof createHarness>["executor"]["downloadSources"]["open"]
|
||||
>
|
||||
>,
|
||||
): Promise<unknown> {
|
||||
if (!opened.ok) return opened;
|
||||
try {
|
||||
for await (const chunk of opened.value.stream(
|
||||
new AbortController().signal,
|
||||
)) {
|
||||
if (!chunk.ok) return chunk;
|
||||
}
|
||||
return { ok: true };
|
||||
} finally {
|
||||
opened.value.close();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user