248 lines
7.3 KiB
TypeScript
248 lines
7.3 KiB
TypeScript
import { PRESIGNED_TRANSFER_PROTOCOL } from "../../src/application/ports/browser-transfer/presigned-transfer.ts";
|
|
import type {
|
|
PresignedDownloadByteSource,
|
|
} from "../../src/application/ports/browser-transfer/presigned-transfer.ts";
|
|
import { RESUMABLE_UPLOAD_PROTOCOL } from "../../src/application/ports/browser-transfer/resumable-upload.ts";
|
|
import type { BrowserDataObservation } from "../../src/application/ports/browser-file-storage/shared.ts";
|
|
import {
|
|
createPresignedCapabilityVault,
|
|
createSingleUsePresignedReplayGuard,
|
|
} from "../../src/adapters/browser-transfer/presigned/presigned-capability-vault.ts";
|
|
import { createPresignedCapabilityHttpProvider } from "../../src/adapters/browser-transfer/presigned/presigned-capability-http-provider.ts";
|
|
import {
|
|
createPresignedTransferExecutor,
|
|
type PresignedTransferExecutorOptions,
|
|
} from "../../src/adapters/browser-transfer/presigned/presigned-transfer-executor.ts";
|
|
import { sha256Hex } from "../../src/adapters/browser-transfer/presigned/incremental-sha256.ts";
|
|
|
|
export const NOW = 1_000_000;
|
|
export const CONTROL_ENDPOINT = "https://api.example/capabilities";
|
|
export const DATA_ORIGIN = "https://objects.example";
|
|
export const DOWNLOAD_PATH = "/files/resource-1";
|
|
export const DOWNLOAD_HREF =
|
|
`${DATA_ORIGIN}${DOWNLOAD_PATH}?sig=do-not-log-this`;
|
|
export const POLICY_HEADER = "x-policy-version";
|
|
export const DIGEST_HEADER = "x-content-sha256";
|
|
export const CHECKSUM_HEADER = "x-checksum-sha256";
|
|
export const UPLOAD_SESSION_ID = "upload-session-1";
|
|
export const REQUEST_BINDING_SHA256 = "c".repeat(64);
|
|
|
|
export function downloadCapabilityPayload(
|
|
bytes: Uint8Array,
|
|
overrides: Readonly<Record<string, unknown>> = {},
|
|
) {
|
|
const digest = sha256Hex(bytes);
|
|
return {
|
|
protocol: PRESIGNED_TRANSFER_PROTOCOL,
|
|
capabilityReceipt: "capability-download-1",
|
|
method: "GET",
|
|
binding: {
|
|
kind: "DOWNLOAD",
|
|
resourceId: "resource-1",
|
|
},
|
|
href: DOWNLOAD_HREF,
|
|
origin: DATA_ORIGIN,
|
|
path: DOWNLOAD_PATH,
|
|
allowedQueryParameters: ["sig"],
|
|
requestHeaders: [
|
|
{ name: "accept", value: "application/octet-stream" },
|
|
],
|
|
requiredResponseHeaders: [
|
|
{ name: POLICY_HEADER, value: "v1" },
|
|
],
|
|
digestRequestHeader: null,
|
|
digestResponseHeader: DIGEST_HEADER,
|
|
receiptResponseHeader: null,
|
|
expectedStatus: 200,
|
|
expectedResponseByteLength: null,
|
|
mediaType: "application/octet-stream",
|
|
byteLength: bytes.byteLength,
|
|
maxBytes: 64,
|
|
expectedSha256: digest,
|
|
expiresAtEpochMs: NOW + 30_000,
|
|
singleUse: true,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
export type CapabilityPayload = ReturnType<typeof downloadCapabilityPayload>;
|
|
|
|
export function uploadCapabilityPayload(input: Readonly<{
|
|
bytes: Uint8Array;
|
|
checksum: string;
|
|
}>, overrides: Readonly<Record<string, unknown>> = {}) {
|
|
return {
|
|
protocol: PRESIGNED_TRANSFER_PROTOCOL,
|
|
capabilityReceipt: "capability-upload-1",
|
|
method: "PUT",
|
|
binding: {
|
|
kind: "UPLOAD_PART",
|
|
protocol: RESUMABLE_UPLOAD_PROTOCOL,
|
|
sessionId: UPLOAD_SESSION_ID,
|
|
requestBindingSha256: REQUEST_BINDING_SHA256,
|
|
uploadBindingSha256: "b".repeat(64),
|
|
partNumber: 1,
|
|
offset: 0,
|
|
idempotencyKey: "part-attempt-1",
|
|
},
|
|
href: `${DATA_ORIGIN}/uploads/session-1/part-1?sig=secret`,
|
|
origin: DATA_ORIGIN,
|
|
path: "/uploads/session-1/part-1",
|
|
allowedQueryParameters: ["sig"],
|
|
requestHeaders: [
|
|
{ name: "content-type", value: "application/octet-stream" },
|
|
{ name: CHECKSUM_HEADER, value: input.checksum },
|
|
],
|
|
requiredResponseHeaders: [
|
|
{ name: POLICY_HEADER, value: "v1" },
|
|
],
|
|
digestRequestHeader: CHECKSUM_HEADER,
|
|
digestResponseHeader: null,
|
|
receiptResponseHeader: "etag",
|
|
expectedStatus: 200,
|
|
expectedResponseByteLength: 0,
|
|
mediaType: "application/octet-stream",
|
|
byteLength: input.bytes.byteLength,
|
|
maxBytes: 64,
|
|
expectedSha256: input.checksum,
|
|
expiresAtEpochMs: NOW + 30_000,
|
|
singleUse: true,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
export function jsonResponse(
|
|
value: unknown,
|
|
url = CONTROL_ENDPOINT,
|
|
): Response {
|
|
return responseWithUrl(
|
|
new Response(JSON.stringify(value), {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
}),
|
|
url,
|
|
);
|
|
}
|
|
|
|
export function downloadResponse(
|
|
body: BodyInit | null,
|
|
payload: CapabilityPayload,
|
|
headers: Record<string, string> = {},
|
|
): Response {
|
|
return responseWithUrl(
|
|
new Response(body, {
|
|
status: payload.expectedStatus as number,
|
|
headers: {
|
|
"Content-Type": String(payload.mediaType),
|
|
"Content-Length": String(payload.byteLength),
|
|
[DIGEST_HEADER]: String(payload.expectedSha256),
|
|
[POLICY_HEADER]: "v1",
|
|
...headers,
|
|
},
|
|
}),
|
|
String(payload.href),
|
|
);
|
|
}
|
|
|
|
export function responseWithUrl(response: Response, href: string): Response {
|
|
Object.defineProperty(response, "url", {
|
|
configurable: true,
|
|
value: href,
|
|
});
|
|
return response;
|
|
}
|
|
|
|
export function createHarness(input: Readonly<{
|
|
fetcher: typeof fetch;
|
|
maxActiveCapabilities?: number;
|
|
now?: () => number;
|
|
digestBytes?: PresignedTransferExecutorOptions["digestBytes"];
|
|
scheduler?: PresignedTransferExecutorOptions["scheduler"];
|
|
observer?: Readonly<{
|
|
record(observation: BrowserDataObservation): void;
|
|
}>;
|
|
}>) {
|
|
const now = input.now ?? (() => NOW);
|
|
const vault = createPresignedCapabilityVault({
|
|
maxActiveCapabilities: input.maxActiveCapabilities ?? 16,
|
|
now,
|
|
});
|
|
const replayGuard = createSingleUsePresignedReplayGuard();
|
|
const provider = createPresignedCapabilityHttpProvider({
|
|
endpoint: CONTROL_ENDPOINT,
|
|
vault,
|
|
allowedDataOrigins: [DATA_ORIGIN],
|
|
allowedDataPathPrefixes: ["/files/", "/uploads/"],
|
|
allowedQueryParameters: ["sig"],
|
|
allowedRequestHeaders: [
|
|
"accept",
|
|
"content-type",
|
|
CHECKSUM_HEADER,
|
|
],
|
|
allowedResponseHeaders: [
|
|
POLICY_HEADER,
|
|
DIGEST_HEADER,
|
|
"etag",
|
|
],
|
|
hardMaxTransferBytes: 64,
|
|
hardMaxUploadResponseBytes: 16,
|
|
maxCapabilityTtlMs: 60_000,
|
|
minimumRemainingLifetimeMs: 1_000,
|
|
timeoutMs: 5_000,
|
|
fetcher: input.fetcher,
|
|
now,
|
|
scheduler: input.scheduler,
|
|
observer: input.observer,
|
|
});
|
|
const executor = createPresignedTransferExecutor({
|
|
vault,
|
|
replayGuard,
|
|
hardMaxTransferBytes: 64,
|
|
hardMaxChunkBytes: 2,
|
|
hardMaxUploadResponseBytes: 16,
|
|
minimumRemainingLifetimeMs: 1_000,
|
|
timeoutMs: 5_000,
|
|
fetcher: input.fetcher,
|
|
now,
|
|
scheduler: input.scheduler,
|
|
digestBytes: input.digestBytes,
|
|
observer: input.observer,
|
|
});
|
|
return { provider, executor, vault };
|
|
}
|
|
|
|
export async function collect(
|
|
source: PresignedDownloadByteSource,
|
|
signal = new AbortController().signal,
|
|
) {
|
|
const results = [];
|
|
for await (const result of source.stream(signal)) {
|
|
results.push(result);
|
|
}
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* BT-PRE-01. The download lease is lazy, so a response-shape rejection is
|
|
* observed on first consumption rather than at open().
|
|
*/
|
|
export 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();
|
|
}
|
|
}
|