Four browser-capability specs never reached the code they were named for. The `PRESIGNED_TRANSFER_V1` envelope gained a top-level `protocol` field, and the fixtures kept answering without it, so every capability was refused before any object request was made: the download and part-upload success paths were asserting against an empty transcript rather than exercising a real GET or PUT. The fixtures now speak the protocol they claim to, and the part-deletion expectation carries the physical effect the adapter reports. A refused capability document also answered `recovery: NONE`, telling the caller there was nothing to be done. The design record fixes this class of refusal as re-issuable and the vault already answers `REISSUE_CAPABILITY` for it, so the HTTP decoder disagreed with both. It now agrees. Lab performance produced no evidence at all. Playwright matches accessible names by substring, so the navigation entry "플랫폼 구성" also matched the home page's "플랫폼 구성 보기" call to action; the locator resolved to two links and the run died on a strict-mode violation before the first measurement. With an exact match the metrics are collected, and they show the named-interaction budget is missed on this machine — a real signal that was previously invisible. The platform overview baseline was captured before the reference routes moved from `integration-defined` to `session-required` and was never regenerated, so the only visual gate that could catch a regression on that page was failing for its own staleness. Regenerated after confirming the diff is exactly that label. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
527 lines
17 KiB
TypeScript
527 lines
17 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
|
|
import type { Route } from "@playwright/test";
|
|
|
|
import { expect, test } from "../support/browser/strict-browser-test.ts";
|
|
|
|
const PAGE_ORIGIN = "http://127.0.0.1:4174";
|
|
const API_ORIGIN = "https://upload-api.example.test";
|
|
const OBJECT_ORIGIN = "https://upload-objects.example.test";
|
|
const SESSION_ID = "session_browser_01";
|
|
const RESOURCE_ID = "resource_browser_01";
|
|
const UPLOAD_PROTOCOL = "PRESIGNED_MULTIPART_V1";
|
|
|
|
type JsonRecord = Readonly<Record<string, unknown>>;
|
|
|
|
function corsHeaders(): Record<string, string> {
|
|
return {
|
|
"access-control-allow-credentials": "true",
|
|
"access-control-allow-headers": "content-type",
|
|
"access-control-allow-methods": "POST, OPTIONS",
|
|
"access-control-allow-origin": PAGE_ORIGIN,
|
|
};
|
|
}
|
|
|
|
async function fulfillJson(
|
|
route: Route,
|
|
status: number,
|
|
value: unknown,
|
|
): Promise<void> {
|
|
const body = JSON.stringify(value);
|
|
await route.fulfill({
|
|
status,
|
|
body,
|
|
headers: {
|
|
...corsHeaders(),
|
|
"content-length": String(Buffer.byteLength(body)),
|
|
"content-type": "application/json; charset=utf-8",
|
|
},
|
|
});
|
|
}
|
|
|
|
test("uploads three presigned parts through native IndexedDB, Web Locks and fetch", async ({
|
|
page,
|
|
}) => {
|
|
let session: JsonRecord | null = null;
|
|
const acceptedParts = new Map<number, JsonRecord>();
|
|
const issuedParts = new Map<number, JsonRecord>();
|
|
const controlRequests: Readonly<{
|
|
path: string;
|
|
body: JsonRecord;
|
|
}>[] = [];
|
|
const uploadedBodies: Readonly<{
|
|
partNumber: number;
|
|
bytes: number[];
|
|
checksum: string | undefined;
|
|
}>[] = [];
|
|
let completedParts: readonly JsonRecord[] = [];
|
|
|
|
await page.route(`${API_ORIGIN}/**`, async (route) => {
|
|
if (route.request().method() === "OPTIONS") {
|
|
await route.fulfill({ status: 204, headers: corsHeaders() });
|
|
return;
|
|
}
|
|
|
|
const path = new URL(route.request().url()).pathname;
|
|
const body = route.request().postDataJSON() as JsonRecord;
|
|
controlRequests.push({ path, body });
|
|
|
|
if (path === "/uploads/create") {
|
|
const fingerprint = body.fingerprint as JsonRecord;
|
|
session = {
|
|
protocol: UPLOAD_PROTOCOL,
|
|
sessionId: SESSION_ID,
|
|
requestBindingSha256: body.requestBindingSha256,
|
|
fingerprint,
|
|
partSizeBytes: body.requestedPartSizeBytes,
|
|
partCount: fingerprint.partCount,
|
|
maxConcurrency: 1,
|
|
expiresAtEpochMs: Date.now() + 5 * 60_000,
|
|
};
|
|
await fulfillJson(route, 201, session);
|
|
return;
|
|
}
|
|
|
|
if (path === "/uploads/status") {
|
|
if (!session) {
|
|
await fulfillJson(route, 404, { state: "NOT_FOUND" });
|
|
return;
|
|
}
|
|
await fulfillJson(route, 200, {
|
|
state: "ACTIVE",
|
|
session,
|
|
acceptedParts: [...acceptedParts.values()].sort(
|
|
(left, right) =>
|
|
Number(left.partNumber) - Number(right.partNumber),
|
|
),
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (path === "/uploads/complete") {
|
|
completedParts = body.orderedParts as readonly JsonRecord[];
|
|
await fulfillJson(route, 200, {
|
|
state: "QUARANTINED",
|
|
protocol: UPLOAD_PROTOCOL,
|
|
sessionId: body.sessionId,
|
|
requestBindingSha256: body.requestBindingSha256,
|
|
fingerprint: body.fingerprint,
|
|
resourceId: RESOURCE_ID,
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (path === "/uploads/abort") {
|
|
await fulfillJson(route, 200, { state: "ABORTED" });
|
|
return;
|
|
}
|
|
|
|
if (path === "/uploads/part-capability") {
|
|
const binding = body.binding as JsonRecord;
|
|
const partNumber = Number(binding.partNumber);
|
|
const descriptor = {
|
|
partNumber,
|
|
offset: binding.offset,
|
|
byteLength: body.byteLength,
|
|
checksumSha256: body.expectedSha256,
|
|
};
|
|
issuedParts.set(partNumber, descriptor);
|
|
const objectPath =
|
|
`/uploads/${SESSION_ID}/parts/${String(partNumber)}`;
|
|
await fulfillJson(route, 200, {
|
|
// The part capability travels in a presigned envelope, so it names the
|
|
// transfer protocol even though its binding names the upload one.
|
|
// Without it every part was refused before any object PUT was made.
|
|
protocol: "PRESIGNED_TRANSFER_V1",
|
|
capabilityReceipt: `browser-part-capability-${String(partNumber)}`,
|
|
method: "PUT",
|
|
binding,
|
|
href:
|
|
`${OBJECT_ORIGIN}${objectPath}?sig=opaque-${String(partNumber)}`,
|
|
origin: OBJECT_ORIGIN,
|
|
path: objectPath,
|
|
allowedQueryParameters: ["sig"],
|
|
requestHeaders: [
|
|
{
|
|
name: "content-type",
|
|
value: body.mediaType,
|
|
},
|
|
{
|
|
name: "x-checksum-sha256",
|
|
value: body.expectedSha256,
|
|
},
|
|
],
|
|
requiredResponseHeaders: [
|
|
{
|
|
name: "x-policy-version",
|
|
value: "v1",
|
|
},
|
|
],
|
|
digestRequestHeader: "x-checksum-sha256",
|
|
digestResponseHeader: null,
|
|
receiptResponseHeader: "etag",
|
|
expectedResponseByteLength: 0,
|
|
expectedStatus: 204,
|
|
mediaType: body.mediaType,
|
|
byteLength: body.byteLength,
|
|
maxBytes: body.byteLength,
|
|
expectedSha256: body.expectedSha256,
|
|
expiresAtEpochMs:
|
|
Number(session?.expiresAtEpochMs) - 10_000,
|
|
singleUse: true,
|
|
});
|
|
return;
|
|
}
|
|
|
|
await route.fulfill({ status: 404, headers: corsHeaders() });
|
|
});
|
|
|
|
await page.route(`${OBJECT_ORIGIN}/**`, async (route) => {
|
|
if (route.request().method() === "OPTIONS") {
|
|
await route.fulfill({
|
|
status: 204,
|
|
headers: {
|
|
"access-control-allow-headers":
|
|
"content-type, x-checksum-sha256",
|
|
"access-control-allow-methods": "PUT, OPTIONS",
|
|
"access-control-allow-origin": "*",
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
const match = new URL(route.request().url()).pathname.match(
|
|
/\/parts\/([1-9][0-9]*)$/u,
|
|
);
|
|
const partNumber = Number(match?.[1]);
|
|
const body = route.request().postDataBuffer();
|
|
if (!body || !Number.isSafeInteger(partNumber)) {
|
|
await route.fulfill({ status: 400 });
|
|
return;
|
|
}
|
|
const checksum = route.request().headers()["x-checksum-sha256"];
|
|
const actualChecksum = createHash("sha256")
|
|
.update(body)
|
|
.digest("hex");
|
|
const descriptor = issuedParts.get(partNumber);
|
|
if (
|
|
!descriptor ||
|
|
checksum !== actualChecksum ||
|
|
descriptor.checksumSha256 !== actualChecksum ||
|
|
descriptor.byteLength !== body.byteLength
|
|
) {
|
|
await route.fulfill({ status: 422 });
|
|
return;
|
|
}
|
|
const receiptToken = `etag-part-${String(partNumber)}`;
|
|
acceptedParts.set(partNumber, {
|
|
...descriptor,
|
|
receiptToken,
|
|
});
|
|
uploadedBodies.push({
|
|
partNumber,
|
|
bytes: [...body],
|
|
checksum,
|
|
});
|
|
await route.fulfill({
|
|
status: 204,
|
|
headers: {
|
|
"access-control-allow-origin": "*",
|
|
"access-control-expose-headers": "etag, x-policy-version",
|
|
etag: `"${receiptToken}"`,
|
|
"x-policy-version": "v1",
|
|
},
|
|
});
|
|
});
|
|
|
|
await page.goto("/");
|
|
const result = await page.evaluate(
|
|
async ({ apiOrigin, objectOrigin }) => {
|
|
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 fetchTransportModulePath =
|
|
"/src/adapters/browser-transfer/resumable-upload/fetch-json-transport.ts";
|
|
const controlPlaneModulePath =
|
|
"/src/adapters/browser-transfer/resumable-upload/http-control-plane-adapter.ts";
|
|
const checkpointModulePath =
|
|
"/src/adapters/browser-transfer/resumable-upload/indexeddb-checkpoint-store.ts";
|
|
const partExecutorModulePath =
|
|
"/src/adapters/browser-transfer/resumable-upload/presigned-upload-part-executor.ts";
|
|
const uploadRuntimeModulePath =
|
|
"/src/adapters/browser-transfer/resumable-upload/resumable-upload-runtime.ts";
|
|
const cancellationModulePath =
|
|
"/src/adapters/browser-transfer/resumable-upload/upload-cancellation-channel.ts";
|
|
const lockModulePath =
|
|
"/src/adapters/browser-transfer/resumable-upload/upload-mutation-lock.ts";
|
|
const [
|
|
{
|
|
createPresignedCapabilityVault,
|
|
createSingleUsePresignedReplayGuard,
|
|
},
|
|
{ createPresignedCapabilityHttpProvider },
|
|
{ createPresignedTransferExecutor },
|
|
{ createResumableUploadFetchJsonTransport },
|
|
{ createResumableUploadHttpControlPlane },
|
|
{ createIndexedDbResumableUploadCheckpointRuntime },
|
|
{ createPresignedUploadPartExecutor },
|
|
{ createResumableUploadRuntime },
|
|
{ createBrowserUploadCancellationChannel },
|
|
{ createResumableUploadWebLock },
|
|
] = await Promise.all([
|
|
import(/* @vite-ignore */ vaultModulePath),
|
|
import(/* @vite-ignore */ providerModulePath),
|
|
import(/* @vite-ignore */ executorModulePath),
|
|
import(/* @vite-ignore */ fetchTransportModulePath),
|
|
import(/* @vite-ignore */ controlPlaneModulePath),
|
|
import(/* @vite-ignore */ checkpointModulePath),
|
|
import(/* @vite-ignore */ partExecutorModulePath),
|
|
import(/* @vite-ignore */ uploadRuntimeModulePath),
|
|
import(/* @vite-ignore */ cancellationModulePath),
|
|
import(/* @vite-ignore */ lockModulePath),
|
|
]);
|
|
|
|
const suffix = crypto.randomUUID().replaceAll("-", "");
|
|
const uploadKey = `upload_browser_${suffix}`;
|
|
const checkpointRuntime =
|
|
createIndexedDbResumableUploadCheckpointRuntime({
|
|
scope: {
|
|
authorityToken: `authority_${suffix}`,
|
|
namespaceToken: `namespace_${suffix}`,
|
|
partitionToken: `partition_${suffix}`,
|
|
},
|
|
blockedTimeoutMs: 2_000,
|
|
});
|
|
const vault = createPresignedCapabilityVault({
|
|
maxActiveCapabilities: 8,
|
|
});
|
|
const capabilityProvider =
|
|
createPresignedCapabilityHttpProvider({
|
|
endpoint: `${apiOrigin}/uploads/part-capability`,
|
|
vault,
|
|
allowedDataOrigins: [objectOrigin],
|
|
allowedDataPathPrefixes: ["/uploads/"],
|
|
allowedQueryParameters: ["sig"],
|
|
allowedRequestHeaders: [
|
|
"content-type",
|
|
"x-checksum-sha256",
|
|
],
|
|
allowedResponseHeaders: ["etag", "x-policy-version"],
|
|
hardMaxTransferBytes: 8,
|
|
hardMaxUploadResponseBytes: 1_024,
|
|
maxCapabilityTtlMs: 10 * 60_000,
|
|
minimumRemainingLifetimeMs: 30_000,
|
|
timeoutMs: 5_000,
|
|
controlPlaneCredentials: "include",
|
|
});
|
|
const transferExecutor = createPresignedTransferExecutor({
|
|
vault,
|
|
replayGuard: createSingleUsePresignedReplayGuard(),
|
|
hardMaxTransferBytes: 8,
|
|
hardMaxChunkBytes: 2,
|
|
hardMaxUploadResponseBytes: 1_024,
|
|
minimumRemainingLifetimeMs: 30_000,
|
|
timeoutMs: 5_000,
|
|
});
|
|
const transport = createResumableUploadFetchJsonTransport({
|
|
endpoints: {
|
|
CREATE_SESSION: `${apiOrigin}/uploads/create`,
|
|
GET_STATUS: `${apiOrigin}/uploads/status`,
|
|
COMPLETE: `${apiOrigin}/uploads/complete`,
|
|
ABORT: `${apiOrigin}/uploads/abort`,
|
|
},
|
|
allowedOrigins: [apiOrigin],
|
|
credentials: "include",
|
|
timeoutMs: 5_000,
|
|
maxRequestBytes: 64 * 1024,
|
|
maxResponseBytes: 64 * 1024,
|
|
maxRetryAfterMs: 1_000,
|
|
});
|
|
const controlPlane = createResumableUploadHttpControlPlane({
|
|
transport,
|
|
partCapabilities: capabilityProvider,
|
|
});
|
|
const cancellationChannelName =
|
|
`browser-upload-cancel-${suffix}`;
|
|
const crossContextCancellation =
|
|
createBrowserUploadCancellationChannel({
|
|
channelName: cancellationChannelName,
|
|
});
|
|
const cancellationPeer =
|
|
createBrowserUploadCancellationChannel({
|
|
channelName: cancellationChannelName,
|
|
});
|
|
const runtime = createResumableUploadRuntime({
|
|
controlPlane,
|
|
partExecutor: createPresignedUploadPartExecutor(
|
|
transferExecutor.uploadParts,
|
|
),
|
|
checkpoints: checkpointRuntime.store,
|
|
mutationLock: createResumableUploadWebLock(
|
|
navigator.locks,
|
|
"browser-resumable-upload",
|
|
),
|
|
...(crossContextCancellation
|
|
? { crossContextCancellation }
|
|
: {}),
|
|
crypto,
|
|
policy: {
|
|
partSizeBytes: 2,
|
|
maxFileBytes: 6,
|
|
maxPartCount: 3,
|
|
maxConcurrency: 2,
|
|
maxInFlightBytes: 16,
|
|
partBufferCopyFactor: 4,
|
|
maxSourceChunkBytes: 2,
|
|
maxRetries: 1,
|
|
retryBaseDelayMs: 1,
|
|
retryMaxDelayMs: 5,
|
|
maxRetryAfterMs: 1_000,
|
|
capabilityRefreshSkewMs: 1_000,
|
|
maxSessionLifetimeMs: 10 * 60_000,
|
|
providerAttemptTimeoutMs: 10_000,
|
|
},
|
|
});
|
|
const bytes = new Uint8Array([1, 2, 3, 4, 5]);
|
|
const progress: string[] = [];
|
|
let upload: unknown;
|
|
let checkpoint: unknown;
|
|
let deletion: unknown;
|
|
let cancellationDelivered = false;
|
|
try {
|
|
if (crossContextCancellation && cancellationPeer) {
|
|
cancellationDelivered = await new Promise<boolean>(
|
|
(resolve) => {
|
|
const timer = setTimeout(() => resolve(false), 2_000);
|
|
const release = cancellationPeer.subscribe((key: string) => {
|
|
if (key !== `upload_probe_${suffix}`) return;
|
|
clearTimeout(timer);
|
|
release();
|
|
resolve(true);
|
|
});
|
|
if (
|
|
!crossContextCancellation.publish(
|
|
`upload_probe_${suffix}`,
|
|
)
|
|
) {
|
|
clearTimeout(timer);
|
|
release();
|
|
resolve(false);
|
|
}
|
|
},
|
|
);
|
|
}
|
|
upload = await runtime.upload({
|
|
uploadKey,
|
|
purpose: "browser_attachment",
|
|
mediaType: "application/octet-stream",
|
|
source: {
|
|
kind: "RANGE_READER",
|
|
reader: {
|
|
byteLength: bytes.byteLength,
|
|
async readRange(input: {
|
|
offset: number;
|
|
length: number;
|
|
signal: AbortSignal;
|
|
}) {
|
|
return input.signal.aborted
|
|
? {
|
|
ok: false as const,
|
|
error: {
|
|
code: "ABORTED" as const,
|
|
operation: "FILE_READ" as const,
|
|
retryable: false,
|
|
recovery: "NONE" as const,
|
|
},
|
|
}
|
|
: {
|
|
ok: true as const,
|
|
value: bytes.slice(
|
|
input.offset,
|
|
input.offset + input.length,
|
|
),
|
|
};
|
|
},
|
|
},
|
|
},
|
|
signal: new AbortController().signal,
|
|
onProgress(value: { phase: string }) {
|
|
progress.push(value.phase);
|
|
},
|
|
});
|
|
checkpoint = await checkpointRuntime.store.read(uploadKey);
|
|
} finally {
|
|
runtime.close();
|
|
cancellationPeer?.close();
|
|
deletion =
|
|
await checkpointRuntime.admin.deletePartition();
|
|
vault.dispose();
|
|
}
|
|
return {
|
|
upload,
|
|
checkpoint,
|
|
deletion,
|
|
progress,
|
|
webLocksAvailable: Boolean(navigator.locks),
|
|
broadcastCancellationAvailable:
|
|
Boolean(crossContextCancellation),
|
|
cancellationDelivered,
|
|
};
|
|
},
|
|
{ apiOrigin: API_ORIGIN, objectOrigin: OBJECT_ORIGIN },
|
|
);
|
|
|
|
expect(result.webLocksAvailable).toBe(true);
|
|
expect(result.broadcastCancellationAvailable).toBe(true);
|
|
expect(result.cancellationDelivered).toBe(true);
|
|
expect(result.upload).toMatchObject({
|
|
ok: true,
|
|
value: {
|
|
state: "QUARANTINED",
|
|
resourceId: RESOURCE_ID,
|
|
byteLength: 5,
|
|
replayed: false,
|
|
},
|
|
});
|
|
expect(result.checkpoint).toEqual({ ok: true, value: null });
|
|
// The deletion reports the physical effect it observed, not only the state it
|
|
// reached, so a caller can tell a delete that happened from one that found
|
|
// nothing to do. The fixture asserts it rather than ignoring it.
|
|
expect(result.deletion).toEqual({
|
|
ok: true,
|
|
value: { state: "DELETED", effect: "APPLIED" },
|
|
});
|
|
expect(
|
|
uploadedBodies
|
|
.slice()
|
|
.sort((left, right) => left.partNumber - right.partNumber)
|
|
.map(({ partNumber, bytes }) => ({ partNumber, bytes })),
|
|
).toEqual([
|
|
{ partNumber: 1, bytes: [1, 2] },
|
|
{ partNumber: 2, bytes: [3, 4] },
|
|
{ partNumber: 3, bytes: [5] },
|
|
]);
|
|
expect(completedParts.map((part) => part.partNumber)).toEqual([
|
|
1, 2, 3,
|
|
]);
|
|
expect(completedParts.map((part) => part.receiptToken)).toEqual([
|
|
"etag-part-1",
|
|
"etag-part-2",
|
|
"etag-part-3",
|
|
]);
|
|
expect(controlRequests.map((request) => request.path)).toEqual([
|
|
"/uploads/create",
|
|
"/uploads/status",
|
|
"/uploads/part-capability",
|
|
"/uploads/part-capability",
|
|
"/uploads/part-capability",
|
|
"/uploads/status",
|
|
"/uploads/complete",
|
|
]);
|
|
expect(JSON.stringify(controlRequests)).not.toContain("?sig=");
|
|
expect(JSON.stringify(controlRequests)).not.toContain(OBJECT_ORIGIN);
|
|
});
|