chore: initialize from frontend template 4dc033c

This commit is contained in:
DongHyeonka
2026-08-13 18:23:26 +09:00
commit 40107eec84
897 changed files with 234824 additions and 0 deletions
@@ -0,0 +1,167 @@
# Image CDN composition
This adapter accepts no source URL or transform query from a feature. Product
composition owns the origin registry and named presets. A backend gateway may
use `runtime.assets`; presentation receives only the narrow
`runtime.presentation` facade plus registry-issued asset and preset
references.
```ts
import {
ImageCdnPolicyRegistry,
createBrowserImageProbe,
createImageCdnRuntime,
createP256ImageCapabilityVerifier,
imageCdnPresetReference,
} from "./index.ts";
const cardImage = imageCdnPresetReference(
"product-card",
"render-product-card-image",
);
const policies = new ImageCdnPolicyRegistry({
applicationOrigin: "https://app.example.com",
origins: [{
originKey: "product-images",
origin: "https://images.example.com",
assetPathPrefix: "/v1/assets/",
minimumPublicMaxAgeSeconds: 31_536_000,
}],
presets: [{
reference: cardImage,
bindingId: "product-card-v1",
width: 640,
height: 360,
fit: "cover",
dprs: [1, 2],
responsiveWidths: [320, 640],
quality: 80,
formats: ["avif", "webp", "jpeg"],
sizes: "(max-width: 640px) 100vw, 640px",
loading: "eager",
decoding: "async",
fetchPriority: "high",
referrerPolicy: "no-referrer",
probeMode: "PRIMARY_REQUIRED",
allowUpscale: false,
maxTransformedPixels: 1_048_576,
maxDecodedBytes: 4_194_304,
maxEncodedBytes: 524_288,
}],
hardLimits: {
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",
],
formatQualityCeilings: {
avif: 80,
jpeg: 85,
png: 90,
webp: 85,
},
},
capability: {
issuer: "image-bff",
acceptedKeyIds: [
"image-signing-2026-02",
"image-signing-2026-01",
],
},
});
const runtime = createImageCdnRuntime({
policies,
subtle: crypto.subtle,
capabilityVerifier: createP256ImageCapabilityVerifier({
subtle: crypto.subtle,
publicKeys: [{
keyId: "image-signing-2026-02",
key: currentImageCapabilityPublicKey,
}, {
keyId: "image-signing-2026-01",
key: previousImageCapabilityPublicKey,
}],
}),
capabilityVerificationTimeoutMs: 5_000,
probe: createBrowserImageProbe(),
observer: safeBrowserDataObserver,
});
// `payload` is a strictly decoded BackendIssuedImageAsset from the BFF.
const accepted = await runtime.assets.acceptBackendIssued(payload, {
signal,
});
if (!accepted.ok) return accepted;
// Expose only this closure to the feature/presentation composition.
const resolveCardImage = (signal: AbortSignal) =>
runtime.presentation.resolve({
asset: accepted.value,
preset: cardImage,
signal,
});
// Application-scope teardown, logout, account/tenant partition change, or
// replacement by a newly composed runtime. Never call this per render.
const closeImageRuntime = (): void => runtime.close();
```
For a public immutable asset, the trusted gateway calls
`acceptPublicImmutable` with only an allowlisted `originKey`, opaque `assetId`
and `revision`, raster metadata and intrinsic dimensions. `applicationOrigin`
must be the deployment's canonical HTTPS origin, without a trailing slash,
and every CDN origin must differ from it. This is required because an
`anonymous` image request omits credentials only when it is cross-origin.
Private descriptors must be backend-signed, remain above the configured
minimum TTL at every resolve, and use an eager, non-low-priority
`PRIMARY_REQUIRED` preset. Digest and signature verification share one
composition-owned deadline and race the caller's abort signal. The probe sends
no credentials, rejects any final URL other than the exact signed URL, and
requires the private response to declare the flag-only directive
`Cache-Control: no-store`.
Before native decode, the adapter parses the bounded PNG, JPEG, WebP or AVIF
container, rejects animation and enforces both pixel and decoded-byte budgets.
Its adapter-owned timeout covers response headers, streamed body consumption
and decode; abort paths cancel the reader and close even a late ImageBitmap.
The client never purges a CDN: public URLs roll forward by revision, while
private delivery relies on backend capability revocation or expiry.
Composition-supplied hard limits may only tighten
`IMAGE_CDN_IMPLEMENTATION_CEILINGS`; configuration cannot raise intrinsic,
source/output pixel, decoded/encoded byte, candidate, URL or capability
lifetime ceilings owned by the adapter. Private verification additionally
reserves one of the bounded `maxConcurrentCapabilityVerifications` slots and
always releases it after success, failure, abort or close.
`acceptedKeyIds` is a bounded, unique overlap set, not the active signing-key
selector. The verifier registry must cover every accepted ID. Rotate by first
deploying the new public key and an old/new overlap set, then switch the
backend signer. Retain the old key for client rollout plus at least
`maxCapabilityLifetimeMs + maxClockSkewMs`; remove it only after old clients
and capabilities are exhausted. A compromised key instead requires backend
revocation, `runtime.close()`, recomposition and a forced client rollout.
`close()` is terminal and idempotent. It aborts in-flight private verification
and probing and replaces the runtime's WeakMap capability registry, immediately
revoking every issued reference without retaining them strongly. Every later
accept or resolve returns closed `UNAVAILABLE`; resuming requires a newly
composed runtime.
@@ -0,0 +1,609 @@
import type {
ImageProbeRequest,
ImageResourceProbePort,
} from "../../../application/ports/browser-transfer/image-cdn.ts";
import {
browserDataFailure,
browserDataSuccess,
} from "../../browser-file-storage/result.ts";
import { parseStaticImageHeaderMetadata } from "./image-header-metadata.ts";
export type DecodedImageFacade = Readonly<{
width: number;
height: number;
close(): void;
}>;
export type ImageProbeScheduler = Readonly<{
setTimeout(callback: () => void, milliseconds: number): unknown;
clearTimeout(handle: unknown): void;
}>;
export type BrowserImageProbeDependencies = Readonly<{
fetcher?: typeof fetch;
createBitmap?: (
image: Blob,
) => Promise<DecodedImageFacade>;
/** Covers fetch headers, streamed body consumption and native decode. */
timeoutMs?: number;
scheduler?: ImageProbeScheduler;
}>;
const DEFAULT_TIMEOUT_MS = 5_000;
const MAXIMUM_TIMEOUT_MS = 60_000;
/**
* Performs one bounded real response/decode probe. It is intentionally a
* separate seam because probing every srcset candidate would defeat responsive
* image loading and consume the entire transfer budget up front.
*/
export function createBrowserImageProbe(
dependencies: BrowserImageProbeDependencies = {},
): ImageResourceProbePort {
const fetcher = (dependencies.fetcher ?? fetch).bind(globalThis);
const createBitmap =
dependencies.createBitmap ??
(typeof createImageBitmap === "function"
? async (image: Blob) => createImageBitmap(image)
: undefined);
const timeoutMs = dependencies.timeoutMs ?? DEFAULT_TIMEOUT_MS;
const scheduler = snapshotScheduler(
dependencies.scheduler ?? defaultScheduler(),
);
if (
!positiveSafeInteger(timeoutMs) ||
timeoutMs > MAXIMUM_TIMEOUT_MS
) {
throw new TypeError("Image probe timeout is invalid.");
}
return Object.freeze({
async probe(request: ImageProbeRequest) {
if (request.signal.aborted) {
return browserDataFailure("ABORTED", "IMAGE_RESOLVE");
}
let url: URL;
try {
url = new URL(request.absoluteUrl);
} catch {
return browserDataFailure("INVALID_INPUT", "IMAGE_RESOLVE");
}
if (
url.protocol !== "https:" ||
url.username !== "" ||
url.password !== "" ||
url.hash !== "" ||
!positiveSafeInteger(request.expectedWidth) ||
!positiveSafeInteger(request.expectedHeight) ||
!positiveSafeInteger(request.maxEncodedBytes) ||
!positiveSafeInteger(request.maxDecodedPixels) ||
!positiveSafeInteger(request.maxDecodedBytes) ||
!withinDecodeBudget(
request.expectedWidth,
request.expectedHeight,
request.maxDecodedPixels,
request.maxDecodedBytes,
) ||
!isRasterMediaType(request.expectedMediaType) ||
request.referrerPolicy !== "no-referrer" &&
request.referrerPolicy !==
"strict-origin-when-cross-origin"
) {
return browserDataFailure("POLICY_REJECTED", "IMAGE_RESOLVE");
}
if (!createBitmap) {
return browserDataFailure("UNSUPPORTED", "IMAGE_RESOLVE");
}
const scope = createProbeAbortScope(
request.signal,
timeoutMs,
scheduler,
);
let response: Response | undefined;
try {
try {
const fetchTask = Promise.resolve(
fetcher(url.href, {
method: "GET",
cache: "no-store",
credentials: "omit",
mode: "cors",
redirect: "error",
referrerPolicy: request.referrerPolicy,
signal: scope.signal,
}),
);
response = await awaitWithAbort(
fetchTask,
scope.signal,
(lateResponse) => {
cancelResponseBody(lateResponse);
},
);
} catch {
return signalFailure(request.signal, scope);
}
if (
response.status !== 200 ||
!response.ok ||
response.redirected ||
["error", "opaque", "opaqueredirect"].includes(
response.type,
) ||
response.url !== url.href ||
!validResponseHeaders(response, request)
) {
cancelResponseBody(response);
return browserDataFailure(
"POLICY_REJECTED",
"IMAGE_RESOLVE",
);
}
let bytes: Uint8Array;
try {
bytes = await readBoundedBody(
response,
request.maxEncodedBytes,
scope.signal,
);
} catch (error) {
if (request.signal.aborted || scope.timedOut()) {
return signalFailure(request.signal, scope);
}
return error instanceof EncodedBodyLimitError
? browserDataFailure(
"LIMIT_EXCEEDED",
"IMAGE_RESOLVE",
)
: browserDataFailure(
"UNAVAILABLE",
"IMAGE_RESOLVE",
{
retryable: true,
recovery: "RETRY",
},
);
}
const declaredLength = response.headers.get(
"content-length",
);
if (
declaredLength !== null &&
Number(declaredLength) !== bytes.byteLength
) {
return browserDataFailure(
"INTEGRITY_FAILED",
"IMAGE_RESOLVE",
);
}
const metadata = parseStaticImageHeaderMetadata(
bytes,
request.expectedMediaType,
);
if (!metadata) {
return browserDataFailure(
"INTEGRITY_FAILED",
"IMAGE_RESOLVE",
);
}
if (
!withinDecodeBudget(
metadata.width,
metadata.height,
request.maxDecodedPixels,
request.maxDecodedBytes,
)
) {
return browserDataFailure(
"LIMIT_EXCEEDED",
"IMAGE_RESOLVE",
);
}
if (
metadata.width !== request.expectedWidth ||
metadata.height !== request.expectedHeight
) {
return browserDataFailure(
"INTEGRITY_FAILED",
"IMAGE_RESOLVE",
);
}
let bitmap: DecodedImageFacade | undefined;
try {
const blobBytes = new Uint8Array(bytes.byteLength);
blobBytes.set(bytes);
const decodeTask = createBitmap(
new Blob([blobBytes.buffer], {
type: request.expectedMediaType,
}),
);
bitmap = await awaitWithAbort(
decodeTask,
scope.signal,
closeBitmap,
);
if (
!positiveSafeInteger(bitmap.width) ||
!positiveSafeInteger(bitmap.height) ||
bitmap.width !== metadata.width ||
bitmap.height !== metadata.height ||
!withinDecodeBudget(
bitmap.width,
bitmap.height,
request.maxDecodedPixels,
request.maxDecodedBytes,
)
) {
return browserDataFailure(
"INTEGRITY_FAILED",
"IMAGE_RESOLVE",
);
}
return browserDataSuccess(
Object.freeze({
absoluteUrl: url.href,
mediaType: request.expectedMediaType,
encodedBytes: bytes.byteLength,
decodedWidth: bitmap.width,
decodedHeight: bitmap.height,
}),
);
} catch {
return request.signal.aborted || scope.timedOut()
? signalFailure(request.signal, scope)
: browserDataFailure(
"INTEGRITY_FAILED",
"IMAGE_RESOLVE",
);
} finally {
if (bitmap) closeBitmap(bitmap);
}
} finally {
scope.release();
}
},
});
}
function validResponseHeaders(
response: Response,
request: ImageProbeRequest,
): boolean {
const rawContentType =
response.headers.get("content-type")?.trim().toLowerCase();
if (
rawContentType !== request.expectedMediaType ||
response.headers.has("set-cookie") ||
response.headers.has("set-cookie2")
) {
return false;
}
const rawLength = response.headers.get("content-length");
const contentEncoding = response.headers.get("content-encoding");
if (
(contentEncoding !== null &&
contentEncoding.trim().toLowerCase() !== "identity") ||
rawLength !== null &&
(!/^(?:0|[1-9]\d*)$/u.test(rawLength) ||
Number(rawLength) > request.maxEncodedBytes)
) {
return false;
}
const vary = response.headers.get("vary");
if (
vary &&
vary
.split(",")
.map((name) => name.trim().toLowerCase())
.some((name) =>
["*", "authorization", "cookie"].includes(name),
)
) {
return false;
}
const directives = parseCacheControl(
response.headers.get("cache-control"),
);
if (!directives) return false;
if (request.delivery === "PRIVATE_SIGNED") {
return (
directives.get("no-store") === true &&
!directives.has("public")
);
}
const maxAge = directives.get("max-age");
const sharedMaxAge = directives.get("s-maxage");
return (
directives.get("public") === true &&
directives.get("immutable") === true &&
!directives.has("private") &&
!directives.has("no-cache") &&
!directives.has("no-store") &&
!directives.has("must-revalidate") &&
!directives.has("proxy-revalidate") &&
typeof maxAge === "string" &&
/^(?:0|[1-9]\d*)$/u.test(maxAge) &&
Number(maxAge) >= request.minimumPublicMaxAgeSeconds &&
(sharedMaxAge === undefined ||
(typeof sharedMaxAge === "string" &&
/^(?:0|[1-9]\d*)$/u.test(sharedMaxAge) &&
Number(sharedMaxAge) >=
request.minimumPublicMaxAgeSeconds))
);
}
function parseCacheControl(
value: string | null,
): ReadonlyMap<string, string | true> | null {
const flagDirectives = new Set([
"immutable",
"must-revalidate",
"no-store",
"private",
"proxy-revalidate",
"public",
]);
const directives = new Map<string, string | true>();
for (const part of value?.split(",") ?? []) {
const trimmedPart = part.trim();
const separator = trimmedPart.indexOf("=");
const name = (
separator < 0
? trimmedPart
: trimmedPart.slice(0, separator)
)
.trim()
.toLowerCase();
if (!name) continue;
if (directives.has(name)) return null;
if (separator < 0) {
directives.set(name, true);
continue;
}
if (flagDirectives.has(name)) return null;
const rawValue = trimmedPart.slice(separator + 1).trim();
if (rawValue === "") return null;
directives.set(name, rawValue.replace(/^"|"$/gu, ""));
}
return directives;
}
class EncodedBodyLimitError extends Error {}
async function readBoundedBody(
response: Response,
maximumBytes: number,
signal: AbortSignal,
): Promise<Uint8Array> {
if (!response.body) {
throw new TypeError("Image response body is unavailable.");
}
const reader = response.body.getReader();
const chunks: Uint8Array[] = [];
let total = 0;
try {
while (true) {
if (signal.aborted) throw abortException();
const next = await awaitWithAbort(
reader.read(),
signal,
() => undefined,
);
if (next.done) break;
if (!(next.value instanceof Uint8Array)) {
throw new TypeError("Image response chunk is invalid.");
}
total += next.value.byteLength;
if (total > maximumBytes) {
throw new EncodedBodyLimitError();
}
chunks.push(Uint8Array.from(next.value));
}
} catch (error) {
cancelReader(reader);
throw error;
} finally {
try {
reader.releaseLock();
} catch {
// The closed result remains authoritative if a host stream is broken.
}
}
const bytes = new Uint8Array(total);
let offset = 0;
for (const chunk of chunks) {
bytes.set(chunk, offset);
offset += chunk.byteLength;
}
return bytes;
}
type ProbeAbortScope = Readonly<{
signal: AbortSignal;
timedOut(): boolean;
release(): void;
}>;
function createProbeAbortScope(
externalSignal: AbortSignal,
timeoutMs: number,
scheduler: ImageProbeScheduler,
): ProbeAbortScope {
const controller = new AbortController();
let timeoutReached = false;
let released = false;
const onExternalAbort = () => {
controller.abort(externalSignal.reason);
};
externalSignal.addEventListener("abort", onExternalAbort, {
once: true,
});
if (externalSignal.aborted) onExternalAbort();
const timeoutHandle = scheduler.setTimeout(() => {
if (released) return;
timeoutReached = true;
controller.abort(abortException());
}, timeoutMs);
return Object.freeze({
signal: controller.signal,
timedOut: () => timeoutReached,
release() {
if (released) return;
released = true;
try {
scheduler.clearTimeout(timeoutHandle);
} catch {
// A broken optional scheduler cannot change a terminal probe result.
}
externalSignal.removeEventListener("abort", onExternalAbort);
},
});
}
function awaitWithAbort<Value>(
task: Promise<Value>,
signal: AbortSignal,
onLateValue: (value: Value) => void,
): Promise<Value> {
return new Promise<Value>((resolve, reject) => {
let settled = false;
const onAbort = () => {
if (settled) return;
settled = true;
signal.removeEventListener("abort", onAbort);
reject(abortException());
};
signal.addEventListener("abort", onAbort, { once: true });
if (signal.aborted) onAbort();
void task.then(
(value) => {
if (settled) {
onLateValue(value);
return;
}
settled = true;
signal.removeEventListener("abort", onAbort);
resolve(value);
},
(error: unknown) => {
if (settled) return;
settled = true;
signal.removeEventListener("abort", onAbort);
reject(error);
},
);
});
}
function signalFailure(
externalSignal: AbortSignal,
scope: ProbeAbortScope,
) {
return externalSignal.aborted
? browserDataFailure("ABORTED", "IMAGE_RESOLVE")
: scope.timedOut()
? browserDataFailure("UNAVAILABLE", "IMAGE_RESOLVE", {
retryable: true,
recovery: "RETRY",
})
: browserDataFailure("UNAVAILABLE", "IMAGE_RESOLVE", {
retryable: true,
recovery: "RETRY",
});
}
function cancelResponseBody(response: Response): void {
try {
const cancellation = response.body?.cancel();
void cancellation?.catch(() => undefined);
} catch {
// Best-effort release cannot change the closed probe result.
}
}
function cancelReader(
reader: ReadableStreamDefaultReader<Uint8Array>,
): void {
try {
void reader.cancel().catch(() => undefined);
} catch {
// Best-effort release cannot change the closed probe result.
}
}
function closeBitmap(bitmap: DecodedImageFacade): void {
try {
bitmap.close();
} catch {
// Decode correctness is independent from best-effort native release.
}
}
function abortException(): DOMException {
return new DOMException("Image probe was aborted.", "AbortError");
}
function withinDecodeBudget(
width: number,
height: number,
maximumPixels: number,
maximumBytes: number,
): boolean {
const pixels = width * height;
const decodedBytes = pixels * 4;
return (
Number.isSafeInteger(pixels) &&
Number.isSafeInteger(decodedBytes) &&
pixels <= maximumPixels &&
decodedBytes <= maximumBytes
);
}
function snapshotScheduler(
scheduler: ImageProbeScheduler,
): ImageProbeScheduler {
if (
!scheduler ||
typeof scheduler.setTimeout !== "function" ||
typeof scheduler.clearTimeout !== "function"
) {
throw new TypeError("Image probe scheduler is invalid.");
}
return Object.freeze({
setTimeout: scheduler.setTimeout.bind(scheduler),
clearTimeout: scheduler.clearTimeout.bind(scheduler),
});
}
function defaultScheduler(): ImageProbeScheduler {
return Object.freeze({
setTimeout(callback: () => void, milliseconds: number) {
return globalThis.setTimeout(callback, milliseconds);
},
clearTimeout(handle: unknown) {
globalThis.clearTimeout(
handle as ReturnType<typeof globalThis.setTimeout>,
);
},
});
}
function isRasterMediaType(
value: string,
): value is ImageProbeRequest["expectedMediaType"] {
return [
"image/avif",
"image/jpeg",
"image/png",
"image/webp",
].includes(value);
}
function positiveSafeInteger(value: number): boolean {
return Number.isSafeInteger(value) && value > 0;
}
@@ -0,0 +1,753 @@
import type {
ImageFit,
ImageOutputFormat,
ImagePresetReference,
ImageRasterMediaType,
} from "../../../application/ports/browser-transfer/image-cdn.ts";
export type ImageCdnOriginPolicy = Readonly<{
originKey: string;
origin: string;
assetPathPrefix: string;
minimumPublicMaxAgeSeconds: number;
}>;
export type ImageCdnPresetPolicy = Readonly<{
reference: ImagePresetReference;
bindingId: string;
width: number;
height: number;
fit: ImageFit;
dprs: readonly number[];
responsiveWidths: readonly number[];
quality: number;
formats: readonly ImageOutputFormat[];
sizes: string;
loading: "eager" | "lazy";
decoding: "async" | "sync";
fetchPriority: "high" | "low" | "auto";
referrerPolicy: "no-referrer" | "strict-origin-when-cross-origin";
probeMode: "NONE" | "PRIMARY_REQUIRED";
allowUpscale: boolean;
maxTransformedPixels: number;
maxDecodedBytes: number;
maxEncodedBytes: number;
}>;
export type ImageCdnHardLimits = Readonly<{
maxIntrinsicWidth: number;
maxIntrinsicHeight: number;
maxSourcePixels: number;
maxCssDimension: number;
maxDpr: number;
maxQuality: number;
maxCandidateCount: number;
maxTransformedPixels: number;
maxDecodedBytes: number;
maxEncodedBytes: number;
maxUrlLength: number;
maxCapabilityLifetimeMs: number;
maxClockSkewMs: number;
minCapabilityRemainingMs: number;
maxPresetBindingsPerCapability: number;
maxConcurrentCapabilityVerifications: number;
allowedSourceMediaTypes: readonly ImageRasterMediaType[];
formatQualityCeilings: Readonly<
Partial<Record<ImageOutputFormat, number>>
>;
}>;
export type ImageCdnCapabilityPolicy = Readonly<{
issuer: string;
acceptedKeyIds: readonly string[];
}>;
export type ImageCdnPolicyRegistryOptions = Readonly<{
applicationOrigin: string;
origins: readonly ImageCdnOriginPolicy[];
presets: readonly ImageCdnPresetPolicy[];
hardLimits: ImageCdnHardLimits;
capability: ImageCdnCapabilityPolicy;
}>;
export type ResolvedImageCdnOrigin = Readonly<{
originKey: string;
origin: string;
assetPathPrefix: string;
minimumPublicMaxAgeSeconds: number;
}>;
export type ImageCandidateGeometry = Readonly<{
cssWidth: number;
cssHeight: number;
dpr: number;
pixelWidth: number;
pixelHeight: number;
pixels: number;
decodedBytes: number;
}>;
export type ResolvedImageCdnPreset = Omit<
ImageCdnPresetPolicy,
"dprs" | "responsiveWidths" | "formats"
> &
Readonly<{
dprs: readonly number[];
responsiveWidths: readonly number[];
formats: readonly ImageOutputFormat[];
candidates: readonly ImageCandidateGeometry[];
}>;
const POLICY_TOKEN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
const PATH_PREFIX = /^\/[A-Za-z0-9/_-]{1,200}\/$/u;
const SAFE_SIZES = /^[^<>"']{1,512}$/u;
const IMAGE_FORMATS = Object.freeze([
"avif",
"jpeg",
"png",
"webp",
] as const);
const IMAGE_MEDIA_TYPES = Object.freeze([
"image/avif",
"image/jpeg",
"image/png",
"image/webp",
] as const);
const IMAGE_FITS = Object.freeze([
"contain",
"cover",
"fill",
"inside",
"outside",
] as const);
const ISSUED_PRESET_REFERENCES = new WeakSet<object>();
export const IMAGE_CDN_IMPLEMENTATION_CEILINGS = Object.freeze({
maxIntrinsicWidth: 16_384,
maxIntrinsicHeight: 16_384,
maxSourcePixels: 67_108_864,
maxCssDimension: 8_192,
maxDpr: 4,
maxQuality: 100,
maxCandidateCount: 32,
maxTransformedPixels: 16_777_216,
maxDecodedBytes: 67_108_864,
maxEncodedBytes: 16_777_216,
maxUrlLength: 8_192,
maxCapabilityLifetimeMs: 86_400_000,
maxClockSkewMs: 300_000,
maxMinimumCapabilityRemainingMs: 3_600_000,
maxPresetBindingsPerCapability: 32,
maxConcurrentCapabilityVerifications: 32,
maxAcceptedKeyIds: 8,
} as const);
const REGISTRY_KEYS = Object.freeze([
"applicationOrigin",
"origins",
"presets",
"hardLimits",
"capability",
] as const);
const ORIGIN_KEYS = Object.freeze([
"originKey",
"origin",
"assetPathPrefix",
"minimumPublicMaxAgeSeconds",
] as const);
const HARD_LIMIT_KEYS = Object.freeze([
"maxIntrinsicWidth",
"maxIntrinsicHeight",
"maxSourcePixels",
"maxCssDimension",
"maxDpr",
"maxQuality",
"maxCandidateCount",
"maxTransformedPixels",
"maxDecodedBytes",
"maxEncodedBytes",
"maxUrlLength",
"maxCapabilityLifetimeMs",
"maxClockSkewMs",
"minCapabilityRemainingMs",
"maxPresetBindingsPerCapability",
"maxConcurrentCapabilityVerifications",
"allowedSourceMediaTypes",
"formatQualityCeilings",
] as const);
const CAPABILITY_KEYS = Object.freeze([
"issuer",
"acceptedKeyIds",
] as const);
const PRESET_KEYS = Object.freeze([
"reference",
"bindingId",
"width",
"height",
"fit",
"dprs",
"responsiveWidths",
"quality",
"formats",
"sizes",
"loading",
"decoding",
"fetchPriority",
"referrerPolicy",
"probeMode",
"allowUpscale",
"maxTransformedPixels",
"maxDecodedBytes",
"maxEncodedBytes",
] as const);
export const IMAGE_FORMAT_MEDIA_TYPE: Readonly<
Record<ImageOutputFormat, ImageRasterMediaType>
> = Object.freeze({
avif: "image/avif",
jpeg: "image/jpeg",
png: "image/png",
webp: "image/webp",
});
/**
* The returned identity must be passed through a narrow feature facade.
* Constructing another reference with the same strings does not grant access.
*/
export function imageCdnPresetReference(
presetKey: string,
intention: string,
): ImagePresetReference {
if (!POLICY_TOKEN.test(presetKey) || !POLICY_TOKEN.test(intention)) {
throw new TypeError("Image CDN preset reference is invalid.");
}
const reference = Object.freeze({
presetKey,
intention,
}) as ImagePresetReference;
ISSUED_PRESET_REFERENCES.add(reference);
return reference;
}
/**
* Immutable composition-time policy registry. Every caller-owned collection
* is copied and all methods return snapshots rather than mutable registry
* state.
*/
export class ImageCdnPolicyRegistry {
readonly #origins: ReadonlyMap<string, ResolvedImageCdnOrigin>;
readonly #presets:
ReadonlyMap<ImagePresetReference, ResolvedImageCdnPreset>;
readonly #presetBindingIds: ReadonlySet<string>;
readonly #hardLimits: ImageCdnHardLimits;
readonly #capability: ImageCdnCapabilityPolicy;
constructor(options: ImageCdnPolicyRegistryOptions) {
if (!hasExactOwnKeys(options, REGISTRY_KEYS)) {
throw new TypeError("Image CDN policy registry is invalid.");
}
const applicationOrigin = snapshotApplicationOrigin(
options.applicationOrigin,
);
this.#hardLimits = snapshotHardLimits(options.hardLimits);
this.#capability = snapshotCapabilityPolicy(options.capability);
if (
!Array.isArray(options.origins) ||
options.origins.length < 1 ||
options.origins.length > 32 ||
!Array.isArray(options.presets) ||
options.presets.length < 1 ||
options.presets.length > 128
) {
throw new TypeError("Image CDN policy registry is invalid.");
}
const origins = new Map<string, ResolvedImageCdnOrigin>();
const absoluteOrigins = new Set<string>();
for (const input of options.origins) {
const origin = snapshotOrigin(input);
if (
origins.has(origin.originKey) ||
absoluteOrigins.has(origin.origin) ||
origin.origin === applicationOrigin
) {
throw new TypeError(
"Image CDN origin policy must be unique and cross-origin.",
);
}
origins.set(origin.originKey, origin);
absoluteOrigins.add(origin.origin);
}
const presets =
new Map<ImagePresetReference, ResolvedImageCdnPreset>();
const semanticReferences = new Set<string>();
const bindingIds = new Set<string>();
for (const input of options.presets) {
const preset = snapshotPreset(input, this.#hardLimits);
const semanticReference =
`${preset.reference.presetKey}:${preset.reference.intention}`;
if (
semanticReferences.has(semanticReference) ||
bindingIds.has(preset.bindingId)
) {
throw new TypeError("Image CDN preset policy is duplicated.");
}
semanticReferences.add(semanticReference);
bindingIds.add(preset.bindingId);
presets.set(preset.reference, preset);
}
this.#origins = origins;
this.#presets = presets;
this.#presetBindingIds = bindingIds;
}
resolveOrigin(originKey: string): ResolvedImageCdnOrigin | null {
return this.#origins.get(originKey) ?? null;
}
resolvePreset(
reference: ImagePresetReference,
): ResolvedImageCdnPreset | null {
if (
!reference ||
typeof reference !== "object" ||
!ISSUED_PRESET_REFERENCES.has(reference)
) {
return null;
}
return this.#presets.get(reference) ?? null;
}
hasPresetBinding(bindingId: string): boolean {
return this.#presetBindingIds.has(bindingId);
}
hardLimits(): ImageCdnHardLimits {
return this.#hardLimits;
}
capabilityPolicy(): ImageCdnCapabilityPolicy {
return this.#capability;
}
}
export function buildImageCandidateGeometry(
input: Readonly<{
width: number;
height: number;
responsiveWidths: readonly number[];
dprs: readonly number[];
}>,
): readonly ImageCandidateGeometry[] {
const candidates = new Map<number, ImageCandidateGeometry>();
for (const cssWidth of input.responsiveWidths) {
const cssHeight = Math.max(
1,
Math.round((cssWidth * input.height) / input.width),
);
for (const dpr of input.dprs) {
const pixelWidth = cssWidth * dpr;
const pixelHeight = cssHeight * dpr;
if (
!Number.isSafeInteger(pixelWidth) ||
!Number.isSafeInteger(pixelHeight)
) {
throw new TypeError(
"Image CDN preset produces fractional pixels.",
);
}
const existing = candidates.get(pixelWidth);
if (!existing || (dpr === 1 && existing.dpr !== 1)) {
const pixels = pixelWidth * pixelHeight;
candidates.set(
pixelWidth,
Object.freeze({
cssWidth,
cssHeight,
dpr,
pixelWidth,
pixelHeight,
pixels,
decodedBytes: pixels * 4,
}),
);
}
}
}
return Object.freeze(
[...candidates.values()].sort(
(left, right) => left.pixelWidth - right.pixelWidth,
),
);
}
function snapshotApplicationOrigin(input: string): string {
if (typeof input !== "string") {
throw new TypeError(
"Image CDN application origin policy is invalid.",
);
}
let parsed: URL;
try {
parsed = new URL(input);
} catch {
throw new TypeError(
"Image CDN application origin policy is invalid.",
);
}
if (
parsed.protocol !== "https:" ||
parsed.username !== "" ||
parsed.password !== "" ||
parsed.pathname !== "/" ||
parsed.search !== "" ||
parsed.hash !== "" ||
input !== parsed.origin
) {
throw new TypeError(
"Image CDN application origin policy is invalid.",
);
}
return parsed.origin;
}
function snapshotOrigin(
input: ImageCdnOriginPolicy,
): ResolvedImageCdnOrigin {
if (!hasExactOwnKeys(input, ORIGIN_KEYS)) {
throw new TypeError("Image CDN origin policy is invalid.");
}
let parsed: URL;
try {
parsed = new URL(input.origin);
} catch {
throw new TypeError("Image CDN origin policy is invalid.");
}
if (
!POLICY_TOKEN.test(input.originKey) ||
parsed.protocol !== "https:" ||
parsed.username !== "" ||
parsed.password !== "" ||
parsed.pathname !== "/" ||
parsed.search !== "" ||
parsed.hash !== "" ||
!PATH_PREFIX.test(input.assetPathPrefix) ||
input.assetPathPrefix.includes("//") ||
input.assetPathPrefix.includes("/../") ||
input.assetPathPrefix.includes("/./") ||
!positiveSafeInteger(input.minimumPublicMaxAgeSeconds) ||
input.minimumPublicMaxAgeSeconds > 315_360_000
) {
throw new TypeError("Image CDN origin policy is invalid.");
}
return Object.freeze({
originKey: input.originKey,
origin: parsed.origin,
assetPathPrefix: input.assetPathPrefix,
minimumPublicMaxAgeSeconds:
input.minimumPublicMaxAgeSeconds,
});
}
function snapshotPreset(
input: ImageCdnPresetPolicy,
hardLimits: ImageCdnHardLimits,
): ResolvedImageCdnPreset {
if (
!hasExactOwnKeys(input, PRESET_KEYS) ||
!input.reference ||
typeof input.reference !== "object" ||
!ISSUED_PRESET_REFERENCES.has(input.reference) ||
!POLICY_TOKEN.test(input.bindingId) ||
!positiveSafeInteger(input.width) ||
input.width > hardLimits.maxCssDimension ||
!positiveSafeInteger(input.height) ||
input.height > hardLimits.maxCssDimension ||
!IMAGE_FITS.includes(input.fit) ||
!Array.isArray(input.dprs) ||
input.dprs.length < 1 ||
!Array.isArray(input.responsiveWidths) ||
input.responsiveWidths.length < 1 ||
!Array.isArray(input.formats) ||
input.formats.length < 1 ||
input.formats.length > IMAGE_FORMATS.length ||
!positiveSafeInteger(input.quality) ||
input.quality > hardLimits.maxQuality ||
!SAFE_SIZES.test(input.sizes) ||
hasControlCharacters(input.sizes) ||
!["eager", "lazy"].includes(input.loading) ||
!["async", "sync"].includes(input.decoding) ||
!["high", "low", "auto"].includes(input.fetchPriority) ||
![
"no-referrer",
"strict-origin-when-cross-origin",
].includes(input.referrerPolicy) ||
!["NONE", "PRIMARY_REQUIRED"].includes(input.probeMode) ||
typeof input.allowUpscale !== "boolean" ||
!positiveSafeInteger(input.maxTransformedPixels) ||
input.maxTransformedPixels > hardLimits.maxTransformedPixels ||
!positiveSafeInteger(input.maxDecodedBytes) ||
input.maxDecodedBytes > hardLimits.maxDecodedBytes ||
!positiveSafeInteger(input.maxEncodedBytes) ||
input.maxEncodedBytes > hardLimits.maxEncodedBytes ||
(input.fetchPriority === "high" && input.loading !== "eager")
) {
throw new TypeError("Image CDN preset policy is invalid.");
}
const dprs = sortedUniqueNumbers(input.dprs);
const responsiveWidths =
sortedUniqueNumbers(input.responsiveWidths);
const formats: ImageOutputFormat[] = [
...new Set<ImageOutputFormat>(input.formats),
];
if (
dprs.length !== input.dprs.length ||
responsiveWidths.length > hardLimits.maxCandidateCount ||
formats.length !== input.formats.length ||
!dprs.includes(1) ||
!responsiveWidths.includes(input.width) ||
dprs.some(
(dpr) =>
!positiveDpr(dpr) ||
dpr > hardLimits.maxDpr,
) ||
responsiveWidths.some(
(width) =>
!positiveSafeInteger(width) ||
width > hardLimits.maxCssDimension,
) ||
formats.some(
(format) =>
!IMAGE_FORMATS.includes(format) ||
hardLimits.formatQualityCeilings[format] === undefined ||
input.quality >
(hardLimits.formatQualityCeilings[format] ?? 0),
)
) {
throw new TypeError("Image CDN preset policy is invalid.");
}
const candidates = buildImageCandidateGeometry({
width: input.width,
height: input.height,
responsiveWidths,
dprs,
});
if (
candidates.length < 1 ||
candidates.length > hardLimits.maxCandidateCount ||
candidates.some(
(candidate) =>
candidate.pixelWidth >
hardLimits.maxIntrinsicWidth ||
candidate.pixelHeight >
hardLimits.maxIntrinsicHeight ||
candidate.pixels > input.maxTransformedPixels ||
candidate.decodedBytes > input.maxDecodedBytes,
)
) {
throw new TypeError("Image CDN preset exceeds its pixel budget.");
}
return Object.freeze({
reference: input.reference,
bindingId: input.bindingId,
width: input.width,
height: input.height,
fit: input.fit,
dprs: Object.freeze(dprs),
responsiveWidths: Object.freeze(responsiveWidths),
quality: input.quality,
formats: Object.freeze(formats),
sizes: input.sizes,
loading: input.loading,
decoding: input.decoding,
fetchPriority: input.fetchPriority,
referrerPolicy: input.referrerPolicy,
probeMode: input.probeMode,
allowUpscale: input.allowUpscale,
maxTransformedPixels: input.maxTransformedPixels,
maxDecodedBytes: input.maxDecodedBytes,
maxEncodedBytes: input.maxEncodedBytes,
candidates,
});
}
function snapshotHardLimits(
input: ImageCdnHardLimits,
): ImageCdnHardLimits {
const ceilings = IMAGE_CDN_IMPLEMENTATION_CEILINGS;
if (
!hasExactOwnKeys(input, HARD_LIMIT_KEYS) ||
!positiveSafeInteger(input.maxIntrinsicWidth) ||
input.maxIntrinsicWidth > ceilings.maxIntrinsicWidth ||
!positiveSafeInteger(input.maxIntrinsicHeight) ||
input.maxIntrinsicHeight > ceilings.maxIntrinsicHeight ||
!positiveSafeInteger(input.maxSourcePixels) ||
input.maxSourcePixels > ceilings.maxSourcePixels ||
!positiveSafeInteger(input.maxCssDimension) ||
input.maxCssDimension > input.maxIntrinsicWidth ||
input.maxCssDimension > ceilings.maxCssDimension ||
!positiveDpr(input.maxDpr) ||
input.maxDpr > ceilings.maxDpr ||
!positiveSafeInteger(input.maxQuality) ||
input.maxQuality > ceilings.maxQuality ||
!positiveSafeInteger(input.maxCandidateCount) ||
input.maxCandidateCount > ceilings.maxCandidateCount ||
!positiveSafeInteger(input.maxTransformedPixels) ||
input.maxTransformedPixels > ceilings.maxTransformedPixels ||
!positiveSafeInteger(input.maxDecodedBytes) ||
input.maxDecodedBytes > ceilings.maxDecodedBytes ||
!positiveSafeInteger(input.maxEncodedBytes) ||
input.maxEncodedBytes > ceilings.maxEncodedBytes ||
!positiveSafeInteger(input.maxUrlLength) ||
input.maxUrlLength > ceilings.maxUrlLength ||
!positiveSafeInteger(input.maxCapabilityLifetimeMs) ||
input.maxCapabilityLifetimeMs >
ceilings.maxCapabilityLifetimeMs ||
!nonNegativeSafeInteger(input.maxClockSkewMs) ||
input.maxClockSkewMs > ceilings.maxClockSkewMs ||
!nonNegativeSafeInteger(input.minCapabilityRemainingMs) ||
input.minCapabilityRemainingMs >
input.maxCapabilityLifetimeMs ||
input.minCapabilityRemainingMs >
ceilings.maxMinimumCapabilityRemainingMs ||
!positiveSafeInteger(input.maxPresetBindingsPerCapability) ||
input.maxPresetBindingsPerCapability >
ceilings.maxPresetBindingsPerCapability ||
!positiveSafeInteger(
input.maxConcurrentCapabilityVerifications,
) ||
input.maxConcurrentCapabilityVerifications >
ceilings.maxConcurrentCapabilityVerifications ||
!Array.isArray(input.allowedSourceMediaTypes) ||
input.allowedSourceMediaTypes.length < 1 ||
!input.formatQualityCeilings ||
typeof input.formatQualityCeilings !== "object" ||
Array.isArray(input.formatQualityCeilings)
) {
throw new TypeError("Image CDN hard limits are invalid.");
}
const allowedSourceMediaTypes = [
...new Set(input.allowedSourceMediaTypes),
];
if (
allowedSourceMediaTypes.length !==
input.allowedSourceMediaTypes.length ||
allowedSourceMediaTypes.some(
(mediaType) => !IMAGE_MEDIA_TYPES.includes(mediaType),
)
) {
throw new TypeError("Image CDN source media policy is invalid.");
}
const formatQualityCeilings:
Partial<Record<ImageOutputFormat, number>> = {};
for (const [format, ceiling] of Object.entries(
input.formatQualityCeilings,
)) {
if (
!IMAGE_FORMATS.includes(format as ImageOutputFormat) ||
!positiveSafeInteger(ceiling) ||
ceiling > input.maxQuality
) {
throw new TypeError("Image CDN format ceiling is invalid.");
}
formatQualityCeilings[format as ImageOutputFormat] = ceiling;
}
return Object.freeze({
maxIntrinsicWidth: input.maxIntrinsicWidth,
maxIntrinsicHeight: input.maxIntrinsicHeight,
maxSourcePixels: input.maxSourcePixels,
maxCssDimension: input.maxCssDimension,
maxDpr: input.maxDpr,
maxQuality: input.maxQuality,
maxCandidateCount: input.maxCandidateCount,
maxTransformedPixels: input.maxTransformedPixels,
maxDecodedBytes: input.maxDecodedBytes,
maxEncodedBytes: input.maxEncodedBytes,
maxUrlLength: input.maxUrlLength,
maxCapabilityLifetimeMs: input.maxCapabilityLifetimeMs,
maxClockSkewMs: input.maxClockSkewMs,
minCapabilityRemainingMs: input.minCapabilityRemainingMs,
maxPresetBindingsPerCapability:
input.maxPresetBindingsPerCapability,
maxConcurrentCapabilityVerifications:
input.maxConcurrentCapabilityVerifications,
allowedSourceMediaTypes: Object.freeze(
allowedSourceMediaTypes,
),
formatQualityCeilings:
Object.freeze(formatQualityCeilings),
});
}
function snapshotCapabilityPolicy(
input: ImageCdnCapabilityPolicy,
): ImageCdnCapabilityPolicy {
if (
!hasExactOwnKeys(input, CAPABILITY_KEYS) ||
!POLICY_TOKEN.test(input.issuer) ||
!Array.isArray(input.acceptedKeyIds) ||
input.acceptedKeyIds.length < 1 ||
input.acceptedKeyIds.length >
IMAGE_CDN_IMPLEMENTATION_CEILINGS.maxAcceptedKeyIds
) {
throw new TypeError("Image CDN capability policy is invalid.");
}
const acceptedKeyIds = [...input.acceptedKeyIds];
if (
new Set(acceptedKeyIds).size !== acceptedKeyIds.length ||
acceptedKeyIds.some((keyId) => !POLICY_TOKEN.test(keyId))
) {
throw new TypeError("Image CDN capability policy is invalid.");
}
return Object.freeze({
issuer: input.issuer,
acceptedKeyIds: Object.freeze(acceptedKeyIds),
});
}
function sortedUniqueNumbers(values: readonly number[]): number[] {
return [...new Set(values)].sort((left, right) => left - right);
}
function positiveDpr(value: number): boolean {
return (
Number.isFinite(value) &&
value > 0 &&
Number.isSafeInteger(value * 100)
);
}
function positiveSafeInteger(value: number): boolean {
return Number.isSafeInteger(value) && value > 0;
}
function nonNegativeSafeInteger(value: number): boolean {
return Number.isSafeInteger(value) && value >= 0;
}
function hasExactOwnKeys(
input: unknown,
keys: readonly string[],
): boolean {
if (!input || typeof input !== "object" || Array.isArray(input)) {
return false;
}
const actual = Object.keys(input).sort();
const expected = [...keys].sort();
return (
actual.length === expected.length &&
actual.every((key, index) => key === expected[index])
);
}
function hasControlCharacters(value: string): boolean {
return [...value].some((character) => {
const codePoint = character.codePointAt(0) ?? 0;
return codePoint < 0x20 || codePoint === 0x7f;
});
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,734 @@
import type { ImageRasterMediaType } from "../../../application/ports/browser-transfer/image-cdn.ts";
export type StaticImageHeaderMetadata = Readonly<{
width: number;
height: number;
}>;
/**
* Parses only the deliberately supported static-image subset. Unknown,
* ambiguous, animated and structurally malformed containers fail closed
* before a native decoder can allocate an output surface.
*/
export function parseStaticImageHeaderMetadata(
bytes: Uint8Array,
mediaType: ImageRasterMediaType,
): StaticImageHeaderMetadata | null {
switch (mediaType) {
case "image/avif":
return parseAvif(bytes);
case "image/jpeg":
return parseJpeg(bytes);
case "image/png":
return parsePng(bytes);
case "image/webp":
return parseWebp(bytes);
}
}
function parsePng(
bytes: Uint8Array,
): StaticImageHeaderMetadata | null {
const signature = [
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
];
if (
bytes.byteLength < 33 ||
!signature.every((value, index) => bytes[index] === value)
) {
return null;
}
const view = dataView(bytes);
let offset = 8;
let dimensions: StaticImageHeaderMetadata | null = null;
let chunkIndex = 0;
let ended = false;
let imageDataSeen = false;
while (offset < bytes.byteLength) {
if (offset + 12 > bytes.byteLength) return null;
const length = view.getUint32(offset);
const type = ascii(bytes, offset + 4, offset + 8);
const payloadStart = offset + 8;
const payloadEnd = payloadStart + length;
const chunkEnd = payloadEnd + 4;
if (
!Number.isSafeInteger(chunkEnd) ||
chunkEnd > bytes.byteLength
) {
return null;
}
if (chunkIndex === 0 && (type !== "IHDR" || length !== 13)) {
return null;
}
if (type === "IHDR") {
if (dimensions || length !== 13) return null;
const width = view.getUint32(payloadStart);
const height = view.getUint32(payloadStart + 4);
dimensions = validDimensions(width, height);
const bitDepth = bytes[payloadStart + 8];
const colorType = bytes[payloadStart + 9];
const compression = bytes[payloadStart + 10];
const filter = bytes[payloadStart + 11];
const interlace = bytes[payloadStart + 12];
if (
!dimensions ||
bitDepth === undefined ||
colorType === undefined ||
!validPngColorDepth(colorType, bitDepth) ||
compression !== 0 ||
filter !== 0 ||
(interlace !== 0 && interlace !== 1)
) {
return null;
}
}
if (type === "acTL" || type === "fcTL" || type === "fdAT") {
return null;
}
if (type === "IDAT") imageDataSeen = true;
if (type === "IEND") {
if (
length !== 0 ||
!imageDataSeen ||
chunkEnd !== bytes.byteLength
) {
return null;
}
ended = true;
}
offset = chunkEnd;
chunkIndex += 1;
if (ended) break;
}
return ended && dimensions ? dimensions : null;
}
function validPngColorDepth(
colorType: number,
bitDepth: number,
): boolean {
const supportedDepths: Readonly<Record<number, readonly number[]>> =
{
0: [1, 2, 4, 8, 16],
2: [8, 16],
3: [1, 2, 4, 8],
4: [8, 16],
6: [8, 16],
};
return supportedDepths[colorType]?.includes(bitDepth) ?? false;
}
function parseJpeg(
bytes: Uint8Array,
): StaticImageHeaderMetadata | null {
if (
bytes.byteLength < 4 ||
bytes[0] !== 0xff ||
bytes[1] !== 0xd8
) {
return null;
}
const supportedStartOfFrame = new Set([0xc0, 0xc1, 0xc2]);
const unsupportedStartOfFrame = new Set([
0xc3, 0xc5, 0xc6, 0xc7, 0xc9, 0xca, 0xcb, 0xcd, 0xce, 0xcf,
]);
const view = dataView(bytes);
let dimensions: StaticImageHeaderMetadata | null = null;
let offset = 2;
while (offset < bytes.byteLength) {
if (bytes[offset] !== 0xff) return null;
while (offset < bytes.byteLength && bytes[offset] === 0xff) {
offset += 1;
}
if (offset >= bytes.byteLength) return null;
const marker = bytes[offset];
offset += 1;
if (marker === undefined || marker === 0x00) return null;
if (marker === 0xd9) return null;
if (marker === 0xda) return dimensions;
if (
marker === 0xd8 ||
marker === 0x01 ||
(marker >= 0xd0 && marker <= 0xd7)
) {
continue;
}
if (offset + 2 > bytes.byteLength) return null;
const segmentLength = view.getUint16(offset);
if (segmentLength < 2) return null;
const segmentEnd = offset + segmentLength;
if (segmentEnd > bytes.byteLength) return null;
if (unsupportedStartOfFrame.has(marker)) return null;
if (supportedStartOfFrame.has(marker)) {
if (dimensions || segmentLength < 8) return null;
const precision = bytes[offset + 2];
const height = view.getUint16(offset + 3);
const width = view.getUint16(offset + 5);
const componentCount = bytes[offset + 7];
dimensions = validDimensions(width, height);
if (
!dimensions ||
precision !== 8 ||
(componentCount !== 1 && componentCount !== 3) ||
segmentLength !== 8 + componentCount * 3
) {
return null;
}
}
offset = segmentEnd;
}
return null;
}
function parseWebp(
bytes: Uint8Array,
): StaticImageHeaderMetadata | null {
if (
bytes.byteLength < 20 ||
ascii(bytes, 0, 4) !== "RIFF" ||
ascii(bytes, 8, 12) !== "WEBP"
) {
return null;
}
const view = dataView(bytes);
const riffLength = view.getUint32(4, true) + 8;
if (riffLength !== bytes.byteLength) return null;
let offset = 12;
let dimensions: StaticImageHeaderMetadata | null = null;
let imagePayloadCount = 0;
let extendedHeaderSeen = false;
let chunkIndex = 0;
while (offset < bytes.byteLength) {
if (offset + 8 > bytes.byteLength) return null;
const type = ascii(bytes, offset, offset + 4);
const length = view.getUint32(offset + 4, true);
const payloadStart = offset + 8;
const payloadEnd = payloadStart + length;
const chunkEnd = payloadEnd + (length % 2);
if (
!Number.isSafeInteger(chunkEnd) ||
chunkEnd > bytes.byteLength
) {
return null;
}
if (
length % 2 === 1 &&
bytes[payloadEnd] !== 0
) {
return null;
}
if (type === "ANIM" || type === "ANMF") return null;
let candidate: StaticImageHeaderMetadata | null = null;
if (type === "VP8X") {
if (
chunkIndex !== 0 ||
extendedHeaderSeen ||
length !== 10 ||
bytes[payloadStart] === undefined ||
(bytes[payloadStart] & 0xc3) !== 0
) {
return null;
}
extendedHeaderSeen = true;
candidate = validDimensions(
readUint24LittleEndian(bytes, payloadStart + 4) + 1,
readUint24LittleEndian(bytes, payloadStart + 7) + 1,
);
} else if (type === "VP8 ") {
imagePayloadCount += 1;
if (
length < 10 ||
bytes[payloadStart + 3] !== 0x9d ||
bytes[payloadStart + 4] !== 0x01 ||
bytes[payloadStart + 5] !== 0x2a
) {
return null;
}
candidate = validDimensions(
view.getUint16(payloadStart + 6, true) & 0x3fff,
view.getUint16(payloadStart + 8, true) & 0x3fff,
);
} else if (type === "VP8L") {
imagePayloadCount += 1;
if (length < 5 || bytes[payloadStart] !== 0x2f) {
return null;
}
const byte1 = bytes[payloadStart + 1];
const byte2 = bytes[payloadStart + 2];
const byte3 = bytes[payloadStart + 3];
const byte4 = bytes[payloadStart + 4];
if (
byte1 === undefined ||
byte2 === undefined ||
byte3 === undefined ||
byte4 === undefined ||
(byte4 & 0xe0) !== 0
) {
return null;
}
candidate = validDimensions(
1 + byte1 + ((byte2 & 0x3f) << 8),
1 +
((byte2 & 0xc0) >> 6) +
(byte3 << 2) +
((byte4 & 0x0f) << 10),
);
}
if (candidate) {
if (
dimensions &&
(dimensions.width !== candidate.width ||
dimensions.height !== candidate.height)
) {
return null;
}
dimensions = candidate;
}
offset = chunkEnd;
chunkIndex += 1;
}
return offset === bytes.byteLength &&
dimensions &&
imagePayloadCount === 1
? dimensions
: null;
}
function parseAvif(
bytes: Uint8Array,
): StaticImageHeaderMetadata | null {
if (bytes.byteLength < 24) return null;
const boxes = parseBoxes(bytes, 0, bytes.byteLength);
if (!boxes || boxes.length < 2 || boxes[0]?.type !== "ftyp") {
return null;
}
const fileType = boxes[0];
const fileTypeLength = fileType
? fileType.payloadEnd - fileType.payloadStart
: 0;
if (
!fileType ||
fileTypeLength < 8 ||
(fileTypeLength - 8) % 4 !== 0
) {
return null;
}
const brands: string[] = [
ascii(bytes, fileType.payloadStart, fileType.payloadStart + 4),
];
for (
let offset = fileType.payloadStart + 8;
offset + 4 <= fileType.payloadEnd;
offset += 4
) {
brands.push(ascii(bytes, offset, offset + 4));
}
if (!brands.includes("avif") || brands.includes("avis")) {
return null;
}
if (
boxes.some((box) => box.type === "moov") ||
!boxes.some(
(box) =>
box.type === "mdat" &&
box.payloadEnd > box.payloadStart,
)
) {
return null;
}
const metadataBoxes = boxes.filter((box) => box.type === "meta");
const metadataBox = metadataBoxes[0];
if (
metadataBoxes.length !== 1 ||
!metadataBox ||
metadataBox.payloadStart + 4 > metadataBox.payloadEnd ||
!zeroFullBoxFlags(bytes, metadataBox.payloadStart)
) {
return null;
}
const metadataChildren = parseBoxes(
bytes,
metadataBox.payloadStart + 4,
metadataBox.payloadEnd,
);
if (!metadataChildren) return null;
const state: AvifMetadataState = {
associations: new Map(),
itemTypes: new Map(),
primaryItemId: null,
properties: new Map(),
propertyCount: 0,
};
let itemInfoSeen = false;
let itemPropertiesSeen = false;
for (const box of metadataChildren) {
if (box.type === "pitm") {
const primaryItemId = parseAvifPrimaryItem(bytes, box);
if (
primaryItemId === null ||
state.primaryItemId !== null
) {
return null;
}
state.primaryItemId = primaryItemId;
} else if (box.type === "iinf") {
if (itemInfoSeen || !parseAvifItemInfo(bytes, box, state)) {
return null;
}
itemInfoSeen = true;
} else if (box.type === "iprp") {
if (
itemPropertiesSeen ||
!parseAvifItemProperties(bytes, box, state)
) {
return null;
}
itemPropertiesSeen = true;
}
}
const primaryItemId = state.primaryItemId;
if (
primaryItemId === null ||
state.itemTypes.get(primaryItemId) !== "av01"
) {
return null;
}
const associatedProperties = state.associations.get(primaryItemId);
if (!associatedProperties) return null;
const associatedExtents: StaticImageHeaderMetadata[] = [];
const seenProperties = new Set<number>();
for (const propertyIndex of associatedProperties) {
if (
propertyIndex < 1 ||
propertyIndex > state.propertyCount ||
seenProperties.has(propertyIndex)
) {
return null;
}
seenProperties.add(propertyIndex);
const dimensions = state.properties.get(propertyIndex);
if (dimensions) associatedExtents.push(dimensions);
}
return associatedExtents.length === 1
? (associatedExtents[0] ?? null)
: null;
}
type IsoBox = Readonly<{
type: string;
payloadStart: number;
payloadEnd: number;
}>;
type AvifMetadataState = {
primaryItemId: number | null;
itemTypes: Map<number, string>;
properties: Map<number, StaticImageHeaderMetadata>;
associations: Map<number, readonly number[]>;
propertyCount: number;
};
function parseAvifPrimaryItem(
bytes: Uint8Array,
box: IsoBox,
): number | null {
const version = bytes[box.payloadStart];
const view = dataView(bytes);
if (!zeroFullBoxFlags(bytes, box.payloadStart)) return null;
if (
version === 0 &&
box.payloadEnd - box.payloadStart === 6
) {
return view.getUint16(box.payloadStart + 4);
}
if (
version === 1 &&
box.payloadEnd - box.payloadStart === 8
) {
return view.getUint32(box.payloadStart + 4);
}
return null;
}
function parseAvifItemInfo(
bytes: Uint8Array,
box: IsoBox,
state: AvifMetadataState,
): boolean {
const start = box.payloadStart;
const end = box.payloadEnd;
const version = bytes[start];
if (
(version !== 0 && version !== 1) ||
!zeroFullBoxFlags(bytes, start)
) {
return false;
}
const entryBytes = version === 0 ? 2 : 4;
if (start + 4 + entryBytes > end) return false;
const view = dataView(bytes);
const declaredEntries =
entryBytes === 2
? view.getUint16(start + 4)
: view.getUint32(start + 4);
const entriesStart = start + 4 + entryBytes;
const boxes = parseBoxes(bytes, entriesStart, end);
if (
!boxes ||
boxes.length !== declaredEntries ||
boxes.some((entry) => entry.type !== "infe")
) {
return false;
}
for (const entry of boxes) {
const itemVersion = bytes[entry.payloadStart];
if (!zeroFullBoxFlags(bytes, entry.payloadStart)) return false;
let itemId: number;
let itemTypeOffset: number;
if (itemVersion === 2) {
if (entry.payloadStart + 12 > entry.payloadEnd) return false;
itemId = view.getUint16(entry.payloadStart + 4);
itemTypeOffset = entry.payloadStart + 8;
} else if (itemVersion === 3) {
if (entry.payloadStart + 14 > entry.payloadEnd) return false;
itemId = view.getUint32(entry.payloadStart + 4);
itemTypeOffset = entry.payloadStart + 10;
} else {
return false;
}
const itemType = ascii(
bytes,
itemTypeOffset,
itemTypeOffset + 4,
);
if (itemType === "grid" || itemType === "iovl") {
return false;
}
if (itemId === 0 || state.itemTypes.has(itemId)) return false;
state.itemTypes.set(itemId, itemType);
}
return true;
}
function parseAvifItemProperties(
bytes: Uint8Array,
box: IsoBox,
state: AvifMetadataState,
): boolean {
const boxes = parseBoxes(
bytes,
box.payloadStart,
box.payloadEnd,
);
if (!boxes) return false;
const propertyContainers = boxes.filter(
(child) => child.type === "ipco",
);
const associationBoxes = boxes.filter(
(child) => child.type === "ipma",
);
const propertyContainer = propertyContainers[0];
if (
propertyContainers.length !== 1 ||
associationBoxes.length < 1 ||
!propertyContainer
) {
return false;
}
const properties = parseBoxes(
bytes,
propertyContainer.payloadStart,
propertyContainer.payloadEnd,
);
if (!properties) return false;
state.propertyCount = properties.length;
for (const [offset, property] of properties.entries()) {
if (property.type !== "ispe") continue;
if (
property.payloadEnd - property.payloadStart !== 12 ||
bytes[property.payloadStart] !== 0 ||
bytes[property.payloadStart + 1] !== 0 ||
bytes[property.payloadStart + 2] !== 0 ||
bytes[property.payloadStart + 3] !== 0
) {
return false;
}
const view = dataView(bytes);
const dimensions = validDimensions(
view.getUint32(property.payloadStart + 4),
view.getUint32(property.payloadStart + 8),
);
if (!dimensions) return false;
state.properties.set(offset + 1, dimensions);
}
return associationBoxes.every((association) =>
parseAvifPropertyAssociations(bytes, association, state)
);
}
function parseAvifPropertyAssociations(
bytes: Uint8Array,
box: IsoBox,
state: AvifMetadataState,
): boolean {
const start = box.payloadStart;
const end = box.payloadEnd;
if (start + 8 > end) return false;
const version = bytes[start];
if (version !== 0 && version !== 1) return false;
const flags =
((bytes[start + 1] ?? 0) << 16) |
((bytes[start + 2] ?? 0) << 8) |
(bytes[start + 3] ?? 0);
if ((flags & ~1) !== 0) return false;
const wideAssociation = (flags & 1) === 1;
const view = dataView(bytes);
const entryCount = view.getUint32(start + 4);
let offset = start + 8;
for (let entry = 0; entry < entryCount; entry += 1) {
const itemIdBytes = version === 0 ? 2 : 4;
if (offset + itemIdBytes + 1 > end) return false;
const itemId =
itemIdBytes === 2
? view.getUint16(offset)
: view.getUint32(offset);
offset += itemIdBytes;
const associationCount = bytes[offset];
if (associationCount === undefined) return false;
offset += 1;
const propertyIndices: number[] = [];
for (
let association = 0;
association < associationCount;
association += 1
) {
const associationBytes = wideAssociation ? 2 : 1;
if (offset + associationBytes > end) return false;
const encoded =
associationBytes === 2
? view.getUint16(offset)
: (bytes[offset] ?? 0);
const propertyIndex =
encoded & (wideAssociation ? 0x7fff : 0x7f);
offset += associationBytes;
if (propertyIndex !== 0) propertyIndices.push(propertyIndex);
}
if (itemId === 0 || state.associations.has(itemId)) {
return false;
}
state.associations.set(itemId, propertyIndices);
}
return offset === end;
}
function parseBoxes(
bytes: Uint8Array,
start: number,
end: number,
): readonly IsoBox[] | null {
if (
!Number.isSafeInteger(start) ||
!Number.isSafeInteger(end) ||
start < 0 ||
end > bytes.byteLength ||
start > end
) {
return null;
}
const boxes: IsoBox[] = [];
const view = dataView(bytes);
let offset = start;
while (offset < end) {
if (offset + 8 > end) return null;
const shortSize = view.getUint32(offset);
const type = ascii(bytes, offset + 4, offset + 8);
let boxSize = shortSize;
let headerSize = 8;
if (shortSize === 0) return null;
if (shortSize === 1) {
if (offset + 16 > end) return null;
const longSize = view.getBigUint64(offset + 8);
if (longSize > BigInt(Number.MAX_SAFE_INTEGER)) return null;
boxSize = Number(longSize);
headerSize = 16;
}
if (boxSize < headerSize || offset + boxSize > end) {
return null;
}
boxes.push(
Object.freeze({
type,
payloadStart: offset + headerSize,
payloadEnd: offset + boxSize,
}),
);
offset += boxSize;
}
return offset === end ? boxes : null;
}
function zeroFullBoxFlags(
bytes: Uint8Array,
offset: number,
): boolean {
return (
bytes[offset + 1] === 0 &&
bytes[offset + 2] === 0 &&
bytes[offset + 3] === 0
);
}
function validDimensions(
width: number,
height: number,
): StaticImageHeaderMetadata | null {
return Number.isSafeInteger(width) &&
Number.isSafeInteger(height) &&
width > 0 &&
height > 0
? Object.freeze({ width, height })
: null;
}
function readUint24LittleEndian(
bytes: Uint8Array,
offset: number,
): number {
const byte0 = bytes[offset];
const byte1 = bytes[offset + 1];
const byte2 = bytes[offset + 2];
if (
byte0 === undefined ||
byte1 === undefined ||
byte2 === undefined
) {
return Number.NaN;
}
return byte0 | (byte1 << 8) | (byte2 << 16);
}
function ascii(
bytes: Uint8Array,
start: number,
end: number,
): string {
let value = "";
for (let offset = start; offset < end; offset += 1) {
const byte = bytes[offset];
if (byte === undefined) return "";
value += String.fromCharCode(byte);
}
return value;
}
function dataView(bytes: Uint8Array): DataView {
return new DataView(
bytes.buffer,
bytes.byteOffset,
bytes.byteLength,
);
}
@@ -0,0 +1,34 @@
export {
createBrowserImageProbe,
type BrowserImageProbeDependencies,
type DecodedImageFacade,
type ImageProbeScheduler,
} from "./browser-image-probe.ts";
export {
IMAGE_FORMAT_MEDIA_TYPE,
IMAGE_CDN_IMPLEMENTATION_CEILINGS,
ImageCdnPolicyRegistry,
buildImageCandidateGeometry,
imageCdnPresetReference,
type ImageCandidateGeometry,
type ImageCdnCapabilityPolicy,
type ImageCdnHardLimits,
type ImageCdnOriginPolicy,
type ImageCdnPolicyRegistryOptions,
type ImageCdnPresetPolicy,
type ResolvedImageCdnOrigin,
type ResolvedImageCdnPreset,
} from "./image-cdn-policy.ts";
export {
canonicalImageCapabilityPayload,
computeImageCapabilityBindingDigestHex,
createImageCdnRuntime,
DEFAULT_IMAGE_CAPABILITY_VERIFICATION_TIMEOUT_MS,
MAX_IMAGE_CAPABILITY_VERIFICATION_TIMEOUT_MS,
type ImageCapabilityVerificationScheduler,
type ImageCdnRuntimeDependencies,
} from "./image-cdn-runtime.ts";
export {
createP256ImageCapabilityVerifier,
type P256ImageCapabilityVerifierOptions,
} from "./p256-image-capability-verifier.ts";
@@ -0,0 +1,134 @@
import type {
ImageCapabilityVerificationRequest,
ImageCapabilityVerifier,
} from "../../../application/ports/browser-transfer/image-cdn.ts";
export type P256ImageCapabilityVerifierOptions = Readonly<{
subtle: Pick<SubtleCrypto, "verify">;
publicKeys: readonly Readonly<{
keyId: string;
key: CryptoKey;
}>[];
}>;
const KEY_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
/**
* Concrete verifier for backend-issued ECDSA P-256/SHA-256 capabilities.
* Signatures use the 64-byte IEEE-P1363 representation required by this
* contract, encoded as unpadded base64url.
*/
export function createP256ImageCapabilityVerifier(
options: P256ImageCapabilityVerifierOptions,
): ImageCapabilityVerifier {
if (
!options ||
typeof options !== "object" ||
!Array.isArray(options.publicKeys) ||
options.publicKeys.length < 1 ||
options.publicKeys.length > 16
) {
throw new TypeError(
"Image capability verifier configuration is invalid.",
);
}
const verify = options.subtle.verify.bind(options.subtle);
const keys = new Map<string, CryptoKey>();
for (const binding of options.publicKeys) {
const algorithmName =
binding.key.algorithm &&
typeof binding.key.algorithm === "object" &&
"name" in binding.key.algorithm
? binding.key.algorithm.name
: null;
const namedCurve =
binding.key.algorithm &&
typeof binding.key.algorithm === "object" &&
"namedCurve" in binding.key.algorithm
? binding.key.algorithm.namedCurve
: null;
if (
!KEY_ID.test(binding.keyId) ||
binding.key.type !== "public" ||
algorithmName !== "ECDSA" ||
namedCurve !== "P-256" ||
!binding.key.usages.includes("verify") ||
keys.has(binding.keyId)
) {
throw new TypeError(
"Image capability public key binding is invalid.",
);
}
keys.set(binding.keyId, binding.key);
}
return Object.freeze({
acceptsKey(keyId: string): boolean {
return KEY_ID.test(keyId) && keys.has(keyId);
},
async verify(
request: ImageCapabilityVerificationRequest,
): Promise<boolean> {
if (
request.algorithm !== "ECDSA_P256_SHA256" ||
!KEY_ID.test(request.keyId) ||
!(request.canonicalPayload instanceof Uint8Array) ||
request.canonicalPayload.byteLength < 1 ||
request.canonicalPayload.byteLength > 8_192
) {
return false;
}
const key = keys.get(request.keyId);
if (!key) return false;
const signature = decodeBase64Url(
request.signatureBase64Url,
);
if (!signature || signature.byteLength !== 64) {
return false;
}
try {
const signatureBytes = new Uint8Array(signature.byteLength);
signatureBytes.set(signature);
const payloadBytes = new Uint8Array(
request.canonicalPayload.byteLength,
);
payloadBytes.set(request.canonicalPayload);
return await verify(
{ name: "ECDSA", hash: "SHA-256" },
key,
signatureBytes.buffer,
payloadBytes.buffer,
);
} catch {
return false;
}
},
});
}
function decodeBase64Url(value: string): Uint8Array | null {
if (
!/^[A-Za-z0-9_-]+$/u.test(value) ||
value.length % 4 === 1
) {
return null;
}
const alphabet =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
const output: number[] = [];
let accumulator = 0;
let bitCount = 0;
for (const character of value) {
const index = alphabet.indexOf(character);
if (index < 0) return null;
accumulator = (accumulator << 6) | index;
bitCount += 6;
if (bitCount >= 8) {
bitCount -= 8;
output.push((accumulator >> bitCount) & 0xff);
accumulator &= (1 << bitCount) - 1;
}
}
if (bitCount > 0 && accumulator !== 0) return null;
return Uint8Array.from(output);
}
+3
View File
@@ -0,0 +1,3 @@
export * from "./image-cdn/index.ts";
export * from "./presigned/index.ts";
export * from "./resumable-upload/index.ts";
@@ -0,0 +1,204 @@
const INITIAL_STATE = new Uint32Array([
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
]);
const ROUND_CONSTANTS = new Uint32Array([
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc,
0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3,
0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5,
0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
]);
export type StreamingSha256Verifier = Readonly<{
update(bytes: Uint8Array): void;
verify(): boolean;
}>;
export function createStreamingSha256Verifier(
expectedSha256: string,
): StreamingSha256Verifier {
const accumulator = new Sha256Accumulator();
let verified = false;
return Object.freeze({
update(bytes: Uint8Array) {
if (verified) throw new TypeError("SHA-256 verifier is finalized.");
accumulator.update(bytes);
},
verify() {
if (verified) throw new TypeError("SHA-256 verifier is finalized.");
verified = true;
return constantTimeHexEqual(
accumulator.digestHex(),
expectedSha256.toLowerCase(),
);
},
});
}
export function sha256Hex(bytes: Uint8Array): string {
const accumulator = new Sha256Accumulator();
accumulator.update(bytes);
return accumulator.digestHex();
}
class Sha256Accumulator {
readonly #state = new Uint32Array(INITIAL_STATE);
readonly #buffer = new Uint8Array(64);
readonly #schedule = new Uint32Array(64);
#bufferLength = 0;
#totalBytes = 0;
#finalized = false;
update(bytes: Uint8Array): void {
if (this.#finalized || !(bytes instanceof Uint8Array)) {
throw new TypeError("SHA-256 input is invalid.");
}
const nextTotal = this.#totalBytes + bytes.byteLength;
if (!Number.isSafeInteger(nextTotal)) {
throw new TypeError("SHA-256 input is too large.");
}
this.#totalBytes = nextTotal;
let offset = 0;
if (this.#bufferLength > 0) {
const available = 64 - this.#bufferLength;
const copied = Math.min(available, bytes.byteLength);
this.#buffer.set(bytes.subarray(0, copied), this.#bufferLength);
this.#bufferLength += copied;
offset += copied;
if (this.#bufferLength === 64) {
this.#compress(this.#buffer);
this.#bufferLength = 0;
}
}
while (offset + 64 <= bytes.byteLength) {
this.#compress(bytes.subarray(offset, offset + 64));
offset += 64;
}
if (offset < bytes.byteLength) {
const remainder = bytes.subarray(offset);
this.#buffer.set(remainder, 0);
this.#bufferLength = remainder.byteLength;
}
}
digestHex(): string {
if (this.#finalized) throw new TypeError("SHA-256 is finalized.");
this.#finalized = true;
const finalLength = this.#bufferLength < 56 ? 64 : 128;
const finalBlocks = new Uint8Array(finalLength);
finalBlocks.set(this.#buffer.subarray(0, this.#bufferLength));
finalBlocks[this.#bufferLength] = 0x80;
const bitLength = BigInt(this.#totalBytes) * 8n;
for (let index = 0; index < 8; index += 1) {
finalBlocks[finalLength - 1 - index] = Number(
(bitLength >> BigInt(index * 8)) & 0xffn,
);
}
for (let offset = 0; offset < finalLength; offset += 64) {
this.#compress(finalBlocks.subarray(offset, offset + 64));
}
return Array.from(this.#state, (word) =>
word.toString(16).padStart(8, "0"),
).join("");
}
#compress(block: Uint8Array): void {
const words = this.#schedule;
const view = new DataView(
block.buffer,
block.byteOffset,
block.byteLength,
);
for (let index = 0; index < 16; index += 1) {
words[index] = view.getUint32(index * 4, false);
}
for (let index = 16; index < 64; index += 1) {
const previous15 = words[index - 15] ?? 0;
const previous2 = words[index - 2] ?? 0;
const sigma0 =
rotateRight(previous15, 7) ^
rotateRight(previous15, 18) ^
(previous15 >>> 3);
const sigma1 =
rotateRight(previous2, 17) ^
rotateRight(previous2, 19) ^
(previous2 >>> 10);
words[index] =
((words[index - 16] ?? 0) +
sigma0 +
(words[index - 7] ?? 0) +
sigma1) >>>
0;
}
let a = this.#state[0] ?? 0;
let b = this.#state[1] ?? 0;
let c = this.#state[2] ?? 0;
let d = this.#state[3] ?? 0;
let e = this.#state[4] ?? 0;
let f = this.#state[5] ?? 0;
let g = this.#state[6] ?? 0;
let h = this.#state[7] ?? 0;
for (let index = 0; index < 64; index += 1) {
const sum1 =
rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25);
const choice = (e & f) ^ (~e & g);
const temporary1 =
(h +
sum1 +
choice +
(ROUND_CONSTANTS[index] ?? 0) +
(words[index] ?? 0)) >>>
0;
const sum0 =
rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22);
const majority = (a & b) ^ (a & c) ^ (b & c);
const temporary2 = (sum0 + majority) >>> 0;
h = g;
g = f;
f = e;
e = (d + temporary1) >>> 0;
d = c;
c = b;
b = a;
a = (temporary1 + temporary2) >>> 0;
}
this.#state[0] = ((this.#state[0] ?? 0) + a) >>> 0;
this.#state[1] = ((this.#state[1] ?? 0) + b) >>> 0;
this.#state[2] = ((this.#state[2] ?? 0) + c) >>> 0;
this.#state[3] = ((this.#state[3] ?? 0) + d) >>> 0;
this.#state[4] = ((this.#state[4] ?? 0) + e) >>> 0;
this.#state[5] = ((this.#state[5] ?? 0) + f) >>> 0;
this.#state[6] = ((this.#state[6] ?? 0) + g) >>> 0;
this.#state[7] = ((this.#state[7] ?? 0) + h) >>> 0;
}
}
function rotateRight(value: number, bits: number): number {
return (value >>> bits) | (value << (32 - bits));
}
function constantTimeHexEqual(left: string, right: string): boolean {
let mismatch = left.length ^ right.length;
const length = Math.max(left.length, right.length);
for (let index = 0; index < length; index += 1) {
mismatch |=
(left.charCodeAt(index) || 0) ^ (right.charCodeAt(index) || 0);
}
return mismatch === 0;
}
@@ -0,0 +1,18 @@
export {
createPresignedCapabilityHttpProvider,
type PresignedCapabilityHttpProvider,
type PresignedCapabilityHttpProviderOptions,
} from "./presigned-capability-http-provider.ts";
export {
createPresignedCapabilityVault,
createSingleUsePresignedReplayGuard,
type PresignedCapabilityBinding,
type PresignedCapabilityRegistration,
type PresignedCapabilityVault,
type PresignedHeaderBinding,
} from "./presigned-capability-vault.ts";
export {
createPresignedTransferExecutor,
type PresignedTransferExecutor,
type PresignedTransferExecutorOptions,
} from "./presigned-transfer-executor.ts";
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,273 @@
import type {
PresignedTransferBinding,
PresignedTransferCapability,
PresignedTransferCapabilityReceipt,
PresignedTransferMethod,
PresignedTransferReplayGuard,
} from "../../../application/ports/browser-transfer/presigned-transfer.ts";
import type { BrowserDataResult } from "../../../application/ports/browser-file-storage/shared.ts";
import {
browserDataFailure,
browserDataSuccess,
} from "../../browser-file-storage/result.ts";
export type PresignedHeaderBinding = Readonly<{
name: string;
value: string;
}>;
export type PresignedCapabilityRegistration = Readonly<{
capabilityReceipt: PresignedTransferCapabilityReceipt;
method: PresignedTransferMethod;
binding: PresignedTransferBinding;
href: string;
origin: string;
path: string;
allowedQueryParameters: readonly string[];
requestHeaders: readonly PresignedHeaderBinding[];
requiredResponseHeaders: readonly PresignedHeaderBinding[];
digestRequestHeader: string | null;
digestResponseHeader: string | null;
receiptResponseHeader: string | null;
expectedStatus: number;
expectedResponseByteLength: number | null;
mediaType: string;
byteLength: number;
maxBytes: number;
expectedSha256: string;
expiresAtEpochMs: number;
}>;
export type PresignedCapabilityBinding = Readonly<
PresignedCapabilityRegistration & {
capability: PresignedTransferCapability;
}
>;
export interface PresignedCapabilityVault {
register(
registration: PresignedCapabilityRegistration,
): BrowserDataResult<PresignedTransferCapability>;
resolve(
capability: PresignedTransferCapability,
): BrowserDataResult<PresignedCapabilityBinding>;
/**
* Atomically retires an exact identity after its single-use replay claim.
* The caller may keep the already-resolved binding on its stack for the
* in-flight request, but the vault must no longer retain or resolve it.
*/
consume(
capability: PresignedTransferCapability,
): BrowserDataResult<true>;
/**
* Best-effort, idempotent retirement for an unused or abandoned identity.
*/
revoke(capability: PresignedTransferCapability): void;
dispose(): void;
}
export function createPresignedCapabilityVault(options: Readonly<{
maxActiveCapabilities: number;
now?: () => number;
}>): PresignedCapabilityVault {
if (
!Number.isSafeInteger(options.maxActiveCapabilities) ||
options.maxActiveCapabilities < 1
) {
throw new TypeError("Presigned capability vault limit is invalid.");
}
const maxActiveCapabilities = options.maxActiveCapabilities;
const now = options.now ?? Date.now;
const byIdentity =
new WeakMap<PresignedTransferCapability, PresignedCapabilityBinding>();
const byReceipt =
new Map<PresignedTransferCapabilityReceipt, PresignedTransferCapability>();
let disposed = false;
function pruneExpired(): void {
const current = now();
if (!Number.isSafeInteger(current)) return;
for (const [receipt, capability] of byReceipt) {
if (capability.expiresAtEpochMs <= current) {
byReceipt.delete(receipt);
byIdentity.delete(capability);
}
}
}
function revoke(capability: PresignedTransferCapability): boolean {
try {
const binding = byIdentity.get(capability);
if (!binding) return false;
byIdentity.delete(capability);
if (byReceipt.get(binding.capabilityReceipt) === capability) {
byReceipt.delete(binding.capabilityReceipt);
}
return true;
} catch {
return false;
}
}
return Object.freeze({
register(
registration: PresignedCapabilityRegistration,
): BrowserDataResult<PresignedTransferCapability> {
if (disposed) {
return browserDataFailure("UNAVAILABLE", "PRESIGNED_TRANSFER");
}
pruneExpired();
if (
byReceipt.has(registration.capabilityReceipt) ||
byReceipt.size >= maxActiveCapabilities
) {
return browserDataFailure(
byReceipt.has(registration.capabilityReceipt)
? "CONFLICT"
: "LIMIT_EXCEEDED",
"PRESIGNED_TRANSFER",
byReceipt.has(registration.capabilityReceipt)
? { recovery: "REISSUE_CAPABILITY" }
: undefined,
);
}
const capability = Object.freeze({
capabilityReceipt: registration.capabilityReceipt,
method: registration.method,
binding: freezeBinding(registration.binding),
mediaType: registration.mediaType,
byteLength: registration.byteLength,
maxBytes: registration.maxBytes,
expectedSha256: registration.expectedSha256,
expiresAtEpochMs: registration.expiresAtEpochMs,
}) as PresignedTransferCapability;
const binding: PresignedCapabilityBinding = Object.freeze({
capability,
capabilityReceipt: capability.capabilityReceipt,
method: capability.method,
binding: capability.binding,
href: registration.href,
origin: registration.origin,
path: registration.path,
allowedQueryParameters: Object.freeze([
...registration.allowedQueryParameters,
]),
requestHeaders: freezeHeaders(registration.requestHeaders),
requiredResponseHeaders: freezeHeaders(
registration.requiredResponseHeaders,
),
digestRequestHeader: registration.digestRequestHeader,
digestResponseHeader: registration.digestResponseHeader,
receiptResponseHeader: registration.receiptResponseHeader,
expectedStatus: registration.expectedStatus,
expectedResponseByteLength:
registration.expectedResponseByteLength,
mediaType: capability.mediaType,
byteLength: capability.byteLength,
maxBytes: capability.maxBytes,
expectedSha256: capability.expectedSha256,
expiresAtEpochMs: capability.expiresAtEpochMs,
});
byIdentity.set(capability, binding);
byReceipt.set(capability.capabilityReceipt, capability);
return browserDataSuccess(capability);
},
resolve(
capability: PresignedTransferCapability,
): BrowserDataResult<PresignedCapabilityBinding> {
if (disposed) {
return browserDataFailure("UNAVAILABLE", "PRESIGNED_TRANSFER");
}
try {
const binding = byIdentity.get(capability);
return binding
? browserDataSuccess(binding)
: browserDataFailure("POLICY_REJECTED", "PRESIGNED_TRANSFER");
} catch {
return browserDataFailure("POLICY_REJECTED", "PRESIGNED_TRANSFER");
}
},
consume(
capability: PresignedTransferCapability,
): BrowserDataResult<true> {
if (disposed) {
return browserDataFailure("UNAVAILABLE", "PRESIGNED_TRANSFER");
}
return revoke(capability)
? browserDataSuccess(true as const)
: browserDataFailure(
"POLICY_REJECTED",
"PRESIGNED_TRANSFER",
);
},
revoke(capability: PresignedTransferCapability): void {
if (disposed) return;
revoke(capability);
},
dispose() {
if (disposed) return;
disposed = true;
for (const capability of byReceipt.values()) {
byIdentity.delete(capability);
}
byReceipt.clear();
},
});
}
export function createSingleUsePresignedReplayGuard():
PresignedTransferReplayGuard {
const claimed = new WeakSet<PresignedTransferCapability>();
return Object.freeze({
claim(
capability: PresignedTransferCapability,
): BrowserDataResult<true> {
try {
if (claimed.has(capability)) {
return browserDataFailure("CONFLICT", "PRESIGNED_TRANSFER", {
recovery: "REISSUE_CAPABILITY",
});
}
claimed.add(capability);
return browserDataSuccess(true as const);
} catch {
return browserDataFailure("POLICY_REJECTED", "PRESIGNED_TRANSFER");
}
},
});
}
function freezeBinding(
binding: PresignedTransferBinding,
): PresignedTransferBinding {
return binding.kind === "DOWNLOAD"
? Object.freeze({
kind: "DOWNLOAD" as const,
resourceId: binding.resourceId,
})
: Object.freeze({
kind: "UPLOAD_PART" as const,
protocol: binding.protocol,
sessionId: binding.sessionId,
requestBindingSha256: binding.requestBindingSha256,
uploadBindingSha256: binding.uploadBindingSha256,
partNumber: binding.partNumber,
offset: binding.offset,
idempotencyKey: binding.idempotencyKey,
});
}
function freezeHeaders(
headers: readonly PresignedHeaderBinding[],
): readonly PresignedHeaderBinding[] {
return Object.freeze(
headers.map((header) =>
Object.freeze({ name: header.name, value: header.value }),
),
);
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,185 @@
import type {
ResumableUploadCheckpoint,
UploadFileFingerprint,
UploadPartDescriptor,
UploadPartReceipt,
} from "../../../application/ports/browser-transfer/resumable-upload.ts";
import { RESUMABLE_UPLOAD_PROTOCOL } from "../../../application/ports/browser-transfer/resumable-upload.ts";
export const SAFE_UPLOAD_KEY = /^[A-Za-z0-9][A-Za-z0-9._~:-]{7,127}$/u;
export const SAFE_REGISTRY_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
export const SAFE_OPAQUE_ID = /^[A-Za-z0-9_-]{8,512}$/u;
export const SHA256_HEX = /^[a-f0-9]{64}$/u;
export const RECEIPT_TOKEN =
/^[A-Za-z0-9][A-Za-z0-9._~:+/=-]{0,511}$/u;
export const MEDIA_TYPE =
/^[a-z0-9!#$&^_.+-]{1,63}\/[a-z0-9!#$&^_.+-]{1,63}$/u;
const CHECKPOINT_KEYS = Object.freeze([
"schemaVersion",
"protocol",
"revision",
"state",
"uploadKey",
"requestBindingSha256",
"fingerprint",
"sessionId",
"sessionExpiresAtEpochMs",
"sessionMaxConcurrency",
"acceptedParts",
"updatedAtEpochMs",
] as const);
const FINGERPRINT_KEYS = Object.freeze([
"algorithm",
"digestHex",
"byteLength",
"partSizeBytes",
"partCount",
] as const);
const PART_KEYS = Object.freeze([
"partNumber",
"offset",
"byteLength",
"checksumSha256",
] as const);
const RECEIPT_KEYS = Object.freeze([...PART_KEYS, "receiptToken"] as const);
export function isUploadFileFingerprint(
value: unknown,
): value is UploadFileFingerprint {
if (!exactRecord(value, FINGERPRINT_KEYS)) return false;
return (
value.algorithm === "SHA-256-PARTS-V1" &&
typeof value.digestHex === "string" &&
SHA256_HEX.test(value.digestHex) &&
positiveSafeInteger(value.byteLength) &&
positiveSafeInteger(value.partSizeBytes) &&
positiveSafeInteger(value.partCount) &&
Math.ceil(value.byteLength / value.partSizeBytes) ===
value.partCount
);
}
export function isUploadPartDescriptor(
value: unknown,
): value is UploadPartDescriptor {
if (!exactRecord(value, PART_KEYS)) return false;
return (
positiveSafeInteger(value.partNumber) &&
nonNegativeSafeInteger(value.offset) &&
positiveSafeInteger(value.byteLength) &&
typeof value.checksumSha256 === "string" &&
SHA256_HEX.test(value.checksumSha256)
);
}
export function isUploadPartReceipt(
value: unknown,
): value is UploadPartReceipt {
return (
exactRecord(value, RECEIPT_KEYS) &&
isUploadPartDescriptor({
partNumber: value.partNumber,
offset: value.offset,
byteLength: value.byteLength,
checksumSha256: value.checksumSha256,
}) &&
typeof value.receiptToken === "string" &&
isSafeUploadReceiptToken(value.receiptToken)
);
}
export function isSafeUploadReceiptToken(value: string): boolean {
return RECEIPT_TOKEN.test(value) && !value.includes("://");
}
export function isResumableUploadCheckpoint(
value: unknown,
): value is ResumableUploadCheckpoint {
if (!exactRecord(value, CHECKPOINT_KEYS)) return false;
if (
value.schemaVersion !== 1 ||
value.protocol !== RESUMABLE_UPLOAD_PROTOCOL ||
!positiveSafeInteger(value.revision) ||
(value.state !== "ACTIVE" && value.state !== "ABORT_PENDING") ||
typeof value.uploadKey !== "string" ||
!SAFE_UPLOAD_KEY.test(value.uploadKey) ||
typeof value.requestBindingSha256 !== "string" ||
!SHA256_HEX.test(value.requestBindingSha256) ||
!isUploadFileFingerprint(value.fingerprint) ||
typeof value.sessionId !== "string" ||
!SAFE_OPAQUE_ID.test(value.sessionId) ||
!positiveSafeInteger(value.sessionExpiresAtEpochMs) ||
!positiveSafeInteger(value.sessionMaxConcurrency) ||
!Array.isArray(value.acceptedParts) ||
value.acceptedParts.length > value.fingerprint.partCount ||
!nonNegativeSafeInteger(value.updatedAtEpochMs)
) {
return false;
}
let previousPartNumber = 0;
for (const part of value.acceptedParts) {
if (
!isUploadPartReceipt(part) ||
part.partNumber <= previousPartNumber ||
!partMatchesFingerprint(part, value.fingerprint)
) {
return false;
}
previousPartNumber = part.partNumber;
}
return true;
}
export function partMatchesFingerprint(
part: UploadPartDescriptor,
fingerprint: UploadFileFingerprint,
): boolean {
if (
part.partNumber < 1 ||
part.partNumber > fingerprint.partCount ||
part.offset !== (part.partNumber - 1) * fingerprint.partSizeBytes
) {
return false;
}
const remaining = fingerprint.byteLength - part.offset;
return (
remaining > 0 &&
part.byteLength === Math.min(fingerprint.partSizeBytes, remaining)
);
}
export function samePart(
left: UploadPartDescriptor,
right: UploadPartDescriptor,
): boolean {
return (
left.partNumber === right.partNumber &&
left.offset === right.offset &&
left.byteLength === right.byteLength &&
left.checksumSha256 === right.checksumSha256
);
}
function exactRecord<const Keys extends readonly string[]>(
value: unknown,
keys: Keys,
): value is Record<Keys[number], unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return false;
}
const actual = Object.keys(value).sort();
const expected = [...keys].sort();
return (
actual.length === expected.length &&
actual.every((key, index) => key === expected[index])
);
}
function positiveSafeInteger(value: unknown): value is number {
return Number.isSafeInteger(value) && (value as number) > 0;
}
function nonNegativeSafeInteger(value: unknown): value is number {
return Number.isSafeInteger(value) && (value as number) >= 0;
}
@@ -0,0 +1,729 @@
import type {
UploadProviderFailure,
UploadProviderResult,
} from "../../../application/ports/browser-transfer/resumable-upload.ts";
import type {
BrowserDataFailureCode,
BrowserDataRecovery,
} from "../../../application/ports/browser-file-storage/shared.ts";
import {
browserDataFailure,
browserDataSuccess,
} from "../../browser-file-storage/result.ts";
import type {
ResumableUploadControlOperation,
ResumableUploadJsonTransport,
} from "./http-control-plane-adapter.ts";
export type ResumableUploadEndpointMap = Readonly<
Record<ResumableUploadControlOperation, string>
>;
export type ResumableUploadFetchTransportDependencies = Readonly<{
endpoints: ResumableUploadEndpointMap;
allowedOrigins: readonly string[];
credentials: "include" | "same-origin";
fetcher?: typeof fetch;
requestHeaders?: readonly Readonly<{ name: string; value: string }>[];
timeoutMs?: number;
maxRequestBytes?: number;
maxResponseBytes?: number;
maxRetryAfterMs?: number;
expectedSuccessStatuses?: Partial<
Readonly<Record<ResumableUploadControlOperation, number>>
>;
}>;
const DEFAULT_SUCCESS_STATUSES: Readonly<
Record<ResumableUploadControlOperation, number>
> = Object.freeze({
CREATE_SESSION: 201,
GET_STATUS: 200,
COMPLETE: 200,
ABORT: 200,
});
const DEFAULT_TIMEOUT_MS = 30_000;
const DEFAULT_MAX_REQUEST_BYTES = 1024 * 1024;
const DEFAULT_MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
const DEFAULT_MAX_RETRY_AFTER_MS = 30_000;
const ABSOLUTE_MAX_JSON_BYTES = 4 * 1024 * 1024;
const HEADER_NAME = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/u;
const OPERATIONS = Object.freeze(
Object.keys(
DEFAULT_SUCCESS_STATUSES,
) as ResumableUploadControlOperation[],
);
const OPERATION_SET: ReadonlySet<string> = new Set(OPERATIONS);
export function createResumableUploadFetchJsonTransport(
input: ResumableUploadFetchTransportDependencies,
): ResumableUploadJsonTransport {
const endpoints = snapshotEndpoints(input.endpoints, input.allowedOrigins);
const fetcher =
input.fetcher ?? globalThis.fetch?.bind(globalThis);
if (
typeof fetcher !== "function" ||
!["include", "same-origin"].includes(input.credentials)
) {
throw new TypeError("Upload fetch transport dependency is invalid.");
}
const headers = snapshotHeaders(input.requestHeaders ?? []);
const timeoutMs = boundedPositiveInteger(
input.timeoutMs ?? DEFAULT_TIMEOUT_MS,
1,
120_000,
"timeout",
);
const maxRequestBytes = boundedPositiveInteger(
input.maxRequestBytes ?? DEFAULT_MAX_REQUEST_BYTES,
1,
ABSOLUTE_MAX_JSON_BYTES,
"request bytes",
);
const maxResponseBytes = boundedPositiveInteger(
input.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES,
1,
ABSOLUTE_MAX_JSON_BYTES,
"response bytes",
);
const maxRetryAfterMs = boundedPositiveInteger(
input.maxRetryAfterMs ?? DEFAULT_MAX_RETRY_AFTER_MS,
1,
60_000,
"Retry-After",
);
const statuses = snapshotStatuses(input.expectedSuccessStatuses);
const transport: ResumableUploadJsonTransport = {
async execute(request) {
const snapshot = snapshotTransportRequest(request);
if (!snapshot) {
return browserDataFailure(
"INVALID_INPUT",
"UPLOAD_SESSION",
);
}
const { operation, body: requestBody, signal } = snapshot;
const endpoint = endpoints[operation];
let body: string;
try {
body = JSON.stringify(requestBody);
} catch {
return failure(
"INVALID_INPUT",
operation,
false,
"NONE",
);
}
if (typeof body !== "string") {
return failure("INVALID_INPUT", operation, false, "NONE");
}
const requestBytes = new TextEncoder().encode(body).byteLength;
if (
requestBytes < 2 ||
requestBytes > maxRequestBytes ||
signal.aborted
) {
return signal.aborted
? failure(
"ABORTED",
operation,
false,
"NONE",
)
: failure(
"LIMIT_EXCEEDED",
operation,
false,
"NONE",
);
}
const attempt = createFetchAttempt(signal, timeoutMs);
try {
const fetchPromise = fetcher(endpoint, {
method: "POST",
headers: headersFor(headers),
body,
signal: attempt.signal,
credentials: input.credentials,
redirect: "error",
referrerPolicy: "no-referrer",
cache: "no-store",
mode: new URL(endpoint).origin === globalThis.location?.origin
? "same-origin"
: "cors",
});
const raced = await Promise.race([
fetchPromise.then(
(value) => {
if (attempt.terminalKind()) {
cancelResponseBody(value);
}
return { kind: "RESPONSE" as const, value };
},
() => ({ kind: "FAILED" as const }),
),
attempt.terminal,
]);
if (raced.kind !== "RESPONSE") {
return attemptFailure(attempt, operation);
}
const response = raced.value;
if (
response.redirected ||
response.type === "opaqueredirect" ||
!sameUrl(response.url, endpoint)
) {
cancelResponseBody(response);
return failure(
"POLICY_REJECTED",
operation,
false,
"NONE",
);
}
if (response.status !== statuses[operation]) {
const failed = statusFailure(
response,
operation,
maxRetryAfterMs,
);
cancelResponseBody(response);
return failed;
}
if (!jsonContentType(response.headers.get("content-type"))) {
cancelResponseBody(response);
return failure(
"CORRUPT_DATA",
operation,
false,
"RECONCILE",
);
}
const decoded = await readBoundedJson(
response,
maxResponseBytes,
operation,
attempt,
);
return decoded.ok
? browserDataSuccess(decoded.value)
: decoded;
} catch {
return attemptFailure(attempt, operation);
} finally {
attempt.release();
}
},
};
return Object.freeze(transport);
}
function snapshotEndpoints(
value: ResumableUploadEndpointMap,
allowedOriginValues: readonly string[],
): ResumableUploadEndpointMap {
if (
!value ||
typeof value !== "object" ||
!Array.isArray(allowedOriginValues) ||
allowedOriginValues.length < 1
) {
throw new TypeError("Upload endpoints are invalid.");
}
const allowedOrigins = new Set(
allowedOriginValues.map((origin) => {
const parsed = new URL(origin);
if (parsed.origin !== parsed.href.replace(/\/$/u, "")) {
throw new TypeError("Allowed upload origin is invalid.");
}
return parsed.origin;
}),
);
const snapshot = Object.create(null) as Record<
ResumableUploadControlOperation,
string
>;
for (const operation of OPERATIONS) {
const endpoint = value[operation];
const parsed = new URL(endpoint);
if (
parsed.protocol !== "https:" ||
!allowedOrigins.has(parsed.origin) ||
parsed.username ||
parsed.password ||
parsed.hash ||
parsed.search
) {
throw new TypeError("Upload endpoint is outside policy.");
}
snapshot[operation] = parsed.href;
}
if (Object.keys(value).length !== 4) {
throw new TypeError("Upload endpoint map is invalid.");
}
return Object.freeze(snapshot);
}
function snapshotHeaders(
input: readonly Readonly<{ name: string; value: string }>[],
): readonly Readonly<{ name: string; value: string }>[] {
const seen = new Set<string>();
const forbidden = new Set([
"accept",
"authorization",
"connection",
"content-type",
"content-length",
"cookie",
"host",
"origin",
"proxy-authorization",
"referer",
"set-cookie",
"transfer-encoding",
]);
return Object.freeze(
input.map((header) => {
const name = header.name.toLowerCase();
if (
!HEADER_NAME.test(name) ||
forbidden.has(name) ||
seen.has(name) ||
typeof header.value !== "string" ||
header.value.length > 2048 ||
hasForbiddenHeaderValueCharacter(header.value)
) {
throw new TypeError("Upload request header is invalid.");
}
seen.add(name);
return Object.freeze({ name, value: header.value });
}),
);
}
function snapshotStatuses(
overrides:
| Partial<
Readonly<Record<ResumableUploadControlOperation, number>>
>
| undefined,
): Readonly<Record<ResumableUploadControlOperation, number>> {
if (
overrides !== undefined &&
(!isPlainRecord(overrides) ||
Object.keys(overrides).some(
(operation) => !isControlOperation(operation),
))
) {
throw new TypeError("Upload success status policy is invalid.");
}
const statuses = Object.freeze(
Object.assign(
Object.create(null) as Record<
ResumableUploadControlOperation,
number
>,
DEFAULT_SUCCESS_STATUSES,
overrides,
),
);
if (
Object.values(statuses).some(
(status) =>
!Number.isSafeInteger(status) || status < 200 || status > 299,
)
) {
throw new TypeError("Upload success status policy is invalid.");
}
return statuses;
}
function headersFor(
configured: readonly Readonly<{ name: string; value: string }>[],
): Headers {
const headers = new Headers({
accept: "application/json",
"content-type": "application/json; charset=utf-8",
});
for (const header of configured) {
headers.set(header.name, header.value);
}
return headers;
}
async function readBoundedJson(
response: Response,
maxBytes: number,
operation: ResumableUploadControlOperation,
attempt: FetchAttempt,
): Promise<UploadProviderResult<unknown>> {
const contentLength = response.headers.get("content-length");
let declaredLength: number | null = null;
if (
contentLength &&
(!/^(0|[1-9][0-9]*)$/u.test(contentLength) ||
Number(contentLength) > maxBytes)
) {
cancelResponseBody(response);
return failure(
"LIMIT_EXCEEDED",
operation,
false,
"RECONCILE",
);
}
if (contentLength !== null) {
declaredLength = Number(contentLength);
}
const reader = response.body?.getReader();
if (!reader) {
return failure(
"CORRUPT_DATA",
operation,
false,
"RECONCILE",
);
}
const chunks: Uint8Array[] = [];
let total = 0;
try {
while (true) {
const raced = await Promise.race([
reader.read().then(
(value) => ({ kind: "READ" as const, value }),
() => ({ kind: "FAILED" as const }),
),
attempt.terminal,
]);
if (raced.kind === "ABORT" || raced.kind === "TIMEOUT") {
cancelReader(reader);
return attemptFailure(attempt, operation);
}
if (raced.kind === "FAILED") {
cancelReader(reader);
return attemptFailure(attempt, operation);
}
const next = raced.value;
if (next.done) break;
if (!(next.value instanceof Uint8Array)) {
cancelReader(reader);
return failure(
"CORRUPT_DATA",
operation,
false,
"RECONCILE",
);
}
total += next.value.byteLength;
if (total > maxBytes) {
cancelReader(reader);
return failure(
"LIMIT_EXCEEDED",
operation,
false,
"RECONCILE",
);
}
chunks.push(Uint8Array.from(next.value));
}
} catch {
cancelReader(reader);
return attemptFailure(attempt, operation);
} finally {
releaseReader(reader);
}
if (declaredLength !== null && declaredLength !== total) {
return failure(
"INTEGRITY_FAILED",
operation,
false,
"RECONCILE",
);
}
const bytes = new Uint8Array(total);
let offset = 0;
for (const chunk of chunks) {
bytes.set(chunk, offset);
offset += chunk.byteLength;
}
try {
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
const value: unknown = JSON.parse(text);
return value && typeof value === "object" && !Array.isArray(value)
? browserDataSuccess(value)
: failure(
"CORRUPT_DATA",
operation,
false,
"RECONCILE",
);
} catch {
return failure(
"CORRUPT_DATA",
operation,
false,
"RECONCILE",
);
}
}
function statusFailure(
response: Response,
operation: ResumableUploadControlOperation,
maxRetryAfterMs: number,
): UploadProviderResult<never> {
if (response.status === 400 || response.status === 422) {
return failure("INVALID_INPUT", operation, false, "NONE");
}
if (response.status === 401 || response.status === 403) {
return failure("PERMISSION_DENIED", operation, false, "NONE");
}
if (response.status === 404) {
return failure("NOT_FOUND", operation, false, "RECONCILE");
}
if (response.status === 409 || response.status === 412) {
return failure("CONFLICT", operation, false, "RECONCILE");
}
if (response.status === 410) {
return failure("EXPIRED_RESOURCE", operation, false, "RESTART");
}
if (response.status === 413) {
return failure("LIMIT_EXCEEDED", operation, false, "NONE");
}
if (response.status === 429) {
const retryAfterMs = parseRetryAfter(
response.headers.get("retry-after"),
);
return retryAfterMs !== null && retryAfterMs <= maxRetryAfterMs
? failure(
"UNAVAILABLE",
operation,
true,
"RESUME",
retryAfterMs,
)
: failure("UNAVAILABLE", operation, false, "RESUME");
}
return response.status >= 500 && response.status <= 599
? failure("UNAVAILABLE", operation, true, "RESUME")
: failure("UNAVAILABLE", operation, false, "RESUME");
}
function failure(
code: BrowserDataFailureCode,
operation: ResumableUploadControlOperation,
retryable: boolean,
recovery: BrowserDataRecovery,
retryAfterMs?: number,
): UploadProviderResult<never> {
const operationMap = {
CREATE_SESSION: "UPLOAD_SESSION",
GET_STATUS: "UPLOAD_RECONCILE",
COMPLETE: "UPLOAD_COMPLETE",
ABORT: "UPLOAD_ABORT",
} as const;
const error: UploadProviderFailure = Object.freeze({
code,
operation: operationMap[operation],
retryable,
recovery,
...(retryAfterMs === undefined ? {} : { retryAfterMs }),
});
return Object.freeze({ ok: false, error });
}
type FetchAttemptTerminal =
| Readonly<{ kind: "ABORT" }>
| Readonly<{ kind: "TIMEOUT" }>;
type FetchAttempt = Readonly<{
signal: AbortSignal;
terminal: Promise<FetchAttemptTerminal>;
terminalKind(): FetchAttemptTerminal["kind"] | null;
release(): void;
}>;
function createFetchAttempt(
parent: AbortSignal,
timeoutMs: number,
): FetchAttempt {
const controller = new AbortController();
let terminalKind: FetchAttemptTerminal["kind"] | null = null;
let resolveTerminal:
| ((value: FetchAttemptTerminal) => void)
| undefined;
const terminal = new Promise<FetchAttemptTerminal>(
(resolve) => {
resolveTerminal = resolve;
},
);
const finish = (kind: FetchAttemptTerminal["kind"]) => {
if (terminalKind) return;
terminalKind = kind;
controller.abort();
resolveTerminal?.(Object.freeze({ kind }));
};
const abort = () => finish("ABORT");
parent.addEventListener("abort", abort, { once: true });
if (parent.aborted) abort();
const timer = setTimeout(() => {
finish("TIMEOUT");
}, timeoutMs);
return Object.freeze({
signal: controller.signal,
terminal,
terminalKind: () => terminalKind,
release() {
clearTimeout(timer);
parent.removeEventListener("abort", abort);
},
});
}
function attemptFailure(
attempt: FetchAttempt,
operation: ResumableUploadControlOperation,
): UploadProviderResult<never> {
return attempt.terminalKind() === "ABORT"
? failure("ABORTED", operation, false, "NONE")
: failure("UNAVAILABLE", operation, true, "RESUME");
}
function cancelResponseBody(response: Response): void {
try {
void response.body?.cancel().catch(() => {
// Response cancellation is best effort after closed classification.
});
} catch {
// A cancellation failure cannot change the already classified result.
}
}
function cancelReader(
reader: ReadableStreamDefaultReader<Uint8Array>,
): void {
try {
void reader.cancel().catch(() => {
// Reader cancellation is best effort after closed classification.
});
} catch {
// A cancellation failure cannot change the already classified result.
}
}
function releaseReader(
reader: ReadableStreamDefaultReader<Uint8Array>,
): void {
try {
reader.releaseLock();
} catch {
// A pending native read may keep the lock until cancellation settles.
}
}
function parseRetryAfter(value: string | null): number | null {
if (!value) return null;
if (/^(0|[1-9][0-9]*)$/u.test(value)) {
const seconds = Number(value);
const milliseconds = seconds * 1000;
return Number.isSafeInteger(milliseconds) ? milliseconds : null;
}
const timestamp = Date.parse(value);
return Number.isFinite(timestamp)
? Math.max(0, timestamp - Date.now())
: null;
}
function jsonContentType(value: string | null): boolean {
return Boolean(
value &&
/^application\/json(?:;\s*charset=utf-8)?$/iu.test(value.trim()),
);
}
function sameUrl(actual: string, expected: string): boolean {
try {
return new URL(actual).href === new URL(expected).href;
} catch {
return false;
}
}
function boundedPositiveInteger(
value: number,
minimum: number,
maximum: number,
label: string,
): number {
if (
!Number.isSafeInteger(value) ||
value < minimum ||
value > maximum
) {
throw new TypeError(`Upload ${label} policy is invalid.`);
}
return value;
}
function isAbortSignal(value: unknown): value is AbortSignal {
return Boolean(
value &&
typeof value === "object" &&
typeof (value as AbortSignal).aborted === "boolean" &&
typeof (value as AbortSignal).addEventListener === "function",
);
}
function isControlOperation(
value: unknown,
): value is ResumableUploadControlOperation {
return typeof value === "string" && OPERATION_SET.has(value);
}
function snapshotTransportRequest(
value: unknown,
): Readonly<{
operation: ResumableUploadControlOperation;
body: Readonly<Record<string, unknown>>;
signal: AbortSignal;
}> | null {
try {
if (!value || typeof value !== "object") return null;
const record = value as Readonly<Record<string, unknown>>;
return isControlOperation(record.operation) &&
isPlainRecord(record.body) &&
isAbortSignal(record.signal)
? Object.freeze({
operation: record.operation,
body: record.body,
signal: record.signal,
})
: null;
} catch {
return null;
}
}
function hasForbiddenHeaderValueCharacter(value: string): boolean {
return [...value].some((character) => {
const codePoint = character.codePointAt(0);
return codePoint !== undefined && (codePoint <= 31 || codePoint === 127);
});
}
function isPlainRecord(
value: unknown,
): value is Readonly<Record<string, unknown>> {
if (!value || typeof value !== "object") {
return false;
}
try {
if (Array.isArray(value)) return false;
const prototype = Object.getPrototypeOf(value);
return prototype === Object.prototype || prototype === null;
} catch {
return false;
}
}
@@ -0,0 +1,612 @@
import type {
PresignedUploadPartCapability,
PresignedUploadPartCapabilityProvider,
} from "../../../application/ports/browser-transfer/presigned-transfer.ts";
import type {
ResumableUploadControlPlane,
UploadFileFingerprint,
UploadPartReceipt,
UploadProviderResult,
UploadSession,
UploadSessionStatus,
} from "../../../application/ports/browser-transfer/resumable-upload.ts";
import { RESUMABLE_UPLOAD_PROTOCOL } from "../../../application/ports/browser-transfer/resumable-upload.ts";
import {
browserDataFailure,
browserDataSuccess,
} from "../../browser-file-storage/result.ts";
import {
isSafeUploadReceiptToken,
isUploadFileFingerprint,
isUploadPartReceipt,
MEDIA_TYPE,
SAFE_OPAQUE_ID,
SAFE_REGISTRY_ID,
SAFE_UPLOAD_KEY,
SHA256_HEX,
} from "./checkpoint-schema.ts";
export type ResumableUploadControlOperation =
| "CREATE_SESSION"
| "GET_STATUS"
| "COMPLETE"
| "ABORT";
/**
* Composition-owned transport. Endpoint URLs, auth headers and raw response
* parsing stay behind this seam. `operation` is a closed endpoint identifier,
* never a caller-provided URL.
*/
export interface ResumableUploadJsonTransport {
execute(input: Readonly<{
operation: ResumableUploadControlOperation;
body: Readonly<Record<string, unknown>>;
signal: AbortSignal;
}>): Promise<UploadProviderResult<unknown>>;
}
export type ResumableUploadHttpControlPlaneDependencies = Readonly<{
transport: ResumableUploadJsonTransport;
partCapabilities: PresignedUploadPartCapabilityProvider;
}>;
const MAX_PART_COUNT = 10_000;
const MAX_RECEIPT_COUNT = 10_000;
export function createResumableUploadHttpControlPlane(
dependencies: ResumableUploadHttpControlPlaneDependencies,
): ResumableUploadControlPlane<PresignedUploadPartCapability> {
const execute = dependencies.transport?.execute;
const issueUploadPart =
dependencies.partCapabilities?.issueUploadPart;
if (
typeof execute !== "function" ||
typeof issueUploadPart !== "function"
) {
throw new TypeError("Upload HTTP control-plane dependency is invalid.");
}
const controlPlane: ResumableUploadControlPlane<PresignedUploadPartCapability> =
{
async createSession(input) {
if (
input.protocol !== RESUMABLE_UPLOAD_PROTOCOL ||
!SAFE_UPLOAD_KEY.test(input.uploadKey) ||
!SAFE_REGISTRY_ID.test(input.purpose) ||
!MEDIA_TYPE.test(input.mediaType) ||
!SHA256_HEX.test(input.requestBindingSha256) ||
!isUploadFileFingerprint(input.fingerprint) ||
!positiveSafeInteger(input.requestedPartSizeBytes) ||
!positiveSafeInteger(input.requestedMaxConcurrency) ||
!safeIdempotencyKey(input.idempotencyKey)
) {
return browserDataFailure(
"INVALID_INPUT",
"UPLOAD_SESSION",
);
}
const response = await invokeJsonTransport(
execute,
dependencies.transport,
"CREATE_SESSION",
Object.freeze({
protocol: input.protocol,
uploadKey: input.uploadKey,
purpose: input.purpose,
mediaType: input.mediaType,
requestBindingSha256: input.requestBindingSha256,
fingerprint: snapshotFingerprint(input.fingerprint),
requestedPartSizeBytes: input.requestedPartSizeBytes,
requestedMaxConcurrency: input.requestedMaxConcurrency,
idempotencyKey: input.idempotencyKey,
}),
input.signal,
"UPLOAD_SESSION",
);
if (!response.ok) return response;
const session = decodeSession(response.value);
return session
? browserDataSuccess(session)
: browserDataFailure(
"CORRUPT_DATA",
"UPLOAD_SESSION",
{ recovery: "RECONCILE" },
);
},
async getStatus(input) {
if (
input.protocol !== RESUMABLE_UPLOAD_PROTOCOL ||
!SAFE_OPAQUE_ID.test(input.sessionId) ||
!SHA256_HEX.test(input.requestBindingSha256) ||
!isUploadFileFingerprint(input.fingerprint)
) {
return browserDataFailure(
"INVALID_INPUT",
"UPLOAD_RECONCILE",
);
}
const response = await invokeJsonTransport(
execute,
dependencies.transport,
"GET_STATUS",
Object.freeze({
protocol: input.protocol,
sessionId: input.sessionId,
requestBindingSha256: input.requestBindingSha256,
fingerprint: snapshotFingerprint(input.fingerprint),
}),
input.signal,
"UPLOAD_RECONCILE",
);
if (!response.ok) return response;
const status = decodeStatus(response.value);
return status
? browserDataSuccess(status)
: browserDataFailure(
"CORRUPT_DATA",
"UPLOAD_RECONCILE",
{ recovery: "RECONCILE" },
);
},
async issuePartCapability(input) {
if (
input.protocol !== RESUMABLE_UPLOAD_PROTOCOL ||
!SAFE_OPAQUE_ID.test(input.sessionId) ||
!SHA256_HEX.test(input.requestBindingSha256) ||
!SHA256_HEX.test(input.uploadBindingSha256) ||
!isUploadFileFingerprint(input.fingerprint) ||
!MEDIA_TYPE.test(input.mediaType) ||
!isUploadPartReceiptShape(input.part) ||
!safeIdempotencyKey(input.idempotencyKey)
) {
return browserDataFailure(
"INVALID_INPUT",
"UPLOAD_PART",
);
}
let issued;
try {
issued = await issueUploadPart.call(
dependencies.partCapabilities,
{
sessionId: input.sessionId,
requestBindingSha256: input.requestBindingSha256,
uploadBindingSha256: input.uploadBindingSha256,
partNumber: input.part.partNumber,
offset: input.part.offset,
byteLength: input.part.byteLength,
checksumSha256: input.part.checksumSha256,
mediaType: input.mediaType,
idempotencyKey: input.idempotencyKey,
signal: input.signal,
},
);
} catch {
return browserDataFailure(
"UNAVAILABLE",
"UPLOAD_PART",
{ retryable: true, recovery: "REISSUE_CAPABILITY" },
);
}
if (!issued.ok) {
return browserDataFailure(
issued.error.code,
"UPLOAD_PART",
{
retryable: issued.error.retryable,
recovery: issued.error.recovery,
},
);
}
const capability = issued.value;
if (
capability.method !== "PUT" ||
capability.binding.kind !== "UPLOAD_PART" ||
capability.binding.protocol !== input.protocol ||
capability.binding.sessionId !== input.sessionId ||
capability.binding.requestBindingSha256 !==
input.requestBindingSha256 ||
capability.binding.uploadBindingSha256 !==
input.uploadBindingSha256 ||
capability.binding.partNumber !== input.part.partNumber ||
capability.binding.offset !== input.part.offset ||
capability.binding.idempotencyKey !== input.idempotencyKey ||
capability.mediaType !== input.mediaType ||
capability.byteLength !== input.part.byteLength ||
capability.maxBytes !== input.part.byteLength ||
capability.expectedSha256 !== input.part.checksumSha256 ||
!positiveSafeInteger(capability.expiresAtEpochMs)
) {
return browserDataFailure(
"POLICY_REJECTED",
"UPLOAD_PART",
{ recovery: "REISSUE_CAPABILITY" },
);
}
return browserDataSuccess(
Object.freeze({
capability,
uploadBindingSha256: input.uploadBindingSha256,
expiresAtEpochMs: capability.expiresAtEpochMs,
}),
);
},
async complete(input) {
if (
input.protocol !== RESUMABLE_UPLOAD_PROTOCOL ||
!SAFE_OPAQUE_ID.test(input.sessionId) ||
!SHA256_HEX.test(input.requestBindingSha256) ||
!isUploadFileFingerprint(input.fingerprint) ||
!safeIdempotencyKey(input.idempotencyKey) ||
!orderedReceipts(
input.orderedParts,
input.fingerprint,
true,
)
) {
return browserDataFailure(
"INVALID_INPUT",
"UPLOAD_COMPLETE",
);
}
const response = await invokeJsonTransport(
execute,
dependencies.transport,
"COMPLETE",
Object.freeze({
protocol: input.protocol,
sessionId: input.sessionId,
requestBindingSha256: input.requestBindingSha256,
fingerprint: snapshotFingerprint(input.fingerprint),
orderedParts: Object.freeze(
input.orderedParts.map(snapshotReceipt),
),
idempotencyKey: input.idempotencyKey,
}),
input.signal,
"UPLOAD_COMPLETE",
);
if (!response.ok) return response;
const completed = decodeCompletion(response.value);
return completed
? browserDataSuccess(completed)
: browserDataFailure(
"CORRUPT_DATA",
"UPLOAD_COMPLETE",
{ recovery: "RECONCILE" },
);
},
async abort(input) {
if (
input.protocol !== RESUMABLE_UPLOAD_PROTOCOL ||
!SAFE_OPAQUE_ID.test(input.sessionId) ||
!SHA256_HEX.test(input.requestBindingSha256) ||
!safeIdempotencyKey(input.idempotencyKey)
) {
return browserDataFailure("INVALID_INPUT", "UPLOAD_ABORT");
}
const response = await invokeJsonTransport(
execute,
dependencies.transport,
"ABORT",
Object.freeze({
protocol: input.protocol,
sessionId: input.sessionId,
requestBindingSha256: input.requestBindingSha256,
idempotencyKey: input.idempotencyKey,
}),
input.signal,
"UPLOAD_ABORT",
);
if (!response.ok) return response;
if (
!exactKeys(response.value, ["state"]) ||
typeof response.value.state !== "string" ||
![
"ABORTED",
"NOT_FOUND",
"EXPIRED",
"ALREADY_COMPLETED",
].includes(response.value.state)
) {
return browserDataFailure(
"CORRUPT_DATA",
"UPLOAD_ABORT",
{ recovery: "RECONCILE" },
);
}
return browserDataSuccess(
Object.freeze({
state: response.value.state as
| "ABORTED"
| "NOT_FOUND"
| "EXPIRED"
| "ALREADY_COMPLETED",
}),
);
},
};
return Object.freeze(controlPlane);
}
async function invokeJsonTransport(
execute: ResumableUploadJsonTransport["execute"],
owner: ResumableUploadJsonTransport,
operation: ResumableUploadControlOperation,
body: Readonly<Record<string, unknown>>,
signal: AbortSignal,
failureOperation:
| "UPLOAD_SESSION"
| "UPLOAD_RECONCILE"
| "UPLOAD_COMPLETE"
| "UPLOAD_ABORT",
): Promise<UploadProviderResult<unknown>> {
try {
const response = await execute.call(owner, {
operation,
body,
signal,
});
if (!response || typeof response !== "object") {
return browserDataFailure("UNAVAILABLE", failureOperation, {
retryable: true,
recovery: "RESUME",
});
}
return response;
} catch {
return browserDataFailure("UNAVAILABLE", failureOperation, {
retryable: true,
recovery: "RESUME",
});
}
}
function decodeSession(value: unknown): UploadSession | null {
if (
!exactKeys(value, [
"protocol",
"sessionId",
"requestBindingSha256",
"fingerprint",
"partSizeBytes",
"partCount",
"maxConcurrency",
"expiresAtEpochMs",
]) ||
value.protocol !== RESUMABLE_UPLOAD_PROTOCOL ||
typeof value.sessionId !== "string" ||
!SAFE_OPAQUE_ID.test(value.sessionId) ||
typeof value.requestBindingSha256 !== "string" ||
!SHA256_HEX.test(value.requestBindingSha256) ||
!isUploadFileFingerprint(value.fingerprint) ||
!positiveSafeInteger(value.partSizeBytes) ||
value.partSizeBytes !== value.fingerprint.partSizeBytes ||
!positiveSafeInteger(value.partCount) ||
value.partCount !== value.fingerprint.partCount ||
value.partCount > MAX_PART_COUNT ||
!positiveSafeInteger(value.maxConcurrency) ||
!positiveSafeInteger(value.expiresAtEpochMs)
) {
return null;
}
return Object.freeze({
protocol: RESUMABLE_UPLOAD_PROTOCOL,
sessionId: value.sessionId,
requestBindingSha256: value.requestBindingSha256,
fingerprint: snapshotFingerprint(value.fingerprint),
partSizeBytes: value.partSizeBytes,
partCount: value.partCount,
maxConcurrency: value.maxConcurrency,
expiresAtEpochMs: value.expiresAtEpochMs,
});
}
function decodeStatus(value: unknown): UploadSessionStatus | null {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
}
const record = value as Record<string, unknown>;
if (
record.state === "ACTIVE" &&
exactKeys(record, ["state", "session", "acceptedParts"])
) {
const session = decodeSession(record.session);
if (
!session ||
!Array.isArray(record.acceptedParts) ||
record.acceptedParts.length > MAX_RECEIPT_COUNT ||
!record.acceptedParts.every(isUploadPartReceipt)
) {
return null;
}
const parts = Object.freeze(
record.acceptedParts.map(snapshotReceipt),
);
return orderedReceipts(parts, session.fingerprint, false)
? Object.freeze({
state: "ACTIVE",
session,
acceptedParts: parts,
})
: null;
}
if (
record.state === "QUARANTINED" &&
exactKeys(record, ["state", "session", "resourceId"])
) {
const session = decodeSession(record.session);
return session &&
typeof record.resourceId === "string" &&
SAFE_OPAQUE_ID.test(record.resourceId)
? Object.freeze({
state: "QUARANTINED",
session,
resourceId: record.resourceId,
})
: null;
}
if (
typeof record.state === "string" &&
["ABORTED", "EXPIRED", "NOT_FOUND"].includes(record.state) &&
exactKeys(record, [
"state",
"protocol",
"sessionId",
"requestBindingSha256",
]) &&
record.protocol === RESUMABLE_UPLOAD_PROTOCOL &&
typeof record.sessionId === "string" &&
SAFE_OPAQUE_ID.test(record.sessionId) &&
typeof record.requestBindingSha256 === "string" &&
SHA256_HEX.test(record.requestBindingSha256)
) {
return Object.freeze({
state: record.state as "ABORTED" | "EXPIRED" | "NOT_FOUND",
protocol: RESUMABLE_UPLOAD_PROTOCOL,
sessionId: record.sessionId,
requestBindingSha256: record.requestBindingSha256,
});
}
return null;
}
function decodeCompletion(
value: unknown,
): Awaited<
ReturnType<
ResumableUploadControlPlane<PresignedUploadPartCapability>["complete"]
>
> extends UploadProviderResult<infer Outcome>
? Outcome | null
: never {
if (
!exactKeys(value, [
"state",
"protocol",
"sessionId",
"requestBindingSha256",
"fingerprint",
"resourceId",
]) ||
value.state !== "QUARANTINED" ||
value.protocol !== RESUMABLE_UPLOAD_PROTOCOL ||
typeof value.sessionId !== "string" ||
!SAFE_OPAQUE_ID.test(value.sessionId) ||
typeof value.requestBindingSha256 !== "string" ||
!SHA256_HEX.test(value.requestBindingSha256) ||
!isUploadFileFingerprint(value.fingerprint) ||
typeof value.resourceId !== "string" ||
!SAFE_OPAQUE_ID.test(value.resourceId)
) {
return null;
}
return Object.freeze({
state: "QUARANTINED",
protocol: RESUMABLE_UPLOAD_PROTOCOL,
sessionId: value.sessionId,
requestBindingSha256: value.requestBindingSha256,
fingerprint: snapshotFingerprint(value.fingerprint),
resourceId: value.resourceId,
});
}
function orderedReceipts(
parts: readonly UploadPartReceipt[],
fingerprint: UploadFileFingerprint,
requireComplete: boolean,
): boolean {
if (
parts.length > fingerprint.partCount ||
parts.length > MAX_RECEIPT_COUNT ||
(requireComplete && parts.length !== fingerprint.partCount)
) {
return false;
}
let previousPartNumber = 0;
return parts.every((part) => {
const valid =
isUploadPartReceipt(part) &&
isSafeUploadReceiptToken(part.receiptToken) &&
part.partNumber > previousPartNumber &&
part.partNumber <= fingerprint.partCount &&
part.offset ===
(part.partNumber - 1) * fingerprint.partSizeBytes &&
part.byteLength ===
Math.min(
fingerprint.partSizeBytes,
fingerprint.byteLength - part.offset,
);
previousPartNumber = part.partNumber;
return valid;
});
}
function isUploadPartReceiptShape(
value: unknown,
): value is Readonly<{
partNumber: number;
offset: number;
byteLength: number;
checksumSha256: string;
}> {
return (
exactKeys(value, [
"partNumber",
"offset",
"byteLength",
"checksumSha256",
]) &&
positiveSafeInteger(value.partNumber) &&
nonNegativeSafeInteger(value.offset) &&
positiveSafeInteger(value.byteLength) &&
typeof value.checksumSha256 === "string" &&
SHA256_HEX.test(value.checksumSha256)
);
}
function snapshotFingerprint(
value: UploadFileFingerprint,
): UploadFileFingerprint {
return Object.freeze({ ...value });
}
function snapshotReceipt(value: UploadPartReceipt): UploadPartReceipt {
return Object.freeze({ ...value });
}
function exactKeys(
value: unknown,
keys: readonly string[],
): value is Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return false;
}
const actual = Object.keys(value).sort();
const expected = [...keys].sort();
return (
actual.length === expected.length &&
actual.every((key, index) => key === expected[index])
);
}
function safeIdempotencyKey(value: string): boolean {
return (
typeof value === "string" &&
value.length >= 16 &&
value.length <= 160 &&
/^[A-Za-z0-9._~-]+$/u.test(value)
);
}
function positiveSafeInteger(value: unknown): value is number {
return Number.isSafeInteger(value) && (value as number) > 0;
}
function nonNegativeSafeInteger(value: unknown): value is number {
return Number.isSafeInteger(value) && (value as number) >= 0;
}
@@ -0,0 +1,40 @@
export {
createResumableUploadFetchJsonTransport,
type ResumableUploadEndpointMap,
type ResumableUploadFetchTransportDependencies,
} from "./fetch-json-transport.ts";
export {
createResumableUploadHttpControlPlane,
type ResumableUploadControlOperation,
type ResumableUploadHttpControlPlaneDependencies,
type ResumableUploadJsonTransport,
} from "./http-control-plane-adapter.ts";
export {
createIndexedDbResumableUploadCheckpointRuntime,
createIndexedDbResumableUploadCheckpointStore,
uploadCheckpointDatabaseName,
type IndexedDbUploadCheckpointDependencies,
type IndexedDbUploadCheckpointRuntime,
type IndexedDbUploadCheckpointScope,
} from "./indexeddb-checkpoint-store.ts";
export { createPresignedUploadPartExecutor } from "./presigned-upload-part-executor.ts";
export {
createResumableUploadRuntime,
type ResumableUploadRuntime,
type ResumableUploadRuntimeDependencies,
} from "./resumable-upload-runtime.ts";
export {
resolveResumableUploadRuntimePolicy,
type ResumableUploadRuntimePolicy,
} from "./runtime-policy.ts";
export {
createBrowserUploadCancellationChannel,
type BrowserUploadCancellationDependencies,
type UploadCancellationBroadcastFacade,
type UploadCancellationChannel,
type UploadCancellationListener,
} from "./upload-cancellation-channel.ts";
export {
createResumableUploadWebLock,
type UploadMutationLock,
} from "./upload-mutation-lock.ts";
@@ -0,0 +1,667 @@
import type {
ResumableUploadCheckpointAdmin,
ResumableUploadCheckpoint,
ResumableUploadCheckpointStore,
} from "../../../application/ports/browser-transfer/resumable-upload.ts";
import type { BrowserDataResult } from "../../../application/ports/browser-file-storage/shared.ts";
import {
browserDataFailure,
browserDataSuccess,
mapBrowserDataException,
} from "../../browser-file-storage/result.ts";
import {
isResumableUploadCheckpoint,
SAFE_OPAQUE_ID,
SAFE_UPLOAD_KEY,
} from "./checkpoint-schema.ts";
const DATABASE_VERSION = 1;
const CHECKPOINT_STORE = "checkpoints";
const GOVERNANCE_STORE = "governance";
const GOVERNANCE_KEY = "scope-binding";
const DEFAULT_BLOCKED_TIMEOUT_MS = 5_000;
export type IndexedDbUploadCheckpointScope = Readonly<{
authorityToken: string;
namespaceToken: string;
partitionToken: string;
}>;
export type IndexedDbUploadCheckpointDependencies = Readonly<{
scope: IndexedDbUploadCheckpointScope;
factory?: IDBFactory;
blockedTimeoutMs?: number;
}>;
export type IndexedDbUploadCheckpointRuntime = Readonly<{
store: ResumableUploadCheckpointStore;
admin: ResumableUploadCheckpointAdmin;
}>;
type ScopeBinding = Readonly<{
key: typeof GOVERNANCE_KEY;
schemaVersion: 1;
authorityToken: string;
namespaceToken: string;
partitionToken: string;
}>;
type OpenFactory = (
name: string,
version?: number,
) => IDBOpenDBRequest;
export function uploadCheckpointDatabaseName(
scope: IndexedDbUploadCheckpointScope,
): string {
const snapshot = snapshotScope(scope);
const components = [
snapshot.authorityToken,
snapshot.namespaceToken,
snapshot.partitionToken,
].map((component) => `${component.length}:${component}`);
return `ca-resumable-upload-v1|${components.join("|")}`;
}
export function createIndexedDbResumableUploadCheckpointStore(
input: IndexedDbUploadCheckpointDependencies,
): ResumableUploadCheckpointStore {
return createIndexedDbResumableUploadCheckpointRuntime(input).store;
}
export function createIndexedDbResumableUploadCheckpointRuntime(
input: IndexedDbUploadCheckpointDependencies,
): IndexedDbUploadCheckpointRuntime {
const scope = snapshotScope(input.scope);
const factory =
input.factory ??
(typeof indexedDB === "undefined" ? undefined : indexedDB);
const blockedTimeoutMs =
input.blockedTimeoutMs ?? DEFAULT_BLOCKED_TIMEOUT_MS;
if (
!Number.isSafeInteger(blockedTimeoutMs) ||
blockedTimeoutMs < 1 ||
blockedTimeoutMs > 30_000
) {
throw new TypeError("Upload checkpoint blocked timeout is invalid.");
}
const openFactory: OpenFactory | undefined = factory
? factory.open.bind(factory)
: undefined;
const deleteFactory =
factory && typeof factory.deleteDatabase === "function"
? factory.deleteDatabase.bind(factory)
: undefined;
const databaseName = uploadCheckpointDatabaseName(scope);
const expectedBinding: ScopeBinding = Object.freeze({
key: GOVERNANCE_KEY,
schemaVersion: 1,
...scope,
});
let database: IDBDatabase | null = null;
let opening: Promise<BrowserDataResult<IDBDatabase>> | null = null;
let closed = false;
async function open(
signal?: AbortSignal,
): Promise<BrowserDataResult<IDBDatabase>> {
if (closed || !openFactory) {
return browserDataFailure("UNAVAILABLE", "UPLOAD_RECONCILE", {
recovery: "RESUME",
});
}
if (signal?.aborted) {
return browserDataFailure("ABORTED", "UPLOAD_RECONCILE");
}
if (database) return browserDataSuccess(database);
if (!opening) {
opening = openAndBind().finally(() => {
opening = null;
});
}
const result = await opening;
if (signal?.aborted) {
return browserDataFailure("ABORTED", "UPLOAD_RECONCILE");
}
return result;
}
async function openAndBind(): Promise<BrowserDataResult<IDBDatabase>> {
let request: IDBOpenDBRequest;
try {
request = openFactory!(databaseName, DATABASE_VERSION);
} catch (error) {
return mapBrowserDataException(error, "UPLOAD_RECONCILE");
}
const opened = await new Promise<BrowserDataResult<IDBDatabase>>(
(resolve) => {
let settled = false;
let blockedTimer: ReturnType<typeof setTimeout> | undefined;
const finish = (result: BrowserDataResult<IDBDatabase>) => {
if (settled) {
if (result.ok) result.value.close();
return;
}
settled = true;
if (blockedTimer) clearTimeout(blockedTimer);
resolve(result);
};
request.onupgradeneeded = () => {
try {
const db = request.result;
if (!db.objectStoreNames.contains(CHECKPOINT_STORE)) {
db.createObjectStore(CHECKPOINT_STORE, {
keyPath: "uploadKey",
});
}
if (!db.objectStoreNames.contains(GOVERNANCE_STORE)) {
db.createObjectStore(GOVERNANCE_STORE, {
keyPath: "key",
});
}
} catch (error) {
try {
request.transaction?.abort();
} catch {
// The open request will surface the original closed failure.
}
finish(mapBrowserDataException(error, "UPLOAD_RECONCILE"));
}
};
request.onblocked = () => {
blockedTimer = setTimeout(() => {
finish(
browserDataFailure("BLOCKED", "UPLOAD_RECONCILE", {
retryable: true,
recovery: "RESUME",
}),
);
}, blockedTimeoutMs);
};
request.onerror = () =>
finish(
mapBrowserDataException(
request.error,
"UPLOAD_RECONCILE",
),
);
request.onsuccess = () => finish(browserDataSuccess(request.result));
},
);
if (!opened.ok) return opened;
if (closed) {
opened.value.close();
return browserDataFailure("UNAVAILABLE", "UPLOAD_RECONCILE", {
recovery: "RESUME",
});
}
const bound = await bindScope(opened.value, expectedBinding);
if (!bound.ok) {
opened.value.close();
return bound;
}
opened.value.onversionchange = () => {
opened.value.close();
if (database === opened.value) database = null;
};
opened.value.onclose = () => {
if (database === opened.value) database = null;
};
database = opened.value;
return browserDataSuccess(opened.value);
}
const storeValue: ResumableUploadCheckpointStore = {
async read(
uploadKey: string,
signal?: AbortSignal,
): Promise<
BrowserDataResult<ResumableUploadCheckpoint | null>
> {
if (!SAFE_UPLOAD_KEY.test(uploadKey)) {
return browserDataFailure(
"INVALID_INPUT",
"UPLOAD_RECONCILE",
);
}
const opened = await open(signal);
if (!opened.ok) return opened;
return await runCheckpointTransaction<
ResumableUploadCheckpoint | null
>(
opened.value,
"readonly",
signal,
(nativeStore, context) => {
const request = nativeStore.get(uploadKey);
request.onerror = () => context.nativeFailure(request.error);
request.onsuccess = () => {
if (request.result === undefined) {
context.succeed(null);
return;
}
if (!isResumableUploadCheckpoint(request.result)) {
context.fail(
browserDataFailure(
"CORRUPT_DATA",
"UPLOAD_RECONCILE",
{ recovery: "RECONCILE" },
),
);
return;
}
context.succeed(
snapshotCheckpoint(request.result),
);
};
},
);
},
async compareAndSwap(
inputValue: Parameters<
ResumableUploadCheckpointStore["compareAndSwap"]
>[0],
): Promise<BrowserDataResult<ResumableUploadCheckpoint>> {
let checkpoint: ResumableUploadCheckpoint;
try {
checkpoint = snapshotCheckpoint(inputValue.checkpoint);
} catch {
return browserDataFailure(
"INVALID_INPUT",
"UPLOAD_RECONCILE",
);
}
const expectedRevision = inputValue.expectedRevision;
if (
(expectedRevision !== null &&
(!Number.isSafeInteger(expectedRevision) ||
expectedRevision < 1)) ||
checkpoint.revision !== (expectedRevision ?? 0) + 1
) {
return browserDataFailure(
"INVALID_INPUT",
"UPLOAD_RECONCILE",
);
}
const opened = await open(inputValue.signal);
if (!opened.ok) return opened;
return await runCheckpointTransaction<ResumableUploadCheckpoint>(
opened.value,
"readwrite",
inputValue.signal,
(nativeStore, context) => {
const request = nativeStore.get(checkpoint.uploadKey);
request.onerror = () => context.nativeFailure(request.error);
request.onsuccess = () => {
const current = request.result;
if (
(expectedRevision === null && current !== undefined) ||
(expectedRevision !== null &&
(!isResumableUploadCheckpoint(current) ||
current.revision !== expectedRevision))
) {
context.fail(
browserDataFailure(
"CONFLICT",
"UPLOAD_RECONCILE",
{ recovery: "RECONCILE" },
),
);
return;
}
const put = nativeStore.put(checkpoint);
put.onerror = () => context.nativeFailure(put.error);
put.onsuccess = () => context.succeed(checkpoint);
};
},
);
},
async remove(
inputValue: Parameters<
ResumableUploadCheckpointStore["remove"]
>[0],
): Promise<BrowserDataResult<void>> {
if (
!SAFE_UPLOAD_KEY.test(inputValue.uploadKey) ||
!Number.isSafeInteger(inputValue.expectedRevision) ||
inputValue.expectedRevision < 1
) {
return browserDataFailure(
"INVALID_INPUT",
"UPLOAD_RECONCILE",
);
}
const opened = await open(inputValue.signal);
if (!opened.ok) return opened;
return await runCheckpointTransaction<void>(
opened.value,
"readwrite",
inputValue.signal,
(nativeStore, context) => {
const request = nativeStore.get(inputValue.uploadKey);
request.onerror = () => context.nativeFailure(request.error);
request.onsuccess = () => {
if (
!isResumableUploadCheckpoint(request.result) ||
request.result.revision !== inputValue.expectedRevision
) {
context.fail(
browserDataFailure(
"CONFLICT",
"UPLOAD_RECONCILE",
{ recovery: "RECONCILE" },
),
);
return;
}
const deletion = nativeStore.delete(inputValue.uploadKey);
deletion.onerror = () =>
context.nativeFailure(deletion.error);
deletion.onsuccess = () => context.succeed(undefined);
};
},
);
},
close() {
closed = true;
database?.close();
database = null;
},
};
const store = Object.freeze(storeValue);
const adminValue: ResumableUploadCheckpointAdmin = {
async deletePartition(
signal?: AbortSignal,
): Promise<
BrowserDataResult<Readonly<{ state: "DELETED" }>>
> {
if (signal?.aborted) {
return browserDataFailure("ABORTED", "UPLOAD_RECONCILE");
}
closed = true;
database?.close();
database = null;
if (!deleteFactory) {
return browserDataFailure(
"UNSUPPORTED",
"UPLOAD_RECONCILE",
{ recovery: "READ_ONLY" },
);
}
let request: IDBOpenDBRequest;
try {
request = deleteFactory(databaseName);
} catch (error) {
return mapBrowserDataException(error, "UPLOAD_RECONCILE");
}
return await new Promise<
BrowserDataResult<Readonly<{ state: "DELETED" }>>
>((resolve) => {
let settled = false;
let blockedTimer: ReturnType<typeof setTimeout> | undefined;
const finish = (
result: BrowserDataResult<Readonly<{ state: "DELETED" }>>,
) => {
if (settled) return;
settled = true;
if (blockedTimer) clearTimeout(blockedTimer);
resolve(result);
};
// IDB deleteDatabase cannot be cancelled after dispatch. AbortSignal is
// intentionally observed only before dispatch so the adapter never
// reports ABORTED while deletion may still commit.
request.onblocked = () => {
blockedTimer = setTimeout(() => {
finish(
browserDataFailure("BLOCKED", "UPLOAD_RECONCILE", {
retryable: true,
recovery: "RELOAD_OTHER_CONTEXTS",
}),
);
}, blockedTimeoutMs);
};
request.onerror = () =>
finish(
mapBrowserDataException(
request.error,
"UPLOAD_RECONCILE",
),
);
request.onsuccess = () =>
finish(
browserDataSuccess(
Object.freeze({ state: "DELETED" as const }),
),
);
});
},
};
const admin = Object.freeze(adminValue);
return Object.freeze({ store, admin });
}
type TransactionContext<Value> = Readonly<{
succeed(value: Value): void;
fail(result: BrowserDataResult<never>): void;
nativeFailure(error: unknown): void;
}>;
async function runCheckpointTransaction<Value>(
database: IDBDatabase,
mode: IDBTransactionMode,
signal: AbortSignal | undefined,
execute: (
store: IDBObjectStore,
context: TransactionContext<Value>,
) => void,
): Promise<BrowserDataResult<Value>> {
if (signal?.aborted) {
return browserDataFailure("ABORTED", "UPLOAD_RECONCILE");
}
return await new Promise<BrowserDataResult<Value>>((resolve) => {
let transaction: IDBTransaction;
try {
transaction = database.transaction(CHECKPOINT_STORE, mode);
} catch (error) {
resolve(mapBrowserDataException(error, "UPLOAD_RECONCILE"));
return;
}
let value: Value | undefined;
let hasValue = false;
let failure: BrowserDataResult<never> | null = null;
let settled = false;
const finish = (result: BrowserDataResult<Value>) => {
if (settled) return;
settled = true;
signal?.removeEventListener("abort", abort);
resolve(result);
};
const abort = () => {
const previousFailure = failure;
failure = browserDataFailure("ABORTED", "UPLOAD_RECONCILE");
try {
transaction.abort();
} catch {
// The transaction may already be durably committed while its
// completion event is still queued. Wait for oncomplete/onabort so we
// never report ABORTED for a mutation that actually committed.
failure = previousFailure;
}
};
signal?.addEventListener("abort", abort, { once: true });
transaction.oncomplete = () => {
if (!hasValue) {
finish(
browserDataFailure("CORRUPT_DATA", "UPLOAD_RECONCILE", {
recovery: "RECONCILE",
}),
);
return;
}
finish(browserDataSuccess(value as Value));
};
transaction.onerror = () => {
// onabort is the terminal transaction signal.
};
transaction.onabort = () =>
finish(
failure ??
mapBrowserDataException(
transaction.error,
"UPLOAD_RECONCILE",
),
);
const context: TransactionContext<Value> = Object.freeze({
succeed(next) {
if (failure) return;
value = next;
hasValue = true;
},
fail(result) {
if (failure) return;
failure = result;
try {
transaction.abort();
} catch {
finish(result);
}
},
nativeFailure(error) {
if (failure) return;
failure = mapBrowserDataException(
error,
"UPLOAD_RECONCILE",
);
try {
transaction.abort();
} catch {
finish(failure);
}
},
});
try {
execute(transaction.objectStore(CHECKPOINT_STORE), context);
} catch (error) {
context.nativeFailure(error);
}
});
}
async function bindScope(
database: IDBDatabase,
expected: ScopeBinding,
): Promise<BrowserDataResult<void>> {
return await new Promise<BrowserDataResult<void>>((resolve) => {
let transaction: IDBTransaction;
try {
transaction = database.transaction(GOVERNANCE_STORE, "readwrite");
} catch (error) {
resolve(mapBrowserDataException(error, "UPLOAD_RECONCILE"));
return;
}
let failure: BrowserDataResult<never> | null = null;
transaction.onerror = () => {
// onabort owns terminal resolution.
};
transaction.onabort = () =>
resolve(
failure ??
mapBrowserDataException(
transaction.error,
"UPLOAD_RECONCILE",
),
);
transaction.oncomplete = () => resolve(browserDataSuccess(undefined));
const store = transaction.objectStore(GOVERNANCE_STORE);
const request = store.get(GOVERNANCE_KEY);
request.onerror = () => {
failure = mapBrowserDataException(
request.error,
"UPLOAD_RECONCILE",
);
transaction.abort();
};
request.onsuccess = () => {
if (request.result === undefined) {
const add = store.add(expected);
add.onerror = () => {
failure = mapBrowserDataException(
add.error,
"UPLOAD_RECONCILE",
);
transaction.abort();
};
return;
}
if (!sameScopeBinding(request.result, expected)) {
failure = browserDataFailure(
"POLICY_REJECTED",
"UPLOAD_RECONCILE",
{ recovery: "READ_ONLY" },
);
transaction.abort();
}
};
});
}
function sameScopeBinding(
value: unknown,
expected: ScopeBinding,
): boolean {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return false;
}
const record = value as Record<string, unknown>;
return (
Object.keys(record).length === 5 &&
record.key === expected.key &&
record.schemaVersion === expected.schemaVersion &&
record.authorityToken === expected.authorityToken &&
record.namespaceToken === expected.namespaceToken &&
record.partitionToken === expected.partitionToken
);
}
function snapshotScope(
value: IndexedDbUploadCheckpointScope,
): IndexedDbUploadCheckpointScope {
if (
!value ||
typeof value !== "object" ||
!SAFE_OPAQUE_ID.test(value.authorityToken) ||
!SAFE_OPAQUE_ID.test(value.namespaceToken) ||
!SAFE_OPAQUE_ID.test(value.partitionToken)
) {
throw new TypeError("Upload checkpoint scope is invalid.");
}
return Object.freeze({
authorityToken: value.authorityToken,
namespaceToken: value.namespaceToken,
partitionToken: value.partitionToken,
});
}
function snapshotCheckpoint(
value: ResumableUploadCheckpoint,
): ResumableUploadCheckpoint {
let cloned: unknown;
try {
cloned = structuredClone(value);
} catch {
throw new TypeError("Upload checkpoint is not cloneable.");
}
if (!isResumableUploadCheckpoint(cloned)) {
throw new TypeError("Upload checkpoint is invalid.");
}
return Object.freeze({
...cloned,
fingerprint: Object.freeze({ ...cloned.fingerprint }),
acceptedParts: Object.freeze(
cloned.acceptedParts.map((part) => Object.freeze({ ...part })),
),
});
}
@@ -0,0 +1,114 @@
import type {
PresignedUploadPartCapability,
PresignedUploadPartPort,
} from "../../../application/ports/browser-transfer/presigned-transfer.ts";
import type {
UploadPartExecutor,
UploadPartReceipt,
UploadProviderResult,
} from "../../../application/ports/browser-transfer/resumable-upload.ts";
import {
browserDataFailure,
browserDataSuccess,
} from "../../browser-file-storage/result.ts";
import {
MEDIA_TYPE,
isSafeUploadReceiptToken,
SHA256_HEX,
} from "./checkpoint-schema.ts";
export function createPresignedUploadPartExecutor(
inputPort: PresignedUploadPartPort,
now: () => number = Date.now,
): UploadPartExecutor<PresignedUploadPartCapability> {
const put = inputPort?.put;
if (typeof put !== "function" || typeof now !== "function") {
throw new TypeError("Presigned upload part dependency is invalid.");
}
const executor: UploadPartExecutor<PresignedUploadPartCapability> = {
async uploadPart(
input: Parameters<
UploadPartExecutor<PresignedUploadPartCapability>["uploadPart"]
>[0],
): Promise<
UploadProviderResult<UploadPartReceipt>
> {
const capability = input.capability;
let nowEpochMs: number;
try {
nowEpochMs = now();
} catch {
return browserDataFailure("UNAVAILABLE", "UPLOAD_PART", {
retryable: true,
recovery: "RESUME",
});
}
if (
!capability ||
capability.method !== "PUT" ||
capability.binding.kind !== "UPLOAD_PART" ||
capability.binding.protocol !== input.protocol ||
capability.binding.sessionId !== input.sessionId ||
capability.binding.requestBindingSha256 !==
input.requestBindingSha256 ||
capability.binding.uploadBindingSha256 !==
input.uploadBindingSha256 ||
capability.binding.partNumber !== input.part.partNumber ||
capability.binding.offset !== input.part.offset ||
capability.binding.idempotencyKey !== input.idempotencyKey ||
capability.mediaType !== input.mediaType ||
!MEDIA_TYPE.test(capability.mediaType) ||
capability.byteLength !== input.part.byteLength ||
capability.maxBytes < capability.byteLength ||
capability.maxBytes !== input.part.byteLength ||
capability.expectedSha256 !== input.part.checksumSha256 ||
!SHA256_HEX.test(capability.expectedSha256) ||
capability.expiresAtEpochMs <= nowEpochMs ||
!(input.bytes instanceof Uint8Array) ||
input.bytes.byteLength !== input.part.byteLength ||
typeof capability.capabilityReceipt !== "string"
) {
return browserDataFailure("POLICY_REJECTED", "UPLOAD_PART");
}
let uploaded;
try {
uploaded = await put.call(inputPort, {
capability,
sessionId: input.sessionId,
requestBindingSha256: input.requestBindingSha256,
uploadBindingSha256: input.uploadBindingSha256,
partNumber: input.part.partNumber,
offset: input.part.offset,
byteLength: input.part.byteLength,
checksumSha256: input.part.checksumSha256,
idempotencyKey: input.idempotencyKey,
bytes: Uint8Array.from(input.bytes),
signal: input.signal,
});
} catch {
return browserDataFailure("UNAVAILABLE", "UPLOAD_PART", {
retryable: true,
recovery: "RESUME",
});
}
if (!uploaded.ok) return uploaded;
if (
uploaded.value.bytesWritten !== input.part.byteLength ||
uploaded.value.checksumSha256 !== input.part.checksumSha256 ||
typeof uploaded.value.receiptToken !== "string" ||
!isSafeUploadReceiptToken(uploaded.value.receiptToken)
) {
return browserDataFailure("INTEGRITY_FAILED", "UPLOAD_PART", {
recovery: "RECONCILE",
});
}
return browserDataSuccess(
Object.freeze({
...input.part,
receiptToken: uploaded.value.receiptToken,
}),
);
},
};
return Object.freeze(executor);
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,112 @@
export type ResumableUploadRuntimePolicy = Readonly<{
partSizeBytes: number;
maxFileBytes: number;
maxPartCount: number;
maxConcurrency: number;
maxInFlightBytes: number;
partBufferCopyFactor: number;
maxSourceChunkBytes: number;
maxRetries: number;
retryBaseDelayMs: number;
retryMaxDelayMs: number;
maxRetryAfterMs: number;
capabilityRefreshSkewMs: number;
maxSessionLifetimeMs: number;
providerAttemptTimeoutMs: number;
}>;
const MIB = 1024 * 1024;
const GIB = 1024 * MIB;
const ABSOLUTE_LIMITS = Object.freeze({
maxPartSizeBytes: 64 * MIB,
maxFileBytes: 100 * GIB,
maxPartCount: 10_000,
maxConcurrency: 8,
maxInFlightBytes: 256 * MIB,
maxPartBufferCopyFactor: 8,
maxSourceChunkBytes: 64 * MIB,
maxRetries: 8,
maxRetryDelayMs: 60_000,
maxRetryAfterMs: 60_000,
maxCapabilityRefreshSkewMs: 5 * 60_000,
maxSessionLifetimeMs: 7 * 24 * 60 * 60_000,
maxProviderAttemptTimeoutMs: 2 * 60_000,
});
const DEFAULT_POLICY: ResumableUploadRuntimePolicy = Object.freeze({
partSizeBytes: 5 * MIB,
maxFileBytes: 5 * GIB,
maxPartCount: 1_024,
maxConcurrency: 3,
maxInFlightBytes: 20 * MIB,
partBufferCopyFactor: 4,
maxSourceChunkBytes: 8 * MIB,
maxRetries: 3,
retryBaseDelayMs: 250,
retryMaxDelayMs: 5_000,
maxRetryAfterMs: 30_000,
capabilityRefreshSkewMs: 5_000,
maxSessionLifetimeMs: 24 * 60 * 60_000,
providerAttemptTimeoutMs: 30_000,
});
export function resolveResumableUploadRuntimePolicy(
input: Partial<ResumableUploadRuntimePolicy> = {},
): ResumableUploadRuntimePolicy {
const policy: ResumableUploadRuntimePolicy = Object.freeze({
...DEFAULT_POLICY,
...input,
});
if (
!positiveSafeInteger(policy.partSizeBytes) ||
policy.partSizeBytes > ABSOLUTE_LIMITS.maxPartSizeBytes ||
!positiveSafeInteger(policy.maxFileBytes) ||
policy.maxFileBytes > ABSOLUTE_LIMITS.maxFileBytes ||
!positiveSafeInteger(policy.maxPartCount) ||
policy.maxPartCount > ABSOLUTE_LIMITS.maxPartCount ||
!positiveSafeInteger(policy.maxConcurrency) ||
policy.maxConcurrency > ABSOLUTE_LIMITS.maxConcurrency ||
!positiveSafeInteger(policy.maxInFlightBytes) ||
policy.maxInFlightBytes > ABSOLUTE_LIMITS.maxInFlightBytes ||
!positiveSafeInteger(policy.partBufferCopyFactor) ||
policy.partBufferCopyFactor >
ABSOLUTE_LIMITS.maxPartBufferCopyFactor ||
policy.maxInFlightBytes <
policy.partSizeBytes * policy.partBufferCopyFactor ||
!positiveSafeInteger(policy.maxSourceChunkBytes) ||
policy.maxSourceChunkBytes >
ABSOLUTE_LIMITS.maxSourceChunkBytes ||
!nonNegativeSafeInteger(policy.maxRetries) ||
policy.maxRetries > ABSOLUTE_LIMITS.maxRetries ||
!positiveSafeInteger(policy.retryBaseDelayMs) ||
policy.retryBaseDelayMs > ABSOLUTE_LIMITS.maxRetryDelayMs ||
!positiveSafeInteger(policy.retryMaxDelayMs) ||
policy.retryMaxDelayMs > ABSOLUTE_LIMITS.maxRetryDelayMs ||
policy.retryBaseDelayMs > policy.retryMaxDelayMs ||
!nonNegativeSafeInteger(policy.maxRetryAfterMs) ||
policy.maxRetryAfterMs > ABSOLUTE_LIMITS.maxRetryAfterMs ||
!nonNegativeSafeInteger(policy.capabilityRefreshSkewMs) ||
policy.capabilityRefreshSkewMs >
ABSOLUTE_LIMITS.maxCapabilityRefreshSkewMs ||
!positiveSafeInteger(policy.maxSessionLifetimeMs) ||
policy.maxSessionLifetimeMs >
ABSOLUTE_LIMITS.maxSessionLifetimeMs ||
!positiveSafeInteger(policy.providerAttemptTimeoutMs) ||
policy.providerAttemptTimeoutMs >
ABSOLUTE_LIMITS.maxProviderAttemptTimeoutMs ||
Math.ceil(policy.maxFileBytes / policy.partSizeBytes) >
policy.maxPartCount
) {
throw new TypeError("Resumable upload policy is invalid.");
}
return policy;
}
function positiveSafeInteger(value: number): boolean {
return Number.isSafeInteger(value) && value > 0;
}
function nonNegativeSafeInteger(value: number): boolean {
return Number.isSafeInteger(value) && value >= 0;
}
@@ -0,0 +1,600 @@
import type {
ResumableUploadSource,
UploadFileFingerprint,
UploadPartDescriptor,
} from "../../../application/ports/browser-transfer/resumable-upload.ts";
import type {
BrowserDataFailure,
BrowserDataOperation,
BrowserDataResult,
} from "../../../application/ports/browser-file-storage/shared.ts";
import {
browserDataFailure,
browserDataSuccess,
} from "../../browser-file-storage/result.ts";
import { samePart } from "./checkpoint-schema.ts";
export type UploadCrypto = Readonly<{
digestSha256(bytes: Uint8Array): Promise<ArrayBuffer>;
}>;
export type UploadSourceSnapshot =
| Readonly<{
kind: "FILE_BYTE_SOURCE";
byteLength: number;
stream(
signal: AbortSignal,
): AsyncIterable<BrowserDataResult<Uint8Array>>;
}>
| Readonly<{
kind: "RANGE_READER";
byteLength: number;
readRange(input: Readonly<{
offset: number;
length: number;
signal: AbortSignal;
}>): Promise<BrowserDataResult<Uint8Array>>;
}>;
export type UploadPartManifest = Readonly<{
fingerprint: UploadFileFingerprint;
parts: readonly UploadPartDescriptor[];
}>;
export function snapshotUploadSource(
source: ResumableUploadSource,
): UploadSourceSnapshot {
if (!source || typeof source !== "object") {
throw new TypeError("Upload source is invalid.");
}
if (source.kind === "FILE_BYTE_SOURCE") {
const bytes = source.bytes;
const stream = bytes?.stream;
if (
typeof stream !== "function" ||
!positiveSafeInteger(bytes.byteLength)
) {
throw new TypeError("Upload byte source is invalid.");
}
return Object.freeze({
kind: "FILE_BYTE_SOURCE" as const,
byteLength: bytes.byteLength,
stream(signal: AbortSignal) {
return stream.call(bytes, signal);
},
});
}
if (source.kind === "RANGE_READER") {
const reader = source.reader;
const readRange = reader?.readRange;
if (
typeof readRange !== "function" ||
!positiveSafeInteger(reader.byteLength)
) {
throw new TypeError("Upload range source is invalid.");
}
return Object.freeze({
kind: "RANGE_READER" as const,
byteLength: reader.byteLength,
async readRange(input) {
return await readRange.call(reader, input);
},
});
}
throw new TypeError("Upload source kind is invalid.");
}
export function snapshotUploadCrypto(crypto: Crypto): UploadCrypto {
const subtle = crypto?.subtle;
const digest = subtle?.digest;
if (typeof digest !== "function") {
throw new TypeError("Upload crypto capability is invalid.");
}
return Object.freeze({
async digestSha256(bytes: Uint8Array): Promise<ArrayBuffer> {
return await digest.call(
subtle,
"SHA-256",
Uint8Array.from(bytes),
);
},
});
}
export async function buildUploadPartManifest(input: Readonly<{
source: UploadSourceSnapshot;
partSizeBytes: number;
maxPartCount: number;
maxSourceChunkBytes: number;
crypto: UploadCrypto;
signal: AbortSignal;
onPreparedBytes?: (bytes: number) => void;
}>): Promise<BrowserDataResult<UploadPartManifest>> {
const parts: UploadPartDescriptor[] = [];
let preparedBytes = 0;
for await (const partResult of iterateUploadParts({
source: input.source,
partSizeBytes: input.partSizeBytes,
maxSourceChunkBytes: input.maxSourceChunkBytes,
signal: input.signal,
operation: "UPLOAD_SESSION",
})) {
if (!partResult.ok) return partResult;
if (parts.length >= input.maxPartCount) {
return browserDataFailure("LIMIT_EXCEEDED", "UPLOAD_SESSION");
}
const checksum = await digestHex(
input.crypto,
partResult.value.bytes,
input.signal,
"UPLOAD_SESSION",
);
if (!checksum.ok) return checksum;
const descriptor: UploadPartDescriptor = Object.freeze({
partNumber: partResult.value.partNumber,
offset: partResult.value.offset,
byteLength: partResult.value.bytes.byteLength,
checksumSha256: checksum.value,
});
parts.push(descriptor);
preparedBytes += descriptor.byteLength;
try {
input.onPreparedBytes?.(preparedBytes);
} catch {
// Progress observation cannot affect transfer correctness.
}
}
if (
parts.length === 0 ||
preparedBytes !== input.source.byteLength
) {
return browserDataFailure("INTEGRITY_FAILED", "UPLOAD_SESSION", {
recovery: "RESELECT",
});
}
const canonical = canonicalPartManifest(
input.source.byteLength,
input.partSizeBytes,
parts,
);
const fingerprintDigest = await digestHex(
input.crypto,
canonical,
input.signal,
"UPLOAD_SESSION",
);
if (!fingerprintDigest.ok) return fingerprintDigest;
const fingerprint: UploadFileFingerprint = Object.freeze({
algorithm: "SHA-256-PARTS-V1",
digestHex: fingerprintDigest.value,
byteLength: input.source.byteLength,
partSizeBytes: input.partSizeBytes,
partCount: parts.length,
});
return browserDataSuccess(
Object.freeze({
fingerprint,
parts: Object.freeze(parts),
}),
);
}
export async function readAndVerifyRangePart(input: Readonly<{
source: Extract<UploadSourceSnapshot, { kind: "RANGE_READER" }>;
part: UploadPartDescriptor;
crypto: UploadCrypto;
signal: AbortSignal;
}>): Promise<BrowserDataResult<Uint8Array>> {
if (input.signal.aborted) {
return browserDataFailure("ABORTED", "UPLOAD_PART");
}
let result: BrowserDataResult<Uint8Array>;
try {
result = await input.source.readRange({
offset: input.part.offset,
length: input.part.byteLength,
signal: input.signal,
});
} catch {
return browserDataFailure("UNAVAILABLE", "UPLOAD_PART", {
retryable: true,
recovery: "RESUME",
});
}
if (!result.ok) return remapFailure(result.error, "UPLOAD_PART");
if (
!(result.value instanceof Uint8Array) ||
result.value.byteLength !== input.part.byteLength
) {
return browserDataFailure("INTEGRITY_FAILED", "UPLOAD_PART", {
recovery: "RESELECT",
});
}
const bytes = Uint8Array.from(result.value);
const checksum = await digestHex(
input.crypto,
bytes,
input.signal,
"UPLOAD_PART",
);
if (!checksum.ok) return checksum;
return checksum.value === input.part.checksumSha256
? browserDataSuccess(bytes)
: browserDataFailure("STALE_RESULT", "UPLOAD_PART", {
recovery: "RESELECT",
});
}
export async function verifyUploadPartBytes(input: Readonly<{
bytes: Uint8Array;
part: UploadPartDescriptor;
crypto: UploadCrypto;
signal: AbortSignal;
}>): Promise<BrowserDataResult<Uint8Array>> {
if (
!(input.bytes instanceof Uint8Array) ||
input.bytes.byteLength !== input.part.byteLength
) {
return browserDataFailure("INTEGRITY_FAILED", "UPLOAD_PART", {
recovery: "RESELECT",
});
}
const bytes = Uint8Array.from(input.bytes);
const checksum = await digestHex(
input.crypto,
bytes,
input.signal,
"UPLOAD_PART",
);
if (!checksum.ok) return checksum;
return checksum.value === input.part.checksumSha256
? browserDataSuccess(bytes)
: browserDataFailure("STALE_RESULT", "UPLOAD_PART", {
recovery: "RESELECT",
});
}
export async function digestRequestBinding(input: Readonly<{
uploadKey: string;
purpose: string;
mediaType: string;
fingerprint: UploadFileFingerprint;
crypto: UploadCrypto;
signal: AbortSignal;
}>): Promise<BrowserDataResult<string>> {
const canonical = new TextEncoder().encode(
[
"RESUMABLE-UPLOAD-BINDING-V1",
input.uploadKey,
input.purpose,
input.mediaType,
input.fingerprint.algorithm,
input.fingerprint.digestHex,
String(input.fingerprint.byteLength),
String(input.fingerprint.partSizeBytes),
String(input.fingerprint.partCount),
].join("\n"),
);
return await digestHex(
input.crypto,
canonical,
input.signal,
"UPLOAD_SESSION",
);
}
export async function deriveUploadIdempotencyKey(input: Readonly<{
label: "CREATE" | "PART" | "COMPLETE" | "ABORT";
requestBindingSha256: string;
sessionId?: string;
part?: UploadPartDescriptor;
crypto: UploadCrypto;
signal: AbortSignal;
}>): Promise<BrowserDataResult<string>> {
const fields = [
"RESUMABLE-UPLOAD-IDEMPOTENCY-V1",
input.label,
input.requestBindingSha256,
input.sessionId ?? "-",
];
if (input.part) {
fields.push(
String(input.part.partNumber),
String(input.part.offset),
String(input.part.byteLength),
input.part.checksumSha256,
);
}
const digest = await digestHex(
input.crypto,
new TextEncoder().encode(fields.join("\n")),
input.signal,
input.label === "PART"
? "UPLOAD_PART"
: input.label === "COMPLETE"
? "UPLOAD_COMPLETE"
: input.label === "ABORT"
? "UPLOAD_ABORT"
: "UPLOAD_SESSION",
);
return digest.ok
? browserDataSuccess(`upload-${input.label.toLowerCase()}-${digest.value}`)
: digest;
}
export async function digestUploadSessionBinding(input: Readonly<{
requestBindingSha256: string;
sessionId: string;
fingerprint: UploadFileFingerprint;
crypto: UploadCrypto;
signal: AbortSignal;
}>): Promise<BrowserDataResult<string>> {
return await digestHex(
input.crypto,
new TextEncoder().encode(
[
"RESUMABLE-UPLOAD-SESSION-BINDING-V1",
input.requestBindingSha256,
input.sessionId,
input.fingerprint.algorithm,
input.fingerprint.digestHex,
String(input.fingerprint.byteLength),
String(input.fingerprint.partSizeBytes),
String(input.fingerprint.partCount),
].join("\n"),
),
input.signal,
"UPLOAD_PART",
);
}
export async function* iterateUploadParts(input: Readonly<{
source: UploadSourceSnapshot;
partSizeBytes: number;
maxSourceChunkBytes: number;
signal: AbortSignal;
operation: "UPLOAD_SESSION" | "UPLOAD_PART";
}>): AsyncIterable<
BrowserDataResult<
Readonly<{
partNumber: number;
offset: number;
bytes: Uint8Array;
}>
>
> {
if (input.source.kind === "RANGE_READER") {
let partNumber = 1;
for (
let offset = 0;
offset < input.source.byteLength;
offset += input.partSizeBytes
) {
if (input.signal.aborted) {
yield browserDataFailure("ABORTED", input.operation);
return;
}
const length = Math.min(
input.partSizeBytes,
input.source.byteLength - offset,
);
let result: BrowserDataResult<Uint8Array>;
try {
result = await input.source.readRange({
offset,
length,
signal: input.signal,
});
} catch {
yield browserDataFailure("UNAVAILABLE", input.operation, {
retryable: true,
recovery: "RESUME",
});
return;
}
if (!result.ok) {
yield remapFailure(result.error, input.operation);
return;
}
if (
!(result.value instanceof Uint8Array) ||
result.value.byteLength !== length
) {
yield browserDataFailure("INTEGRITY_FAILED", input.operation, {
recovery: "RESELECT",
});
return;
}
yield browserDataSuccess(
Object.freeze({
partNumber,
offset,
bytes: Uint8Array.from(result.value),
}),
);
partNumber += 1;
}
return;
}
let iterable: AsyncIterable<BrowserDataResult<Uint8Array>>;
try {
iterable = input.source.stream(input.signal);
} catch {
yield browserDataFailure("UNAVAILABLE", input.operation, {
retryable: true,
recovery: "RESUME",
});
return;
}
let partNumber = 1;
let offset = 0;
let totalBytes = 0;
let buffer = new Uint8Array(input.partSizeBytes);
let bufferedBytes = 0;
try {
for await (const chunkResult of iterable) {
if (input.signal.aborted) {
yield browserDataFailure("ABORTED", input.operation);
return;
}
if (!chunkResult.ok) {
yield remapFailure(chunkResult.error, input.operation);
return;
}
const chunk = chunkResult.value;
if (
!(chunk instanceof Uint8Array) ||
chunk.byteLength < 1 ||
chunk.byteLength > input.maxSourceChunkBytes ||
totalBytes + chunk.byteLength > input.source.byteLength
) {
yield browserDataFailure(
chunk instanceof Uint8Array &&
chunk.byteLength > input.maxSourceChunkBytes
? "LIMIT_EXCEEDED"
: "INTEGRITY_FAILED",
input.operation,
{ recovery: "RESELECT" },
);
return;
}
let position = 0;
while (position < chunk.byteLength) {
const length = Math.min(
buffer.byteLength - bufferedBytes,
chunk.byteLength - position,
);
buffer.set(chunk.subarray(position, position + length), bufferedBytes);
position += length;
bufferedBytes += length;
totalBytes += length;
if (bufferedBytes === buffer.byteLength) {
yield browserDataSuccess(
Object.freeze({
partNumber,
offset,
bytes: buffer,
}),
);
offset += buffer.byteLength;
partNumber += 1;
buffer = new Uint8Array(input.partSizeBytes);
bufferedBytes = 0;
}
}
}
} catch {
yield browserDataFailure("UNAVAILABLE", input.operation, {
retryable: true,
recovery: "RESUME",
});
return;
}
if (totalBytes !== input.source.byteLength) {
yield browserDataFailure("INTEGRITY_FAILED", input.operation, {
recovery: "RESELECT",
});
return;
}
if (bufferedBytes > 0) {
yield browserDataSuccess(
Object.freeze({
partNumber,
offset,
bytes: buffer.slice(0, bufferedBytes),
}),
);
}
}
export function findManifestPart(
manifest: UploadPartManifest,
partNumber: number,
): UploadPartDescriptor | null {
return manifest.parts[partNumber - 1] ?? null;
}
export function verifyPartAgainstManifest(
part: UploadPartDescriptor,
manifest: UploadPartManifest,
): boolean {
const expected = findManifestPart(manifest, part.partNumber);
return Boolean(expected && samePart(part, expected));
}
async function digestHex(
crypto: UploadCrypto,
bytes: Uint8Array,
signal: AbortSignal,
operation: BrowserDataOperation,
): Promise<BrowserDataResult<string>> {
if (signal.aborted) {
return browserDataFailure("ABORTED", operation);
}
try {
const digest = new Uint8Array(await crypto.digestSha256(bytes));
if (signal.aborted) {
return browserDataFailure("ABORTED", operation);
}
if (digest.byteLength !== 32) {
return browserDataFailure("UNAVAILABLE", operation, {
retryable: true,
recovery: "RESUME",
});
}
return browserDataSuccess(
Array.from(
digest,
(byte) => byte.toString(16).padStart(2, "0"),
).join(""),
);
} catch {
return browserDataFailure("UNAVAILABLE", operation, {
retryable: true,
recovery: "RESUME",
});
}
}
function canonicalPartManifest(
byteLength: number,
partSizeBytes: number,
parts: readonly UploadPartDescriptor[],
): Uint8Array {
return new TextEncoder().encode(
[
"SHA-256-PARTS-V1",
String(byteLength),
String(partSizeBytes),
String(parts.length),
...parts.map((part) =>
[
part.partNumber,
part.offset,
part.byteLength,
part.checksumSha256,
].join(":"),
),
].join("\n"),
);
}
function remapFailure(
failure: BrowserDataFailure,
operation: BrowserDataOperation,
): BrowserDataResult<never> {
return Object.freeze({
ok: false,
error: Object.freeze({
code: failure.code,
operation,
retryable: failure.retryable,
recovery: failure.recovery,
}),
});
}
function positiveSafeInteger(value: unknown): value is number {
return Number.isSafeInteger(value) && (value as number) > 0;
}
@@ -0,0 +1,222 @@
import {
SAFE_REGISTRY_ID,
SAFE_UPLOAD_KEY,
} from "./checkpoint-schema.ts";
export type UploadCancellationListener = (
uploadKey: string,
) => void;
/**
* Ephemeral same-origin coordination only. Messages are never persisted and
* backend abort/idempotency remains the authoritative state transition.
*
* A runtime that receives this dependency owns it and closes it with the
* runtime. Do not share one channel instance between runtimes.
*/
export interface UploadCancellationChannel {
publish(uploadKey: string): boolean;
subscribe(listener: UploadCancellationListener): () => void;
close(): void;
}
export type UploadCancellationBroadcastFacade = Readonly<{
postMessage(message: unknown): void;
addEventListener(
type: "message",
listener: (event: Readonly<{ data: unknown }>) => void,
): void;
removeEventListener(
type: "message",
listener: (event: Readonly<{ data: unknown }>) => void,
): void;
close(): void;
}>;
export type BrowserUploadCancellationDependencies = Readonly<{
channelName?: string;
host?: Record<string, unknown>;
createChannel?: (
channelName: string,
) => UploadCancellationBroadcastFacade;
}>;
const DEFAULT_CHANNEL_NAME = "ca-resumable-upload-cancel-v1";
const PROTOCOL = "RESUMABLE_UPLOAD_CANCEL_V1";
const MESSAGE_KEYS = Object.freeze([
"protocol",
"uploadKey",
] as const);
/**
* Creates a strict BroadcastChannel-backed cancellation signal.
*
* Unsupported or policy-disabled BroadcastChannel returns `undefined`; upload
* correctness still relies on Web Locks, durable CAS and backend idempotency,
* while an explicit abort waits for the lock under its caller deadline.
*/
export function createBrowserUploadCancellationChannel(
dependencies: BrowserUploadCancellationDependencies = {},
): UploadCancellationChannel | undefined {
const channelName =
dependencies.channelName ?? DEFAULT_CHANNEL_NAME;
if (!SAFE_REGISTRY_ID.test(channelName)) {
throw new TypeError(
"Upload cancellation channel name is invalid.",
);
}
let channel: UploadCancellationBroadcastFacade;
try {
channel = dependencies.createChannel
? dependencies.createChannel(channelName)
: createNativeChannel(
dependencies.host ??
(globalThis as unknown as Record<string, unknown>),
channelName,
);
} catch {
return undefined;
}
if (!isBroadcastFacade(channel)) return undefined;
const listeners = new Set<UploadCancellationListener>();
let closed = false;
const receive = (event: Readonly<{ data: unknown }>) => {
if (closed || !isCancellationMessage(event.data)) return;
for (const listener of [...listeners]) {
try {
listener(event.data.uploadKey);
} catch {
// One feature listener cannot prevent delivery to other runtimes.
}
}
};
try {
channel.addEventListener("message", receive);
} catch {
try {
channel.close();
} catch {
// Construction still fails closed when cleanup is unavailable.
}
return undefined;
}
return Object.freeze({
publish(uploadKey: string): boolean {
if (closed || !SAFE_UPLOAD_KEY.test(uploadKey)) return false;
try {
channel.postMessage(
Object.freeze({
protocol: PROTOCOL,
uploadKey,
}),
);
return true;
} catch {
return false;
}
},
subscribe(
listener: UploadCancellationListener,
): () => void {
if (closed || typeof listener !== "function") {
throw new TypeError(
"Upload cancellation listener is invalid.",
);
}
listeners.add(listener);
let subscribed = true;
return () => {
if (!subscribed) return;
subscribed = false;
listeners.delete(listener);
};
},
close(): void {
if (closed) return;
closed = true;
listeners.clear();
try {
channel.removeEventListener("message", receive);
} catch {
// Closing remains terminal even if the host rejects cleanup.
}
try {
channel.close();
} catch {
// Closing remains terminal even if the host rejects cleanup.
}
},
});
}
function createNativeChannel(
host: Record<string, unknown>,
channelName: string,
): UploadCancellationBroadcastFacade {
const constructor = safeGet(host, "BroadcastChannel");
if (typeof constructor !== "function") {
throw new TypeError("BroadcastChannel is unavailable.");
}
return Reflect.construct(constructor, [
channelName,
]) as UploadCancellationBroadcastFacade;
}
function isBroadcastFacade(
value: unknown,
): value is UploadCancellationBroadcastFacade {
if (!value || typeof value !== "object") return false;
const candidate = value as Record<string, unknown>;
return [
"postMessage",
"addEventListener",
"removeEventListener",
"close",
].every((method) => typeof safeGet(candidate, method) === "function");
}
function isCancellationMessage(
value: unknown,
): value is Readonly<{
protocol: typeof PROTOCOL;
uploadKey: string;
}> {
if (
!value ||
typeof value !== "object" ||
Array.isArray(value)
) {
return false;
}
const keys = Object.keys(value).sort();
const expected = [...MESSAGE_KEYS].sort();
if (
keys.length !== expected.length ||
!keys.every((key, index) => key === expected[index])
) {
return false;
}
const candidate = value as Record<string, unknown>;
return (
candidate.protocol === PROTOCOL &&
typeof candidate.uploadKey === "string" &&
SAFE_UPLOAD_KEY.test(candidate.uploadKey)
);
}
function safeGet(
target: Record<string, unknown>,
property: string,
): unknown {
try {
return Reflect.get(target, property);
} catch {
return undefined;
}
}
@@ -0,0 +1,58 @@
import { SAFE_REGISTRY_ID, SAFE_UPLOAD_KEY } from "./checkpoint-schema.ts";
type LockManagerLike = {
request<Value>(
name: string,
options: Readonly<{ mode: "exclusive"; signal?: AbortSignal }>,
callback: (lock: unknown) => Promise<Value>,
): Promise<Value>;
};
export interface UploadMutationLock {
run<Value>(
uploadKey: string,
signal: AbortSignal,
task: () => Promise<Value>,
): Promise<Value>;
}
export function createResumableUploadWebLock(
lockManager: LockManager,
lockNamespace = "ca-resumable-upload-v1",
): UploadMutationLock {
if (!SAFE_REGISTRY_ID.test(lockNamespace)) {
throw new TypeError("Upload mutation lock namespace is invalid.");
}
const request = (lockManager as unknown as LockManagerLike)?.request;
if (typeof request !== "function") {
throw new TypeError("Upload mutation lock manager is invalid.");
}
return Object.freeze({
async run<Value>(
uploadKey: string,
signal: AbortSignal,
task: () => Promise<Value>,
): Promise<Value> {
if (!SAFE_UPLOAD_KEY.test(uploadKey)) {
throw new TypeError("Upload mutation lock key is invalid.");
}
if (signal.aborted) {
throw new DOMException("The operation was aborted.", "AbortError");
}
return await (request.call(
lockManager,
`${lockNamespace}:${uploadKey}`,
{ mode: "exclusive", signal },
async (lock) => {
if (!lock) {
throw new DOMException(
"The upload mutation lock is unavailable.",
"InvalidStateError",
);
}
return await task();
},
) as Promise<Value>);
},
});
}