BT-X-01: add the shared abortable-operation utility with golden tests for first terminal owner, idempotent close, listener and timer cleanup under a throwing scheduler, observed late rejection and late-handle compensation. It carries no subsystem result taxonomy. BT-PRE-01: make the presigned download lease lazy and single-start. open() now validates, claims and consumes the capability without any network I/O; the fetch, the transfer deadline and the expiry recheck happen at first stream consumption. The source gained close(), which discards an unused lease with no I/O and otherwise cancels the body and releases the scope exactly once. BT-UP-01: require removeEventListener in the AbortSignal structural guard and isolate release cleanup so a hostile facade cannot replace a typed terminal result with a rejection. BT-UP-03: deleteDatabase cannot be cancelled after dispatch, so a blocked deadline now returns PENDING with effect UNKNOWN instead of a failure that reads as NOT_APPLIED. A realm-scoped (factory, databaseName) registry prevents recreating the partition until the native request settles. BT-UP-04: reject non-finite and negative upload clocks as a dependency failure instead of letting them bypass every capability expiry comparison. BT-IMG-02: replace the naive Cache-Control quote stripping with a quote- and escape-aware tokenizer, so max-age="60 or 60" is no longer read as 60 and a comma inside a quoted extension is not a directive boundary. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2226 lines
61 KiB
TypeScript
2226 lines
61 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
|
|
import type {
|
|
BackendIssuedImageAsset,
|
|
ImageCapabilityVerifier,
|
|
ImagePresetReference,
|
|
ImageProbeRequest,
|
|
PublicImmutableImageAsset,
|
|
} from "../../src/application/ports/browser-transfer/image-cdn.ts";
|
|
import {
|
|
createBrowserImageProbe,
|
|
type ImageProbeScheduler,
|
|
} from "../../src/adapters/browser-transfer/image-cdn/browser-image-probe.ts";
|
|
import {
|
|
IMAGE_CDN_IMPLEMENTATION_CEILINGS,
|
|
ImageCdnPolicyRegistry,
|
|
imageCdnPresetReference,
|
|
type ImageCdnHardLimits,
|
|
type ImageCdnPresetPolicy,
|
|
} from "../../src/adapters/browser-transfer/image-cdn/image-cdn-policy.ts";
|
|
import {
|
|
computeImageCapabilityBindingDigestHex,
|
|
createImageCdnRuntime,
|
|
DEFAULT_IMAGE_CAPABILITY_VERIFICATION_TIMEOUT_MS,
|
|
MAX_IMAGE_CAPABILITY_VERIFICATION_TIMEOUT_MS,
|
|
type ImageCapabilityVerificationScheduler,
|
|
} from "../../src/adapters/browser-transfer/image-cdn/image-cdn-runtime.ts";
|
|
import { createP256ImageCapabilityVerifier } from "../../src/adapters/browser-transfer/image-cdn/p256-image-capability-verifier.ts";
|
|
|
|
const hardLimits: ImageCdnHardLimits = Object.freeze({
|
|
maxIntrinsicWidth: 4_096,
|
|
maxIntrinsicHeight: 4_096,
|
|
maxSourcePixels: 16_777_216,
|
|
maxCssDimension: 2_048,
|
|
maxDpr: 2,
|
|
maxQuality: 90,
|
|
maxCandidateCount: 8,
|
|
maxTransformedPixels: 1_048_576,
|
|
maxDecodedBytes: 4_194_304,
|
|
maxEncodedBytes: 524_288,
|
|
maxUrlLength: 2_048,
|
|
maxCapabilityLifetimeMs: 3_600_000,
|
|
maxClockSkewMs: 60_000,
|
|
minCapabilityRemainingMs: 30_000,
|
|
maxPresetBindingsPerCapability: 8,
|
|
maxConcurrentCapabilityVerifications: 8,
|
|
allowedSourceMediaTypes: [
|
|
"image/avif",
|
|
"image/jpeg",
|
|
"image/png",
|
|
"image/webp",
|
|
] as const,
|
|
formatQualityCeilings: {
|
|
avif: 80,
|
|
jpeg: 85,
|
|
png: 90,
|
|
webp: 85,
|
|
},
|
|
});
|
|
|
|
function acceptsTestImageKey(keyId: string): boolean {
|
|
return keyId === "image-signing-2026-01";
|
|
}
|
|
|
|
function createPreset(
|
|
reference: ImagePresetReference,
|
|
overrides: Partial<ImageCdnPresetPolicy> = {},
|
|
): ImageCdnPresetPolicy {
|
|
return {
|
|
reference,
|
|
bindingId: "card-landscape-v1",
|
|
width: 640,
|
|
height: 360,
|
|
fit: "cover",
|
|
dprs: [2, 1],
|
|
responsiveWidths: [640, 320],
|
|
quality: 80,
|
|
formats: ["avif", "webp", "jpeg"],
|
|
sizes: "(max-width: 640px) 100vw, 640px",
|
|
loading: "eager",
|
|
decoding: "async",
|
|
fetchPriority: "high",
|
|
referrerPolicy: "strict-origin-when-cross-origin",
|
|
probeMode: "NONE",
|
|
allowUpscale: false,
|
|
maxTransformedPixels: 1_048_576,
|
|
maxDecodedBytes: 4_194_304,
|
|
maxEncodedBytes: 524_288,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function createPolicies(
|
|
presets: readonly ImageCdnPresetPolicy[],
|
|
overrides: Readonly<{
|
|
hardLimits?: ImageCdnHardLimits;
|
|
acceptedKeyIds?: readonly string[];
|
|
}> = {},
|
|
): ImageCdnPolicyRegistry {
|
|
return new ImageCdnPolicyRegistry({
|
|
applicationOrigin: "https://app.example.test",
|
|
origins: [
|
|
{
|
|
originKey: "product-images",
|
|
origin: "https://images.example.test",
|
|
assetPathPrefix: "/v1/assets/",
|
|
minimumPublicMaxAgeSeconds: 31_536_000,
|
|
},
|
|
],
|
|
presets,
|
|
hardLimits: overrides.hardLimits ?? hardLimits,
|
|
capability: {
|
|
issuer: "image-bff",
|
|
acceptedKeyIds: overrides.acceptedKeyIds ?? [
|
|
"image-signing-2026-01",
|
|
],
|
|
},
|
|
});
|
|
}
|
|
|
|
function publicAsset(
|
|
overrides: Partial<PublicImmutableImageAsset> = {},
|
|
): PublicImmutableImageAsset {
|
|
return {
|
|
kind: "ALLOWLISTED_PUBLIC",
|
|
originKey: "product-images",
|
|
assetId: "asset_Q3x8pL",
|
|
revision: "rev_a8N2kP4z",
|
|
mediaType: "image/jpeg",
|
|
contentKind: "RASTER_STATIC",
|
|
intrinsicWidth: 2_048,
|
|
intrinsicHeight: 1_152,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
async function privateAsset(
|
|
overrides: Partial<BackendIssuedImageAsset> = {},
|
|
): Promise<BackendIssuedImageAsset> {
|
|
const base: BackendIssuedImageAsset = {
|
|
kind: "BACKEND_ISSUED_PRIVATE",
|
|
issuer: "image-bff",
|
|
originKey: "product-images",
|
|
assetId: "asset_Q3x8pL",
|
|
revision: "rev_a8N2kP4z",
|
|
mediaType: "image/jpeg",
|
|
contentKind: "RASTER_STATIC",
|
|
intrinsicWidth: 2_048,
|
|
intrinsicHeight: 1_152,
|
|
capabilityId: "cap_Z8m2Q7pR",
|
|
issuedAtEpochMs: 1_000_000,
|
|
expiresAtEpochMs: 1_300_000,
|
|
allowedPresetBindingIds: ["card-landscape-v1"],
|
|
signature: {
|
|
algorithm: "ECDSA_P256_SHA256",
|
|
keyId: "image-signing-2026-01",
|
|
capabilityBindingDigestHex: "0".repeat(64),
|
|
valueBase64Url: "A".repeat(86),
|
|
},
|
|
...overrides,
|
|
};
|
|
const descriptor = {
|
|
...base,
|
|
signature: {
|
|
...base.signature,
|
|
...(overrides.signature ?? {}),
|
|
},
|
|
};
|
|
const digest = await computeImageCapabilityBindingDigestHex(
|
|
globalThis.crypto.subtle,
|
|
descriptor,
|
|
);
|
|
return {
|
|
...descriptor,
|
|
signature: {
|
|
...descriptor.signature,
|
|
capabilityBindingDigestHex: digest,
|
|
},
|
|
};
|
|
}
|
|
|
|
describe("production image CDN runtime", () => {
|
|
it("builds sorted, duplicate-free responsive candidates from a named preset", async () => {
|
|
const preset = imageCdnPresetReference(
|
|
"card-landscape",
|
|
"render-public-product-image",
|
|
);
|
|
const runtime = createImageCdnRuntime({
|
|
policies: createPolicies([createPreset(preset)]),
|
|
now: () => 1_000_000,
|
|
subtle: globalThis.crypto.subtle,
|
|
capabilityVerifier: {
|
|
acceptsKey: acceptsTestImageKey,
|
|
verify: vi.fn().mockResolvedValue(true),
|
|
},
|
|
});
|
|
const accepted = runtime.assets.acceptPublicImmutable(
|
|
publicAsset(),
|
|
);
|
|
expect(accepted.ok).toBe(true);
|
|
if (!accepted.ok) return;
|
|
|
|
const resolved = await runtime.presentation.resolve({
|
|
asset: accepted.value,
|
|
preset,
|
|
});
|
|
expect(resolved.ok).toBe(true);
|
|
if (!resolved.ok) return;
|
|
|
|
const widthDescriptors = resolved.value.srcSet
|
|
.split(", ")
|
|
.map((candidate) =>
|
|
Number(candidate.match(/ (\d+)w$/u)?.[1]),
|
|
);
|
|
expect(widthDescriptors).toEqual([320, 640, 1_280]);
|
|
expect(new Set(widthDescriptors).size).toBe(3);
|
|
expect(resolved.value.sources.map((source) => source.type)).toEqual(
|
|
["image/avif", "image/webp"],
|
|
);
|
|
expect(resolved.value.fallbackMediaType).toBe("image/jpeg");
|
|
expect(resolved.value).toMatchObject({
|
|
width: 640,
|
|
height: 360,
|
|
loading: "eager",
|
|
decoding: "async",
|
|
fetchPriority: "high",
|
|
crossOrigin: "anonymous",
|
|
delivery: {
|
|
class: "PUBLIC_IMMUTABLE",
|
|
browserCache: "PUBLIC_IMMUTABLE",
|
|
sharedCache: "PUBLIC_IMMUTABLE",
|
|
purge: "REVISION_ROLLOVER",
|
|
expiresAtEpochMs: null,
|
|
},
|
|
decodeBudget: {
|
|
maximumCandidatePixels: 921_600,
|
|
maximumDecodedBytes: 3_686_400,
|
|
},
|
|
});
|
|
|
|
const primary = new URL(resolved.value.src);
|
|
expect(primary.origin).toBe("https://images.example.test");
|
|
expect(primary.pathname).toBe(
|
|
"/v1/assets/asset_Q3x8pL/rev_a8N2kP4z",
|
|
);
|
|
expect([...primary.searchParams.keys()]).toEqual([
|
|
"dpr",
|
|
"fit",
|
|
"format",
|
|
"height",
|
|
"preset",
|
|
"quality",
|
|
"width",
|
|
]);
|
|
expect(Object.fromEntries(primary.searchParams)).toEqual({
|
|
dpr: "1",
|
|
fit: "cover",
|
|
format: "jpeg",
|
|
height: "360",
|
|
preset: "card-landscape-v1",
|
|
quality: "80",
|
|
width: "640",
|
|
});
|
|
});
|
|
|
|
it("rejects raw URL/query/transform input and forged preset or asset references", async () => {
|
|
const preset = imageCdnPresetReference(
|
|
"card-landscape",
|
|
"render-public-product-image",
|
|
);
|
|
const runtime = createImageCdnRuntime({
|
|
policies: createPolicies([createPreset(preset)]),
|
|
now: () => 1_000_000,
|
|
subtle: globalThis.crypto.subtle,
|
|
capabilityVerifier: {
|
|
acceptsKey: acceptsTestImageKey,
|
|
verify: vi.fn().mockResolvedValue(true),
|
|
},
|
|
});
|
|
|
|
expect(
|
|
runtime.assets.acceptPublicImmutable({
|
|
...publicAsset(),
|
|
sourceUrl: `${"java"}script:alert(1)`,
|
|
} as PublicImmutableImageAsset),
|
|
).toMatchObject({
|
|
ok: false,
|
|
error: { code: "INVALID_INPUT" },
|
|
});
|
|
expect(
|
|
runtime.assets.acceptPublicImmutable(
|
|
publicAsset({
|
|
assetId: "../../admin?width=9999",
|
|
}),
|
|
),
|
|
).toMatchObject({
|
|
ok: false,
|
|
error: { code: "POLICY_REJECTED" },
|
|
});
|
|
expect(
|
|
runtime.assets.acceptPublicImmutable(
|
|
publicAsset({
|
|
mediaType: "image/svg+xml",
|
|
} as unknown as Partial<PublicImmutableImageAsset>),
|
|
),
|
|
).toMatchObject({
|
|
ok: false,
|
|
error: { code: "POLICY_REJECTED" },
|
|
});
|
|
|
|
const accepted = runtime.assets.acceptPublicImmutable(
|
|
publicAsset(),
|
|
);
|
|
expect(accepted.ok).toBe(true);
|
|
if (!accepted.ok) return;
|
|
await expect(
|
|
runtime.presentation.resolve({
|
|
asset: accepted.value,
|
|
preset,
|
|
width: 9_999,
|
|
query: "format=svg",
|
|
src: "data:text/html,active",
|
|
} as Parameters<typeof runtime.presentation.resolve>[0]),
|
|
).resolves.toMatchObject({
|
|
ok: false,
|
|
error: { code: "INVALID_INPUT" },
|
|
});
|
|
await expect(
|
|
runtime.presentation.resolve({
|
|
asset: accepted.value,
|
|
preset: imageCdnPresetReference(
|
|
"card-landscape",
|
|
"render-public-product-image",
|
|
),
|
|
}),
|
|
).resolves.toMatchObject({
|
|
ok: false,
|
|
error: { code: "POLICY_REJECTED" },
|
|
});
|
|
await expect(
|
|
runtime.presentation.resolve({
|
|
asset: {} as typeof accepted.value,
|
|
preset,
|
|
}),
|
|
).resolves.toMatchObject({
|
|
ok: false,
|
|
error: { code: "POLICY_REJECTED" },
|
|
});
|
|
});
|
|
|
|
it("deep-snapshots origin and preset policy against TOCTOU mutation", async () => {
|
|
const presetReference = imageCdnPresetReference(
|
|
"card-landscape",
|
|
"render-public-product-image",
|
|
);
|
|
const origin = {
|
|
originKey: "product-images",
|
|
origin: "https://images.example.test",
|
|
assetPathPrefix: "/v1/assets/",
|
|
minimumPublicMaxAgeSeconds: 31_536_000,
|
|
};
|
|
const preset = createPreset(presetReference);
|
|
const policies = new ImageCdnPolicyRegistry({
|
|
applicationOrigin: "https://app.example.test",
|
|
origins: [origin],
|
|
presets: [preset],
|
|
hardLimits,
|
|
capability: {
|
|
issuer: "image-bff",
|
|
acceptedKeyIds: ["image-signing-2026-01"],
|
|
},
|
|
});
|
|
origin.origin = "https://attacker.invalid";
|
|
origin.assetPathPrefix = "/stolen/";
|
|
(preset.dprs as number[])[0] = 4;
|
|
(preset.formats as string[])[0] = "svg";
|
|
(preset as { quality: number }).quality = 100;
|
|
|
|
const runtime = createImageCdnRuntime({ policies });
|
|
const accepted = runtime.assets.acceptPublicImmutable(
|
|
publicAsset(),
|
|
);
|
|
expect(accepted.ok).toBe(true);
|
|
if (!accepted.ok) return;
|
|
const resolved = await runtime.presentation.resolve({
|
|
asset: accepted.value,
|
|
preset: presetReference,
|
|
});
|
|
expect(resolved.ok).toBe(true);
|
|
if (!resolved.ok) return;
|
|
const url = new URL(resolved.value.src);
|
|
expect(url.origin).toBe("https://images.example.test");
|
|
expect(url.pathname.startsWith("/v1/assets/")).toBe(true);
|
|
expect(url.searchParams.get("quality")).toBe("80");
|
|
expect(resolved.value.sources[0]?.type).toBe("image/avif");
|
|
});
|
|
|
|
it("rejects unsafe origins and preset budgets at composition time", () => {
|
|
const preset = imageCdnPresetReference(
|
|
"unsafe",
|
|
"reject-policy",
|
|
);
|
|
expect(
|
|
() =>
|
|
new ImageCdnPolicyRegistry({
|
|
applicationOrigin: "https://app.example.test",
|
|
origins: [
|
|
{
|
|
originKey: "bad",
|
|
origin: "http://user:password@example.test/?q=x#x",
|
|
assetPathPrefix: "/../assets/",
|
|
minimumPublicMaxAgeSeconds: 1,
|
|
},
|
|
],
|
|
presets: [createPreset(preset)],
|
|
hardLimits,
|
|
capability: {
|
|
issuer: "image-bff",
|
|
acceptedKeyIds: ["image-signing-2026-01"],
|
|
},
|
|
}),
|
|
).toThrow(/origin policy/u);
|
|
expect(
|
|
() =>
|
|
createPolicies([
|
|
createPreset(preset, {
|
|
maxTransformedPixels: 100,
|
|
maxDecodedBytes: 400,
|
|
}),
|
|
]),
|
|
).toThrow(/pixel budget/u);
|
|
|
|
const validRegistryOptions = {
|
|
origins: [
|
|
{
|
|
originKey: "product-images",
|
|
origin: "https://images.example.test",
|
|
assetPathPrefix: "/v1/assets/",
|
|
minimumPublicMaxAgeSeconds: 31_536_000,
|
|
},
|
|
],
|
|
presets: [createPreset(preset)],
|
|
hardLimits,
|
|
capability: {
|
|
issuer: "image-bff",
|
|
acceptedKeyIds: ["image-signing-2026-01"],
|
|
},
|
|
};
|
|
expect(
|
|
() =>
|
|
new ImageCdnPolicyRegistry({
|
|
...validRegistryOptions,
|
|
applicationOrigin: "https://images.example.test",
|
|
}),
|
|
).toThrow(/cross-origin/u);
|
|
for (const applicationOrigin of [
|
|
"http://app.example.test",
|
|
"https://app.example.test/",
|
|
]) {
|
|
expect(
|
|
() =>
|
|
new ImageCdnPolicyRegistry({
|
|
...validRegistryOptions,
|
|
applicationOrigin,
|
|
}),
|
|
).toThrow(/application origin/u);
|
|
}
|
|
});
|
|
|
|
it("enforces immutable implementation ceilings on every resource-bearing hard limit", () => {
|
|
const preset = imageCdnPresetReference(
|
|
"absolute-ceilings",
|
|
"bound-image-composition",
|
|
);
|
|
const ceiling = IMAGE_CDN_IMPLEMENTATION_CEILINGS;
|
|
const overCeiling: readonly Partial<ImageCdnHardLimits>[] = [
|
|
{ maxIntrinsicWidth: ceiling.maxIntrinsicWidth + 1 },
|
|
{ maxIntrinsicHeight: ceiling.maxIntrinsicHeight + 1 },
|
|
{ maxSourcePixels: ceiling.maxSourcePixels + 1 },
|
|
{ maxCssDimension: ceiling.maxCssDimension + 1 },
|
|
{ maxDpr: ceiling.maxDpr + 1 },
|
|
{ maxQuality: ceiling.maxQuality + 1 },
|
|
{ maxCandidateCount: ceiling.maxCandidateCount + 1 },
|
|
{
|
|
maxTransformedPixels:
|
|
ceiling.maxTransformedPixels + 1,
|
|
},
|
|
{ maxDecodedBytes: ceiling.maxDecodedBytes + 1 },
|
|
{ maxEncodedBytes: ceiling.maxEncodedBytes + 1 },
|
|
{ maxUrlLength: ceiling.maxUrlLength + 1 },
|
|
{
|
|
maxCapabilityLifetimeMs:
|
|
ceiling.maxCapabilityLifetimeMs + 1,
|
|
},
|
|
{ maxClockSkewMs: ceiling.maxClockSkewMs + 1 },
|
|
{
|
|
minCapabilityRemainingMs:
|
|
ceiling.maxMinimumCapabilityRemainingMs + 1,
|
|
},
|
|
{
|
|
maxPresetBindingsPerCapability:
|
|
ceiling.maxPresetBindingsPerCapability + 1,
|
|
},
|
|
{
|
|
maxConcurrentCapabilityVerifications:
|
|
ceiling.maxConcurrentCapabilityVerifications + 1,
|
|
},
|
|
];
|
|
for (const override of overCeiling) {
|
|
expect(() =>
|
|
createPolicies([createPreset(preset)], {
|
|
hardLimits: { ...hardLimits, ...override },
|
|
}),
|
|
).toThrow(/hard limits/u);
|
|
}
|
|
|
|
const boundaryLimits: ImageCdnHardLimits = {
|
|
...hardLimits,
|
|
maxIntrinsicWidth: ceiling.maxIntrinsicWidth,
|
|
maxIntrinsicHeight: ceiling.maxIntrinsicHeight,
|
|
maxSourcePixels: ceiling.maxSourcePixels,
|
|
maxCssDimension: ceiling.maxCssDimension,
|
|
maxDpr: ceiling.maxDpr,
|
|
maxQuality: ceiling.maxQuality,
|
|
maxCandidateCount: ceiling.maxCandidateCount,
|
|
maxTransformedPixels: ceiling.maxTransformedPixels,
|
|
maxDecodedBytes: ceiling.maxDecodedBytes,
|
|
maxEncodedBytes: ceiling.maxEncodedBytes,
|
|
maxUrlLength: ceiling.maxUrlLength,
|
|
maxCapabilityLifetimeMs:
|
|
ceiling.maxCapabilityLifetimeMs,
|
|
maxClockSkewMs: ceiling.maxClockSkewMs,
|
|
minCapabilityRemainingMs:
|
|
ceiling.maxMinimumCapabilityRemainingMs,
|
|
maxPresetBindingsPerCapability:
|
|
ceiling.maxPresetBindingsPerCapability,
|
|
maxConcurrentCapabilityVerifications:
|
|
ceiling.maxConcurrentCapabilityVerifications,
|
|
};
|
|
expect(() =>
|
|
createPolicies([createPreset(preset)], {
|
|
hardLimits: boundaryLimits,
|
|
}),
|
|
).not.toThrow();
|
|
});
|
|
|
|
it("snapshots a bounded unique capability-key overlap set", () => {
|
|
const preset = imageCdnPresetReference(
|
|
"key-policy",
|
|
"rotate-image-signing-key",
|
|
);
|
|
for (const acceptedKeyIds of [
|
|
[],
|
|
["image-key-current", "image-key-current"],
|
|
Array.from(
|
|
{
|
|
length:
|
|
IMAGE_CDN_IMPLEMENTATION_CEILINGS.maxAcceptedKeyIds +
|
|
1,
|
|
},
|
|
(_, index) => `image-key-${index}`,
|
|
),
|
|
["image-key-current", "invalid/key"],
|
|
]) {
|
|
expect(() =>
|
|
createPolicies([createPreset(preset)], {
|
|
acceptedKeyIds,
|
|
}),
|
|
).toThrow(/capability policy/u);
|
|
}
|
|
|
|
const mutableKeyIds = [
|
|
"image-signing-2026-02",
|
|
"image-signing-2026-01",
|
|
];
|
|
const policies = createPolicies([createPreset(preset)], {
|
|
acceptedKeyIds: mutableKeyIds,
|
|
});
|
|
mutableKeyIds[0] = "image-signing-attacker";
|
|
expect(policies.capabilityPolicy().acceptedKeyIds).toEqual([
|
|
"image-signing-2026-02",
|
|
"image-signing-2026-01",
|
|
]);
|
|
expect(
|
|
Object.isFrozen(
|
|
policies.capabilityPolicy().acceptedKeyIds,
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("accepts old and new overlap keys only when policy and verifier registries both cover them", async () => {
|
|
const preset = imageCdnPresetReference(
|
|
"key-overlap",
|
|
"verify-image-key-rotation",
|
|
);
|
|
const acceptedKeyIds = [
|
|
"image-signing-2026-02",
|
|
"image-signing-2026-01",
|
|
] as const;
|
|
const policies = createPolicies([createPreset(preset)], {
|
|
acceptedKeyIds,
|
|
});
|
|
const verifierKeys = new Set<string>(acceptedKeyIds);
|
|
const verify = vi.fn(
|
|
async (request: { keyId: string }) =>
|
|
verifierKeys.has(request.keyId),
|
|
);
|
|
const runtime = createImageCdnRuntime({
|
|
policies,
|
|
now: () => 1_000_000,
|
|
subtle: globalThis.crypto.subtle,
|
|
capabilityVerifier: {
|
|
acceptsKey: (keyId) => verifierKeys.has(keyId),
|
|
verify,
|
|
},
|
|
});
|
|
const previous = await privateAsset();
|
|
const current = await privateAsset({
|
|
signature: {
|
|
algorithm: "ECDSA_P256_SHA256",
|
|
keyId: "image-signing-2026-02",
|
|
capabilityBindingDigestHex: "0".repeat(64),
|
|
valueBase64Url: "A".repeat(86),
|
|
},
|
|
});
|
|
await expect(
|
|
runtime.assets.acceptBackendIssued(previous),
|
|
).resolves.toMatchObject({ ok: true });
|
|
await expect(
|
|
runtime.assets.acceptBackendIssued(current),
|
|
).resolves.toMatchObject({ ok: true });
|
|
expect(verify.mock.calls.map(([request]) => request.keyId)).toEqual(
|
|
["image-signing-2026-01", "image-signing-2026-02"],
|
|
);
|
|
|
|
const unknown = await privateAsset({
|
|
signature: {
|
|
algorithm: "ECDSA_P256_SHA256",
|
|
keyId: "image-signing-2027-01",
|
|
capabilityBindingDigestHex: "0".repeat(64),
|
|
valueBase64Url: "A".repeat(86),
|
|
},
|
|
});
|
|
await expect(
|
|
runtime.assets.acceptBackendIssued(unknown),
|
|
).resolves.toMatchObject({
|
|
ok: false,
|
|
error: { code: "POLICY_REJECTED" },
|
|
});
|
|
expect(verify).toHaveBeenCalledTimes(2);
|
|
|
|
expect(() =>
|
|
createImageCdnRuntime({
|
|
policies,
|
|
subtle: globalThis.crypto.subtle,
|
|
capabilityVerifier: {
|
|
acceptsKey: (keyId) =>
|
|
keyId === "image-signing-2026-02",
|
|
verify: vi.fn().mockResolvedValue(true),
|
|
},
|
|
}),
|
|
).toThrow(/registry does not cover/u);
|
|
expect(() =>
|
|
createImageCdnRuntime({
|
|
policies,
|
|
subtle: globalThis.crypto.subtle,
|
|
capabilityVerifier: {
|
|
acceptsKey() {
|
|
throw new TypeError("registry unavailable");
|
|
},
|
|
verify: vi.fn().mockResolvedValue(true),
|
|
},
|
|
}),
|
|
).toThrow(/registry does not cover/u);
|
|
});
|
|
|
|
it("binds a private descriptor to its signed snapshot and exact preset", async () => {
|
|
const preset = imageCdnPresetReference(
|
|
"card-landscape",
|
|
"render-private-product-image",
|
|
);
|
|
const policies = createPolicies([
|
|
createPreset(preset, {
|
|
probeMode: "PRIMARY_REQUIRED",
|
|
}),
|
|
]);
|
|
let releaseVerification:
|
|
((value: boolean) => void) | undefined;
|
|
const verifierMethod = vi.fn(
|
|
() =>
|
|
new Promise<boolean>((resolve) => {
|
|
releaseVerification = resolve;
|
|
}),
|
|
);
|
|
const verifier: ImageCapabilityVerifier = {
|
|
acceptsKey: acceptsTestImageKey,
|
|
verify: verifierMethod,
|
|
};
|
|
let now = 1_000_000;
|
|
const runtime = createImageCdnRuntime({
|
|
policies,
|
|
now: () => now,
|
|
subtle: globalThis.crypto.subtle,
|
|
capabilityVerifier: verifier,
|
|
probe: {
|
|
async probe(request) {
|
|
return {
|
|
ok: true,
|
|
value: {
|
|
absoluteUrl: request.absoluteUrl,
|
|
mediaType: request.expectedMediaType,
|
|
encodedBytes: 1_024,
|
|
decodedWidth: request.expectedWidth,
|
|
decodedHeight: request.expectedHeight,
|
|
},
|
|
};
|
|
},
|
|
},
|
|
});
|
|
(
|
|
verifier as {
|
|
verify: ImageCapabilityVerifier["verify"];
|
|
}
|
|
).verify = vi.fn().mockResolvedValue(false);
|
|
const mutable = (await privateAsset()) as {
|
|
-readonly [Key in keyof BackendIssuedImageAsset]:
|
|
BackendIssuedImageAsset[Key];
|
|
};
|
|
const pending = runtime.assets.acceptBackendIssued(mutable);
|
|
await vi.waitFor(() => {
|
|
expect(verifierMethod).toHaveBeenCalledOnce();
|
|
});
|
|
mutable.assetId = "asset_ATTACKER";
|
|
mutable.revision = "rev_ATTACKER";
|
|
mutable.expiresAtEpochMs = 9_999_999;
|
|
releaseVerification?.(true);
|
|
const accepted = await pending;
|
|
expect(accepted.ok).toBe(true);
|
|
if (!accepted.ok) return;
|
|
|
|
const resolved = await runtime.presentation.resolve({
|
|
asset: accepted.value,
|
|
preset,
|
|
signal: new AbortController().signal,
|
|
});
|
|
expect(resolved.ok).toBe(true);
|
|
if (!resolved.ok) return;
|
|
const url = new URL(resolved.value.src);
|
|
expect(url.pathname).toContain(
|
|
"/asset_Q3x8pL/rev_a8N2kP4z",
|
|
);
|
|
expect(url.pathname).not.toContain("ATTACKER");
|
|
expect([...url.searchParams.keys()]).toEqual([
|
|
"binding",
|
|
"capability",
|
|
"dpr",
|
|
"expires",
|
|
"fit",
|
|
"format",
|
|
"height",
|
|
"key",
|
|
"preset",
|
|
"quality",
|
|
"signature",
|
|
"width",
|
|
]);
|
|
expect(resolved.value).toMatchObject({
|
|
loading: "eager",
|
|
referrerPolicy: "no-referrer",
|
|
crossOrigin: "anonymous",
|
|
delivery: {
|
|
class: "PRIVATE_SIGNED",
|
|
browserCache: "NO_STORE",
|
|
sharedCache: "FORBIDDEN",
|
|
purge: "CAPABILITY_REVOCATION_OR_EXPIRY",
|
|
expiresAtEpochMs: 1_300_000,
|
|
},
|
|
});
|
|
|
|
now = 1_275_001;
|
|
await expect(
|
|
runtime.presentation.resolve({
|
|
asset: accepted.value,
|
|
preset,
|
|
}),
|
|
).resolves.toMatchObject({
|
|
ok: false,
|
|
error: { code: "EXPIRED_RESOURCE" },
|
|
});
|
|
});
|
|
|
|
it("rejects invalid binding/signature and private lazy or low-priority presets", async () => {
|
|
const eager = imageCdnPresetReference(
|
|
"private-eager",
|
|
"render-private",
|
|
);
|
|
const lazy = imageCdnPresetReference(
|
|
"private-lazy",
|
|
"render-private",
|
|
);
|
|
const policies = createPolicies([
|
|
createPreset(eager, {
|
|
bindingId: "private-eager-v1",
|
|
}),
|
|
createPreset(lazy, {
|
|
bindingId: "private-lazy-v1",
|
|
loading: "lazy",
|
|
fetchPriority: "low",
|
|
}),
|
|
]);
|
|
const runtime = createImageCdnRuntime({
|
|
policies,
|
|
now: () => 1_000_000,
|
|
subtle: globalThis.crypto.subtle,
|
|
capabilityVerifier: {
|
|
acceptsKey: acceptsTestImageKey,
|
|
verify: vi.fn().mockResolvedValue(true),
|
|
},
|
|
});
|
|
const issued = await privateAsset({
|
|
allowedPresetBindingIds: [
|
|
"private-eager-v1",
|
|
"private-lazy-v1",
|
|
],
|
|
});
|
|
const accepted = await runtime.assets.acceptBackendIssued(issued);
|
|
expect(accepted.ok).toBe(true);
|
|
if (!accepted.ok) return;
|
|
await expect(
|
|
runtime.presentation.resolve({
|
|
asset: accepted.value,
|
|
preset: eager,
|
|
signal: new AbortController().signal,
|
|
}),
|
|
).resolves.toMatchObject({
|
|
ok: false,
|
|
error: { code: "POLICY_REJECTED" },
|
|
});
|
|
await expect(
|
|
runtime.presentation.resolve({
|
|
asset: accepted.value,
|
|
preset: lazy,
|
|
}),
|
|
).resolves.toMatchObject({
|
|
ok: false,
|
|
error: { code: "POLICY_REJECTED" },
|
|
});
|
|
|
|
await expect(
|
|
runtime.assets.acceptBackendIssued({
|
|
...issued,
|
|
signature: {
|
|
...issued.signature,
|
|
capabilityBindingDigestHex: "f".repeat(64),
|
|
},
|
|
}),
|
|
).resolves.toMatchObject({
|
|
ok: false,
|
|
error: { code: "INTEGRITY_FAILED" },
|
|
});
|
|
|
|
const rejectingRuntime = createImageCdnRuntime({
|
|
policies,
|
|
now: () => 1_000_000,
|
|
subtle: globalThis.crypto.subtle,
|
|
capabilityVerifier: {
|
|
acceptsKey: acceptsTestImageKey,
|
|
verify: vi.fn().mockResolvedValue(false),
|
|
},
|
|
});
|
|
await expect(
|
|
rejectingRuntime.assets.acceptBackendIssued(issued),
|
|
).resolves.toMatchObject({
|
|
ok: false,
|
|
error: { code: "INTEGRITY_FAILED" },
|
|
});
|
|
});
|
|
|
|
it("bounds digest and capability verification with a runtime-owned deadline and caller abort", async () => {
|
|
const preset = imageCdnPresetReference(
|
|
"deadline",
|
|
"verify-private-capability",
|
|
);
|
|
const policies = createPolicies([createPreset(preset)]);
|
|
const issued = await privateAsset();
|
|
const observations: Record<string, unknown>[] = [];
|
|
|
|
const verifierDeadline =
|
|
manualCapabilityVerificationScheduler();
|
|
const stalledVerifier = vi.fn(
|
|
() => new Promise<boolean>(() => undefined),
|
|
);
|
|
const verifierRuntime = createImageCdnRuntime({
|
|
policies,
|
|
now: () => 1_000_000,
|
|
subtle: globalThis.crypto.subtle,
|
|
capabilityVerifier: {
|
|
acceptsKey: acceptsTestImageKey,
|
|
verify: stalledVerifier,
|
|
},
|
|
capabilityVerificationScheduler:
|
|
verifierDeadline.scheduler,
|
|
observer: {
|
|
record(observation) {
|
|
observations.push(observation);
|
|
},
|
|
},
|
|
});
|
|
const verifierPending =
|
|
verifierRuntime.assets.acceptBackendIssued(issued);
|
|
await vi.waitFor(() => {
|
|
expect(stalledVerifier).toHaveBeenCalledOnce();
|
|
});
|
|
expect(verifierDeadline.delays).toEqual([
|
|
DEFAULT_IMAGE_CAPABILITY_VERIFICATION_TIMEOUT_MS,
|
|
]);
|
|
verifierDeadline.fire();
|
|
await expect(verifierPending).resolves.toMatchObject({
|
|
ok: false,
|
|
error: {
|
|
code: "UNAVAILABLE",
|
|
retryable: true,
|
|
recovery: "RETRY",
|
|
},
|
|
});
|
|
expect(verifierDeadline.clearTimeout).toHaveBeenCalledOnce();
|
|
expect(observations).toEqual([
|
|
expect.objectContaining({
|
|
operation: "IMAGE_RESOLVE",
|
|
outcome: "FAILED",
|
|
failureCode: "UNAVAILABLE",
|
|
countBucket: "ZERO",
|
|
}),
|
|
]);
|
|
|
|
const digestDeadline =
|
|
manualCapabilityVerificationScheduler();
|
|
const stalledDigest = vi.fn(
|
|
() => new Promise<ArrayBuffer>(() => undefined),
|
|
);
|
|
const digestVerifier = vi.fn().mockResolvedValue(true);
|
|
const digestRuntime = createImageCdnRuntime({
|
|
policies,
|
|
now: () => 1_000_000,
|
|
subtle: {
|
|
digest: stalledDigest as SubtleCrypto["digest"],
|
|
},
|
|
capabilityVerifier: {
|
|
acceptsKey: acceptsTestImageKey,
|
|
verify: digestVerifier,
|
|
},
|
|
capabilityVerificationTimeoutMs:
|
|
MAX_IMAGE_CAPABILITY_VERIFICATION_TIMEOUT_MS,
|
|
capabilityVerificationScheduler: digestDeadline.scheduler,
|
|
});
|
|
const digestPending =
|
|
digestRuntime.assets.acceptBackendIssued(issued);
|
|
await vi.waitFor(() => {
|
|
expect(stalledDigest).toHaveBeenCalledOnce();
|
|
});
|
|
expect(digestDeadline.delays).toEqual([
|
|
MAX_IMAGE_CAPABILITY_VERIFICATION_TIMEOUT_MS,
|
|
]);
|
|
digestDeadline.fire();
|
|
await expect(digestPending).resolves.toMatchObject({
|
|
ok: false,
|
|
error: { code: "UNAVAILABLE" },
|
|
});
|
|
expect(digestVerifier).not.toHaveBeenCalled();
|
|
|
|
const abortDeadline =
|
|
manualCapabilityVerificationScheduler();
|
|
const abortVerifier = vi.fn(
|
|
() => new Promise<boolean>(() => undefined),
|
|
);
|
|
const abortRuntime = createImageCdnRuntime({
|
|
policies,
|
|
now: () => 1_000_000,
|
|
subtle: globalThis.crypto.subtle,
|
|
capabilityVerifier: {
|
|
acceptsKey: acceptsTestImageKey,
|
|
verify: abortVerifier,
|
|
},
|
|
capabilityVerificationScheduler: abortDeadline.scheduler,
|
|
});
|
|
const controller = new AbortController();
|
|
const abortPending = abortRuntime.assets.acceptBackendIssued(
|
|
issued,
|
|
{ signal: controller.signal },
|
|
);
|
|
await vi.waitFor(() => {
|
|
expect(abortVerifier).toHaveBeenCalledOnce();
|
|
});
|
|
controller.abort();
|
|
await expect(abortPending).resolves.toMatchObject({
|
|
ok: false,
|
|
error: { code: "ABORTED" },
|
|
});
|
|
expect(abortDeadline.clearTimeout).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it("bounds concurrent capability verification and releases the slot after abort", async () => {
|
|
const preset = imageCdnPresetReference(
|
|
"verification-concurrency",
|
|
"bound-image-verification-concurrency",
|
|
);
|
|
const policies = createPolicies([createPreset(preset)], {
|
|
hardLimits: {
|
|
...hardLimits,
|
|
maxConcurrentCapabilityVerifications: 1,
|
|
},
|
|
});
|
|
let verificationAttempt = 0;
|
|
const verify = vi.fn(() => {
|
|
verificationAttempt += 1;
|
|
return verificationAttempt === 1
|
|
? new Promise<boolean>(() => undefined)
|
|
: Promise.resolve(true);
|
|
});
|
|
const runtime = createImageCdnRuntime({
|
|
policies,
|
|
now: () => 1_000_000,
|
|
subtle: globalThis.crypto.subtle,
|
|
capabilityVerifier: {
|
|
acceptsKey: acceptsTestImageKey,
|
|
verify,
|
|
},
|
|
});
|
|
const issued = await privateAsset();
|
|
const controller = new AbortController();
|
|
const first = runtime.assets.acceptBackendIssued(issued, {
|
|
signal: controller.signal,
|
|
});
|
|
await vi.waitFor(() => {
|
|
expect(verify).toHaveBeenCalledOnce();
|
|
});
|
|
await expect(
|
|
runtime.assets.acceptBackendIssued(issued),
|
|
).resolves.toMatchObject({
|
|
ok: false,
|
|
error: { code: "LIMIT_EXCEEDED" },
|
|
});
|
|
expect(verify).toHaveBeenCalledOnce();
|
|
|
|
controller.abort();
|
|
await expect(first).resolves.toMatchObject({
|
|
ok: false,
|
|
error: { code: "ABORTED" },
|
|
});
|
|
await expect(
|
|
runtime.assets.acceptBackendIssued(issued),
|
|
).resolves.toMatchObject({ ok: true });
|
|
expect(verify).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("fails fast on invalid capability verification deadline composition", () => {
|
|
const preset = imageCdnPresetReference(
|
|
"deadline-config",
|
|
"validate-private-capability-deadline",
|
|
);
|
|
const policies = createPolicies([createPreset(preset)]);
|
|
expect(
|
|
DEFAULT_IMAGE_CAPABILITY_VERIFICATION_TIMEOUT_MS,
|
|
).toBe(5_000);
|
|
expect(MAX_IMAGE_CAPABILITY_VERIFICATION_TIMEOUT_MS).toBe(
|
|
60_000,
|
|
);
|
|
for (const capabilityVerificationTimeoutMs of [
|
|
0,
|
|
1.5,
|
|
MAX_IMAGE_CAPABILITY_VERIFICATION_TIMEOUT_MS + 1,
|
|
]) {
|
|
expect(() =>
|
|
createImageCdnRuntime({
|
|
policies,
|
|
capabilityVerificationTimeoutMs,
|
|
}),
|
|
).toThrow(/verification timeout/u);
|
|
}
|
|
expect(() =>
|
|
createImageCdnRuntime({
|
|
policies,
|
|
capabilityVerificationScheduler: {
|
|
setTimeout: null,
|
|
clearTimeout: vi.fn(),
|
|
} as unknown as ImageCapabilityVerificationScheduler,
|
|
}),
|
|
).toThrow(/verification scheduler/u);
|
|
});
|
|
|
|
it("closes terminally and idempotently, revoking old references and rejecting every later operation", async () => {
|
|
const preset = imageCdnPresetReference(
|
|
"lifecycle",
|
|
"close-image-runtime",
|
|
);
|
|
const runtime = createImageCdnRuntime({
|
|
policies: createPolicies([createPreset(preset)]),
|
|
now: () => 1_000_000,
|
|
subtle: globalThis.crypto.subtle,
|
|
capabilityVerifier: {
|
|
acceptsKey: acceptsTestImageKey,
|
|
verify: vi.fn().mockResolvedValue(true),
|
|
},
|
|
});
|
|
const accepted =
|
|
runtime.assets.acceptPublicImmutable(publicAsset());
|
|
expect(accepted.ok).toBe(true);
|
|
if (!accepted.ok) return;
|
|
await expect(
|
|
runtime.presentation.resolve({
|
|
asset: accepted.value,
|
|
preset,
|
|
}),
|
|
).resolves.toMatchObject({ ok: true });
|
|
const acceptedPrivate =
|
|
await runtime.assets.acceptBackendIssued(
|
|
await privateAsset(),
|
|
);
|
|
expect(acceptedPrivate.ok).toBe(true);
|
|
if (!acceptedPrivate.ok) return;
|
|
|
|
runtime.close();
|
|
expect(() => runtime.close()).not.toThrow();
|
|
await expect(
|
|
runtime.presentation.resolve({
|
|
asset: accepted.value,
|
|
preset,
|
|
}),
|
|
).resolves.toMatchObject({
|
|
ok: false,
|
|
error: {
|
|
code: "UNAVAILABLE",
|
|
retryable: false,
|
|
recovery: "NONE",
|
|
},
|
|
});
|
|
await expect(
|
|
runtime.presentation.resolve({
|
|
asset: acceptedPrivate.value,
|
|
preset,
|
|
signal: new AbortController().signal,
|
|
}),
|
|
).resolves.toMatchObject({
|
|
ok: false,
|
|
error: { code: "UNAVAILABLE" },
|
|
});
|
|
expect(
|
|
runtime.assets.acceptPublicImmutable(publicAsset()),
|
|
).toMatchObject({
|
|
ok: false,
|
|
error: { code: "UNAVAILABLE" },
|
|
});
|
|
await expect(
|
|
runtime.assets.acceptBackendIssued(await privateAsset()),
|
|
).resolves.toMatchObject({
|
|
ok: false,
|
|
error: { code: "UNAVAILABLE" },
|
|
});
|
|
});
|
|
|
|
it("close aborts stalled private verification and image probing", async () => {
|
|
const verifierPreset = imageCdnPresetReference(
|
|
"close-verifier",
|
|
"abort-image-verification",
|
|
);
|
|
const verifierDeadline =
|
|
manualCapabilityVerificationScheduler();
|
|
const stalledVerifier = vi.fn(
|
|
() => new Promise<boolean>(() => undefined),
|
|
);
|
|
const verifierRuntime = createImageCdnRuntime({
|
|
policies: createPolicies([
|
|
createPreset(verifierPreset),
|
|
]),
|
|
now: () => 1_000_000,
|
|
subtle: globalThis.crypto.subtle,
|
|
capabilityVerifier: {
|
|
acceptsKey: acceptsTestImageKey,
|
|
verify: stalledVerifier,
|
|
},
|
|
capabilityVerificationScheduler:
|
|
verifierDeadline.scheduler,
|
|
});
|
|
const verificationPending =
|
|
verifierRuntime.assets.acceptBackendIssued(
|
|
await privateAsset(),
|
|
);
|
|
await vi.waitFor(() => {
|
|
expect(stalledVerifier).toHaveBeenCalledOnce();
|
|
});
|
|
verifierRuntime.close();
|
|
await expect(verificationPending).resolves.toMatchObject({
|
|
ok: false,
|
|
error: { code: "UNAVAILABLE", retryable: false },
|
|
});
|
|
expect(verifierDeadline.clearTimeout).toHaveBeenCalledOnce();
|
|
|
|
const probePreset = imageCdnPresetReference(
|
|
"close-probe",
|
|
"abort-image-probe",
|
|
);
|
|
const stalledProbe = vi.fn(
|
|
(_request: ImageProbeRequest) =>
|
|
new Promise<never>(() => undefined),
|
|
);
|
|
const probeRuntime = createImageCdnRuntime({
|
|
policies: createPolicies([
|
|
createPreset(probePreset, {
|
|
probeMode: "PRIMARY_REQUIRED",
|
|
}),
|
|
]),
|
|
probe: { probe: stalledProbe },
|
|
});
|
|
const accepted =
|
|
probeRuntime.assets.acceptPublicImmutable(publicAsset());
|
|
expect(accepted.ok).toBe(true);
|
|
if (!accepted.ok) return;
|
|
const caller = new AbortController();
|
|
const probePending = probeRuntime.presentation.resolve({
|
|
asset: accepted.value,
|
|
preset: probePreset,
|
|
signal: caller.signal,
|
|
});
|
|
await vi.waitFor(() => {
|
|
expect(stalledProbe).toHaveBeenCalledOnce();
|
|
});
|
|
const runtimeSignal =
|
|
stalledProbe.mock.calls[0]?.[0].signal;
|
|
expect(runtimeSignal).not.toBe(caller.signal);
|
|
probeRuntime.close();
|
|
await expect(probePending).resolves.toMatchObject({
|
|
ok: false,
|
|
error: { code: "UNAVAILABLE", retryable: false },
|
|
});
|
|
expect(runtimeSignal?.aborted).toBe(true);
|
|
});
|
|
|
|
it("requires and validates the bounded probe receipt when the preset says so", async () => {
|
|
const preset = imageCdnPresetReference(
|
|
"probed",
|
|
"render-verified-image",
|
|
);
|
|
const policy = createPreset(preset, {
|
|
bindingId: "probed-v1",
|
|
probeMode: "PRIMARY_REQUIRED",
|
|
});
|
|
const withoutProbe = createImageCdnRuntime({
|
|
policies: createPolicies([policy]),
|
|
});
|
|
const noProbeAsset =
|
|
withoutProbe.assets.acceptPublicImmutable(publicAsset());
|
|
expect(noProbeAsset.ok).toBe(true);
|
|
if (!noProbeAsset.ok) return;
|
|
await expect(
|
|
withoutProbe.presentation.resolve({
|
|
asset: noProbeAsset.value,
|
|
preset,
|
|
signal: new AbortController().signal,
|
|
}),
|
|
).resolves.toMatchObject({
|
|
ok: false,
|
|
error: { code: "UNSUPPORTED" },
|
|
});
|
|
|
|
const probe = vi.fn(async (request: ImageProbeRequest) => ({
|
|
ok: true as const,
|
|
value: {
|
|
absoluteUrl: request.absoluteUrl,
|
|
mediaType: request.expectedMediaType,
|
|
encodedBytes: 1_024,
|
|
decodedWidth: request.expectedWidth,
|
|
decodedHeight: request.expectedHeight,
|
|
},
|
|
}));
|
|
const runtime = createImageCdnRuntime({
|
|
policies: createPolicies([policy]),
|
|
probe: { probe },
|
|
});
|
|
const accepted =
|
|
runtime.assets.acceptPublicImmutable(publicAsset());
|
|
expect(accepted.ok).toBe(true);
|
|
if (!accepted.ok) return;
|
|
const resolved = await runtime.presentation.resolve({
|
|
asset: accepted.value,
|
|
preset,
|
|
signal: new AbortController().signal,
|
|
});
|
|
expect(resolved.ok).toBe(true);
|
|
expect(probe).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
expectedMediaType: "image/jpeg",
|
|
expectedWidth: 640,
|
|
expectedHeight: 360,
|
|
maxEncodedBytes: 524_288,
|
|
maxDecodedBytes: 4_194_304,
|
|
delivery: "PUBLIC_IMMUTABLE",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("records only safe terminal buckets for accept, probe and resolve", async () => {
|
|
const preset = imageCdnPresetReference(
|
|
"observed",
|
|
"render-observed-image",
|
|
);
|
|
const observations: Record<string, unknown>[] = [];
|
|
const observer = {
|
|
record(observation: Record<string, unknown>) {
|
|
observations.push(observation);
|
|
},
|
|
};
|
|
const runtime = createImageCdnRuntime({
|
|
policies: createPolicies([
|
|
createPreset(preset, {
|
|
bindingId: "observed-v1",
|
|
probeMode: "PRIMARY_REQUIRED",
|
|
}),
|
|
]),
|
|
observer,
|
|
probe: {
|
|
async probe(request) {
|
|
return {
|
|
ok: true,
|
|
value: {
|
|
absoluteUrl: request.absoluteUrl,
|
|
mediaType: request.expectedMediaType,
|
|
encodedBytes: 2_048,
|
|
decodedWidth: request.expectedWidth,
|
|
decodedHeight: request.expectedHeight,
|
|
},
|
|
};
|
|
},
|
|
},
|
|
});
|
|
observer.record = () => {
|
|
throw new TypeError("mutated observer must not be observed");
|
|
};
|
|
const accepted =
|
|
runtime.assets.acceptPublicImmutable(publicAsset());
|
|
expect(accepted.ok).toBe(true);
|
|
if (!accepted.ok) return;
|
|
const resolved = await runtime.presentation.resolve({
|
|
asset: accepted.value,
|
|
preset,
|
|
signal: new AbortController().signal,
|
|
});
|
|
expect(resolved.ok).toBe(true);
|
|
expect(observations).toHaveLength(3);
|
|
for (const observation of observations) {
|
|
expect(observation).toMatchObject({
|
|
operation: "IMAGE_RESOLVE",
|
|
outcome: "SUCCEEDED",
|
|
});
|
|
expect(Object.keys(observation)).not.toEqual(
|
|
expect.arrayContaining([
|
|
"assetId",
|
|
"preset",
|
|
"capability",
|
|
"signature",
|
|
"url",
|
|
]),
|
|
);
|
|
expect(JSON.stringify(observation)).not.toContain(
|
|
"images.example.test",
|
|
);
|
|
}
|
|
expect(observations.map((value) => value.countBucket)).toEqual([
|
|
"ONE",
|
|
"ONE",
|
|
"TWO_TO_TEN",
|
|
]);
|
|
expect(observations.map((value) => value.byteBucket)).toEqual([
|
|
undefined,
|
|
"LT1MIB",
|
|
"LT1MIB",
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("browser image probe", () => {
|
|
const imageUrl =
|
|
"https://images.example.test/v1/assets/a/rev?format=png";
|
|
const request = (
|
|
overrides: Partial<ImageProbeRequest> = {},
|
|
): ImageProbeRequest => ({
|
|
absoluteUrl: imageUrl,
|
|
expectedMediaType: "image/png",
|
|
expectedWidth: 640,
|
|
expectedHeight: 360,
|
|
maxEncodedBytes: 1_024,
|
|
maxDecodedPixels: 230_400,
|
|
maxDecodedBytes: 921_600,
|
|
delivery: "PUBLIC_IMMUTABLE",
|
|
minimumPublicMaxAgeSeconds: 31_536_000,
|
|
referrerPolicy: "no-referrer",
|
|
signal: new AbortController().signal,
|
|
...overrides,
|
|
});
|
|
|
|
it("parses all supported static headers before decode and closes each bitmap", async () => {
|
|
const samples = [
|
|
{
|
|
mediaType: "image/png" as const,
|
|
bytes: pngBytes(640, 360),
|
|
},
|
|
{
|
|
mediaType: "image/jpeg" as const,
|
|
bytes: jpegBytes(640, 360),
|
|
},
|
|
{
|
|
mediaType: "image/webp" as const,
|
|
bytes: webpBytes(640, 360),
|
|
},
|
|
{
|
|
mediaType: "image/avif" as const,
|
|
bytes: avifBytes(640, 360),
|
|
},
|
|
];
|
|
const close = vi.fn();
|
|
for (const sample of samples) {
|
|
const exactUrl = imageUrl.replace(
|
|
"format=png",
|
|
`format=${sample.mediaType.slice("image/".length)}`,
|
|
);
|
|
const fetcher = vi.fn(async () =>
|
|
responseAt(exactUrl, sample.bytes, {
|
|
status: 200,
|
|
headers: publicImageHeaders(
|
|
sample.mediaType,
|
|
sample.bytes.byteLength,
|
|
),
|
|
}),
|
|
);
|
|
const probe = createBrowserImageProbe({
|
|
fetcher: fetcher as typeof fetch,
|
|
createBitmap: vi.fn(async () => ({
|
|
width: 640,
|
|
height: 360,
|
|
close,
|
|
})),
|
|
});
|
|
|
|
await expect(
|
|
probe.probe(
|
|
request({
|
|
absoluteUrl: exactUrl,
|
|
expectedMediaType: sample.mediaType,
|
|
}),
|
|
),
|
|
).resolves.toMatchObject({
|
|
ok: true,
|
|
value: {
|
|
absoluteUrl: exactUrl,
|
|
mediaType: sample.mediaType,
|
|
encodedBytes: sample.bytes.byteLength,
|
|
decodedWidth: 640,
|
|
decodedHeight: 360,
|
|
},
|
|
});
|
|
expect(fetcher).toHaveBeenCalledWith(
|
|
exactUrl,
|
|
expect.objectContaining({
|
|
credentials: "omit",
|
|
redirect: "error",
|
|
mode: "cors",
|
|
cache: "no-store",
|
|
referrerPolicy: "no-referrer",
|
|
}),
|
|
);
|
|
}
|
|
expect(close).toHaveBeenCalledTimes(samples.length);
|
|
});
|
|
|
|
it("fails closed on duplicate/conflicting cache directives and oversized bodies", async () => {
|
|
const png = pngBytes(640, 360);
|
|
const createBitmap = vi.fn(async () => ({
|
|
width: 640,
|
|
height: 360,
|
|
close: vi.fn(),
|
|
}));
|
|
for (const cacheControl of [
|
|
// BT-IMG-02. Unmatched quotes must not be unwrapped into a bare number.
|
|
'public, max-age="31536000, immutable',
|
|
'public, max-age=31536000", immutable',
|
|
'public, max-age="31536000\\", immutable',
|
|
"public, public, max-age=31536000, immutable",
|
|
"public, max-age=31536000, s-maxage=60, immutable",
|
|
"public, max-age=31536000, immutable, must-revalidate",
|
|
"public=1, max-age=31536000, immutable",
|
|
"public, max-age=31536000, immutable=true",
|
|
]) {
|
|
const probe = createBrowserImageProbe({
|
|
fetcher: (async () =>
|
|
responseAt(imageUrl, png, {
|
|
status: 200,
|
|
headers: {
|
|
"cache-control": cacheControl,
|
|
"content-type": "image/png",
|
|
},
|
|
})) as typeof fetch,
|
|
createBitmap,
|
|
});
|
|
await expect(probe.probe(request())).resolves.toMatchObject({
|
|
ok: false,
|
|
error: { code: "POLICY_REJECTED" },
|
|
});
|
|
}
|
|
|
|
const probe = createBrowserImageProbe({
|
|
fetcher: (async () =>
|
|
responseAt(imageUrl, new Uint8Array(2_048), {
|
|
status: 200,
|
|
headers: {
|
|
"cache-control":
|
|
"public, max-age=31536000, immutable",
|
|
"content-type": "image/png",
|
|
},
|
|
})) as typeof fetch,
|
|
createBitmap,
|
|
});
|
|
await expect(probe.probe(request())).resolves.toMatchObject({
|
|
ok: false,
|
|
error: { code: "LIMIT_EXCEEDED" },
|
|
});
|
|
expect(createBitmap).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("rejects non-identity content encoding and mismatched declared lengths", async () => {
|
|
const png = pngBytes(640, 360);
|
|
const createBitmap = vi.fn();
|
|
const cases = [
|
|
{
|
|
headers: {
|
|
"content-encoding": "gzip",
|
|
"content-length": String(png.byteLength),
|
|
},
|
|
code: "POLICY_REJECTED",
|
|
},
|
|
{
|
|
headers: {
|
|
"content-length": String(png.byteLength + 1),
|
|
},
|
|
code: "INTEGRITY_FAILED",
|
|
},
|
|
];
|
|
for (const invalid of cases) {
|
|
const headers = publicImageHeaders("image/png");
|
|
for (const [name, value] of Object.entries(invalid.headers)) {
|
|
headers.set(name, value);
|
|
}
|
|
const probe = createBrowserImageProbe({
|
|
fetcher: (async () =>
|
|
responseAt(imageUrl, png, {
|
|
status: 200,
|
|
headers,
|
|
})) as typeof fetch,
|
|
createBitmap,
|
|
});
|
|
await expect(probe.probe(request())).resolves.toMatchObject({
|
|
ok: false,
|
|
error: { code: invalid.code },
|
|
});
|
|
}
|
|
expect(createBitmap).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("rejects malicious dimensions and animated PNG/WebP before native decode", async () => {
|
|
const createBitmap = vi.fn(async () => ({
|
|
width: 640,
|
|
height: 360,
|
|
close: vi.fn(),
|
|
}));
|
|
const cases = [
|
|
{
|
|
bytes: pngBytes(20_000, 20_000),
|
|
mediaType: "image/png" as const,
|
|
code: "LIMIT_EXCEEDED",
|
|
},
|
|
{
|
|
bytes: pngBytes(320, 180),
|
|
mediaType: "image/png" as const,
|
|
code: "INTEGRITY_FAILED",
|
|
},
|
|
{
|
|
bytes: jpegBytes(320, 180),
|
|
mediaType: "image/jpeg" as const,
|
|
code: "INTEGRITY_FAILED",
|
|
},
|
|
{
|
|
bytes: webpBytes(10_000, 10_000),
|
|
mediaType: "image/webp" as const,
|
|
code: "LIMIT_EXCEEDED",
|
|
},
|
|
{
|
|
bytes: avifBytes(20_000, 20_000),
|
|
mediaType: "image/avif" as const,
|
|
code: "LIMIT_EXCEEDED",
|
|
},
|
|
{
|
|
bytes: pngBytes(640, 360, true),
|
|
mediaType: "image/png" as const,
|
|
code: "INTEGRITY_FAILED",
|
|
},
|
|
{
|
|
bytes: webpBytes(640, 360, true),
|
|
mediaType: "image/webp" as const,
|
|
code: "INTEGRITY_FAILED",
|
|
},
|
|
{
|
|
bytes: avifBytes(640, 360, "avis"),
|
|
mediaType: "image/avif" as const,
|
|
code: "INTEGRITY_FAILED",
|
|
},
|
|
];
|
|
for (const malicious of cases) {
|
|
const url = imageUrl.replace(
|
|
"format=png",
|
|
`format=${malicious.mediaType.slice("image/".length)}`,
|
|
);
|
|
const probe = createBrowserImageProbe({
|
|
fetcher: (async () =>
|
|
responseAt(url, malicious.bytes, {
|
|
status: 200,
|
|
headers: publicImageHeaders(
|
|
malicious.mediaType,
|
|
malicious.bytes.byteLength,
|
|
),
|
|
})) as typeof fetch,
|
|
createBitmap,
|
|
});
|
|
await expect(
|
|
probe.probe(
|
|
request({
|
|
absoluteUrl: url,
|
|
expectedMediaType: malicious.mediaType,
|
|
}),
|
|
),
|
|
).resolves.toMatchObject({
|
|
ok: false,
|
|
error: { code: malicious.code },
|
|
});
|
|
}
|
|
expect(createBitmap).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("enforces private no-store, omitted credentials and the exact final URL", async () => {
|
|
const png = pngBytes(640, 360);
|
|
const fetcher = vi.fn(async () =>
|
|
responseAt(imageUrl, png, {
|
|
status: 200,
|
|
headers: {
|
|
"cache-control": "private, no-store",
|
|
"content-type": "image/png",
|
|
},
|
|
}),
|
|
);
|
|
const probe = createBrowserImageProbe({
|
|
fetcher: fetcher as typeof fetch,
|
|
createBitmap: async () => ({
|
|
width: 640,
|
|
height: 360,
|
|
close: vi.fn(),
|
|
}),
|
|
});
|
|
await expect(
|
|
probe.probe(
|
|
request({
|
|
delivery: "PRIVATE_SIGNED",
|
|
minimumPublicMaxAgeSeconds: 0,
|
|
}),
|
|
),
|
|
).resolves.toMatchObject({ ok: true });
|
|
expect(fetcher).toHaveBeenCalledWith(
|
|
imageUrl,
|
|
expect.objectContaining({
|
|
cache: "no-store",
|
|
credentials: "omit",
|
|
redirect: "error",
|
|
}),
|
|
);
|
|
|
|
for (const response of [
|
|
responseAt(imageUrl, png, {
|
|
status: 200,
|
|
headers: {
|
|
"cache-control": "private, no-store=value",
|
|
"content-type": "image/png",
|
|
},
|
|
}),
|
|
responseAt(imageUrl, png, {
|
|
status: 200,
|
|
headers: {
|
|
"cache-control": "public, no-store",
|
|
"content-type": "image/png",
|
|
},
|
|
}),
|
|
responseAt(
|
|
"https://images.example.test/v1/assets/other",
|
|
png,
|
|
{
|
|
status: 200,
|
|
headers: {
|
|
"cache-control": "private, no-store",
|
|
"content-type": "image/png",
|
|
},
|
|
},
|
|
),
|
|
]) {
|
|
const rejectingProbe = createBrowserImageProbe({
|
|
fetcher: (async () => response) as typeof fetch,
|
|
createBitmap: async () => ({
|
|
width: 640,
|
|
height: 360,
|
|
close: vi.fn(),
|
|
}),
|
|
});
|
|
await expect(
|
|
rejectingProbe.probe(
|
|
request({
|
|
delivery: "PRIVATE_SIGNED",
|
|
minimumPublicMaxAgeSeconds: 0,
|
|
}),
|
|
),
|
|
).resolves.toMatchObject({
|
|
ok: false,
|
|
error: { code: "POLICY_REJECTED" },
|
|
});
|
|
}
|
|
|
|
await expect(
|
|
probe.probe(
|
|
request({
|
|
expectedMediaType: "image/svg+xml",
|
|
} as unknown as Partial<ImageProbeRequest>),
|
|
),
|
|
).resolves.toMatchObject({
|
|
ok: false,
|
|
error: { code: "POLICY_REJECTED" },
|
|
});
|
|
});
|
|
|
|
it("times out a stalled body, aborts the composed signal and cancels its reader", async () => {
|
|
const manual = manualImageProbeScheduler();
|
|
const cancel = vi.fn(async () => undefined);
|
|
const releaseLock = vi.fn();
|
|
const read = vi.fn(
|
|
() =>
|
|
new Promise<ReadableStreamReadResult<Uint8Array>>(
|
|
() => undefined,
|
|
),
|
|
);
|
|
const response = {
|
|
body: {
|
|
getReader: () => ({ cancel, read, releaseLock }),
|
|
},
|
|
headers: publicImageHeaders("image/png"),
|
|
ok: true,
|
|
redirected: false,
|
|
status: 200,
|
|
type: "cors",
|
|
url: imageUrl,
|
|
} as unknown as Response;
|
|
const fetcher = vi.fn(
|
|
async (_input: RequestInfo | URL, _init?: RequestInit) =>
|
|
response,
|
|
);
|
|
const probe = createBrowserImageProbe({
|
|
fetcher: fetcher as typeof fetch,
|
|
createBitmap: vi.fn(),
|
|
timeoutMs: 1_000,
|
|
scheduler: manual.scheduler,
|
|
});
|
|
const probeRequest = request();
|
|
const pending = probe.probe(probeRequest);
|
|
await vi.waitFor(() => {
|
|
expect(read).toHaveBeenCalledOnce();
|
|
});
|
|
const composedSignal = fetcher.mock.calls[0]?.[1]?.signal as
|
|
| AbortSignal
|
|
| null
|
|
| undefined;
|
|
expect(composedSignal).not.toBe(probeRequest.signal);
|
|
manual.fire();
|
|
|
|
await expect(pending).resolves.toMatchObject({
|
|
ok: false,
|
|
error: {
|
|
code: "UNAVAILABLE",
|
|
retryable: true,
|
|
recovery: "RETRY",
|
|
},
|
|
});
|
|
expect(cancel).toHaveBeenCalledOnce();
|
|
expect(releaseLock).toHaveBeenCalledOnce();
|
|
expect(composedSignal?.aborted).toBe(true);
|
|
});
|
|
|
|
it("times out stalled decode and closes a bitmap that resolves late", async () => {
|
|
const manual = manualImageProbeScheduler();
|
|
const close = vi.fn();
|
|
let finishDecode:
|
|
((bitmap: {
|
|
width: number;
|
|
height: number;
|
|
close(): void;
|
|
}) => void) | undefined;
|
|
const createBitmap = vi.fn(
|
|
() =>
|
|
new Promise<{
|
|
width: number;
|
|
height: number;
|
|
close(): void;
|
|
}>((resolve) => {
|
|
finishDecode = resolve;
|
|
}),
|
|
);
|
|
const png = pngBytes(640, 360);
|
|
const probe = createBrowserImageProbe({
|
|
fetcher: (async () =>
|
|
responseAt(imageUrl, png, {
|
|
status: 200,
|
|
headers: publicImageHeaders("image/png", png.byteLength),
|
|
})) as typeof fetch,
|
|
createBitmap,
|
|
timeoutMs: 1_000,
|
|
scheduler: manual.scheduler,
|
|
});
|
|
const pending = probe.probe(request());
|
|
await vi.waitFor(() => {
|
|
expect(createBitmap).toHaveBeenCalledOnce();
|
|
});
|
|
manual.fire();
|
|
await expect(pending).resolves.toMatchObject({
|
|
ok: false,
|
|
error: { code: "UNAVAILABLE" },
|
|
});
|
|
|
|
finishDecode?.({ width: 640, height: 360, close });
|
|
await vi.waitFor(() => {
|
|
expect(close).toHaveBeenCalledOnce();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("P-256 image capability verifier", () => {
|
|
it("rejects an ECDSA public key on any curve other than P-256", async () => {
|
|
const generated = await globalThis.crypto.subtle.generateKey(
|
|
{ name: "ECDSA", namedCurve: "P-384" },
|
|
false,
|
|
["sign", "verify"],
|
|
);
|
|
if (!("publicKey" in generated)) {
|
|
throw new TypeError("Expected an ECDSA key pair.");
|
|
}
|
|
expect(() =>
|
|
createP256ImageCapabilityVerifier({
|
|
subtle: globalThis.crypto.subtle,
|
|
publicKeys: [
|
|
{
|
|
keyId: "image-signing-wrong-curve",
|
|
key: generated.publicKey,
|
|
},
|
|
],
|
|
}),
|
|
).toThrow(/public key binding/u);
|
|
});
|
|
|
|
it("verifies the exact canonical payload and rejects tampering", async () => {
|
|
const generated = await globalThis.crypto.subtle.generateKey(
|
|
{ name: "ECDSA", namedCurve: "P-256" },
|
|
false,
|
|
["sign", "verify"],
|
|
);
|
|
if (!("privateKey" in generated)) {
|
|
throw new TypeError("Expected an ECDSA key pair.");
|
|
}
|
|
const payload = new TextEncoder().encode(
|
|
'["image-cdn-capability-v1","bound"]',
|
|
);
|
|
const payloadBuffer = new Uint8Array(payload.byteLength);
|
|
payloadBuffer.set(payload);
|
|
const signature = await globalThis.crypto.subtle.sign(
|
|
{ name: "ECDSA", hash: "SHA-256" },
|
|
generated.privateKey,
|
|
payloadBuffer.buffer,
|
|
);
|
|
const verifier = createP256ImageCapabilityVerifier({
|
|
subtle: globalThis.crypto.subtle,
|
|
publicKeys: [
|
|
{
|
|
keyId: "image-signing-2026-01",
|
|
key: generated.publicKey,
|
|
},
|
|
],
|
|
});
|
|
expect(verifier.acceptsKey("image-signing-2026-01")).toBe(
|
|
true,
|
|
);
|
|
expect(verifier.acceptsKey("image-signing-unknown")).toBe(
|
|
false,
|
|
);
|
|
const signatureBase64Url = base64Url(
|
|
new Uint8Array(signature),
|
|
);
|
|
await expect(
|
|
verifier.verify({
|
|
algorithm: "ECDSA_P256_SHA256",
|
|
keyId: "image-signing-2026-01",
|
|
canonicalPayload: payload,
|
|
signatureBase64Url,
|
|
}),
|
|
).resolves.toBe(true);
|
|
const tampered = Uint8Array.from(payload);
|
|
tampered[0] ^= 1;
|
|
await expect(
|
|
verifier.verify({
|
|
algorithm: "ECDSA_P256_SHA256",
|
|
keyId: "image-signing-2026-01",
|
|
canonicalPayload: tampered,
|
|
signatureBase64Url,
|
|
}),
|
|
).resolves.toBe(false);
|
|
});
|
|
});
|
|
|
|
function responseAt(
|
|
url: string,
|
|
body: Uint8Array,
|
|
init: ResponseInit,
|
|
): Response {
|
|
const responseBytes = new Uint8Array(body.byteLength);
|
|
responseBytes.set(body);
|
|
const response = new Response(responseBytes.buffer, init);
|
|
Object.defineProperty(response, "url", {
|
|
configurable: false,
|
|
enumerable: true,
|
|
value: url,
|
|
});
|
|
return response;
|
|
}
|
|
|
|
function publicImageHeaders(
|
|
mediaType: string,
|
|
contentLength?: number,
|
|
): Headers {
|
|
const headers = new Headers({
|
|
"cache-control":
|
|
"public, max-age=31536000, s-maxage=31536000, immutable",
|
|
"content-type": mediaType,
|
|
vary: "Accept-Encoding",
|
|
});
|
|
if (contentLength !== undefined) {
|
|
headers.set("content-length", String(contentLength));
|
|
}
|
|
return headers;
|
|
}
|
|
|
|
function pngBytes(
|
|
width: number,
|
|
height: number,
|
|
animated = false,
|
|
): Uint8Array {
|
|
const header = new Uint8Array(13);
|
|
const headerView = new DataView(header.buffer);
|
|
headerView.setUint32(0, width);
|
|
headerView.setUint32(4, height);
|
|
header[8] = 8;
|
|
header[9] = 6;
|
|
return concatenateBytes([
|
|
Uint8Array.from([
|
|
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
|
|
]),
|
|
pngChunk("IHDR", header),
|
|
...(animated
|
|
? [pngChunk("acTL", new Uint8Array(8))]
|
|
: []),
|
|
pngChunk("IDAT", new Uint8Array()),
|
|
pngChunk("IEND", new Uint8Array()),
|
|
]);
|
|
}
|
|
|
|
function pngChunk(type: string, payload: Uint8Array): Uint8Array {
|
|
const chunk = new Uint8Array(12 + payload.byteLength);
|
|
const view = new DataView(chunk.buffer);
|
|
view.setUint32(0, payload.byteLength);
|
|
writeAscii(chunk, 4, type);
|
|
chunk.set(payload, 8);
|
|
return chunk;
|
|
}
|
|
|
|
function jpegBytes(width: number, height: number): Uint8Array {
|
|
return Uint8Array.from([
|
|
0xff,
|
|
0xd8,
|
|
0xff,
|
|
0xc0,
|
|
0x00,
|
|
0x11,
|
|
0x08,
|
|
(height >>> 8) & 0xff,
|
|
height & 0xff,
|
|
(width >>> 8) & 0xff,
|
|
width & 0xff,
|
|
0x03,
|
|
0x01,
|
|
0x11,
|
|
0x00,
|
|
0x02,
|
|
0x11,
|
|
0x00,
|
|
0x03,
|
|
0x11,
|
|
0x00,
|
|
0xff,
|
|
0xda,
|
|
]);
|
|
}
|
|
|
|
function webpBytes(
|
|
width: number,
|
|
height: number,
|
|
animated = false,
|
|
): Uint8Array {
|
|
const chunkType = animated ? "VP8X" : "VP8 ";
|
|
const payload = new Uint8Array(10);
|
|
if (animated) {
|
|
payload[0] = 0x02;
|
|
writeUint24LittleEndian(payload, 4, width - 1);
|
|
writeUint24LittleEndian(payload, 7, height - 1);
|
|
} else {
|
|
payload.set([0x9d, 0x01, 0x2a], 3);
|
|
const view = new DataView(payload.buffer);
|
|
view.setUint16(6, width, true);
|
|
view.setUint16(8, height, true);
|
|
}
|
|
const chunk = concatenateBytes([
|
|
asciiBytes(chunkType),
|
|
littleEndianUint32(payload.byteLength),
|
|
payload,
|
|
]);
|
|
return concatenateBytes([
|
|
asciiBytes("RIFF"),
|
|
littleEndianUint32(4 + chunk.byteLength),
|
|
asciiBytes("WEBP"),
|
|
chunk,
|
|
]);
|
|
}
|
|
|
|
function avifBytes(
|
|
width: number,
|
|
height: number,
|
|
brand = "avif",
|
|
): Uint8Array {
|
|
const fileType = isoBox(
|
|
"ftyp",
|
|
concatenateBytes([
|
|
asciiBytes(brand),
|
|
new Uint8Array(4),
|
|
asciiBytes(brand),
|
|
]),
|
|
);
|
|
const spatialExtent = new Uint8Array(12);
|
|
const extentView = new DataView(spatialExtent.buffer);
|
|
extentView.setUint32(4, width);
|
|
extentView.setUint32(8, height);
|
|
const primaryItem = new Uint8Array(6);
|
|
new DataView(primaryItem.buffer).setUint16(4, 1);
|
|
const itemInfoEntry = new Uint8Array(13);
|
|
itemInfoEntry[0] = 2;
|
|
const itemInfoView = new DataView(itemInfoEntry.buffer);
|
|
itemInfoView.setUint16(4, 1);
|
|
writeAscii(itemInfoEntry, 8, "av01");
|
|
const itemInfo = new Uint8Array(6);
|
|
new DataView(itemInfo.buffer).setUint16(4, 1);
|
|
const propertyAssociation = new Uint8Array(12);
|
|
const associationView = new DataView(
|
|
propertyAssociation.buffer,
|
|
);
|
|
associationView.setUint32(4, 1);
|
|
associationView.setUint16(8, 1);
|
|
propertyAssociation[10] = 1;
|
|
propertyAssociation[11] = 0x81;
|
|
const properties = isoBox(
|
|
"iprp",
|
|
concatenateBytes([
|
|
isoBox("ipco", isoBox("ispe", spatialExtent)),
|
|
isoBox("ipma", propertyAssociation),
|
|
]),
|
|
);
|
|
const metadata = isoBox(
|
|
"meta",
|
|
concatenateBytes([
|
|
new Uint8Array(4),
|
|
isoBox("pitm", primaryItem),
|
|
isoBox(
|
|
"iinf",
|
|
concatenateBytes([
|
|
itemInfo,
|
|
isoBox("infe", itemInfoEntry),
|
|
]),
|
|
),
|
|
properties,
|
|
]),
|
|
);
|
|
return concatenateBytes([
|
|
fileType,
|
|
metadata,
|
|
isoBox("mdat", Uint8Array.of(0)),
|
|
]);
|
|
}
|
|
|
|
function isoBox(type: string, payload: Uint8Array): Uint8Array {
|
|
const box = new Uint8Array(8 + payload.byteLength);
|
|
const view = new DataView(box.buffer);
|
|
view.setUint32(0, box.byteLength);
|
|
writeAscii(box, 4, type);
|
|
box.set(payload, 8);
|
|
return box;
|
|
}
|
|
|
|
function littleEndianUint32(value: number): Uint8Array {
|
|
const bytes = new Uint8Array(4);
|
|
new DataView(bytes.buffer).setUint32(0, value, true);
|
|
return bytes;
|
|
}
|
|
|
|
function writeUint24LittleEndian(
|
|
bytes: Uint8Array,
|
|
offset: number,
|
|
value: number,
|
|
): void {
|
|
bytes[offset] = value & 0xff;
|
|
bytes[offset + 1] = (value >>> 8) & 0xff;
|
|
bytes[offset + 2] = (value >>> 16) & 0xff;
|
|
}
|
|
|
|
function asciiBytes(value: string): Uint8Array {
|
|
return Uint8Array.from(
|
|
[...value].map((character) => character.charCodeAt(0)),
|
|
);
|
|
}
|
|
|
|
function writeAscii(
|
|
target: Uint8Array,
|
|
offset: number,
|
|
value: string,
|
|
): void {
|
|
target.set(asciiBytes(value), offset);
|
|
}
|
|
|
|
function concatenateBytes(
|
|
chunks: readonly Uint8Array[],
|
|
): Uint8Array {
|
|
const combined = new Uint8Array(
|
|
chunks.reduce((total, chunk) => total + chunk.byteLength, 0),
|
|
);
|
|
let offset = 0;
|
|
for (const chunk of chunks) {
|
|
combined.set(chunk, offset);
|
|
offset += chunk.byteLength;
|
|
}
|
|
return combined;
|
|
}
|
|
|
|
function manualImageProbeScheduler(): Readonly<{
|
|
scheduler: ImageProbeScheduler;
|
|
fire(): void;
|
|
}> {
|
|
let callback: (() => void) | undefined;
|
|
return {
|
|
scheduler: {
|
|
setTimeout(nextCallback) {
|
|
callback = nextCallback;
|
|
return 1;
|
|
},
|
|
clearTimeout: vi.fn(),
|
|
},
|
|
fire() {
|
|
if (!callback) {
|
|
throw new TypeError("No image probe timeout is scheduled.");
|
|
}
|
|
callback();
|
|
},
|
|
};
|
|
}
|
|
|
|
function manualCapabilityVerificationScheduler(): Readonly<{
|
|
scheduler: ImageCapabilityVerificationScheduler;
|
|
delays: readonly number[];
|
|
clearTimeout: ReturnType<typeof vi.fn>;
|
|
fire(): void;
|
|
}> {
|
|
let callback: (() => void) | undefined;
|
|
const delays: number[] = [];
|
|
const clearTimeout = vi.fn();
|
|
return {
|
|
scheduler: {
|
|
setTimeout(nextCallback, milliseconds) {
|
|
callback = nextCallback;
|
|
delays.push(milliseconds);
|
|
return 1;
|
|
},
|
|
clearTimeout,
|
|
},
|
|
delays,
|
|
clearTimeout,
|
|
fire() {
|
|
if (!callback) {
|
|
throw new TypeError(
|
|
"No capability verification timeout is scheduled.",
|
|
);
|
|
}
|
|
callback();
|
|
},
|
|
};
|
|
}
|
|
|
|
function base64Url(bytes: Uint8Array): string {
|
|
let binary = "";
|
|
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
return btoa(binary)
|
|
.replace(/\+/gu, "-")
|
|
.replace(/\//gu, "_")
|
|
.replace(/=+$/gu, "");
|
|
}
|