282 lines
7.3 KiB
TypeScript
282 lines
7.3 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
|
|
import {
|
|
BrowserFilePolicyRegistry,
|
|
browserFilePolicyReference,
|
|
} from "../../src/adapters/browser-files/browser-file-policy-registry.ts";
|
|
import { BrowserFileVault } from "../../src/adapters/browser-files/browser-file-vault.ts";
|
|
import {
|
|
BrowserTransientPreview,
|
|
ObjectUrlLeaseRegistry,
|
|
} from "../../src/adapters/browser-files/object-url-lease.ts";
|
|
|
|
const hardLimits = Object.freeze({
|
|
maxInspectionBytes: 64,
|
|
maxRetainedFileBytes: 1_024,
|
|
maxPreviewBytes: 1_024,
|
|
maxObjectUrlBytes: 1_024,
|
|
maxTransferBytes: 1_024,
|
|
});
|
|
|
|
describe("browser file composition policy registry", () => {
|
|
it("deep-snapshots every policy axis and permits only limit reductions", () => {
|
|
const reference = browserFilePolicyReference(
|
|
"documents",
|
|
"select-preview-download-pdf",
|
|
);
|
|
const selection = {
|
|
policyId: "documents-v1",
|
|
purpose: "document",
|
|
classification: "PERSONAL" as const,
|
|
multiple: false,
|
|
maxCount: 1,
|
|
maxFileBytes: 100,
|
|
maxTotalBytes: 100,
|
|
allowEmpty: false,
|
|
accept: [
|
|
{ mediaType: "application/pdf", extensions: [".pdf"] },
|
|
],
|
|
};
|
|
const inspection = {
|
|
policyId: "documents-v1",
|
|
maxInspectionBytes: 8,
|
|
acceptedSignatures: [
|
|
{
|
|
mediaType: "application/pdf",
|
|
extensions: [".pdf"],
|
|
patterns: [{ offset: 0, bytes: [0x25, 0x50] }],
|
|
},
|
|
],
|
|
};
|
|
const preview = {
|
|
allowedMediaTypes: ["application/pdf"],
|
|
maxPreviewBytes: 100,
|
|
};
|
|
const download = {
|
|
strategy: "PROMPT_AND_STREAM" as const,
|
|
mediaType: "application/pdf",
|
|
safeExtension: ".pdf",
|
|
maxTransferBytes: 100,
|
|
maxBufferedBytes: 50,
|
|
integrity: "REQUIRED" as const,
|
|
};
|
|
const registry = new BrowserFilePolicyRegistry({
|
|
profiles: [
|
|
{
|
|
reference,
|
|
selection,
|
|
inspection,
|
|
preview,
|
|
download,
|
|
},
|
|
],
|
|
hardLimits,
|
|
});
|
|
|
|
selection.maxFileBytes = 1;
|
|
selection.accept[0]!.extensions[0] = ".exe";
|
|
inspection.acceptedSignatures[0]!.patterns[0]!.bytes[0] = 0;
|
|
preview.allowedMediaTypes[0] = "text/html";
|
|
(
|
|
download as { strategy: string }
|
|
).strategy = "BROWSER_MANAGED";
|
|
download.safeExtension = ".exe";
|
|
|
|
expect(
|
|
registry.resolveSelection(reference, {
|
|
maxFileBytes: 80,
|
|
}),
|
|
).toMatchObject({
|
|
ok: true,
|
|
value: {
|
|
maxFileBytes: 80,
|
|
accept: [{ extensions: [".pdf"] }],
|
|
},
|
|
});
|
|
expect(
|
|
registry.resolveSelection(reference, {
|
|
maxFileBytes: 101,
|
|
}),
|
|
).toMatchObject({
|
|
ok: false,
|
|
error: { code: "LIMIT_EXCEEDED" },
|
|
});
|
|
expect(registry.resolveInspection(reference)).toMatchObject({
|
|
ok: true,
|
|
value: {
|
|
acceptedSignatures: [
|
|
{ patterns: [{ bytes: [0x25, 0x50] }] },
|
|
],
|
|
},
|
|
});
|
|
const resolvedPreview = registry.resolvePreview(reference);
|
|
expect(resolvedPreview.ok).toBe(true);
|
|
if (resolvedPreview.ok) {
|
|
expect(
|
|
resolvedPreview.value.allowedMediaTypes.has(
|
|
"application/pdf",
|
|
),
|
|
).toBe(true);
|
|
expect(
|
|
resolvedPreview.value.allowedMediaTypes.has("text/html"),
|
|
).toBe(false);
|
|
(
|
|
resolvedPreview.value.allowedMediaTypes as Set<string>
|
|
).clear();
|
|
expect(
|
|
registry.resolvePreview(reference),
|
|
).toMatchObject({
|
|
ok: true,
|
|
value: {
|
|
allowedMediaTypes: new Set(["application/pdf"]),
|
|
},
|
|
});
|
|
}
|
|
expect(
|
|
registry.resolveDownload(reference, {}),
|
|
).toMatchObject({
|
|
ok: true,
|
|
value: {
|
|
strategy: "PROMPT_AND_STREAM",
|
|
safeExtension: ".pdf",
|
|
},
|
|
});
|
|
expect(
|
|
registry.resolveDownload(
|
|
browserFilePolicyReference("unknown", "unknown"),
|
|
{},
|
|
),
|
|
).toMatchObject({
|
|
ok: false,
|
|
error: { code: "POLICY_REJECTED" },
|
|
});
|
|
expect(
|
|
registry.resolveSelection(
|
|
browserFilePolicyReference(
|
|
"documents",
|
|
"select-preview-download-pdf",
|
|
),
|
|
),
|
|
).toMatchObject({
|
|
ok: false,
|
|
error: { code: "POLICY_REJECTED" },
|
|
});
|
|
expect(
|
|
registry.resolveSelection(
|
|
{
|
|
policyKey: "documents",
|
|
intention: "select-preview-download-pdf",
|
|
} as typeof reference,
|
|
),
|
|
).toMatchObject({
|
|
ok: false,
|
|
error: { code: "POLICY_REJECTED" },
|
|
});
|
|
});
|
|
|
|
it("binds verification receipts to the exact profile, not a reused policyId", async () => {
|
|
const profileA = browserFilePolicyReference(
|
|
"images-a",
|
|
"preview-avatar",
|
|
);
|
|
const profileB = browserFilePolicyReference(
|
|
"images-b",
|
|
"preview-banner",
|
|
);
|
|
const inspection = {
|
|
policyId: "shared-png-v1",
|
|
maxInspectionBytes: 8,
|
|
acceptedSignatures: [
|
|
{
|
|
mediaType: "image/png",
|
|
extensions: [".png"],
|
|
patterns: [{ offset: 0, bytes: [0x89, 0x50] }],
|
|
},
|
|
],
|
|
};
|
|
const policies = new BrowserFilePolicyRegistry({
|
|
profiles: [profileA, profileB].map((reference) => ({
|
|
reference,
|
|
inspection,
|
|
preview: {
|
|
allowedMediaTypes: ["image/png"],
|
|
maxPreviewBytes: 16,
|
|
},
|
|
})),
|
|
hardLimits,
|
|
});
|
|
const vault = new BrowserFileVault({
|
|
policies,
|
|
createReference: () => "file:profile-bound",
|
|
createVerificationReceipt: () =>
|
|
"verification:profile-bound",
|
|
});
|
|
const captured = vault.captureFiles(
|
|
[
|
|
new File(
|
|
[new Uint8Array([0x89, 0x50, 1])],
|
|
"avatar.png",
|
|
{ type: "image/png", lastModified: 1 },
|
|
),
|
|
],
|
|
{
|
|
policyId: "capture-v1",
|
|
purpose: "capture",
|
|
classification: "PERSONAL",
|
|
multiple: false,
|
|
maxCount: 1,
|
|
maxFileBytes: 16,
|
|
maxTotalBytes: 16,
|
|
allowEmpty: false,
|
|
accept: [
|
|
{ mediaType: "image/png", extensions: [".png"] },
|
|
],
|
|
},
|
|
);
|
|
expect(captured.ok).toBe(true);
|
|
if (!captured.ok || !captured.value[0]) return;
|
|
const inspected = await vault.inspect({
|
|
ref: captured.value[0].ref,
|
|
policy: profileA,
|
|
signal: new AbortController().signal,
|
|
});
|
|
expect(inspected.ok).toBe(true);
|
|
if (!inspected.ok || !inspected.value.verificationReceipt) {
|
|
return;
|
|
}
|
|
const createObjectURL = vi.fn(() => "blob:profile-bound");
|
|
const previews = new BrowserTransientPreview({
|
|
files: vault,
|
|
policies,
|
|
hardMaxPreviewBytes: 16,
|
|
leases: new ObjectUrlLeaseRegistry({
|
|
createObjectURL,
|
|
revokeObjectURL() {},
|
|
}),
|
|
});
|
|
|
|
expect(
|
|
await previews.create({
|
|
ref: captured.value[0].ref,
|
|
verificationReceipt:
|
|
inspected.value.verificationReceipt,
|
|
policy: profileB,
|
|
signal: new AbortController().signal,
|
|
}),
|
|
).toMatchObject({
|
|
ok: false,
|
|
error: { code: "POLICY_REJECTED" },
|
|
});
|
|
expect(createObjectURL).not.toHaveBeenCalled();
|
|
expect(
|
|
await previews.create({
|
|
ref: captured.value[0].ref,
|
|
verificationReceipt:
|
|
inspected.value.verificationReceipt,
|
|
policy: profileA,
|
|
signal: new AbortController().signal,
|
|
}),
|
|
).toMatchObject({ ok: true });
|
|
});
|
|
});
|