674 lines
19 KiB
TypeScript
674 lines
19 KiB
TypeScript
import { describe, expect, expectTypeOf, it } from "vitest";
|
|
|
|
import {
|
|
MemoryDurableObjectStore,
|
|
MemoryBrowserFilePolicyRegistry,
|
|
MemoryExampleQuarantinedUploadAdapter,
|
|
MemoryFileSelectionAdapter,
|
|
MemoryPublicResponseCache,
|
|
MemoryStorageDurabilityAdapter,
|
|
MemoryStructuredOfflineStore,
|
|
MemoryTransientPreviewAdapter,
|
|
RecordingDownloadDeliveryAdapter,
|
|
asByteCount,
|
|
asBrowserManagedDownloadCapabilityReceipt,
|
|
asDurableObjectId,
|
|
asLocalFileRef,
|
|
asObjectGeneration,
|
|
browserFilePolicyReference,
|
|
failure,
|
|
sanitizeDownloadFileName,
|
|
} from "../../recipes/frontend-capabilities/index.ts";
|
|
import type {
|
|
BrowserFileComposition,
|
|
BrowserPersistencePolicy,
|
|
DurableObjectStorageComposition,
|
|
ExampleBackendUploadComposition,
|
|
PublicResponseCacheComposition,
|
|
StorageDurabilityComposition,
|
|
StructuredOfflineStorageComposition,
|
|
} from "../../recipes/frontend-capabilities/index.ts";
|
|
|
|
async function* byteChunks(
|
|
...values: ReadonlyArray<string>
|
|
): AsyncIterable<Readonly<{ ok: true; value: Uint8Array }>> {
|
|
for (const value of values) {
|
|
yield {
|
|
ok: true,
|
|
value: new TextEncoder().encode(value),
|
|
};
|
|
}
|
|
}
|
|
|
|
async function* failedByteChunks() {
|
|
yield failure(
|
|
"NOT_READABLE",
|
|
true,
|
|
"The fixture source became unreadable.",
|
|
);
|
|
}
|
|
|
|
describe("production browser file and storage recipe contracts", () => {
|
|
it("separates picker dismissal, metadata policy and bounded content reads", async () => {
|
|
const ref = asLocalFileRef("opaque-local-1");
|
|
const selectionPolicy = {
|
|
policyId: "support-attachment-v1",
|
|
purpose: "support-attachment",
|
|
classification: "PERSONAL" as const,
|
|
multiple: false,
|
|
maxCount: 1,
|
|
maxFileBytes: asByteCount(10),
|
|
maxTotalBytes: asByteCount(10),
|
|
allowEmpty: false,
|
|
accept: [{ mediaType: "text/plain", extensions: [".txt"] }],
|
|
};
|
|
const filePolicy = browserFilePolicyReference(
|
|
"support-attachment",
|
|
"select-inspect-preview-text",
|
|
);
|
|
const policies = new MemoryBrowserFilePolicyRegistry([
|
|
{
|
|
reference: filePolicy,
|
|
selection: selectionPolicy,
|
|
inspection: {
|
|
policyId: selectionPolicy.policyId,
|
|
maxInspectionBytes: asByteCount(1_445),
|
|
acceptedSignatures: [
|
|
{
|
|
mediaType: "text/plain",
|
|
extensions: [".txt"],
|
|
patterns: [
|
|
{
|
|
offset: asByteCount(0),
|
|
bytes: [104],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
preview: {
|
|
allowedMediaTypes: ["text/plain"],
|
|
maxPreviewBytes: asByteCount(10),
|
|
},
|
|
},
|
|
]);
|
|
const adapter = new MemoryFileSelectionAdapter(
|
|
[
|
|
{
|
|
ref,
|
|
displayName: "notes.txt",
|
|
bytes: new TextEncoder().encode("hello"),
|
|
reportedMediaType: "text/plain",
|
|
detectedMediaType: "text/plain",
|
|
signature: "MATCHED",
|
|
},
|
|
],
|
|
policies,
|
|
);
|
|
|
|
adapter.setNextOutcome("DISMISSED");
|
|
expect(await adapter.select({ policy: filePolicy })).toEqual({
|
|
ok: true,
|
|
value: { kind: "DISMISSED" },
|
|
});
|
|
|
|
const selected = await adapter.select({ policy: filePolicy });
|
|
expect(selected.ok && selected.value.kind).toBe("SELECTED");
|
|
expect(
|
|
await adapter.select({
|
|
policy: browserFilePolicyReference(
|
|
"support-attachment",
|
|
"select-inspect-preview-text",
|
|
),
|
|
}),
|
|
).toMatchObject({
|
|
ok: false,
|
|
failure: { code: "POLICY_REJECTED" },
|
|
});
|
|
const inspected = await adapter.inspect({
|
|
ref,
|
|
policy: filePolicy,
|
|
signal: new AbortController().signal,
|
|
});
|
|
expect(inspected).toMatchObject({
|
|
ok: true,
|
|
value: {
|
|
detectedMediaType: "text/plain",
|
|
signature: "MATCHED",
|
|
},
|
|
});
|
|
if (!inspected.ok || !inspected.value.verificationReceipt) return;
|
|
expect(
|
|
await adapter.readRange({
|
|
ref,
|
|
offset: asByteCount(1),
|
|
length: asByteCount(3),
|
|
signal: new AbortController().signal,
|
|
}),
|
|
).toEqual({
|
|
ok: true,
|
|
value: new TextEncoder().encode("ell"),
|
|
});
|
|
const previews = new MemoryTransientPreviewAdapter(
|
|
adapter,
|
|
policies,
|
|
);
|
|
const preview = await previews.create({
|
|
ref,
|
|
verificationReceipt: inspected.value.verificationReceipt,
|
|
policy: filePolicy,
|
|
maxPreviewBytes: asByteCount(10),
|
|
signal: new AbortController().signal,
|
|
});
|
|
expect(preview.ok).toBe(true);
|
|
expect(previews.activeLeaseCount).toBe(1);
|
|
if (preview.ok) {
|
|
preview.value.release();
|
|
preview.value.release();
|
|
}
|
|
expect(previews.activeLeaseCount).toBe(0);
|
|
previews.dispose();
|
|
expect(
|
|
await previews.create({
|
|
ref,
|
|
verificationReceipt: inspected.value.verificationReceipt,
|
|
policy: filePolicy,
|
|
maxPreviewBytes: asByteCount(10),
|
|
signal: new AbortController().signal,
|
|
}),
|
|
).toMatchObject({
|
|
ok: false,
|
|
failure: { code: "PROVIDER_UNAVAILABLE" },
|
|
});
|
|
adapter.release(ref);
|
|
expect(
|
|
(
|
|
await adapter.readRange({
|
|
ref,
|
|
offset: asByteCount(0),
|
|
length: asByteCount(1),
|
|
signal: new AbortController().signal,
|
|
})
|
|
).ok,
|
|
).toBe(false);
|
|
});
|
|
|
|
it("keeps the optional backend upload example resumable and quarantined", async () => {
|
|
const adapter = new MemoryExampleQuarantinedUploadAdapter(
|
|
asByteCount(16),
|
|
asByteCount(4),
|
|
() => Date.parse("2026-07-27T00:00:00.000Z"),
|
|
);
|
|
const signal = new AbortController().signal;
|
|
const created = await adapter.create({
|
|
purpose: "avatar",
|
|
byteLength: asByteCount(4),
|
|
detectedMediaType: "image/png",
|
|
signal,
|
|
});
|
|
expect(created.ok).toBe(true);
|
|
if (!created.ok) return;
|
|
|
|
const part = await adapter.uploadPart({
|
|
sessionId: created.value.sessionId,
|
|
partNumber: 1,
|
|
offset: asByteCount(0),
|
|
bytes: new Uint8Array([1, 2, 3, 4]),
|
|
checksumSha256: "part-checksum",
|
|
idempotencyKey: "part-one-attempt",
|
|
signal,
|
|
});
|
|
expect(part.ok).toBe(true);
|
|
const replay = await adapter.uploadPart({
|
|
sessionId: created.value.sessionId,
|
|
partNumber: 1,
|
|
offset: asByteCount(0),
|
|
bytes: new Uint8Array([1, 2, 3, 4]),
|
|
checksumSha256: "part-checksum",
|
|
idempotencyKey: "part-one-attempt",
|
|
signal,
|
|
});
|
|
expect(replay).toEqual(part);
|
|
expect(
|
|
await adapter.uploadPart({
|
|
sessionId: created.value.sessionId,
|
|
partNumber: 1,
|
|
offset: asByteCount(0),
|
|
bytes: new Uint8Array([9, 9, 9, 9]),
|
|
checksumSha256: "different-request-body",
|
|
idempotencyKey: "part-one-attempt",
|
|
signal,
|
|
}),
|
|
).toMatchObject({ ok: false, failure: { code: "CONFLICT" } });
|
|
if (!part.ok) return;
|
|
expect(
|
|
await adapter.complete({
|
|
sessionId: created.value.sessionId,
|
|
parts: [part.value],
|
|
signal,
|
|
}),
|
|
).toMatchObject({
|
|
ok: true,
|
|
value: { state: "QUARANTINED" },
|
|
});
|
|
});
|
|
|
|
it("keeps each browser technology independently composable", () => {
|
|
expectTypeOf<keyof BrowserFileComposition>().toEqualTypeOf<
|
|
"picker" | "content" | "previews" | "downloads"
|
|
>();
|
|
expectTypeOf<
|
|
keyof StructuredOfflineStorageComposition<unknown>
|
|
>().toEqualTypeOf<"store" | "maintenance">();
|
|
expectTypeOf<
|
|
keyof StorageDurabilityComposition
|
|
>().toEqualTypeOf<"durability">();
|
|
expectTypeOf<
|
|
keyof DurableObjectStorageComposition
|
|
>().toEqualTypeOf<"store" | "maintenance">();
|
|
expectTypeOf<
|
|
keyof PublicResponseCacheComposition
|
|
>().toEqualTypeOf<"cache">();
|
|
expectTypeOf<
|
|
keyof ExampleBackendUploadComposition
|
|
>().toEqualTypeOf<"upload">();
|
|
expectTypeOf<
|
|
BrowserPersistencePolicy["logoutAction"]
|
|
>().toEqualTypeOf<
|
|
"KEEP_ORIGIN_SHARED" | "PURGE_PARTITION" | "EXPORT_THEN_PURGE"
|
|
>();
|
|
expectTypeOf<
|
|
BrowserPersistencePolicy["accountDeletionAction"]
|
|
>().toEqualTypeOf<"KEEP_ORIGIN_SHARED" | "PURGE_PARTITION">();
|
|
expectTypeOf<
|
|
BrowserPersistencePolicy["pressureAction"]
|
|
>().toEqualTypeOf<"EVICT_RECONSTRUCTABLE" | "RETAIN">();
|
|
});
|
|
|
|
it("distinguishes browser handoff from a verified streamed save", async () => {
|
|
const browserPolicy = browserFilePolicyReference(
|
|
"reports",
|
|
"browser-managed-pdf",
|
|
);
|
|
const streamPolicy = browserFilePolicyReference(
|
|
"exports",
|
|
"stream-text",
|
|
);
|
|
const policies = new MemoryBrowserFilePolicyRegistry([
|
|
{
|
|
reference: browserPolicy,
|
|
download: {
|
|
strategy: "BROWSER_MANAGED",
|
|
mediaType: "application/pdf",
|
|
safeExtension: ".pdf",
|
|
maxTransferBytes: asByteCount(10),
|
|
maxBufferedBytes: asByteCount(10),
|
|
integrity: "OPTIONAL",
|
|
},
|
|
},
|
|
{
|
|
reference: streamPolicy,
|
|
download: {
|
|
strategy: "PROMPT_AND_STREAM",
|
|
mediaType: "text/plain",
|
|
safeExtension: ".txt",
|
|
maxTransferBytes: asByteCount(10),
|
|
maxBufferedBytes: asByteCount(10),
|
|
integrity: "REQUIRED",
|
|
},
|
|
},
|
|
]);
|
|
const capabilityReceipt =
|
|
asBrowserManagedDownloadCapabilityReceipt(
|
|
"capability:report",
|
|
);
|
|
const adapter = new RecordingDownloadDeliveryAdapter(
|
|
policies,
|
|
{
|
|
resolve(input) {
|
|
return {
|
|
ok: true,
|
|
value: {
|
|
capabilityReceipt,
|
|
href: "/downloads/report",
|
|
resourceId: input.resourceId,
|
|
mediaType: "application/pdf",
|
|
safeExtension: ".pdf",
|
|
maxBytes: asByteCount(10),
|
|
expiresAtEpochMs: 2_000,
|
|
},
|
|
};
|
|
},
|
|
},
|
|
() => 1_000,
|
|
);
|
|
const progress: number[] = [];
|
|
expect(sanitizeDownloadFileName("../CON\u202e.exe")).toBe("download.bin");
|
|
|
|
const handedOff = await adapter.deliver({
|
|
policy: browserPolicy,
|
|
source: {
|
|
kind: "BROWSER_MANAGED_RESOURCE",
|
|
resourceId: "opaque-resource",
|
|
capabilityReceipt,
|
|
},
|
|
suggestedFileName: "report.pdf",
|
|
maxTransferBytes: asByteCount(10),
|
|
maxBufferedBytes: asByteCount(10),
|
|
signal: new AbortController().signal,
|
|
onProgress() {},
|
|
});
|
|
expect(handedOff).toMatchObject({
|
|
ok: true,
|
|
value: { kind: "BROWSER_HANDOFF" },
|
|
});
|
|
expect(
|
|
await adapter.deliver({
|
|
policy: browserPolicy,
|
|
source: {
|
|
kind: "BROWSER_MANAGED_RESOURCE",
|
|
resourceId: "opaque-resource",
|
|
capabilityReceipt:
|
|
asBrowserManagedDownloadCapabilityReceipt(
|
|
"capability:forged",
|
|
),
|
|
},
|
|
suggestedFileName: "report.pdf",
|
|
signal: new AbortController().signal,
|
|
onProgress() {},
|
|
}),
|
|
).toMatchObject({
|
|
ok: false,
|
|
failure: { code: "POLICY_REJECTED" },
|
|
});
|
|
|
|
const saved = await adapter.deliver({
|
|
policy: streamPolicy,
|
|
source: {
|
|
kind: "GENERATED",
|
|
bytes: {
|
|
byteLength: asByteCount(4),
|
|
stream: () => byteChunks("ab", "cd"),
|
|
},
|
|
expectedSha256:
|
|
"88d4266fd4e6338d13b845fcf289579d209c897823b9217da3e161936f031589",
|
|
},
|
|
suggestedFileName: "safe.txt",
|
|
maxTransferBytes: asByteCount(10),
|
|
maxBufferedBytes: asByteCount(10),
|
|
signal: new AbortController().signal,
|
|
onProgress(value) {
|
|
progress.push(value.transferredBytes);
|
|
},
|
|
});
|
|
expect(saved).toMatchObject({
|
|
ok: true,
|
|
value: {
|
|
kind: "SAVED",
|
|
bytesWritten: 4,
|
|
integrity: "VERIFIED",
|
|
},
|
|
});
|
|
expect(progress).toEqual([2, 4, 4]);
|
|
});
|
|
|
|
it("commits IndexedDB-like mutations atomically with CAS and idempotency", async () => {
|
|
const store = new MemoryStructuredOfflineStore<{ title: string }>({
|
|
maxBytes: 10_000,
|
|
now: () => 1_000,
|
|
});
|
|
expect(
|
|
(
|
|
await store.open({
|
|
partitionKey: "opaque-account-partition",
|
|
onLifecycle() {},
|
|
})
|
|
).ok,
|
|
).toBe(true);
|
|
const createMutation = {
|
|
kind: "PUT",
|
|
id: "one",
|
|
value: { title: "first" },
|
|
payloadVersion: 1,
|
|
expiresAtEpochMs: null,
|
|
revision: { kind: "MUST_NOT_EXIST" },
|
|
} as const;
|
|
const committed = await store.commit({
|
|
idempotencyKey: "create-one",
|
|
mutations: [createMutation],
|
|
});
|
|
expect(committed).toMatchObject({
|
|
ok: true,
|
|
value: { replayed: false, revisions: { one: 1 } },
|
|
});
|
|
expect(await store.commit({
|
|
idempotencyKey: "create-one",
|
|
mutations: [createMutation],
|
|
})).toMatchObject({ ok: true, value: { replayed: true } });
|
|
expect(
|
|
await store.commit({
|
|
idempotencyKey: "create-one",
|
|
mutations: [
|
|
{
|
|
kind: "DELETE",
|
|
id: "one",
|
|
revision: { kind: "ANY" },
|
|
},
|
|
],
|
|
}),
|
|
).toMatchObject({ ok: false, failure: { code: "CONFLICT" } });
|
|
|
|
const rejectedBatch = await store.commit({
|
|
idempotencyKey: "atomic-conflict",
|
|
mutations: [
|
|
{
|
|
kind: "PUT",
|
|
id: "two",
|
|
value: { title: "must-not-commit" },
|
|
payloadVersion: 1,
|
|
expiresAtEpochMs: null,
|
|
revision: { kind: "MUST_NOT_EXIST" },
|
|
},
|
|
{
|
|
kind: "PUT",
|
|
id: "one",
|
|
value: { title: "stale" },
|
|
payloadVersion: 1,
|
|
expiresAtEpochMs: null,
|
|
revision: { kind: "MATCH", revision: 999 },
|
|
},
|
|
],
|
|
});
|
|
expect(rejectedBatch).toMatchObject({
|
|
ok: false,
|
|
failure: { code: "CONFLICT" },
|
|
});
|
|
expect(await store.read("two")).toEqual({ ok: true, value: null });
|
|
});
|
|
|
|
it("makes quota and persistence downgrade explicit", async () => {
|
|
const store = new MemoryStructuredOfflineStore<{ value: string }>({
|
|
maxBytes: 16,
|
|
});
|
|
await store.open({ partitionKey: "partition", onLifecycle() {} });
|
|
expect(
|
|
await store.commit({
|
|
idempotencyKey: "too-large",
|
|
mutations: [
|
|
{
|
|
kind: "PUT",
|
|
id: "one",
|
|
value: { value: "far larger than the approved storage budget" },
|
|
payloadVersion: 1,
|
|
expiresAtEpochMs: null,
|
|
revision: { kind: "MUST_NOT_EXIST" },
|
|
},
|
|
],
|
|
}),
|
|
).toMatchObject({
|
|
ok: false,
|
|
failure: { code: "QUOTA_EXCEEDED" },
|
|
});
|
|
|
|
const durability = new MemoryStorageDurabilityAdapter(
|
|
70,
|
|
100,
|
|
"DENIED",
|
|
);
|
|
expect(await durability.inspect()).toMatchObject({
|
|
ok: true,
|
|
value: { pressure: "PRESSURE" },
|
|
});
|
|
expect(
|
|
await durability.requestPersistence({
|
|
reason: "PROTECT_UNSYNCED_USER_DATA",
|
|
userInitiated: true,
|
|
}),
|
|
).toEqual({ ok: true, value: "DENIED" });
|
|
});
|
|
|
|
it("keeps partially written OPFS objects invisible until logical commit", async () => {
|
|
const store = new MemoryDurableObjectStore(asByteCount(32));
|
|
const id = asDurableObjectId("opaque-object");
|
|
store.failNextPutAfter("FILES_READY");
|
|
const failed = await store.put({
|
|
id,
|
|
expectedGeneration: null,
|
|
source: byteChunks("ab", "cd"),
|
|
declaredByteLength: asByteCount(4),
|
|
mediaType: "application/octet-stream",
|
|
dataClass: "RECONSTRUCTABLE",
|
|
retention: { kind: "EXPIRES", expiresAt: "2026-08-01T00:00:00.000Z" },
|
|
signal: new AbortController().signal,
|
|
onProgress() {},
|
|
});
|
|
expect(failed).toMatchObject({
|
|
ok: false,
|
|
failure: { code: "PROVIDER_UNAVAILABLE" },
|
|
});
|
|
expect(await store.open(id)).toMatchObject({
|
|
ok: false,
|
|
failure: { code: "NOT_FOUND" },
|
|
});
|
|
expect(
|
|
await store.reconcile({ timeBudgetMs: 100, maxEntries: 10 }),
|
|
).toMatchObject({ ok: true, value: { purgedCount: 1 } });
|
|
|
|
expect(
|
|
await store.put({
|
|
id,
|
|
expectedGeneration: null,
|
|
source: failedByteChunks(),
|
|
declaredByteLength: asByteCount(4),
|
|
mediaType: null,
|
|
dataClass: "RECONSTRUCTABLE",
|
|
retention: { kind: "EXPLICIT_DELETE" },
|
|
signal: new AbortController().signal,
|
|
onProgress() {},
|
|
}),
|
|
).toMatchObject({
|
|
ok: false,
|
|
failure: { code: "NOT_READABLE" },
|
|
});
|
|
expect(
|
|
await store.reconcile({ timeBudgetMs: 100, maxEntries: 10 }),
|
|
).toMatchObject({ ok: true, value: { purgedCount: 1 } });
|
|
|
|
const committed = await store.put({
|
|
id,
|
|
expectedGeneration: null,
|
|
source: byteChunks("abcd"),
|
|
declaredByteLength: asByteCount(4),
|
|
mediaType: null,
|
|
dataClass: "RECONSTRUCTABLE",
|
|
retention: { kind: "EXPLICIT_DELETE" },
|
|
signal: new AbortController().signal,
|
|
onProgress() {},
|
|
});
|
|
expect(committed).toMatchObject({
|
|
ok: true,
|
|
value: {
|
|
generation: 1,
|
|
integrity: { algorithm: "SHA-256-TREE-V1" },
|
|
},
|
|
});
|
|
const opened = await store.open(id);
|
|
expect(opened.ok).toBe(true);
|
|
if (opened.ok) {
|
|
const chunks: number[] = [];
|
|
for await (const chunkResult of opened.value.chunks) {
|
|
expect(chunkResult.ok).toBe(true);
|
|
if (chunkResult.ok) chunks.push(...chunkResult.value);
|
|
}
|
|
expect(new TextDecoder().decode(new Uint8Array(chunks))).toBe(
|
|
"abcd",
|
|
);
|
|
}
|
|
expect(
|
|
await store.remove({
|
|
id,
|
|
expectedGeneration: asObjectGeneration(2),
|
|
}),
|
|
).toMatchObject({ ok: false, failure: { code: "CONFLICT" } });
|
|
});
|
|
|
|
it("stages and activates only exact public Cache Storage representations", async () => {
|
|
const cache = new MemoryPublicResponseCache({
|
|
origin: "https://app.example.test",
|
|
maxEntryBytes: 1_000,
|
|
allowedMediaTypes: new Set(["text/javascript"]),
|
|
});
|
|
const entry = {
|
|
url: "https://app.example.test/assets/app.js?v=1#ignored",
|
|
byteLength: asByteCount(100),
|
|
mediaType: "text/javascript",
|
|
integritySha256: "sha256-entry",
|
|
requestCredentials: "OMIT" as const,
|
|
dataClass: "PUBLIC" as const,
|
|
};
|
|
expect(
|
|
await cache.stageRelease({
|
|
releaseId: "release-1",
|
|
manifestDigest: "manifest-1",
|
|
entries: [entry],
|
|
signal: new AbortController().signal,
|
|
}),
|
|
).toEqual({ ok: true, value: undefined });
|
|
expect(
|
|
await cache.activateRelease({
|
|
releaseId: "release-1",
|
|
manifestDigest: "manifest-1",
|
|
expectedPreviousReleaseId: null,
|
|
}),
|
|
).toEqual({ ok: true, value: undefined });
|
|
expect(
|
|
await cache.lookup({
|
|
url: "https://app.example.test/assets/app.js?v=1",
|
|
requestCredentials: "OMIT",
|
|
hasAuthorization: false,
|
|
}),
|
|
).toMatchObject({ ok: true, value: { kind: "HIT" } });
|
|
expect(
|
|
await cache.lookup({
|
|
url: "https://app.example.test/assets/app.js?v=2",
|
|
requestCredentials: "OMIT",
|
|
hasAuthorization: false,
|
|
}),
|
|
).toEqual({
|
|
ok: true,
|
|
value: { kind: "MISS", reason: "NOT_FOUND" },
|
|
});
|
|
expect(
|
|
await cache.stageRelease({
|
|
releaseId: "poisoned",
|
|
manifestDigest: "bad",
|
|
entries: [{ ...entry, url: "https://app.example.test/config.json" }],
|
|
signal: new AbortController().signal,
|
|
}),
|
|
).toMatchObject({
|
|
ok: false,
|
|
failure: { code: "POLICY_REJECTED" },
|
|
});
|
|
});
|
|
});
|