367 lines
11 KiB
TypeScript
367 lines
11 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
|
|
import { expect, test } from "../support/browser/strict-browser-test.ts";
|
|
|
|
const RESOURCE_ID = "browser-artifact-1";
|
|
const OBJECT_ORIGIN = "https://objects.example.test";
|
|
const OBJECT_PATH = "/files/browser-artifact-1";
|
|
const OBJECT_HREF =
|
|
`${OBJECT_ORIGIN}${OBJECT_PATH}?sig=opaque-browser-signature`;
|
|
const DOWNLOAD_BYTES = Buffer.from([
|
|
0x10, 0x20, 0x30, 0x40, 0x50,
|
|
0x60, 0x70, 0x80, 0x90, 0xa0,
|
|
]);
|
|
const DOWNLOAD_SHA256 = createHash("sha256")
|
|
.update(DOWNLOAD_BYTES)
|
|
.digest("hex");
|
|
|
|
test("issues an opaque capability and streams a verified object into the writable save path", async ({
|
|
page,
|
|
}) => {
|
|
const bffRequests: unknown[] = [];
|
|
const objectRequests: Readonly<{
|
|
url: string;
|
|
method: string;
|
|
accept: string | undefined;
|
|
cookie: string | undefined;
|
|
}>[] = [];
|
|
|
|
await page.route("**/api/download-capability", async (route) => {
|
|
bffRequests.push(route.request().postDataJSON());
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
capabilityReceipt: "browser-download-capability-1",
|
|
method: "GET",
|
|
binding: {
|
|
kind: "DOWNLOAD",
|
|
resourceId: RESOURCE_ID,
|
|
},
|
|
href: OBJECT_HREF,
|
|
origin: OBJECT_ORIGIN,
|
|
path: OBJECT_PATH,
|
|
allowedQueryParameters: ["sig"],
|
|
requestHeaders: [
|
|
{
|
|
name: "accept",
|
|
value: "application/octet-stream",
|
|
},
|
|
],
|
|
requiredResponseHeaders: [
|
|
{
|
|
name: "x-policy-version",
|
|
value: "v1",
|
|
},
|
|
],
|
|
digestRequestHeader: null,
|
|
digestResponseHeader: "x-content-sha256",
|
|
receiptResponseHeader: null,
|
|
expectedStatus: 200,
|
|
expectedResponseByteLength: null,
|
|
mediaType: "application/octet-stream",
|
|
byteLength: DOWNLOAD_BYTES.byteLength,
|
|
maxBytes: 64,
|
|
expectedSha256: DOWNLOAD_SHA256,
|
|
expiresAtEpochMs: Date.now() + 5 * 60_000,
|
|
singleUse: true,
|
|
}),
|
|
});
|
|
});
|
|
await page.route(`${OBJECT_ORIGIN}/**`, async (route) => {
|
|
const headers = route.request().headers();
|
|
objectRequests.push({
|
|
url: route.request().url(),
|
|
method: route.request().method(),
|
|
accept: headers.accept,
|
|
cookie: headers.cookie,
|
|
});
|
|
await route.fulfill({
|
|
status: 200,
|
|
body: DOWNLOAD_BYTES,
|
|
headers: {
|
|
"access-control-allow-origin": "*",
|
|
"access-control-expose-headers":
|
|
"content-length, x-content-sha256, x-policy-version",
|
|
"content-length": String(DOWNLOAD_BYTES.byteLength),
|
|
"content-type": "application/octet-stream",
|
|
"x-content-sha256": DOWNLOAD_SHA256,
|
|
"x-policy-version": "v1",
|
|
},
|
|
});
|
|
});
|
|
|
|
await page.goto("/");
|
|
const result = await page.evaluate(
|
|
async ({ resourceId }) => {
|
|
const vaultModulePath =
|
|
"/src/adapters/browser-transfer/presigned/presigned-capability-vault.ts";
|
|
const providerModulePath =
|
|
"/src/adapters/browser-transfer/presigned/presigned-capability-http-provider.ts";
|
|
const executorModulePath =
|
|
"/src/adapters/browser-transfer/presigned/presigned-transfer-executor.ts";
|
|
const policyModulePath =
|
|
"/src/adapters/browser-files/browser-file-policy-registry.ts";
|
|
const deliveryModulePath =
|
|
"/src/adapters/browser-files/download-delivery-adapter.ts";
|
|
const [
|
|
{
|
|
createPresignedCapabilityVault,
|
|
createSingleUsePresignedReplayGuard,
|
|
},
|
|
{ createPresignedCapabilityHttpProvider },
|
|
{ createPresignedTransferExecutor },
|
|
{
|
|
BrowserFilePolicyRegistry,
|
|
browserFilePolicyReference,
|
|
},
|
|
{ createDownloadDeliveryAdapter },
|
|
] = await Promise.all([
|
|
import(
|
|
/* @vite-ignore */ vaultModulePath
|
|
) as Promise<
|
|
typeof import("../../src/adapters/browser-transfer/presigned/presigned-capability-vault.ts")
|
|
>,
|
|
import(
|
|
/* @vite-ignore */ providerModulePath
|
|
) as Promise<
|
|
typeof import("../../src/adapters/browser-transfer/presigned/presigned-capability-http-provider.ts")
|
|
>,
|
|
import(
|
|
/* @vite-ignore */ executorModulePath
|
|
) as Promise<
|
|
typeof import("../../src/adapters/browser-transfer/presigned/presigned-transfer-executor.ts")
|
|
>,
|
|
import(
|
|
/* @vite-ignore */ policyModulePath
|
|
) as Promise<
|
|
typeof import("../../src/adapters/browser-files/browser-file-policy-registry.ts")
|
|
>,
|
|
import(
|
|
/* @vite-ignore */ deliveryModulePath
|
|
) as Promise<
|
|
typeof import("../../src/adapters/browser-files/download-delivery-adapter.ts")
|
|
>,
|
|
]);
|
|
|
|
const vault = createPresignedCapabilityVault({
|
|
maxActiveCapabilities: 4,
|
|
});
|
|
const provider = createPresignedCapabilityHttpProvider({
|
|
endpoint: `${location.origin}/api/download-capability`,
|
|
vault,
|
|
allowedDataOrigins: ["https://objects.example.test"],
|
|
allowedDataPathPrefixes: ["/files/"],
|
|
allowedQueryParameters: ["sig"],
|
|
allowedRequestHeaders: ["accept"],
|
|
allowedResponseHeaders: [
|
|
"x-content-sha256",
|
|
"x-policy-version",
|
|
],
|
|
hardMaxTransferBytes: 64,
|
|
hardMaxUploadResponseBytes: 1_024,
|
|
maxCapabilityTtlMs: 10 * 60_000,
|
|
minimumRemainingLifetimeMs: 30_000,
|
|
timeoutMs: 5_000,
|
|
allowInsecureLocalhost: true,
|
|
});
|
|
const executor = createPresignedTransferExecutor({
|
|
vault,
|
|
replayGuard: createSingleUsePresignedReplayGuard(),
|
|
hardMaxTransferBytes: 64,
|
|
hardMaxChunkBytes: 3,
|
|
hardMaxUploadResponseBytes: 1_024,
|
|
minimumRemainingLifetimeMs: 30_000,
|
|
timeoutMs: 5_000,
|
|
});
|
|
const signal = new AbortController().signal;
|
|
const issued = await provider.issueDownload({
|
|
resourceId,
|
|
signal,
|
|
});
|
|
if (!issued.ok) {
|
|
vault.dispose();
|
|
return {
|
|
issued,
|
|
delivery: null,
|
|
capabilityKeys: [],
|
|
capabilityBindingKeys: [],
|
|
written: [],
|
|
writeChunkSizes: [],
|
|
writableClosed: false,
|
|
writableAborted: false,
|
|
progressPhases: [],
|
|
};
|
|
}
|
|
|
|
const downloadPolicy = browserFilePolicyReference(
|
|
"browser-presigned-download",
|
|
"save-verified-artifact",
|
|
);
|
|
const policies = new BrowserFilePolicyRegistry({
|
|
profiles: [
|
|
{
|
|
reference: downloadPolicy,
|
|
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 written: number[] = [];
|
|
const writeChunkSizes: number[] = [];
|
|
let writableClosed = false;
|
|
let writableAborted = false;
|
|
const downloads = createDownloadDeliveryAdapter({
|
|
host: {
|
|
handoff() {
|
|
throw new TypeError(
|
|
"Writable delivery must not use browser handoff.",
|
|
);
|
|
},
|
|
},
|
|
policies,
|
|
hardMaxObjectUrlBytes: 64,
|
|
hardMaxTransferBytes: 64,
|
|
browserManagedCapabilities: {
|
|
resolve() {
|
|
throw new TypeError(
|
|
"Browser-managed capability is not used.",
|
|
);
|
|
},
|
|
},
|
|
openAuthorizedSource:
|
|
executor.downloadSources.open.bind(
|
|
executor.downloadSources,
|
|
),
|
|
showSaveFilePicker: async () => ({
|
|
async createWritable() {
|
|
return new WritableStream<Uint8Array>({
|
|
write(chunk) {
|
|
writeChunkSizes.push(chunk.byteLength);
|
|
written.push(...chunk);
|
|
},
|
|
close() {
|
|
writableClosed = true;
|
|
},
|
|
abort() {
|
|
writableAborted = true;
|
|
},
|
|
});
|
|
},
|
|
}),
|
|
createTransferId: () => "transfer:browser-presigned",
|
|
progressMinIntervalMs: 0,
|
|
userActivation: { isActive: true },
|
|
});
|
|
const progressPhases: string[] = [];
|
|
const capabilityKeys = Object.keys(issued.value).sort();
|
|
const capabilityBindingKeys = Object.keys(
|
|
issued.value.binding,
|
|
).sort();
|
|
const delivery = await downloads.deliver({
|
|
policy: downloadPolicy,
|
|
source: {
|
|
kind: "AUTHORIZED_STREAM_RESOURCE",
|
|
resourceId,
|
|
capability: issued.value,
|
|
},
|
|
suggestedFileName: "browser-artifact.bin",
|
|
signal,
|
|
onProgress(progress) {
|
|
progressPhases.push(progress.phase);
|
|
},
|
|
});
|
|
downloads.dispose();
|
|
vault.dispose();
|
|
return {
|
|
issued: {
|
|
ok: true as const,
|
|
exposedHref: "href" in issued.value,
|
|
exposedRequestHeaders: "requestHeaders" in issued.value,
|
|
},
|
|
delivery,
|
|
capabilityKeys,
|
|
capabilityBindingKeys,
|
|
written,
|
|
writeChunkSizes,
|
|
writableClosed,
|
|
writableAborted,
|
|
progressPhases,
|
|
};
|
|
},
|
|
{ resourceId: RESOURCE_ID },
|
|
);
|
|
|
|
expect(bffRequests).toEqual([
|
|
{
|
|
method: "GET",
|
|
binding: {
|
|
kind: "DOWNLOAD",
|
|
resourceId: RESOURCE_ID,
|
|
},
|
|
},
|
|
]);
|
|
expect(objectRequests).toEqual([
|
|
{
|
|
url: OBJECT_HREF,
|
|
method: "GET",
|
|
accept: "application/octet-stream",
|
|
cookie: undefined,
|
|
},
|
|
]);
|
|
expect(result.issued).toEqual({
|
|
ok: true,
|
|
exposedHref: false,
|
|
exposedRequestHeaders: false,
|
|
});
|
|
expect(result.capabilityKeys).toEqual([
|
|
"binding",
|
|
"byteLength",
|
|
"capabilityReceipt",
|
|
"expectedSha256",
|
|
"expiresAtEpochMs",
|
|
"maxBytes",
|
|
"mediaType",
|
|
"method",
|
|
]);
|
|
expect(result.capabilityBindingKeys).toEqual([
|
|
"kind",
|
|
"resourceId",
|
|
]);
|
|
expect(result.delivery).toEqual({
|
|
ok: true,
|
|
value: {
|
|
kind: "SAVED",
|
|
transferId: "transfer:browser-presigned",
|
|
bytesWritten: DOWNLOAD_BYTES.byteLength,
|
|
integrity: "VERIFIED",
|
|
},
|
|
});
|
|
expect(result.written).toEqual([...DOWNLOAD_BYTES]);
|
|
expect(result.writeChunkSizes).toEqual([3, 3, 3, 1]);
|
|
expect(result.writableClosed).toBe(true);
|
|
expect(result.writableAborted).toBe(false);
|
|
expect(result.progressPhases).toEqual([
|
|
"PREPARING",
|
|
"TRANSFERRING",
|
|
"TRANSFERRING",
|
|
"TRANSFERRING",
|
|
"TRANSFERRING",
|
|
"VERIFYING",
|
|
"FINALIZING",
|
|
]);
|
|
});
|