Files
clean-architecture-frontend…/src/adapters/browser-transfer/presigned/presigned-transfer-executor.ts
T

1189 lines
34 KiB
TypeScript

import type {
PresignedDownloadCapability,
PresignedDownloadByteSource,
PresignedDownloadSourcePort,
PresignedTransferCapability,
PresignedTransferReplayGuard,
PresignedUploadPartCapability,
PresignedUploadPartOutcome,
PresignedUploadPartPort,
} from "../../../application/ports/browser-transfer/presigned-transfer.ts";
import { RESUMABLE_UPLOAD_PROTOCOL } from "../../../application/ports/browser-transfer/resumable-upload.ts";
import type {
BrowserDataFailureCode,
BrowserDataObserver,
BrowserDataOperation,
BrowserDataRecovery,
BrowserDataResult,
} from "../../../application/ports/browser-file-storage/shared.ts";
import {
browserDataFailure,
browserDataSuccess,
observeBrowserData,
} from "../../browser-file-storage/result.ts";
import {
createStreamingSha256Verifier,
type StreamingSha256Verifier,
} from "./incremental-sha256.ts";
import type {
PresignedCapabilityBinding,
PresignedCapabilityVault,
} from "./presigned-capability-vault.ts";
const SHA256 = /^[a-f0-9]{64}$/;
const RECEIPT_TOKEN = /^[A-Za-z0-9][A-Za-z0-9._~:+/=-]{0,511}$/;
type Scheduler = Readonly<{
setTimeout(callback: () => void, milliseconds: number): unknown;
clearTimeout(handle: unknown): void;
}>;
export type PresignedTransferExecutorOptions = Readonly<{
vault: PresignedCapabilityVault;
replayGuard: PresignedTransferReplayGuard;
hardMaxTransferBytes: number;
hardMaxChunkBytes: number;
hardMaxUploadResponseBytes: number;
minimumRemainingLifetimeMs: number;
timeoutMs: number;
fetcher?: typeof fetch;
now?: () => number;
scheduler?: Scheduler;
createStreamingVerifier?: (
expectedSha256: string,
) => StreamingSha256Verifier;
digestBytes?: (
bytes: Uint8Array,
) => Promise<string>;
observer?: BrowserDataObserver;
}>;
export type PresignedTransferExecutor = Readonly<{
downloadSources: PresignedDownloadSourcePort;
uploadParts: PresignedUploadPartPort;
}>;
/**
* Consumes only exact handles issued into the supplied identity vault. Raw
* href/query/header values never enter either public executor method.
*/
export function createPresignedTransferExecutor(
options: PresignedTransferExecutorOptions,
): PresignedTransferExecutor {
const resolve = options.vault.resolve.bind(options.vault);
const consume = options.vault.consume.bind(options.vault);
const claim = options.replayGuard.claim.bind(options.replayGuard);
const hardMaxTransferBytes = positiveSafeInteger(
options.hardMaxTransferBytes,
);
const hardMaxChunkBytes = positiveSafeInteger(
options.hardMaxChunkBytes,
);
if (hardMaxChunkBytes > hardMaxTransferBytes) {
throw new TypeError("Presigned chunk limit exceeds transfer limit.");
}
const hardMaxUploadResponseBytes = positiveSafeInteger(
options.hardMaxUploadResponseBytes,
);
const minimumRemainingLifetimeMs = positiveSafeInteger(
options.minimumRemainingLifetimeMs,
);
const timeoutMs = positiveSafeInteger(options.timeoutMs);
const fetcher = (options.fetcher ?? fetch).bind(globalThis);
const now = options.now ?? Date.now;
const scheduler =
options.scheduler ??
({
setTimeout: (callback, milliseconds) =>
globalThis.setTimeout(callback, milliseconds),
clearTimeout: (handle) =>
globalThis.clearTimeout(
handle as ReturnType<typeof globalThis.setTimeout>,
),
} satisfies Scheduler);
const createVerifier =
options.createStreamingVerifier ?? createStreamingSha256Verifier;
if (options.digestBytes === undefined && !globalThis.crypto?.subtle) {
throw new TypeError(
"WebCrypto SHA-256 is required for presigned uploads.",
);
}
const digestBytes = options.digestBytes ?? digestSha256WithWebCrypto;
const observer = options.observer;
async function openDownload(
input: Parameters<PresignedDownloadSourcePort["open"]>[0],
): Promise<BrowserDataResult<PresignedDownloadByteSource>> {
let resourceId: string;
let capability: PresignedDownloadCapability;
let signal: AbortSignal;
try {
resourceId = input.resourceId;
capability = input.capability;
signal = input.signal;
if (!isAbortSignal(signal)) {
throw new TypeError("Abort signal is invalid.");
}
} catch {
return browserDataFailure("INVALID_INPUT", "PRESIGNED_TRANSFER");
}
if (signal.aborted) {
return browserDataFailure("ABORTED", "PRESIGNED_TRANSFER");
}
const resolved = resolve(capability);
if (!resolved.ok) return resolved;
const binding = resolved.value;
if (
binding.method !== "GET" ||
binding.binding.kind !== "DOWNLOAD" ||
binding.binding.resourceId !== resourceId ||
capability.binding.kind !== "DOWNLOAD" ||
capability.binding.resourceId !== resourceId ||
!validCommonBinding(binding, capability, hardMaxTransferBytes)
) {
return browserDataFailure("POLICY_REJECTED", "PRESIGNED_TRANSFER");
}
const active = validateExpiry(
capability,
minimumRemainingLifetimeMs,
now(),
);
if (!active.ok) return active;
const claimed = claim(capability);
if (!claimed.ok) return claimed;
const consumed = consume(capability);
if (!consumed.ok) return consumed;
const scope = createAbortScope(signal, timeoutMs, scheduler);
try {
const response = await fetcher(binding.href, {
method: "GET",
headers: headersFor(binding),
credentials: "omit",
redirect: "error",
referrerPolicy: "no-referrer",
cache: "no-store",
signal: scope.signal,
});
const validated = validateDownloadResponse(
response,
binding,
);
if (!validated.ok) {
cancelBody(response);
scope.release();
return validated;
}
const source = createDownloadSource({
response,
binding,
capability,
externalSignal: signal,
scope,
hardMaxChunkBytes,
createVerifier,
observer,
});
return browserDataSuccess(source);
} catch {
scope.release();
return transferFailure(signal, scope.timedOut());
}
}
async function putUploadPart(
input: Parameters<PresignedUploadPartPort["put"]>[0],
): Promise<BrowserDataResult<PresignedUploadPartOutcome>> {
let capability: PresignedUploadPartCapability;
let bytes: Uint8Array<ArrayBuffer>;
let request: Readonly<{
sessionId: string;
requestBindingSha256: string;
uploadBindingSha256: string;
partNumber: number;
offset: number;
byteLength: number;
checksumSha256: string;
idempotencyKey: string;
signal: AbortSignal;
}>;
try {
capability = input.capability;
if (!(input.bytes instanceof Uint8Array)) {
throw new TypeError("Upload bytes are invalid.");
}
if (
input.bytes.byteLength > hardMaxTransferBytes ||
input.bytes.byteLength !== input.byteLength
) {
return browserDataFailure(
input.bytes.byteLength > hardMaxTransferBytes
? "LIMIT_EXCEEDED"
: "INTEGRITY_FAILED",
"PRESIGNED_TRANSFER",
);
}
if (!isAbortSignal(input.signal)) {
throw new TypeError("Abort signal is invalid.");
}
// Snapshot before hashing or network awaits so caller mutation cannot
// alter the verified bytes after the capability check.
bytes = input.bytes.slice();
request = Object.freeze({
sessionId: opaqueId(input.sessionId),
requestBindingSha256: normalizedSha256(
input.requestBindingSha256,
),
uploadBindingSha256: normalizedSha256(
input.uploadBindingSha256,
),
partNumber: positiveSafeInteger(input.partNumber),
offset: nonNegativeSafeInteger(input.offset),
byteLength: positiveSafeInteger(input.byteLength),
checksumSha256: normalizedSha256(input.checksumSha256),
idempotencyKey: opaqueId(input.idempotencyKey),
signal: input.signal,
});
} catch {
return browserDataFailure("INVALID_INPUT", "PRESIGNED_TRANSFER");
}
if (request.signal.aborted) {
return browserDataFailure("ABORTED", "PRESIGNED_TRANSFER");
}
const resolved = resolve(capability);
if (!resolved.ok) return resolved;
const binding = resolved.value;
if (
binding.method !== "PUT" ||
binding.binding.kind !== "UPLOAD_PART" ||
capability.binding.kind !== "UPLOAD_PART" ||
binding.binding.protocol !== RESUMABLE_UPLOAD_PROTOCOL ||
!validCommonBinding(binding, capability, hardMaxTransferBytes) ||
binding.binding.sessionId !== request.sessionId ||
binding.binding.requestBindingSha256 !==
request.requestBindingSha256 ||
binding.binding.uploadBindingSha256 !==
request.uploadBindingSha256 ||
binding.binding.partNumber !== request.partNumber ||
binding.binding.offset !== request.offset ||
binding.binding.idempotencyKey !== request.idempotencyKey ||
capability.byteLength !== request.byteLength ||
capability.expectedSha256 !== request.checksumSha256 ||
bytes.byteLength !== capability.byteLength
) {
return browserDataFailure("POLICY_REJECTED", "PRESIGNED_TRANSFER");
}
let actualDigest: string;
try {
actualDigest = normalizedSha256(await digestBytes(bytes));
} catch {
return browserDataFailure(
"INTEGRITY_FAILED",
"PRESIGNED_TRANSFER",
);
}
if (actualDigest !== capability.expectedSha256) {
return browserDataFailure(
"INTEGRITY_FAILED",
"PRESIGNED_TRANSFER",
);
}
if (request.signal.aborted) {
return browserDataFailure("ABORTED", "PRESIGNED_TRANSFER");
}
const active = validateExpiry(
capability,
minimumRemainingLifetimeMs,
now(),
);
if (!active.ok) return active;
const claimed = claim(capability);
if (!claimed.ok) return claimed;
const consumed = consume(capability);
if (!consumed.ok) return consumed;
const scope = createAbortScope(request.signal, timeoutMs, scheduler);
try {
const response = await fetcher(binding.href, {
method: "PUT",
headers: headersFor(binding),
body: bytes.buffer,
credentials: "omit",
redirect: "error",
referrerPolicy: "no-referrer",
cache: "no-store",
signal: scope.signal,
});
const validated = validateUploadResponse(
response,
binding,
hardMaxUploadResponseBytes,
);
if (!validated.ok) {
cancelBody(response);
return validated;
}
const drained = await drainUploadResponse(
response,
validated.value.responseByteLength,
hardMaxUploadResponseBytes,
scope.signal,
);
if (!drained.ok) return drained;
return browserDataSuccess(
Object.freeze({
bytesWritten: bytes.byteLength,
checksumSha256: capability.expectedSha256,
receiptToken: validated.value.receiptToken,
} satisfies PresignedUploadPartOutcome),
);
} catch {
return transferFailure(request.signal, scope.timedOut());
} finally {
scope.release();
}
}
const downloadSources: PresignedDownloadSourcePort = Object.freeze({
async open(
input: Parameters<PresignedDownloadSourcePort["open"]>[0],
) {
let result: BrowserDataResult<PresignedDownloadByteSource>;
try {
result = await openDownload(input);
} catch {
result = browserDataFailure(
"UNAVAILABLE",
"PRESIGNED_TRANSFER",
{
retryable: true,
recovery: "REISSUE_CAPABILITY",
},
);
}
observeTransferResult(
observer,
"PRESIGNED_TRANSFER",
result,
result.ok
? result.value.byteLength
: safeInputByteLength(input),
);
return result;
},
});
const uploadParts: PresignedUploadPartPort = Object.freeze({
async put(
input: Parameters<PresignedUploadPartPort["put"]>[0],
) {
let result: BrowserDataResult<PresignedUploadPartOutcome>;
try {
result = await putUploadPart(input);
} catch {
result = browserDataFailure(
"UNAVAILABLE",
"PRESIGNED_TRANSFER",
{
retryable: true,
recovery: "REISSUE_CAPABILITY",
},
);
}
observeTransferResult(
observer,
"UPLOAD_PART",
result,
result.ok
? result.value.bytesWritten
: safeInputByteLength(input),
);
return result;
},
});
return Object.freeze({ downloadSources, uploadParts });
}
function createDownloadSource(input: Readonly<{
response: Response;
binding: PresignedCapabilityBinding;
capability: PresignedDownloadCapability;
externalSignal: AbortSignal;
scope: ReturnType<typeof createAbortScope>;
hardMaxChunkBytes: number;
createVerifier: (
expectedSha256: string,
) => StreamingSha256Verifier;
observer: BrowserDataObserver | undefined;
}>): PresignedDownloadByteSource {
let started = false;
return Object.freeze({
byteLength: input.capability.byteLength,
capability: input.capability,
integrity: "VERIFIED_ON_SUCCESSFUL_EXHAUSTION" as const,
async *stream(
consumerSignal: AbortSignal,
): AsyncIterable<BrowserDataResult<Uint8Array>> {
if (!isAbortSignal(consumerSignal)) {
started = true;
cancelBody(input.response);
input.scope.release();
const failure = browserDataFailure(
"INVALID_INPUT",
"PRESIGNED_TRANSFER",
);
observeTransferResult(input.observer, "DOWNLOAD", failure, 0);
yield failure;
return;
}
if (started) {
const failure = browserDataFailure(
"CONFLICT",
"PRESIGNED_TRANSFER",
{
recovery: "REISSUE_CAPABILITY",
},
);
observeTransferResult(input.observer, "DOWNLOAD", failure, 0);
yield failure;
return;
}
started = true;
let combined:
| ReturnType<typeof combineConsumerAbort>
| undefined;
let reader:
| ReadableStreamDefaultReader<Uint8Array>
| undefined;
let completed = false;
let transferred = 0;
let terminalFailureCode: BrowserDataFailureCode | undefined;
const fail = (
code: BrowserDataFailureCode,
options: Readonly<{
retryable?: boolean;
recovery?: BrowserDataRecovery;
}> = {},
): BrowserDataResult<never> => {
terminalFailureCode = code;
return browserDataFailure(code, "PRESIGNED_TRANSFER", options);
};
try {
combined = combineConsumerAbort(
input.scope,
consumerSignal,
);
const verifier = input.createVerifier(
input.capability.expectedSha256,
);
if (!input.response.body) {
let verified = false;
try {
verified =
input.capability.byteLength === 0 && verifier.verify();
} catch {
verified = false;
}
if (verified) {
completed = true;
return;
}
yield fail("INTEGRITY_FAILED");
return;
}
reader = input.response.body.getReader();
while (true) {
if (
input.externalSignal.aborted ||
consumerSignal.aborted
) {
yield fail("ABORTED");
return;
}
if (input.scope.timedOut()) {
yield fail("UNAVAILABLE", {
retryable: true,
recovery: "REISSUE_CAPABILITY",
});
return;
}
const result = await readWithSignal(
reader,
input.scope.signal,
);
if (result.done) break;
const chunk = result.value;
if (!(chunk instanceof Uint8Array)) {
yield fail("CORRUPT_DATA");
return;
}
const chunkEnd = transferred + chunk.byteLength;
if (
!Number.isSafeInteger(chunkEnd) ||
chunkEnd > input.capability.maxBytes
) {
yield fail("LIMIT_EXCEEDED");
return;
}
if (chunkEnd > input.capability.byteLength) {
yield fail("INTEGRITY_FAILED");
return;
}
// Browser fetch chunk sizing is implementation-defined. Re-slice into
// owned bounded chunks instead of rejecting a valid large chunk.
for (
let offset = 0;
offset < chunk.byteLength;
offset += input.hardMaxChunkBytes
) {
if (
input.externalSignal.aborted ||
consumerSignal.aborted
) {
yield fail("ABORTED");
return;
}
if (input.scope.timedOut()) {
yield fail("UNAVAILABLE", {
retryable: true,
recovery: "REISSUE_CAPABILITY",
});
return;
}
const owned = chunk
.subarray(
offset,
Math.min(
chunk.byteLength,
offset + input.hardMaxChunkBytes,
),
)
.slice();
try {
verifier.update(owned);
} catch {
yield fail("INTEGRITY_FAILED");
return;
}
transferred += owned.byteLength;
yield browserDataSuccess(owned);
}
}
let verified = false;
try {
verified = verifier.verify();
} catch {
verified = false;
}
if (
transferred !== input.capability.byteLength ||
!verified
) {
yield fail("INTEGRITY_FAILED");
return;
}
completed = true;
} catch {
if (
input.externalSignal.aborted ||
consumerSignal.aborted
) {
yield fail("ABORTED");
} else if (input.scope.timedOut()) {
yield fail("UNAVAILABLE", {
retryable: true,
recovery: "REISSUE_CAPABILITY",
});
} else {
yield fail("NOT_READABLE", {
retryable: true,
recovery: "REISSUE_CAPABILITY",
});
}
} finally {
combined?.release();
if (!completed) {
if (reader) cancelReader(reader);
else cancelBody(input.response);
}
try {
reader?.releaseLock();
} catch {
// Reader cleanup cannot change stream success or failure.
}
input.scope.release();
observeBrowserData(input.observer, {
operation: "DOWNLOAD",
outcome: completed
? "SUCCEEDED"
: terminalFailureCode
? "FAILED"
: "DEGRADED",
...(terminalFailureCode
? { failureCode: terminalFailureCode }
: {}),
byteBucket: byteBucket(transferred),
});
}
},
});
}
function validateDownloadResponse(
response: Response,
binding: PresignedCapabilityBinding,
): BrowserDataResult<true> {
if (
response.redirected ||
response.type === "opaqueredirect" ||
!responseUrlMatches(
response,
binding.href,
)
) {
return browserDataFailure("POLICY_REJECTED", "PRESIGNED_TRANSFER");
}
if (response.status !== binding.expectedStatus) {
return statusFailure(response.status);
}
if (!validateRequiredHeaders(response, binding)) {
return browserDataFailure(
"INTEGRITY_FAILED",
"PRESIGNED_TRANSFER",
);
}
const mediaType = response.headers
.get("content-type")
?.trim()
.toLowerCase();
const declaredLength = response.headers.get("content-length");
const digestHeader = binding.digestResponseHeader;
const contentEncoding = response.headers.get("content-encoding");
if (
mediaType !== binding.mediaType ||
declaredLength === null ||
Number(declaredLength) !== binding.byteLength ||
digestHeader === null ||
response.headers.get(digestHeader)?.trim().toLowerCase() !==
binding.expectedSha256 ||
(contentEncoding !== null &&
contentEncoding.trim().toLowerCase() !== "identity") ||
response.headers.has("content-range") ||
(binding.byteLength > 0 && !response.body)
) {
return browserDataFailure(
"INTEGRITY_FAILED",
"PRESIGNED_TRANSFER",
);
}
return browserDataSuccess(true);
}
function validateUploadResponse(
response: Response,
binding: PresignedCapabilityBinding,
hardMaxUploadResponseBytes: number,
): BrowserDataResult<
Readonly<{
receiptToken: string;
responseByteLength: number;
}>
> {
if (
response.redirected ||
response.type === "opaqueredirect" ||
!responseUrlMatches(
response,
binding.href,
)
) {
return browserDataFailure("POLICY_REJECTED", "PRESIGNED_TRANSFER");
}
if (response.status !== binding.expectedStatus) {
return statusFailure(response.status);
}
if (!validateRequiredHeaders(response, binding)) {
return browserDataFailure(
"INTEGRITY_FAILED",
"PRESIGNED_TRANSFER",
);
}
const headerName = binding.receiptResponseHeader;
const expectedResponseByteLength =
binding.expectedResponseByteLength;
const declaredLength = normalizedContentLength(
response.headers.get("content-length"),
response.status === 204 && expectedResponseByteLength === 0,
);
if (
!headerName ||
expectedResponseByteLength === null ||
expectedResponseByteLength > hardMaxUploadResponseBytes ||
declaredLength === null ||
declaredLength !== expectedResponseByteLength ||
(expectedResponseByteLength > 0 && !response.body)
) {
return browserDataFailure("POLICY_REJECTED", "PRESIGNED_TRANSFER");
}
const receipt = normalizeReceiptToken(
response.headers.get(headerName),
);
return receipt
? browserDataSuccess(
Object.freeze({
receiptToken: receipt,
responseByteLength: expectedResponseByteLength,
}),
)
: browserDataFailure("INTEGRITY_FAILED", "PRESIGNED_TRANSFER");
}
async function drainUploadResponse(
response: Response,
expectedByteLength: number,
hardMaxBytes: number,
signal: AbortSignal,
): Promise<BrowserDataResult<true>> {
if (!response.body) {
return expectedByteLength === 0
? browserDataSuccess(true)
: browserDataFailure(
"INTEGRITY_FAILED",
"PRESIGNED_TRANSFER",
);
}
const reader = response.body.getReader();
let completed = false;
let total = 0;
try {
while (true) {
const result = await readWithSignal(reader, signal);
if (result.done) break;
if (!(result.value instanceof Uint8Array)) {
return browserDataFailure(
"CORRUPT_DATA",
"PRESIGNED_TRANSFER",
);
}
total += result.value.byteLength;
if (!Number.isSafeInteger(total) || total > hardMaxBytes) {
return browserDataFailure(
"LIMIT_EXCEEDED",
"PRESIGNED_TRANSFER",
);
}
if (total > expectedByteLength) {
return browserDataFailure(
"INTEGRITY_FAILED",
"PRESIGNED_TRANSFER",
);
}
}
if (total !== expectedByteLength) {
return browserDataFailure(
"INTEGRITY_FAILED",
"PRESIGNED_TRANSFER",
);
}
completed = true;
return browserDataSuccess(true);
} finally {
if (!completed) cancelReader(reader);
try {
reader.releaseLock();
} catch {
// Reader cleanup cannot alter the already classified result.
}
}
}
function validateRequiredHeaders(
response: Response,
binding: PresignedCapabilityBinding,
): boolean {
return binding.requiredResponseHeaders.every(
(header) => response.headers.get(header.name) === header.value,
);
}
function normalizedContentLength(
value: string | null,
allowImplicitZero: boolean,
): number | null {
if (value === null) return allowImplicitZero ? 0 : null;
const trimmed = value.trim();
if (!/^(?:0|[1-9][0-9]{0,15})$/.test(trimmed)) return null;
const length = Number(trimmed);
return Number.isSafeInteger(length) ? length : null;
}
function headersFor(binding: PresignedCapabilityBinding): Headers {
const headers = new Headers();
for (const header of binding.requestHeaders) {
headers.set(header.name, header.value);
}
return headers;
}
function validCommonBinding(
binding: PresignedCapabilityBinding,
capability: PresignedTransferCapability,
hardMaxTransferBytes: number,
): boolean {
return (
binding.capability === capability &&
binding.capabilityReceipt === capability.capabilityReceipt &&
binding.method === capability.method &&
binding.binding === capability.binding &&
binding.mediaType === capability.mediaType &&
binding.byteLength === capability.byteLength &&
binding.maxBytes === capability.maxBytes &&
binding.expectedSha256 === capability.expectedSha256 &&
binding.expiresAtEpochMs === capability.expiresAtEpochMs &&
capability.byteLength >= 0 &&
capability.byteLength <= capability.maxBytes &&
capability.maxBytes <= hardMaxTransferBytes &&
SHA256.test(capability.expectedSha256) &&
Object.isFrozen(capability) &&
Object.isFrozen(capability.binding)
);
}
function validateExpiry(
capability: PresignedTransferCapability,
minimumRemainingLifetimeMs: number,
nowEpochMs: number,
): BrowserDataResult<true> {
const remainingLifetimeMs =
capability.expiresAtEpochMs - nowEpochMs;
if (
!Number.isSafeInteger(nowEpochMs) ||
nowEpochMs < 0 ||
!Number.isSafeInteger(remainingLifetimeMs)
) {
return browserDataFailure("POLICY_REJECTED", "PRESIGNED_TRANSFER");
}
return remainingLifetimeMs >= minimumRemainingLifetimeMs
? browserDataSuccess(true)
: browserDataFailure(
"EXPIRED_RESOURCE",
"PRESIGNED_TRANSFER",
{ recovery: "REISSUE_CAPABILITY" },
);
}
function statusFailure(
status: number,
): BrowserDataResult<never> {
let code: BrowserDataFailureCode = "UNAVAILABLE";
let recovery: BrowserDataRecovery = "REISSUE_CAPABILITY";
let retryable = status === 429 || status >= 500;
if (status === 401 || status === 403) {
code = "PERMISSION_DENIED";
recovery = "REISSUE_CAPABILITY";
retryable = false;
} else if (status === 404) {
code = "NOT_FOUND";
recovery = "NONE";
retryable = false;
} else if (status === 409) {
code = "CONFLICT";
retryable = false;
} else if (status === 410) {
code = "EXPIRED_RESOURCE";
retryable = false;
} else if (status === 413) {
code = "LIMIT_EXCEEDED";
recovery = "NONE";
retryable = false;
} else if (status >= 400 && status < 500 && status !== 429) {
code = "POLICY_REJECTED";
recovery = "NONE";
retryable = false;
}
return browserDataFailure(code, "PRESIGNED_TRANSFER", {
retryable,
recovery,
});
}
function transferFailure(
externalSignal: AbortSignal,
timedOut: boolean,
): BrowserDataResult<never> {
if (externalSignal.aborted) {
return browserDataFailure("ABORTED", "PRESIGNED_TRANSFER");
}
return browserDataFailure(
timedOut ? "UNAVAILABLE" : "NOT_READABLE",
"PRESIGNED_TRANSFER",
{
retryable: true,
recovery: "REISSUE_CAPABILITY",
},
);
}
function createAbortScope(
external: AbortSignal,
timeoutMs: number,
scheduler: Scheduler,
) {
const controller = new AbortController();
let timedOut = false;
let released = false;
const onAbort = () => controller.abort(external.reason);
const releaseListener = () => {
external.removeEventListener("abort", onAbort);
};
external.addEventListener("abort", onAbort, { once: true });
if (external.aborted) onAbort();
const timer = scheduler.setTimeout(() => {
timedOut = true;
controller.abort("timeout");
releaseListener();
}, timeoutMs);
return Object.freeze({
signal: controller.signal,
timedOut: () => timedOut,
abort(reason?: unknown) {
controller.abort(reason);
},
release() {
if (released) return;
released = true;
scheduler.clearTimeout(timer);
releaseListener();
},
});
}
function combineConsumerAbort(
scope: ReturnType<typeof createAbortScope>,
consumer: AbortSignal,
) {
const onAbort = () => scope.abort(consumer.reason);
consumer.addEventListener("abort", onAbort, { once: true });
if (consumer.aborted) onAbort();
return Object.freeze({
release() {
consumer.removeEventListener("abort", onAbort);
},
});
}
function cancelBody(response: Response): void {
try {
void response.body?.cancel().catch(() => {
// Cancellation is best effort after the result is classified.
});
} catch {
// A response already classified as failure/success is not reclassified by
// best-effort body cancellation.
}
}
function cancelReader(
reader: ReadableStreamDefaultReader<Uint8Array>,
): void {
try {
void reader.cancel().catch(() => {
// Cancellation is best effort after the result is classified.
});
} catch {
// Reader cancellation cannot alter the terminal closed result.
}
}
function readWithSignal(
reader: ReadableStreamDefaultReader<Uint8Array>,
signal: AbortSignal,
): Promise<ReadableStreamReadResult<Uint8Array>> {
if (signal.aborted) {
return Promise.reject(new DOMException("Aborted", "AbortError"));
}
return new Promise((resolve, reject) => {
const onAbort = () => {
reject(new DOMException("Aborted", "AbortError"));
};
signal.addEventListener("abort", onAbort, { once: true });
reader.read().then(
(result) => {
signal.removeEventListener("abort", onAbort);
resolve(result);
},
(error: unknown) => {
signal.removeEventListener("abort", onAbort);
reject(error);
},
);
});
}
function normalizeReceiptToken(value: string | null): string | null {
if (!value) return null;
const trimmed = value.trim();
const unquoted =
trimmed.length >= 2 &&
trimmed.startsWith("\"") &&
trimmed.endsWith("\"")
? trimmed.slice(1, -1)
: trimmed;
return RECEIPT_TOKEN.test(unquoted) && !unquoted.includes("://")
? unquoted
: null;
}
function responseUrlMatches(
response: Response,
expectedHref: string,
): boolean {
if (response.url.length === 0) return false;
try {
return new URL(response.url).href === new URL(expectedHref).href;
} catch {
return false;
}
}
function normalizedSha256(value: unknown): string {
if (typeof value !== "string") throw new TypeError("SHA-256 is invalid.");
const normalized = value.toLowerCase();
if (!SHA256.test(normalized)) throw new TypeError("SHA-256 is invalid.");
return normalized;
}
function opaqueId(value: unknown): string {
if (
typeof value !== "string" ||
value.length === 0 ||
value.length > 256 ||
!/^[a-z0-9][a-z0-9._:-]*$/i.test(value)
) {
throw new TypeError("Opaque identifier is invalid.");
}
return value;
}
function positiveSafeInteger(value: unknown): number {
if (!Number.isSafeInteger(value) || (value as number) <= 0) {
throw new TypeError("Expected a positive safe integer.");
}
return value as number;
}
function nonNegativeSafeInteger(value: unknown): number {
if (!Number.isSafeInteger(value) || (value as number) < 0) {
throw new TypeError("Expected a non-negative safe integer.");
}
return value as number;
}
async function digestSha256WithWebCrypto(
bytes: Uint8Array,
): Promise<string> {
if (!(bytes instanceof Uint8Array) || !globalThis.crypto?.subtle) {
throw new TypeError("WebCrypto SHA-256 is unavailable.");
}
const digest = await globalThis.crypto.subtle.digest(
"SHA-256",
bytes.slice(),
);
return Array.from(new Uint8Array(digest), (value) =>
value.toString(16).padStart(2, "0"),
).join("");
}
function isAbortSignal(value: unknown): value is AbortSignal {
try {
if (!value || typeof value !== "object") return false;
const abortedGetter = Object.getOwnPropertyDescriptor(
AbortSignal.prototype,
"aborted",
)?.get;
return Boolean(
abortedGetter &&
typeof abortedGetter.call(value) === "boolean" &&
typeof (value as AbortSignal).addEventListener === "function" &&
typeof (value as AbortSignal).removeEventListener === "function",
);
} catch {
return false;
}
}
function safeInputByteLength(value: unknown): number | undefined {
try {
if (!value || typeof value !== "object") return undefined;
const input = value as Readonly<{
byteLength?: unknown;
bytes?: unknown;
capability?: unknown;
}>;
if (
Number.isSafeInteger(input.byteLength) &&
(input.byteLength as number) >= 0
) {
return input.byteLength as number;
}
if (input.bytes instanceof Uint8Array) {
return input.bytes.byteLength;
}
if (
input.capability &&
typeof input.capability === "object" &&
Number.isSafeInteger(
(input.capability as Readonly<{ byteLength?: unknown }>)
.byteLength,
)
) {
const byteLength = (
input.capability as Readonly<{ byteLength: number }>
).byteLength;
return byteLength >= 0 ? byteLength : undefined;
}
} catch {
return undefined;
}
return undefined;
}
function observeTransferResult(
observer: BrowserDataObserver | undefined,
operation: BrowserDataOperation,
result: BrowserDataResult<unknown>,
byteLength?: number,
): void {
const bucket = byteBucket(byteLength);
observeBrowserData(observer, {
operation,
outcome: result.ok ? "SUCCEEDED" : "FAILED",
...(!result.ok ? { failureCode: result.error.code } : {}),
...(bucket !== undefined ? { byteBucket: bucket } : {}),
});
}
function byteBucket(
byteLength: number | undefined,
):
| "ZERO"
| "LT1MIB"
| "1_TO_9MIB"
| "10_TO_99MIB"
| "GTE100MIB"
| undefined {
if (
byteLength === undefined ||
!Number.isSafeInteger(byteLength) ||
byteLength < 0
) {
return undefined;
}
if (byteLength === 0) return "ZERO";
if (byteLength < 1_048_576) return "LT1MIB";
if (byteLength < 10_485_760) return "1_TO_9MIB";
if (byteLength < 104_857_600) return "10_TO_99MIB";
return "GTE100MIB";
}