@ttt-productions/media-processing-core 0.8.11 → 0.8.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,8 +9,8 @@ export declare const R2_RETRY_MAX_BACKOFF_MS = 5000;
9
9
  export declare const R2_RETRY_AFTER_CAP_MS = 30000;
10
10
  /** HTTP statuses treated as transient and safe to retry. */
11
11
  export declare const R2_RETRYABLE_STATUSES: ReadonlySet<number>;
12
- export type R2RetryableOperation = "putFile" | "copy" | "readToFile";
13
- /** Every R2 store operation an `R2StorageError` can name; `delete` is one-shot. */
12
+ export type R2RetryableOperation = "putFile" | "copy" | "readToFile" | "deleteCopy";
13
+ /** Every R2 store operation an `R2StorageError` can name; `delete` never retries. */
14
14
  export type R2Operation = R2RetryableOperation | "delete";
15
15
  /** Deterministic seams for tests; production defaults are internal (see below). */
16
16
  export interface R2RetrySeams {
@@ -1 +1 @@
1
- {"version":3,"file":"r2-retry.d.ts","sourceRoot":"","sources":["../../src/server/r2-retry.ts"],"names":[],"mappings":"AAgBA,kFAAkF;AAClF,eAAO,MAAM,uBAAuB,IAAI,CAAC;AACzC,sDAAsD;AACtD,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAC1C,0DAA0D;AAC1D,eAAO,MAAM,uBAAuB,OAAQ,CAAC;AAC7C;qCACqC;AACrC,eAAO,MAAM,qBAAqB,QAAS,CAAC;AAE5C,4DAA4D;AAC5D,eAAO,MAAM,qBAAqB,EAAE,WAAW,CAAC,MAAM,CAA2C,CAAC;AAElG,MAAM,MAAM,oBAAoB,GAAG,SAAS,GAAG,MAAM,GAAG,YAAY,CAAC;AAErE,mFAAmF;AACnF,MAAM,MAAM,WAAW,GAAG,oBAAoB,GAAG,QAAQ,CAAC;AAE1D,mFAAmF;AACnF,MAAM,WAAW,YAAY;IAC3B,uCAAuC;IACvC,KAAK,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,gCAAgC;IAChC,MAAM,EAAE,MAAM,MAAM,CAAC;IACrB,oFAAoF;IACpF,GAAG,EAAE,MAAM,MAAM,CAAC;CACnB;AAED,gCAAgC;AAChC,wBAAgB,mBAAmB,IAAI,YAAY,CAMlD;AAsDD;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAwB/D;AAMD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAiBtG;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE;IACvC,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC1B,0EAA0E;IAC1E,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,YAAY,CAAC;CACrB,GAAG,MAAM,CAYT;AAMD;;;;;;;;;;;GAWG;AACH,qBAAa,cAAe,SAAQ,KAAK;IACvC,QAAQ,CAAC,SAAS,EAAE,WAAW,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,oGAAoG;IACpG,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;gBAEnB,MAAM,EAAE;QAClB,SAAS,EAAE,WAAW,CAAC;QACvB,MAAM,EAAE,MAAM,CAAC;QACf,GAAG,EAAE,MAAM,CAAC;QACZ,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,KAAK,CAAC,EAAE,OAAO,CAAC;KACjB;CAgBF;AAWD,MAAM,MAAM,eAAe,CAAC,CAAC,IACzB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,GAC7B;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC;AAyB7C;;;;;GAKG;AACH,wBAAsB,gBAAgB,CAAC,CAAC,EAAE,MAAM,EAAE;IAChD,SAAS,EAAE,oBAAoB,CAAC;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,YAAY,CAAC;IACpB,UAAU,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC;CACpE,GAAG,OAAO,CAAC,CAAC,CAAC,CAoCb"}
1
+ {"version":3,"file":"r2-retry.d.ts","sourceRoot":"","sources":["../../src/server/r2-retry.ts"],"names":[],"mappings":"AAiBA,kFAAkF;AAClF,eAAO,MAAM,uBAAuB,IAAI,CAAC;AACzC,sDAAsD;AACtD,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAC1C,0DAA0D;AAC1D,eAAO,MAAM,uBAAuB,OAAQ,CAAC;AAC7C;qCACqC;AACrC,eAAO,MAAM,qBAAqB,QAAS,CAAC;AAE5C,4DAA4D;AAC5D,eAAO,MAAM,qBAAqB,EAAE,WAAW,CAAC,MAAM,CAA2C,CAAC;AAElG,MAAM,MAAM,oBAAoB,GAAG,SAAS,GAAG,MAAM,GAAG,YAAY,GAAG,YAAY,CAAC;AAEpF,qFAAqF;AACrF,MAAM,MAAM,WAAW,GAAG,oBAAoB,GAAG,QAAQ,CAAC;AAE1D,mFAAmF;AACnF,MAAM,WAAW,YAAY;IAC3B,uCAAuC;IACvC,KAAK,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,gCAAgC;IAChC,MAAM,EAAE,MAAM,MAAM,CAAC;IACrB,oFAAoF;IACpF,GAAG,EAAE,MAAM,MAAM,CAAC;CACnB;AAED,gCAAgC;AAChC,wBAAgB,mBAAmB,IAAI,YAAY,CAMlD;AAsDD;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAwB/D;AAMD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAiBtG;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE;IACvC,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC1B,0EAA0E;IAC1E,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,YAAY,CAAC;CACrB,GAAG,MAAM,CAYT;AAMD;;;;;;;;;;;GAWG;AACH,qBAAa,cAAe,SAAQ,KAAK;IACvC,QAAQ,CAAC,SAAS,EAAE,WAAW,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,oGAAoG;IACpG,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;gBAEnB,MAAM,EAAE;QAClB,SAAS,EAAE,WAAW,CAAC;QACvB,MAAM,EAAE,MAAM,CAAC;QACf,GAAG,EAAE,MAAM,CAAC;QACZ,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,KAAK,CAAC,EAAE,OAAO,CAAC;KACjB;CAgBF;AAWD,MAAM,MAAM,eAAe,CAAC,CAAC,IACzB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,GAC7B;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC;AAyB7C;;;;;GAKG;AACH,wBAAsB,gBAAgB,CAAC,CAAC,EAAE,MAAM,EAAE;IAChD,SAAS,EAAE,oBAAoB,CAAC;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,YAAY,CAAC;IACpB,UAAU,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC;CACpE,GAAG,OAAO,CAAC,CAAC,CAAC,CAoCb"}
@@ -5,10 +5,11 @@
5
5
  // only public surface is the `R2StorageError` type, which storage-ops re-exports
6
6
  // so callers can recognize an exhausted storage operation.
7
7
  //
8
- // The policy applies to the idempotent-against-a-deterministic-key operations
9
- // only (putFile, copy, readToFile). `delete` is intentionally one-shot and never
10
- // routes through here. Signing/config failures, local filesystem errors, and
11
- // deterministic HTTP 4xx responses are never retried.
8
+ // The policy applies to the idempotent-against-a-deterministic-key requests
9
+ // only (putFile, copy, readToFile, and deleteCopy's read of the destination).
10
+ // Every DELETE request is intentionally one-shot and never routes through here.
11
+ // Signing/config failures, local filesystem errors, and deterministic HTTP 4xx
12
+ // responses are never retried.
12
13
  // ---------------------------------------------------------------------------
13
14
  // Fixed internal policy — never exposed as tunables on the public factory.
14
15
  // ---------------------------------------------------------------------------
@@ -1 +1 @@
1
- {"version":3,"file":"r2-retry.js","sourceRoot":"","sources":["../../src/server/r2-retry.ts"],"names":[],"mappings":"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","sourcesContent":["// Bounded transient-retry policy for the R2 object store.\n//\n// PRIVATE sibling of storage-ops.ts — imported ONLY by that module. It is\n// deliberately NOT re-exported from the server barrel (`server/index.ts`); the\n// only public surface is the `R2StorageError` type, which storage-ops re-exports\n// so callers can recognize an exhausted storage operation.\n//\n// The policy applies to the idempotent-against-a-deterministic-key operations\n// only (putFile, copy, readToFile). `delete` is intentionally one-shot and never\n// routes through here. Signing/config failures, local filesystem errors, and\n// deterministic HTTP 4xx responses are never retried.\n\n// ---------------------------------------------------------------------------\n// Fixed internal policy — never exposed as tunables on the public factory.\n// ---------------------------------------------------------------------------\n\n/** TOTAL attempts (initial + retries) — three totals means at most TWO delays. */\nexport const R2_REQUEST_MAX_ATTEMPTS = 3;\n/** Base delay for full-jitter exponential backoff. */\nexport const R2_RETRY_BASE_DELAY_MS = 250;\n/** Ceiling on the exponential backoff (before jitter). */\nexport const R2_RETRY_MAX_BACKOFF_MS = 5_000;\n/** Hard cap on an honored server `Retry-After` so one response can't eat the\n * caller's whole timeout budget. */\nexport const R2_RETRY_AFTER_CAP_MS = 30_000;\n\n/** HTTP statuses treated as transient and safe to retry. */\nexport const R2_RETRYABLE_STATUSES: ReadonlySet<number> = new Set([408, 429, 500, 502, 503, 504]);\n\nexport type R2RetryableOperation = \"putFile\" | \"copy\" | \"readToFile\";\n\n/** Every R2 store operation an `R2StorageError` can name; `delete` is one-shot. */\nexport type R2Operation = R2RetryableOperation | \"delete\";\n\n/** Deterministic seams for tests; production defaults are internal (see below). */\nexport interface R2RetrySeams {\n /** Resolve after `ms` milliseconds. */\n sleep: (ms: number) => Promise<void>;\n /** Uniform random in [0, 1). */\n random: () => number;\n /** Epoch-millisecond clock — keeps HTTP-date `Retry-After` tests off wall-clock. */\n now: () => number;\n}\n\n/** Production seam defaults. */\nexport function defaultR2RetrySeams(): R2RetrySeams {\n return {\n sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),\n random: Math.random,\n now: Date.now,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Transient-error classification\n// ---------------------------------------------------------------------------\n\n// Recognized transient transport failure codes (undici/fetch surface these on\n// the error or its `.cause`). A broken connection to R2 lands here.\nconst RETRYABLE_NETWORK_ERROR_CODES: ReadonlySet<string> = new Set([\n \"ECONNRESET\",\n \"ECONNREFUSED\",\n \"ECONNABORTED\",\n \"ETIMEDOUT\",\n \"EPIPE\",\n \"ENOTFOUND\",\n \"EAI_AGAIN\",\n \"ENETUNREACH\",\n \"ENETDOWN\",\n \"EHOSTUNREACH\",\n \"EHOSTDOWN\",\n \"UND_ERR_CONNECT_TIMEOUT\",\n \"UND_ERR_HEADERS_TIMEOUT\",\n \"UND_ERR_BODY_TIMEOUT\",\n \"UND_ERR_SOCKET\",\n]);\n\n// Deterministic local filesystem errors are NEVER retried (disk full, perms,\n// missing dir, etc.). This guard wins over any transport classification so a\n// local write failure during readToFile piping can't be mistaken for transient.\nconst LOCAL_FS_ERROR_CODES: ReadonlySet<string> = new Set([\n \"ENOSPC\",\n \"EACCES\",\n \"EISDIR\",\n \"ENOENT\",\n \"EROFS\",\n \"EMFILE\",\n \"ENFILE\",\n \"EBADF\",\n \"EEXIST\",\n \"EPERM\",\n \"ENAMETOOLONG\",\n \"ELOOP\",\n \"ENOTDIR\",\n \"EDQUOT\",\n]);\n\nfunction codeOf(err: unknown): string | undefined {\n if (err && typeof err === \"object\") {\n const c = (err as { code?: unknown }).code;\n if (typeof c === \"string\") return c;\n }\n return undefined;\n}\n\n/**\n * True only for recognized transient transport failures. An explicit\n * `AbortError` and deterministic local-filesystem errors always return false —\n * an abort must never become another attempt.\n */\nexport function isRetryableTransportError(err: unknown): boolean {\n if (!err || typeof err !== \"object\") return false;\n const e = err as { name?: unknown; cause?: unknown };\n\n // NEVER convert an explicit abort into another attempt.\n if (e.name === \"AbortError\") return false;\n\n const selfCode = codeOf(err);\n const causeCode = codeOf(e.cause);\n\n // Deterministic local filesystem errors are not transient — decided first so a\n // local write error can never be reclassified as a transport failure.\n if (selfCode && LOCAL_FS_ERROR_CODES.has(selfCode)) return false;\n if (causeCode && LOCAL_FS_ERROR_CODES.has(causeCode)) return false;\n\n // Recognized transient network codes on the error or its cause.\n if (selfCode && RETRYABLE_NETWORK_ERROR_CODES.has(selfCode)) return true;\n if (causeCode && RETRYABLE_NETWORK_ERROR_CODES.has(causeCode)) return true;\n\n // fetch()/undici surface transport failures as `TypeError: fetch failed`\n // (often with an unrecognized or absent cause) — treat those as transient.\n if (err instanceof TypeError || e.name === \"TypeError\") return true;\n\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// Retry-After parsing + delay computation\n// ---------------------------------------------------------------------------\n\n/**\n * Parse a `Retry-After` header value into a non-negative millisecond delay.\n * Supports both the delta-seconds form (`\"120\"`) and the HTTP-date form\n * (`\"Wed, 21 Oct 2015 07:28:00 GMT\"`). Returns null when absent/unparseable.\n * An HTTP-date in the past clamps to 0.\n */\nexport function parseRetryAfterMs(headerValue: string | null | undefined, nowMs: number): number | null {\n if (headerValue == null) return null;\n const trimmed = headerValue.trim();\n if (trimmed === \"\") return null;\n\n // delta-seconds form\n if (/^\\d+$/.test(trimmed)) {\n const seconds = Number(trimmed);\n if (!Number.isFinite(seconds) || seconds < 0) return null;\n return seconds * 1000;\n }\n\n // HTTP-date form\n const dateMs = Date.parse(trimmed);\n if (Number.isNaN(dateMs)) return null;\n const delta = dateMs - nowMs;\n return delta > 0 ? delta : 0;\n}\n\n/**\n * Compute the pre-next-attempt delay. If the failed response carries a valid\n * `Retry-After`, honor it (capped, no backoff added on top). Otherwise use\n * full-jitter exponential backoff: `random(0, min(maxBackoff, base * 2^retryIndex))`.\n */\nexport function computeR2DelayMs(params: {\n response: Response | null;\n /** 0-based index of the retry about to happen (0 for the first retry). */\n retryIndex: number;\n seams: R2RetrySeams;\n}): number {\n const { response, retryIndex, seams } = params;\n\n if (response) {\n const retryAfterMs = parseRetryAfterMs(response.headers.get(\"retry-after\"), seams.now());\n if (retryAfterMs !== null) {\n return Math.min(retryAfterMs, R2_RETRY_AFTER_CAP_MS);\n }\n }\n\n const ceiling = Math.min(R2_RETRY_MAX_BACKOFF_MS, R2_RETRY_BASE_DELAY_MS * 2 ** retryIndex);\n return Math.floor(seams.random() * ceiling);\n}\n\n// ---------------------------------------------------------------------------\n// Exhaustion error\n// ---------------------------------------------------------------------------\n\n/**\n * Thrown when an R2 operation fails: a retried operation exhausts its attempts or\n * hits a non-retryable response, or a one-shot delete fails. Carries\n * operation/bucket/key/attempt/status context, the S3 error code parsed from the\n * response body, and the bounded body itself as properties — never credentials,\n * authorization headers, or full signed URLs. Callers classify it from those\n * properties, never from the message.\n *\n * The message names only the operation, the attempts, the status, and the S3 error\n * code. The bucket, the key, and the raw body — which can echo either — stay out of\n * it, so an escaping error never carries an object key to monitoring.\n */\nexport class R2StorageError extends Error {\n readonly operation: R2Operation;\n readonly bucket: string;\n readonly key: string;\n readonly attempts: number;\n readonly status?: number;\n /** The S3 `<Code>` of the response body (`NoSuchKey`, `PreconditionFailed`, …), when it had one. */\n readonly s3ErrorCode?: string;\n readonly responseText?: string;\n\n constructor(params: {\n operation: R2Operation;\n bucket: string;\n key: string;\n attempts: number;\n status?: number;\n responseText?: string;\n cause?: unknown;\n }) {\n const code = parseS3ErrorCode(params.responseText);\n const detail = [params.status !== undefined ? `status ${params.status}` : undefined, code].filter(Boolean).join(\" \");\n super(\n `R2 ${params.operation} failed after ${params.attempts} attempt(s)${detail ? `: ${detail}` : \"\"}`,\n params.cause !== undefined ? { cause: params.cause } : undefined,\n );\n this.name = \"R2StorageError\";\n this.operation = params.operation;\n this.bucket = params.bucket;\n this.key = params.key;\n this.attempts = params.attempts;\n this.status = params.status;\n this.s3ErrorCode = code;\n this.responseText = params.responseText;\n }\n}\n\n/** The `<Code>` of an S3 XML error body; only a plain alphanumeric token is taken. */\nfunction parseS3ErrorCode(responseText: string | undefined): string | undefined {\n return responseText ? /<Code>([A-Za-z0-9]{1,64})<\\/Code>/.exec(responseText)?.[1] : undefined;\n}\n\n// ---------------------------------------------------------------------------\n// Retry driver\n// ---------------------------------------------------------------------------\n\nexport type R2AttemptResult<T> =\n | { kind: \"success\"; value: T }\n | { kind: \"response\"; response: Response };\n\n// Strip ASCII control characters (0x00-0x1F, 0x7F) and bound length. Done with a\n// code-point filter rather than a control-character regex (which `no-control-regex`\n// forbids and which would embed literal control bytes in source).\nfunction sanitizeBodyText(text: string): string {\n let out = \"\";\n for (const ch of text.slice(0, 1000)) {\n const code = ch.codePointAt(0) ?? 0;\n out += code < 0x20 || code === 0x7f ? \" \" : ch;\n }\n return out.trim().slice(0, 500);\n}\n\n// Bounded, sanitized read of a failed response body. Also drains the body so\n// the underlying socket can be reused before the next attempt. R2 error bodies\n// are short XML and never contain our credentials; sanitize defensively regardless.\nasync function safeReadBoundedBody(res: Response): Promise<string> {\n try {\n return sanitizeBodyText(await res.text());\n } catch {\n return \"<no body>\";\n }\n}\n\n/**\n * Drive up to `R2_REQUEST_MAX_ATTEMPTS` attempts of a single storage operation.\n * `runAttempt` builds a FRESH signed request each call and returns either a\n * success value or the raw `Response` for the driver to classify. Thrown errors\n * are classified: only recognized transient transport failures are retried.\n */\nexport async function fetchWithR2Retry<T>(params: {\n operation: R2RetryableOperation;\n bucket: string;\n key: string;\n seams: R2RetrySeams;\n runAttempt: (attemptNumber: number) => Promise<R2AttemptResult<T>>;\n}): Promise<T> {\n const { operation, bucket, key, seams, runAttempt } = params;\n\n for (let attempt = 1; attempt <= R2_REQUEST_MAX_ATTEMPTS; attempt++) {\n let result: R2AttemptResult<T>;\n try {\n result = await runAttempt(attempt);\n } catch (err) {\n // Signing failures, explicit aborts, and deterministic local-fs errors are\n // never retried; only recognized transient transport failures are.\n if (!isRetryableTransportError(err)) throw err;\n if (attempt >= R2_REQUEST_MAX_ATTEMPTS) {\n throw new R2StorageError({ operation, bucket, key, attempts: attempt, cause: err });\n }\n await seams.sleep(computeR2DelayMs({ response: null, retryIndex: attempt - 1, seams }));\n continue;\n }\n\n if (result.kind === \"success\") return result.value;\n\n const response = result.response;\n const status = response.status;\n // Consume the failed body before the next attempt.\n const responseText = await safeReadBoundedBody(response);\n\n if (R2_RETRYABLE_STATUSES.has(status) && attempt < R2_REQUEST_MAX_ATTEMPTS) {\n await seams.sleep(computeR2DelayMs({ response, retryIndex: attempt - 1, seams }));\n continue;\n }\n\n // Non-retryable status, or a retryable status with attempts exhausted.\n throw new R2StorageError({ operation, bucket, key, attempts: attempt, status, responseText });\n }\n\n // Unreachable: the loop always returns or throws.\n throw new R2StorageError({ operation, bucket, key, attempts: R2_REQUEST_MAX_ATTEMPTS });\n}\n"]}
1
+ {"version":3,"file":"r2-retry.js","sourceRoot":"","sources":["../../src/server/r2-retry.ts"],"names":[],"mappings":"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","sourcesContent":["// Bounded transient-retry policy for the R2 object store.\n//\n// PRIVATE sibling of storage-ops.ts — imported ONLY by that module. It is\n// deliberately NOT re-exported from the server barrel (`server/index.ts`); the\n// only public surface is the `R2StorageError` type, which storage-ops re-exports\n// so callers can recognize an exhausted storage operation.\n//\n// The policy applies to the idempotent-against-a-deterministic-key requests\n// only (putFile, copy, readToFile, and deleteCopy's read of the destination).\n// Every DELETE request is intentionally one-shot and never routes through here.\n// Signing/config failures, local filesystem errors, and deterministic HTTP 4xx\n// responses are never retried.\n\n// ---------------------------------------------------------------------------\n// Fixed internal policy — never exposed as tunables on the public factory.\n// ---------------------------------------------------------------------------\n\n/** TOTAL attempts (initial + retries) — three totals means at most TWO delays. */\nexport const R2_REQUEST_MAX_ATTEMPTS = 3;\n/** Base delay for full-jitter exponential backoff. */\nexport const R2_RETRY_BASE_DELAY_MS = 250;\n/** Ceiling on the exponential backoff (before jitter). */\nexport const R2_RETRY_MAX_BACKOFF_MS = 5_000;\n/** Hard cap on an honored server `Retry-After` so one response can't eat the\n * caller's whole timeout budget. */\nexport const R2_RETRY_AFTER_CAP_MS = 30_000;\n\n/** HTTP statuses treated as transient and safe to retry. */\nexport const R2_RETRYABLE_STATUSES: ReadonlySet<number> = new Set([408, 429, 500, 502, 503, 504]);\n\nexport type R2RetryableOperation = \"putFile\" | \"copy\" | \"readToFile\" | \"deleteCopy\";\n\n/** Every R2 store operation an `R2StorageError` can name; `delete` never retries. */\nexport type R2Operation = R2RetryableOperation | \"delete\";\n\n/** Deterministic seams for tests; production defaults are internal (see below). */\nexport interface R2RetrySeams {\n /** Resolve after `ms` milliseconds. */\n sleep: (ms: number) => Promise<void>;\n /** Uniform random in [0, 1). */\n random: () => number;\n /** Epoch-millisecond clock — keeps HTTP-date `Retry-After` tests off wall-clock. */\n now: () => number;\n}\n\n/** Production seam defaults. */\nexport function defaultR2RetrySeams(): R2RetrySeams {\n return {\n sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),\n random: Math.random,\n now: Date.now,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Transient-error classification\n// ---------------------------------------------------------------------------\n\n// Recognized transient transport failure codes (undici/fetch surface these on\n// the error or its `.cause`). A broken connection to R2 lands here.\nconst RETRYABLE_NETWORK_ERROR_CODES: ReadonlySet<string> = new Set([\n \"ECONNRESET\",\n \"ECONNREFUSED\",\n \"ECONNABORTED\",\n \"ETIMEDOUT\",\n \"EPIPE\",\n \"ENOTFOUND\",\n \"EAI_AGAIN\",\n \"ENETUNREACH\",\n \"ENETDOWN\",\n \"EHOSTUNREACH\",\n \"EHOSTDOWN\",\n \"UND_ERR_CONNECT_TIMEOUT\",\n \"UND_ERR_HEADERS_TIMEOUT\",\n \"UND_ERR_BODY_TIMEOUT\",\n \"UND_ERR_SOCKET\",\n]);\n\n// Deterministic local filesystem errors are NEVER retried (disk full, perms,\n// missing dir, etc.). This guard wins over any transport classification so a\n// local write failure during readToFile piping can't be mistaken for transient.\nconst LOCAL_FS_ERROR_CODES: ReadonlySet<string> = new Set([\n \"ENOSPC\",\n \"EACCES\",\n \"EISDIR\",\n \"ENOENT\",\n \"EROFS\",\n \"EMFILE\",\n \"ENFILE\",\n \"EBADF\",\n \"EEXIST\",\n \"EPERM\",\n \"ENAMETOOLONG\",\n \"ELOOP\",\n \"ENOTDIR\",\n \"EDQUOT\",\n]);\n\nfunction codeOf(err: unknown): string | undefined {\n if (err && typeof err === \"object\") {\n const c = (err as { code?: unknown }).code;\n if (typeof c === \"string\") return c;\n }\n return undefined;\n}\n\n/**\n * True only for recognized transient transport failures. An explicit\n * `AbortError` and deterministic local-filesystem errors always return false —\n * an abort must never become another attempt.\n */\nexport function isRetryableTransportError(err: unknown): boolean {\n if (!err || typeof err !== \"object\") return false;\n const e = err as { name?: unknown; cause?: unknown };\n\n // NEVER convert an explicit abort into another attempt.\n if (e.name === \"AbortError\") return false;\n\n const selfCode = codeOf(err);\n const causeCode = codeOf(e.cause);\n\n // Deterministic local filesystem errors are not transient — decided first so a\n // local write error can never be reclassified as a transport failure.\n if (selfCode && LOCAL_FS_ERROR_CODES.has(selfCode)) return false;\n if (causeCode && LOCAL_FS_ERROR_CODES.has(causeCode)) return false;\n\n // Recognized transient network codes on the error or its cause.\n if (selfCode && RETRYABLE_NETWORK_ERROR_CODES.has(selfCode)) return true;\n if (causeCode && RETRYABLE_NETWORK_ERROR_CODES.has(causeCode)) return true;\n\n // fetch()/undici surface transport failures as `TypeError: fetch failed`\n // (often with an unrecognized or absent cause) — treat those as transient.\n if (err instanceof TypeError || e.name === \"TypeError\") return true;\n\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// Retry-After parsing + delay computation\n// ---------------------------------------------------------------------------\n\n/**\n * Parse a `Retry-After` header value into a non-negative millisecond delay.\n * Supports both the delta-seconds form (`\"120\"`) and the HTTP-date form\n * (`\"Wed, 21 Oct 2015 07:28:00 GMT\"`). Returns null when absent/unparseable.\n * An HTTP-date in the past clamps to 0.\n */\nexport function parseRetryAfterMs(headerValue: string | null | undefined, nowMs: number): number | null {\n if (headerValue == null) return null;\n const trimmed = headerValue.trim();\n if (trimmed === \"\") return null;\n\n // delta-seconds form\n if (/^\\d+$/.test(trimmed)) {\n const seconds = Number(trimmed);\n if (!Number.isFinite(seconds) || seconds < 0) return null;\n return seconds * 1000;\n }\n\n // HTTP-date form\n const dateMs = Date.parse(trimmed);\n if (Number.isNaN(dateMs)) return null;\n const delta = dateMs - nowMs;\n return delta > 0 ? delta : 0;\n}\n\n/**\n * Compute the pre-next-attempt delay. If the failed response carries a valid\n * `Retry-After`, honor it (capped, no backoff added on top). Otherwise use\n * full-jitter exponential backoff: `random(0, min(maxBackoff, base * 2^retryIndex))`.\n */\nexport function computeR2DelayMs(params: {\n response: Response | null;\n /** 0-based index of the retry about to happen (0 for the first retry). */\n retryIndex: number;\n seams: R2RetrySeams;\n}): number {\n const { response, retryIndex, seams } = params;\n\n if (response) {\n const retryAfterMs = parseRetryAfterMs(response.headers.get(\"retry-after\"), seams.now());\n if (retryAfterMs !== null) {\n return Math.min(retryAfterMs, R2_RETRY_AFTER_CAP_MS);\n }\n }\n\n const ceiling = Math.min(R2_RETRY_MAX_BACKOFF_MS, R2_RETRY_BASE_DELAY_MS * 2 ** retryIndex);\n return Math.floor(seams.random() * ceiling);\n}\n\n// ---------------------------------------------------------------------------\n// Exhaustion error\n// ---------------------------------------------------------------------------\n\n/**\n * Thrown when an R2 operation fails: a retried operation exhausts its attempts or\n * hits a non-retryable response, or a one-shot delete fails. Carries\n * operation/bucket/key/attempt/status context, the S3 error code parsed from the\n * response body, and the bounded body itself as properties — never credentials,\n * authorization headers, or full signed URLs. Callers classify it from those\n * properties, never from the message.\n *\n * The message names only the operation, the attempts, the status, and the S3 error\n * code. The bucket, the key, and the raw body — which can echo either — stay out of\n * it, so an escaping error never carries an object key to monitoring.\n */\nexport class R2StorageError extends Error {\n readonly operation: R2Operation;\n readonly bucket: string;\n readonly key: string;\n readonly attempts: number;\n readonly status?: number;\n /** The S3 `<Code>` of the response body (`NoSuchKey`, `PreconditionFailed`, …), when it had one. */\n readonly s3ErrorCode?: string;\n readonly responseText?: string;\n\n constructor(params: {\n operation: R2Operation;\n bucket: string;\n key: string;\n attempts: number;\n status?: number;\n responseText?: string;\n cause?: unknown;\n }) {\n const code = parseS3ErrorCode(params.responseText);\n const detail = [params.status !== undefined ? `status ${params.status}` : undefined, code].filter(Boolean).join(\" \");\n super(\n `R2 ${params.operation} failed after ${params.attempts} attempt(s)${detail ? `: ${detail}` : \"\"}`,\n params.cause !== undefined ? { cause: params.cause } : undefined,\n );\n this.name = \"R2StorageError\";\n this.operation = params.operation;\n this.bucket = params.bucket;\n this.key = params.key;\n this.attempts = params.attempts;\n this.status = params.status;\n this.s3ErrorCode = code;\n this.responseText = params.responseText;\n }\n}\n\n/** The `<Code>` of an S3 XML error body; only a plain alphanumeric token is taken. */\nfunction parseS3ErrorCode(responseText: string | undefined): string | undefined {\n return responseText ? /<Code>([A-Za-z0-9]{1,64})<\\/Code>/.exec(responseText)?.[1] : undefined;\n}\n\n// ---------------------------------------------------------------------------\n// Retry driver\n// ---------------------------------------------------------------------------\n\nexport type R2AttemptResult<T> =\n | { kind: \"success\"; value: T }\n | { kind: \"response\"; response: Response };\n\n// Strip ASCII control characters (0x00-0x1F, 0x7F) and bound length. Done with a\n// code-point filter rather than a control-character regex (which `no-control-regex`\n// forbids and which would embed literal control bytes in source).\nfunction sanitizeBodyText(text: string): string {\n let out = \"\";\n for (const ch of text.slice(0, 1000)) {\n const code = ch.codePointAt(0) ?? 0;\n out += code < 0x20 || code === 0x7f ? \" \" : ch;\n }\n return out.trim().slice(0, 500);\n}\n\n// Bounded, sanitized read of a failed response body. Also drains the body so\n// the underlying socket can be reused before the next attempt. R2 error bodies\n// are short XML and never contain our credentials; sanitize defensively regardless.\nasync function safeReadBoundedBody(res: Response): Promise<string> {\n try {\n return sanitizeBodyText(await res.text());\n } catch {\n return \"<no body>\";\n }\n}\n\n/**\n * Drive up to `R2_REQUEST_MAX_ATTEMPTS` attempts of a single storage operation.\n * `runAttempt` builds a FRESH signed request each call and returns either a\n * success value or the raw `Response` for the driver to classify. Thrown errors\n * are classified: only recognized transient transport failures are retried.\n */\nexport async function fetchWithR2Retry<T>(params: {\n operation: R2RetryableOperation;\n bucket: string;\n key: string;\n seams: R2RetrySeams;\n runAttempt: (attemptNumber: number) => Promise<R2AttemptResult<T>>;\n}): Promise<T> {\n const { operation, bucket, key, seams, runAttempt } = params;\n\n for (let attempt = 1; attempt <= R2_REQUEST_MAX_ATTEMPTS; attempt++) {\n let result: R2AttemptResult<T>;\n try {\n result = await runAttempt(attempt);\n } catch (err) {\n // Signing failures, explicit aborts, and deterministic local-fs errors are\n // never retried; only recognized transient transport failures are.\n if (!isRetryableTransportError(err)) throw err;\n if (attempt >= R2_REQUEST_MAX_ATTEMPTS) {\n throw new R2StorageError({ operation, bucket, key, attempts: attempt, cause: err });\n }\n await seams.sleep(computeR2DelayMs({ response: null, retryIndex: attempt - 1, seams }));\n continue;\n }\n\n if (result.kind === \"success\") return result.value;\n\n const response = result.response;\n const status = response.status;\n // Consume the failed body before the next attempt.\n const responseText = await safeReadBoundedBody(response);\n\n if (R2_RETRYABLE_STATUSES.has(status) && attempt < R2_REQUEST_MAX_ATTEMPTS) {\n await seams.sleep(computeR2DelayMs({ response, retryIndex: attempt - 1, seams }));\n continue;\n }\n\n // Non-retryable status, or a retryable status with attempts exhausted.\n throw new R2StorageError({ operation, bucket, key, attempts: attempt, status, responseText });\n }\n\n // Unreachable: the loop always returns or throws.\n throw new R2StorageError({ operation, bucket, key, attempts: R2_REQUEST_MAX_ATTEMPTS });\n}\n"]}
@@ -30,6 +30,12 @@ export interface ObjectCopyResult {
30
30
  */
31
31
  alreadyPresent: boolean;
32
32
  }
33
+ /**
34
+ * What `deleteCopy` found at the destination: `deleted` — a copy of the source, now removed;
35
+ * `absent` — no object; `notThisCopy` — an object this package did not copy from that
36
+ * source, left untouched.
37
+ */
38
+ export type DeleteCopyResult = "deleted" | "absent" | "notThisCopy";
33
39
  export interface MediaObjectStore {
34
40
  /**
35
41
  * Upload a local file to the store at `key`. Objects are written with a
@@ -69,6 +75,23 @@ export interface MediaObjectStore {
69
75
  }): Promise<ObjectReadResult>;
70
76
  /** Delete an object. Missing objects are a no-op, not an error. */
71
77
  delete(key: string): Promise<void>;
78
+ /**
79
+ * Delete `toKey` only when the object there is a copy of `fromKey` this package made: its
80
+ * own recorded provenance must name `fromKey` as its source. Only the destination is read,
81
+ * so this works after the source is gone. Any other object — a copy of another source, or
82
+ * one with no provenance — is left untouched and answers `notThisCopy`.
83
+ *
84
+ * Firebase Storage deletes only the generation it checked; an object replaced after the
85
+ * check is checked again, never deleted blind. R2's DeleteObject takes no precondition, so
86
+ * there the delete follows the check unconditioned. That gap is harmless only under the key
87
+ * contract: keys are write-once and a copy never replaces an object, so in the gap the
88
+ * checked object can only be removed; and a copy's key is derived from its source, so the
89
+ * only object that can land there afterwards is another copy of the same source.
90
+ */
91
+ deleteCopy(args: {
92
+ fromKey: string;
93
+ toKey: string;
94
+ }): Promise<DeleteCopyResult>;
72
95
  }
73
96
  export interface CreateR2ObjectStoreArgs {
74
97
  accountId: string;
@@ -1 +1 @@
1
- {"version":3,"file":"storage-ops.d.ts","sourceRoot":"","sources":["../../src/server/storage-ops.ts"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAGzD,OAAO,EAGL,cAAc,EAEf,MAAM,eAAe,CAAC;AAKvB,OAAO,EAAE,cAAc,EAAE,CAAC;AAE1B;;;;GAIG;AACH,qBAAa,wBAAyB,SAAQ,KAAK;aACrB,GAAG,EAAE,MAAM;gBAAX,GAAG,EAAE,MAAM;CAIxC;AAED,mFAAmF;AACnF,MAAM,MAAM,MAAM,GAAG,UAAU,CAAC,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC;AAEzE,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ;;;;OAIG;IACH,cAAc,EAAE,OAAO,CAAC;CACzB;AAiCD,MAAM,WAAW,gBAAgB;IAC/B;;;;;OAKG;IACH,OAAO,CAAC,IAAI,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;IACpG;;;;;;;;OAQG;IACH,IAAI,CAAC,IAAI,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAC1E;;;;;;;OAOG;IACH,UAAU,CAAC,IAAI,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAChF,mEAAmE;IACnE,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpC;AAQD,MAAM,WAAW,uBAAuB;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gCAAgC;IAChC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,2EAA2E;IAC3E,SAAS,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1C,iFAAiF;IACjF,UAAU,CAAC,EAAE,MAAM,MAAM,CAAC;IAC1B,+FAA+F;IAC/F,OAAO,CAAC,EAAE,MAAM,MAAM,CAAC;CACxB;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,uBAAuB,GAAG,gBAAgB,CA0LnF;AAiBD,MAAM,WAAW,oCAAoC;IACnD,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,iFAAiF;AACjF,wBAAgB,gCAAgC,CAAC,IAAI,EAAE,oCAAoC,GAAG,gBAAgB,CAE7G;AAED,MAAM,WAAW,6BAA8B,SAAQ,oCAAoC;IACzF;6EACyE;IACzE,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,sFAAsF;AACtF,wBAAgB,iCAAiC,CAAC,IAAI,EAAE,6BAA6B,GAAG,gBAAgB,CAEvG;AA4FD,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAsBlD"}
1
+ {"version":3,"file":"storage-ops.d.ts","sourceRoot":"","sources":["../../src/server/storage-ops.ts"],"names":[],"mappings":"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"}
@@ -58,6 +58,17 @@ function copyProvenance(fromKey, sourceVersion) {
58
58
  function holdsProvenance(readMetadata, provenance) {
59
59
  return Object.entries(provenance).every(([name, value]) => readMetadata(name) === value);
60
60
  }
61
+ /**
62
+ * Whether an object's custom metadata records a copy of `fromKey`, of whichever source
63
+ * version: `deleteCopy` reads only the destination, so it cannot know the source's version.
64
+ * A record naming the source without a version is not provenance this package wrote.
65
+ */
66
+ function recordsCopyOf(readMetadata, fromKey) {
67
+ const version = readMetadata(COPY_SOURCE_VERSION_METADATA_KEY);
68
+ return (readMetadata(COPY_SOURCE_METADATA_KEY) === encodeURIComponent(fromKey) &&
69
+ typeof version === "string" &&
70
+ version !== "");
71
+ }
61
72
  /** The content headers a copy carries from its source: HTTP header name → Cloud Storage field. */
62
73
  const CARRIED_CONTENT_HEADERS = {
63
74
  "content-type": "contentType",
@@ -91,9 +102,9 @@ export function createR2ObjectStore(args) {
91
102
  }
92
103
  // A GET whose body is discarded unread, not a HEAD: a HEAD's error response carries no
93
104
  // body, so the S3 error code that tells a missing key from a missing bucket would be lost.
94
- async function readObjectHeaders(key) {
105
+ async function readObjectHeaders(key, operation) {
95
106
  return fetchWithR2Retry({
96
- operation: "copy",
107
+ operation,
97
108
  bucket: args.bucket,
98
109
  key,
99
110
  seams,
@@ -153,7 +164,7 @@ export function createR2ObjectStore(args) {
153
164
  });
154
165
  },
155
166
  async copy({ fromKey, toKey }) {
156
- const source = await readObjectHeaders(fromKey);
167
+ const source = await readObjectHeaders(fromKey, "copy");
157
168
  const { etag, version } = readSourceVersion(source, fromKey);
158
169
  const provenance = copyProvenance(fromKey, version);
159
170
  const headers = {
@@ -196,7 +207,7 @@ export function createR2ObjectStore(args) {
196
207
  // after it was read; only an existing destination can be this copy's result.
197
208
  let destination;
198
209
  try {
199
- destination = await readObjectHeaders(toKey);
210
+ destination = await readObjectHeaders(toKey, "copy");
200
211
  }
201
212
  catch (readError) {
202
213
  if (isObjectNotFoundError(readError))
@@ -248,6 +259,36 @@ export function createR2ObjectStore(args) {
248
259
  });
249
260
  }
250
261
  },
262
+ async deleteCopy({ fromKey, toKey }) {
263
+ let destination;
264
+ try {
265
+ destination = await readObjectHeaders(toKey, "deleteCopy");
266
+ }
267
+ catch (e) {
268
+ if (isObjectNotFoundError(e))
269
+ return "absent";
270
+ throw e;
271
+ }
272
+ if (!recordsCopyOf((name) => destination.get(`x-amz-meta-${name}`), fromKey))
273
+ return "notThisCopy";
274
+ // Unconditioned: R2's DeleteObject honors no precondition header, so none is sent — one
275
+ // it silently ignored would only look like protection. `MediaObjectStore.deleteCopy`
276
+ // states why the gap after the check is harmless. One-shot, like `delete`.
277
+ const res = await signedFetch(objectUrl(toKey), { method: "DELETE" });
278
+ if (res.ok)
279
+ return "deleted";
280
+ const error = new R2StorageError({
281
+ operation: "deleteCopy",
282
+ bucket: args.bucket,
283
+ key: toKey,
284
+ attempts: 1,
285
+ status: res.status,
286
+ responseText: await safeText(res),
287
+ });
288
+ if (isObjectNotFoundError(error))
289
+ return "absent";
290
+ throw error;
291
+ },
251
292
  };
252
293
  }
253
294
  async function safeText(res) {
@@ -263,6 +304,12 @@ async function safeText(res) {
263
304
  // ---------------------------------------------------------------------------
264
305
  /** Custom-metadata key Firebase Storage reads download tokens from. */
265
306
  const DOWNLOAD_TOKENS_METADATA_KEY = "firebaseStorageDownloadTokens";
307
+ /**
308
+ * How many times `deleteCopy` checks a Firebase Storage destination before giving up. Keys
309
+ * are write-once, so one change between the check and the delete is already rare; an object
310
+ * still changing after that is left alone and the precondition failure is rethrown.
311
+ */
312
+ const DELETE_COPY_MAX_READS = 2;
266
313
  /** Firebase Storage store for deployed environments. Token-free (see header). */
267
314
  export function createFirebaseStorageObjectStore(args) {
268
315
  return createFirebaseStore(args.bucket, null);
@@ -350,6 +397,38 @@ function createFirebaseStore(bucket, downloadToken) {
350
397
  return { sizeBytes: size, contentType };
351
398
  },
352
399
  delete: deleteObject,
400
+ async deleteCopy({ fromKey, toKey }) {
401
+ const file = bucket.file(toKey);
402
+ for (let read = 1;; read++) {
403
+ let custom;
404
+ let generation;
405
+ try {
406
+ const [destination] = await file.getMetadata();
407
+ custom = destination.metadata ?? undefined;
408
+ generation = destination.generation;
409
+ }
410
+ catch (e) {
411
+ if (isObjectNotFoundError(e))
412
+ return "absent";
413
+ throw e;
414
+ }
415
+ if (!recordsCopyOf((name) => custom?.[name], fromKey))
416
+ return "notThisCopy";
417
+ // The delete names the generation just checked, so it can only remove that object.
418
+ const checked = requireStorageObjectGeneration({ generation }, { bucket: bucket.name, key: toKey });
419
+ try {
420
+ await file.delete({ ifGenerationMatch: checked });
421
+ return "deleted";
422
+ }
423
+ catch (e) {
424
+ if (isObjectNotFoundError(e))
425
+ return "absent";
426
+ // A 412 means the object changed after it was checked: check what is there now.
427
+ if (!isPreconditionFailedError(e) || read >= DELETE_COPY_MAX_READS)
428
+ throw e;
429
+ }
430
+ }
431
+ },
353
432
  };
354
433
  }
355
434
  /** The Cloud Storage Admin SDK's numeric `412`: a generation precondition did not hold. */
@@ -1 +1 @@
1
- {"version":3,"file":"storage-ops.js","sourceRoot":"","sources":["../../src/server/storage-ops.ts"],"names":[],"mappings":"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","sourcesContent":["// Final-media object-store chokepoint — every final-media write/copy/delete in\n// the system goes through a MediaObjectStore. Inline storage-provider calls for\n// FINAL media are forbidden outside this module. (Upload STAGING stays plain\n// Firebase Storage and is not this module's concern.)\n//\n// Implementations (the consuming app picks one per environment):\n// - R2 (S3-compatible) for deployed environments whose final media lives in\n// R2 and is served by an edge gateway.\n// - Firebase Storage for deployed environments whose final media lives in\n// Firebase Storage. Token-free: it writes and copies no download token, so\n// its objects are readable only through the app's own access path (Storage\n// rules). Firebase can still mint a token later (a client getDownloadURL, the\n// console); the app's read path must never do so.\n// - Firebase Storage emulator for local dev/tests — the Firebase Storage store\n// plus a FIXED caller-supplied download token on every object, so\n// deterministic emulator URLs work. Production NEVER mints download tokens.\n//\n// Writes return `{ key }` — never URLs. Display URLs are built at render time\n// from asset refs (see the consuming app's media-asset-url helper).\n\nimport { createReadStream, createWriteStream } from \"node:fs\";\nimport { stat } from \"node:fs/promises\";\nimport { Readable } from \"node:stream\";\nimport { pipeline } from \"node:stream/promises\";\nimport { AwsClient } from \"aws4fetch\";\nimport type { getStorage } from \"firebase-admin/storage\";\nimport { NEUTRAL_CONTENT_TYPE } from \"@ttt-productions/media-schemas\";\nimport { isObjectNotFoundError } from \"./gcs-errors.js\";\nimport {\n defaultR2RetrySeams,\n fetchWithR2Retry,\n R2StorageError,\n type R2RetrySeams,\n} from \"./r2-retry.js\";\nimport { requireStorageObjectGeneration, UnreadableStorageObjectMetadataError } from \"./staged-object.js\";\n\n// Re-export the exhaustion error so callers can recognize an exhausted R2\n// operation. The rest of the retry helper stays private to this module.\nexport { R2StorageError };\n\n/**\n * A copy found an object at its destination key that is not this copy's result, and\n * wrote nothing; that object is untouched. The key is a PROPERTY, never message text, so\n * an escaping error never carries an object key to monitoring.\n */\nexport class ObjectAlreadyExistsError extends Error {\n constructor(public readonly key: string) {\n super(\"copy destination already holds another object\");\n this.name = \"ObjectAlreadyExistsError\";\n }\n}\n\n/** Firebase Admin Storage Bucket — inferred to avoid @google-cloud/storage dep. */\nexport type Bucket = ReturnType<ReturnType<typeof getStorage>[\"bucket\"]>;\n\nexport interface ObjectWriteResult {\n key: string;\n sizeBytes: number;\n contentType: string;\n}\n\nexport interface ObjectReadResult {\n sizeBytes: number;\n contentType: string;\n}\n\nexport interface ObjectCopyResult {\n key: string;\n /**\n * `true` when the destination already held this copy's result — the same source version,\n * committed by an earlier attempt whose answer was lost or by a concurrent identical\n * copy; `false` when this call committed it.\n */\n alreadyPresent: boolean;\n}\n\n// Every copy records on its destination which source version it holds. A copy whose\n// destination already exists compares this record with its own, so it recognizes its\n// own earlier result and never mistakes another writer's object for it. The values are\n// URI-encoded because R2 carries custom metadata in HTTP headers, which must be ASCII.\nconst COPY_SOURCE_METADATA_KEY = \"copy-source\";\nconst COPY_SOURCE_VERSION_METADATA_KEY = \"copy-source-version\";\n\ntype CopyProvenance = Record<typeof COPY_SOURCE_METADATA_KEY | typeof COPY_SOURCE_VERSION_METADATA_KEY, string>;\n\nfunction copyProvenance(fromKey: string, sourceVersion: string): CopyProvenance {\n return {\n [COPY_SOURCE_METADATA_KEY]: encodeURIComponent(fromKey),\n [COPY_SOURCE_VERSION_METADATA_KEY]: encodeURIComponent(sourceVersion),\n };\n}\n\n/** Whether an object's custom metadata, read through `readMetadata`, names exactly this provenance. */\nfunction holdsProvenance(readMetadata: (name: string) => unknown, provenance: CopyProvenance): boolean {\n return Object.entries(provenance).every(([name, value]) => readMetadata(name) === value);\n}\n\n/** The content headers a copy carries from its source: HTTP header name → Cloud Storage field. */\nconst CARRIED_CONTENT_HEADERS = {\n \"content-type\": \"contentType\",\n \"cache-control\": \"cacheControl\",\n \"content-disposition\": \"contentDisposition\",\n \"content-encoding\": \"contentEncoding\",\n} as const;\n\ntype CarriedContentField = (typeof CARRIED_CONTENT_HEADERS)[keyof typeof CARRIED_CONTENT_HEADERS];\n\nexport interface MediaObjectStore {\n /**\n * Upload a local file to the store at `key`. Objects are written with a\n * one-year `immutable` cache-control, so keys are write-once: a replacement is\n * written under a NEW key and the app repoints its reference to it. Overwriting\n * an existing key would keep serving the old bytes from caches for up to a year.\n */\n putFile(args: { localPath: string; key: string; contentType?: string }): Promise<ObjectWriteResult>;\n /**\n * Server-side copy within the store. It never overwrites and is idempotent: it commits\n * only when `toKey` holds no object, in one call that writes the destination fully\n * formed (the source version's bytes and content headers, its provenance, and this\n * store's token policy). When `toKey` already holds this copy's result — a retry of\n * the same copy of the same source version — it resolves with `alreadyPresent: true`;\n * when it holds anything else, it throws `ObjectAlreadyExistsError` and leaves that\n * object untouched. It never deletes anything.\n */\n copy(args: { fromKey: string; toKey: string }): Promise<ObjectCopyResult>;\n /**\n * Download an object's bytes from the store to a local file (streamed, so a\n * large video never sits fully in memory). Used by safety evidence capture to\n * pull final media — which may live in a different backend (R2) — before\n * writing it into the separate (GCS) evidence vault: capture is inherently\n * read-here-then-write, NOT a server-side copy, because source and vault can\n * be different backends.\n */\n readToFile(args: { key: string; localPath: string }): Promise<ObjectReadResult>;\n /** Delete an object. Missing objects are a no-op, not an error. */\n delete(key: string): Promise<void>;\n}\n\nconst DEFAULT_CACHE_CONTROL = \"public, max-age=31536000, immutable\";\n\n// ---------------------------------------------------------------------------\n// R2 (S3-compatible) store — deployed environments\n// ---------------------------------------------------------------------------\n\nexport interface CreateR2ObjectStoreArgs {\n accountId: string;\n accessKeyId: string;\n secretAccessKey: string;\n bucket: string;\n /** Override for tests; defaults to the real R2 S3 endpoint. */\n endpoint?: string;\n /** Override fetch for tests. */\n fetchImpl?: typeof fetch;\n /** Test seam: override the retry sleep (defaults to real `setTimeout`). */\n sleepImpl?: (ms: number) => Promise<void>;\n /** Test seam: override the backoff jitter source (defaults to `Math.random`). */\n randomImpl?: () => number;\n /** Test seam: override the clock used for HTTP-date `Retry-After` (defaults to `Date.now`). */\n nowImpl?: () => number;\n}\n\nexport function createR2ObjectStore(args: CreateR2ObjectStoreArgs): MediaObjectStore {\n const endpoint = (args.endpoint ?? `https://${args.accountId}.r2.cloudflarestorage.com`).replace(/\\/$/, \"\");\n const client = new AwsClient({\n accessKeyId: args.accessKeyId,\n secretAccessKey: args.secretAccessKey,\n service: \"s3\",\n region: \"auto\",\n });\n const doFetch = args.fetchImpl ?? fetch;\n\n // Retry seams: production defaults are internal; tests inject deterministic\n // sleep/random/now. putFile/copy/readToFile ride the bounded retry helper;\n // delete stays one-shot (never routes through it — see below).\n const defaults = defaultR2RetrySeams();\n const seams: R2RetrySeams = {\n sleep: args.sleepImpl ?? defaults.sleep,\n random: args.randomImpl ?? defaults.random,\n now: args.nowImpl ?? defaults.now,\n };\n\n const objectUrl = (key: string) =>\n `${endpoint}/${args.bucket}/${key.split(\"/\").map(encodeURIComponent).join(\"/\")}`;\n\n async function signedFetch(url: string, init: RequestInit & { headers?: Record<string, string> }) {\n const signed = await client.sign(url, { ...init, aws: { allHeaders: true } } as any);\n return doFetch(signed, { ...(init.body ? { duplex: \"half\" } : {}) } as any);\n }\n\n // A GET whose body is discarded unread, not a HEAD: a HEAD's error response carries no\n // body, so the S3 error code that tells a missing key from a missing bucket would be lost.\n async function readObjectHeaders(key: string): Promise<Headers> {\n return fetchWithR2Retry({\n operation: \"copy\",\n bucket: args.bucket,\n key,\n seams,\n runAttempt: async () => {\n const res = await signedFetch(objectUrl(key), { method: \"GET\" });\n if (!res.ok) return { kind: \"response\" as const, response: res };\n await res.body?.cancel();\n return { kind: \"success\" as const, value: res.headers };\n },\n });\n }\n\n // R2 exposes no object version id, so a source version is its ETag, last-modified time,\n // and size together: replacing the source changes at least one of them unless the new\n // content has the same MD5-derived ETag and lands within the same second. A multipart\n // upload's ETag is not the content's MD5, but it is the same kind of version token.\n function readSourceVersion(headers: Headers, key: string): { etag: string; version: string } {\n const etag = headers.get(\"etag\");\n if (!etag) throw new UnreadableStorageObjectMetadataError(args.bucket, key, \"etag\");\n const lastModified = headers.get(\"last-modified\");\n if (!lastModified) throw new UnreadableStorageObjectMetadataError(args.bucket, key, \"lastModified\");\n const size = headers.get(\"content-length\");\n if (!size) throw new UnreadableStorageObjectMetadataError(args.bucket, key, \"size\");\n return { etag, version: `${etag} ${lastModified} ${size}` };\n }\n\n return {\n async putFile({ localPath, key, contentType }) {\n // stat() once, outside the retry loop: a missing input file is a\n // deterministic local error and must fail immediately, never retry.\n const { size } = await stat(localPath);\n const ct = contentType ?? getMimeFromExt(extractExt(localPath));\n return fetchWithR2Retry({\n operation: \"putFile\",\n bucket: args.bucket,\n key,\n seams,\n // Fresh signed request AND a fresh read stream on every attempt — a\n // consumed stream is never reused.\n runAttempt: async () => {\n const res = await signedFetch(objectUrl(key), {\n method: \"PUT\",\n headers: {\n \"content-type\": ct,\n \"content-length\": String(size),\n \"cache-control\": DEFAULT_CACHE_CONTROL,\n },\n body: createReadStream(localPath) as unknown as BodyInit,\n });\n if (res.ok) {\n return { kind: \"success\" as const, value: { key, sizeBytes: size, contentType: ct } };\n }\n return { kind: \"response\" as const, response: res };\n },\n });\n },\n\n async copy({ fromKey, toKey }) {\n const source = await readObjectHeaders(fromKey);\n const { etag, version } = readSourceVersion(source, fromKey);\n const provenance = copyProvenance(fromKey, version);\n const headers: Record<string, string> = {\n \"x-amz-copy-source\": `/${args.bucket}/${fromKey.split(\"/\").map(encodeURIComponent).join(\"/\")}`,\n // Pins the copy to the source version the provenance names.\n \"x-amz-copy-source-if-match\": etag,\n // R2's destination-conditional CopyObject extension, checked when the copy commits;\n // S3's x-amz-copy-source-if-* headers condition only the source.\n \"cf-copy-destination-if-none-match\": \"*\",\n // The destination takes exactly the headers below rather than the source's, so it\n // commits fully formed in this one call.\n \"x-amz-metadata-directive\": \"REPLACE\",\n };\n for (const name of Object.keys(CARRIED_CONTENT_HEADERS)) {\n const value = source.get(name);\n if (value !== null) headers[name] = value;\n }\n for (const [name, value] of Object.entries(provenance)) headers[`x-amz-meta-${name}`] = value;\n\n try {\n await fetchWithR2Retry({\n operation: \"copy\",\n bucket: args.bucket,\n key: toKey,\n seams,\n runAttempt: async () => {\n const res = await signedFetch(objectUrl(toKey), { method: \"PUT\", headers });\n if (res.ok) return { kind: \"success\" as const, value: undefined };\n return { kind: \"response\" as const, response: res };\n },\n });\n return { key: toKey, alreadyPresent: false };\n } catch (e) {\n if (!(e instanceof R2StorageError && e.status === 412)) throw e;\n // R2 answers 412 both for an existing destination and for a source that changed\n // after it was read; only an existing destination can be this copy's result.\n let destination: Headers;\n try {\n destination = await readObjectHeaders(toKey);\n } catch (readError) {\n if (isObjectNotFoundError(readError)) throw e;\n throw readError;\n }\n if (holdsProvenance((name) => destination.get(`x-amz-meta-${name}`), provenance)) {\n return { key: toKey, alreadyPresent: true };\n }\n throw new ObjectAlreadyExistsError(toKey);\n }\n },\n\n async readToFile({ key, localPath }) {\n return fetchWithR2Retry({\n operation: \"readToFile\",\n bucket: args.bucket,\n key,\n seams,\n runAttempt: async () => {\n const res = await signedFetch(objectUrl(key), { method: \"GET\" });\n if (!res.ok || !res.body) {\n return { kind: \"response\" as const, response: res };\n }\n const contentType = res.headers.get(\"content-type\") ?? NEUTRAL_CONTENT_TYPE;\n // Pipe ONLY after a successful response, and open the destination in\n // truncating mode ('w') on every attempt so a partial download from a\n // prior attempt can never prefix/corrupt this one. res.body is a web\n // ReadableStream; stream it to disk so large media never sits fully in\n // memory. A transient body/network stream failure here throws and is\n // retried by the helper; a deterministic local fs error is not.\n await pipeline(\n Readable.fromWeb(res.body as Parameters<typeof Readable.fromWeb>[0]),\n createWriteStream(localPath, { flags: \"w\" }),\n );\n const { size } = await stat(localPath);\n return { kind: \"success\" as const, value: { sizeBytes: size, contentType } };\n },\n });\n },\n\n async delete(key) {\n // Intentionally one-shot: delete is never retried (see r2-retry.ts header).\n const res = await signedFetch(objectUrl(key), { method: \"DELETE\" });\n // S3 DELETE returns 204 even for missing keys; anything else non-ok is real.\n if (!res.ok && res.status !== 404) {\n throw new R2StorageError({\n operation: \"delete\",\n bucket: args.bucket,\n key,\n attempts: 1,\n status: res.status,\n responseText: await safeText(res),\n });\n }\n },\n };\n}\n\nasync function safeText(res: Response): Promise<string> {\n try {\n return (await res.text()).slice(0, 500);\n } catch {\n return \"<no body>\";\n }\n}\n\n// ---------------------------------------------------------------------------\n// Firebase Storage stores — deployed (token-free) and emulator (fixed token)\n// ---------------------------------------------------------------------------\n\n/** Custom-metadata key Firebase Storage reads download tokens from. */\nconst DOWNLOAD_TOKENS_METADATA_KEY = \"firebaseStorageDownloadTokens\";\n\nexport interface CreateFirebaseStorageObjectStoreArgs {\n bucket: Bucket;\n}\n\n/** Firebase Storage store for deployed environments. Token-free (see header). */\nexport function createFirebaseStorageObjectStore(args: CreateFirebaseStorageObjectStoreArgs): MediaObjectStore {\n return createFirebaseStore(args.bucket, null);\n}\n\nexport interface CreateEmulatorObjectStoreArgs extends CreateFirebaseStorageObjectStoreArgs {\n /** Fixed token attached to every object so deterministic emulator URLs work\n * (token URLs bypass rules in the emulator). Pass the app's constant. */\n downloadToken: string;\n}\n\n/** Local dev / tests only: the Firebase Storage store plus a fixed download token. */\nexport function createFirebaseEmulatorObjectStore(args: CreateEmulatorObjectStoreArgs): MediaObjectStore {\n return createFirebaseStore(args.bucket, args.downloadToken);\n}\n\n/** `downloadToken` is the store's token policy: `null` = token-free, a string = that token on every object. */\nfunction createFirebaseStore(bucket: Bucket, downloadToken: string | null): MediaObjectStore {\n async function deleteObject(key: string): Promise<void> {\n try {\n await bucket.file(key).delete();\n } catch (e) {\n if (isObjectNotFoundError(e)) return;\n throw e;\n }\n }\n\n return {\n async putFile({ localPath, key, contentType }) {\n const { size } = await stat(localPath);\n const ct = contentType ?? getMimeFromExt(extractExt(localPath));\n // An upload replaces the object and all of its metadata, so a token-free\n // put carries no custom metadata at all.\n await bucket.upload(localPath, {\n destination: key,\n metadata: {\n contentType: ct,\n cacheControl: DEFAULT_CACHE_CONTROL,\n ...(downloadToken === null ? {} : { metadata: { [DOWNLOAD_TOKENS_METADATA_KEY]: downloadToken } }),\n },\n });\n return { key, sizeBytes: size, contentType: ct };\n },\n\n async copy({ fromKey, toKey }) {\n const [source] = await bucket.file(fromKey).getMetadata();\n const generation = requireStorageObjectGeneration(source, { bucket: bucket.name, key: fromKey });\n const provenance = copyProvenance(fromKey, generation);\n const contentHeaders: Partial<Record<CarriedContentField, string>> = {};\n for (const field of Object.values(CARRIED_CONTENT_HEADERS)) {\n const value = source[field];\n if (typeof value === \"string\" && value !== \"\") contentHeaders[field] = value;\n }\n try {\n // A rewrite's resource REPLACES the destination's metadata rather than merging with\n // the source's, so this one call commits the destination fully formed: the source's\n // content headers, the provenance, and this store's token policy — never a token the\n // source carried. The source is pinned to the generation the provenance names, so a\n // 412 can only mean the destination exists; a replaced source fails as not-found.\n await bucket.file(fromKey, { generation }).copy(bucket.file(toKey), {\n ...contentHeaders,\n metadata: downloadToken === null ? provenance : { ...provenance, [DOWNLOAD_TOKENS_METADATA_KEY]: downloadToken },\n preconditionOpts: { ifGenerationMatch: 0 },\n });\n return { key: toKey, alreadyPresent: false };\n } catch (e) {\n if (!isPreconditionFailedError(e)) throw e;\n let destinationMetadata: Record<string, unknown> | undefined;\n try {\n const [destination] = await bucket.file(toKey).getMetadata();\n destinationMetadata = destination.metadata ?? undefined;\n } catch (readError) {\n if (isObjectNotFoundError(readError)) throw e;\n throw readError;\n }\n if (holdsProvenance((name) => destinationMetadata?.[name], provenance)) {\n return { key: toKey, alreadyPresent: true };\n }\n throw new ObjectAlreadyExistsError(toKey);\n }\n },\n\n async readToFile({ key, localPath }) {\n const file = bucket.file(key);\n await file.download({ destination: localPath });\n const [meta] = await file.getMetadata();\n const { size } = await stat(localPath);\n const contentType = (meta.contentType as string | undefined) ?? NEUTRAL_CONTENT_TYPE;\n return { sizeBytes: size, contentType };\n },\n\n delete: deleteObject,\n };\n}\n\n/** The Cloud Storage Admin SDK's numeric `412`: a generation precondition did not hold. */\nfunction isPreconditionFailedError(e: unknown): boolean {\n return (e as { code?: unknown } | null | undefined)?.code === 412;\n}\n\n// ---------------------------------------------------------------------------\n\nfunction extractExt(localPath: string): string {\n return localPath.split(\".\").pop() || \"\";\n}\n\nexport function getMimeFromExt(ext: string): string {\n switch (ext.toLowerCase()) {\n case \"jpg\":\n case \"jpeg\":\n return \"image/jpeg\";\n case \"png\":\n return \"image/png\";\n case \"webp\":\n return \"image/webp\";\n case \"mp4\":\n return \"video/mp4\";\n case \"webm\":\n return \"video/webm\";\n case \"m4a\":\n return \"audio/mp4\";\n case \"aac\":\n return \"audio/aac\";\n case \"mp3\":\n return \"audio/mpeg\";\n default:\n return NEUTRAL_CONTENT_TYPE;\n }\n}\n"]}
1
+ {"version":3,"file":"storage-ops.js","sourceRoot":"","sources":["../../src/server/storage-ops.ts"],"names":[],"mappings":"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","sourcesContent":["// Final-media object-store chokepoint — every final-media write/copy/delete in\n// the system goes through a MediaObjectStore. Inline storage-provider calls for\n// FINAL media are forbidden outside this module. (Upload STAGING stays plain\n// Firebase Storage and is not this module's concern.)\n//\n// Implementations (the consuming app picks one per environment):\n// - R2 (S3-compatible) for deployed environments whose final media lives in\n// R2 and is served by an edge gateway.\n// - Firebase Storage for deployed environments whose final media lives in\n// Firebase Storage. Token-free: it writes and copies no download token, so\n// its objects are readable only through the app's own access path (Storage\n// rules). Firebase can still mint a token later (a client getDownloadURL, the\n// console); the app's read path must never do so.\n// - Firebase Storage emulator for local dev/tests — the Firebase Storage store\n// plus a FIXED caller-supplied download token on every object, so\n// deterministic emulator URLs work. Production NEVER mints download tokens.\n//\n// Writes return `{ key }` — never URLs. Display URLs are built at render time\n// from asset refs (see the consuming app's media-asset-url helper).\n\nimport { createReadStream, createWriteStream } from \"node:fs\";\nimport { stat } from \"node:fs/promises\";\nimport { Readable } from \"node:stream\";\nimport { pipeline } from \"node:stream/promises\";\nimport { AwsClient } from \"aws4fetch\";\nimport type { getStorage } from \"firebase-admin/storage\";\nimport { NEUTRAL_CONTENT_TYPE } from \"@ttt-productions/media-schemas\";\nimport { isObjectNotFoundError } from \"./gcs-errors.js\";\nimport {\n defaultR2RetrySeams,\n fetchWithR2Retry,\n R2StorageError,\n type R2RetrySeams,\n} from \"./r2-retry.js\";\nimport { requireStorageObjectGeneration, UnreadableStorageObjectMetadataError } from \"./staged-object.js\";\n\n// Re-export the exhaustion error so callers can recognize an exhausted R2\n// operation. The rest of the retry helper stays private to this module.\nexport { R2StorageError };\n\n/**\n * A copy found an object at its destination key that is not this copy's result, and\n * wrote nothing; that object is untouched. The key is a PROPERTY, never message text, so\n * an escaping error never carries an object key to monitoring.\n */\nexport class ObjectAlreadyExistsError extends Error {\n constructor(public readonly key: string) {\n super(\"copy destination already holds another object\");\n this.name = \"ObjectAlreadyExistsError\";\n }\n}\n\n/** Firebase Admin Storage Bucket — inferred to avoid @google-cloud/storage dep. */\nexport type Bucket = ReturnType<ReturnType<typeof getStorage>[\"bucket\"]>;\n\nexport interface ObjectWriteResult {\n key: string;\n sizeBytes: number;\n contentType: string;\n}\n\nexport interface ObjectReadResult {\n sizeBytes: number;\n contentType: string;\n}\n\nexport interface ObjectCopyResult {\n key: string;\n /**\n * `true` when the destination already held this copy's result — the same source version,\n * committed by an earlier attempt whose answer was lost or by a concurrent identical\n * copy; `false` when this call committed it.\n */\n alreadyPresent: boolean;\n}\n\n/**\n * What `deleteCopy` found at the destination: `deleted` — a copy of the source, now removed;\n * `absent` — no object; `notThisCopy` — an object this package did not copy from that\n * source, left untouched.\n */\nexport type DeleteCopyResult = \"deleted\" | \"absent\" | \"notThisCopy\";\n\n// Every copy records on its destination which source version it holds. A copy whose\n// destination already exists compares this record with its own, so it recognizes its\n// own earlier result and never mistakes another writer's object for it. The values are\n// URI-encoded because R2 carries custom metadata in HTTP headers, which must be ASCII.\nconst COPY_SOURCE_METADATA_KEY = \"copy-source\";\nconst COPY_SOURCE_VERSION_METADATA_KEY = \"copy-source-version\";\n\ntype CopyProvenance = Record<typeof COPY_SOURCE_METADATA_KEY | typeof COPY_SOURCE_VERSION_METADATA_KEY, string>;\n\nfunction copyProvenance(fromKey: string, sourceVersion: string): CopyProvenance {\n return {\n [COPY_SOURCE_METADATA_KEY]: encodeURIComponent(fromKey),\n [COPY_SOURCE_VERSION_METADATA_KEY]: encodeURIComponent(sourceVersion),\n };\n}\n\n/** Whether an object's custom metadata, read through `readMetadata`, names exactly this provenance. */\nfunction holdsProvenance(readMetadata: (name: string) => unknown, provenance: CopyProvenance): boolean {\n return Object.entries(provenance).every(([name, value]) => readMetadata(name) === value);\n}\n\n/**\n * Whether an object's custom metadata records a copy of `fromKey`, of whichever source\n * version: `deleteCopy` reads only the destination, so it cannot know the source's version.\n * A record naming the source without a version is not provenance this package wrote.\n */\nfunction recordsCopyOf(readMetadata: (name: string) => unknown, fromKey: string): boolean {\n const version = readMetadata(COPY_SOURCE_VERSION_METADATA_KEY);\n return (\n readMetadata(COPY_SOURCE_METADATA_KEY) === encodeURIComponent(fromKey) &&\n typeof version === \"string\" &&\n version !== \"\"\n );\n}\n\n/** The content headers a copy carries from its source: HTTP header name → Cloud Storage field. */\nconst CARRIED_CONTENT_HEADERS = {\n \"content-type\": \"contentType\",\n \"cache-control\": \"cacheControl\",\n \"content-disposition\": \"contentDisposition\",\n \"content-encoding\": \"contentEncoding\",\n} as const;\n\ntype CarriedContentField = (typeof CARRIED_CONTENT_HEADERS)[keyof typeof CARRIED_CONTENT_HEADERS];\n\nexport interface MediaObjectStore {\n /**\n * Upload a local file to the store at `key`. Objects are written with a\n * one-year `immutable` cache-control, so keys are write-once: a replacement is\n * written under a NEW key and the app repoints its reference to it. Overwriting\n * an existing key would keep serving the old bytes from caches for up to a year.\n */\n putFile(args: { localPath: string; key: string; contentType?: string }): Promise<ObjectWriteResult>;\n /**\n * Server-side copy within the store. It never overwrites and is idempotent: it commits\n * only when `toKey` holds no object, in one call that writes the destination fully\n * formed (the source version's bytes and content headers, its provenance, and this\n * store's token policy). When `toKey` already holds this copy's result — a retry of\n * the same copy of the same source version — it resolves with `alreadyPresent: true`;\n * when it holds anything else, it throws `ObjectAlreadyExistsError` and leaves that\n * object untouched. It never deletes anything.\n */\n copy(args: { fromKey: string; toKey: string }): Promise<ObjectCopyResult>;\n /**\n * Download an object's bytes from the store to a local file (streamed, so a\n * large video never sits fully in memory). Used by safety evidence capture to\n * pull final media — which may live in a different backend (R2) — before\n * writing it into the separate (GCS) evidence vault: capture is inherently\n * read-here-then-write, NOT a server-side copy, because source and vault can\n * be different backends.\n */\n readToFile(args: { key: string; localPath: string }): Promise<ObjectReadResult>;\n /** Delete an object. Missing objects are a no-op, not an error. */\n delete(key: string): Promise<void>;\n /**\n * Delete `toKey` only when the object there is a copy of `fromKey` this package made: its\n * own recorded provenance must name `fromKey` as its source. Only the destination is read,\n * so this works after the source is gone. Any other object — a copy of another source, or\n * one with no provenance — is left untouched and answers `notThisCopy`.\n *\n * Firebase Storage deletes only the generation it checked; an object replaced after the\n * check is checked again, never deleted blind. R2's DeleteObject takes no precondition, so\n * there the delete follows the check unconditioned. That gap is harmless only under the key\n * contract: keys are write-once and a copy never replaces an object, so in the gap the\n * checked object can only be removed; and a copy's key is derived from its source, so the\n * only object that can land there afterwards is another copy of the same source.\n */\n deleteCopy(args: { fromKey: string; toKey: string }): Promise<DeleteCopyResult>;\n}\n\nconst DEFAULT_CACHE_CONTROL = \"public, max-age=31536000, immutable\";\n\n// ---------------------------------------------------------------------------\n// R2 (S3-compatible) store — deployed environments\n// ---------------------------------------------------------------------------\n\nexport interface CreateR2ObjectStoreArgs {\n accountId: string;\n accessKeyId: string;\n secretAccessKey: string;\n bucket: string;\n /** Override for tests; defaults to the real R2 S3 endpoint. */\n endpoint?: string;\n /** Override fetch for tests. */\n fetchImpl?: typeof fetch;\n /** Test seam: override the retry sleep (defaults to real `setTimeout`). */\n sleepImpl?: (ms: number) => Promise<void>;\n /** Test seam: override the backoff jitter source (defaults to `Math.random`). */\n randomImpl?: () => number;\n /** Test seam: override the clock used for HTTP-date `Retry-After` (defaults to `Date.now`). */\n nowImpl?: () => number;\n}\n\nexport function createR2ObjectStore(args: CreateR2ObjectStoreArgs): MediaObjectStore {\n const endpoint = (args.endpoint ?? `https://${args.accountId}.r2.cloudflarestorage.com`).replace(/\\/$/, \"\");\n const client = new AwsClient({\n accessKeyId: args.accessKeyId,\n secretAccessKey: args.secretAccessKey,\n service: \"s3\",\n region: \"auto\",\n });\n const doFetch = args.fetchImpl ?? fetch;\n\n // Retry seams: production defaults are internal; tests inject deterministic\n // sleep/random/now. putFile/copy/readToFile ride the bounded retry helper;\n // delete stays one-shot (never routes through it — see below).\n const defaults = defaultR2RetrySeams();\n const seams: R2RetrySeams = {\n sleep: args.sleepImpl ?? defaults.sleep,\n random: args.randomImpl ?? defaults.random,\n now: args.nowImpl ?? defaults.now,\n };\n\n const objectUrl = (key: string) =>\n `${endpoint}/${args.bucket}/${key.split(\"/\").map(encodeURIComponent).join(\"/\")}`;\n\n async function signedFetch(url: string, init: RequestInit & { headers?: Record<string, string> }) {\n const signed = await client.sign(url, { ...init, aws: { allHeaders: true } } as any);\n return doFetch(signed, { ...(init.body ? { duplex: \"half\" } : {}) } as any);\n }\n\n // A GET whose body is discarded unread, not a HEAD: a HEAD's error response carries no\n // body, so the S3 error code that tells a missing key from a missing bucket would be lost.\n async function readObjectHeaders(key: string, operation: \"copy\" | \"deleteCopy\"): Promise<Headers> {\n return fetchWithR2Retry({\n operation,\n bucket: args.bucket,\n key,\n seams,\n runAttempt: async () => {\n const res = await signedFetch(objectUrl(key), { method: \"GET\" });\n if (!res.ok) return { kind: \"response\" as const, response: res };\n await res.body?.cancel();\n return { kind: \"success\" as const, value: res.headers };\n },\n });\n }\n\n // R2 exposes no object version id, so a source version is its ETag, last-modified time,\n // and size together: replacing the source changes at least one of them unless the new\n // content has the same MD5-derived ETag and lands within the same second. A multipart\n // upload's ETag is not the content's MD5, but it is the same kind of version token.\n function readSourceVersion(headers: Headers, key: string): { etag: string; version: string } {\n const etag = headers.get(\"etag\");\n if (!etag) throw new UnreadableStorageObjectMetadataError(args.bucket, key, \"etag\");\n const lastModified = headers.get(\"last-modified\");\n if (!lastModified) throw new UnreadableStorageObjectMetadataError(args.bucket, key, \"lastModified\");\n const size = headers.get(\"content-length\");\n if (!size) throw new UnreadableStorageObjectMetadataError(args.bucket, key, \"size\");\n return { etag, version: `${etag} ${lastModified} ${size}` };\n }\n\n return {\n async putFile({ localPath, key, contentType }) {\n // stat() once, outside the retry loop: a missing input file is a\n // deterministic local error and must fail immediately, never retry.\n const { size } = await stat(localPath);\n const ct = contentType ?? getMimeFromExt(extractExt(localPath));\n return fetchWithR2Retry({\n operation: \"putFile\",\n bucket: args.bucket,\n key,\n seams,\n // Fresh signed request AND a fresh read stream on every attempt — a\n // consumed stream is never reused.\n runAttempt: async () => {\n const res = await signedFetch(objectUrl(key), {\n method: \"PUT\",\n headers: {\n \"content-type\": ct,\n \"content-length\": String(size),\n \"cache-control\": DEFAULT_CACHE_CONTROL,\n },\n body: createReadStream(localPath) as unknown as BodyInit,\n });\n if (res.ok) {\n return { kind: \"success\" as const, value: { key, sizeBytes: size, contentType: ct } };\n }\n return { kind: \"response\" as const, response: res };\n },\n });\n },\n\n async copy({ fromKey, toKey }) {\n const source = await readObjectHeaders(fromKey, \"copy\");\n const { etag, version } = readSourceVersion(source, fromKey);\n const provenance = copyProvenance(fromKey, version);\n const headers: Record<string, string> = {\n \"x-amz-copy-source\": `/${args.bucket}/${fromKey.split(\"/\").map(encodeURIComponent).join(\"/\")}`,\n // Pins the copy to the source version the provenance names.\n \"x-amz-copy-source-if-match\": etag,\n // R2's destination-conditional CopyObject extension, checked when the copy commits;\n // S3's x-amz-copy-source-if-* headers condition only the source.\n \"cf-copy-destination-if-none-match\": \"*\",\n // The destination takes exactly the headers below rather than the source's, so it\n // commits fully formed in this one call.\n \"x-amz-metadata-directive\": \"REPLACE\",\n };\n for (const name of Object.keys(CARRIED_CONTENT_HEADERS)) {\n const value = source.get(name);\n if (value !== null) headers[name] = value;\n }\n for (const [name, value] of Object.entries(provenance)) headers[`x-amz-meta-${name}`] = value;\n\n try {\n await fetchWithR2Retry({\n operation: \"copy\",\n bucket: args.bucket,\n key: toKey,\n seams,\n runAttempt: async () => {\n const res = await signedFetch(objectUrl(toKey), { method: \"PUT\", headers });\n if (res.ok) return { kind: \"success\" as const, value: undefined };\n return { kind: \"response\" as const, response: res };\n },\n });\n return { key: toKey, alreadyPresent: false };\n } catch (e) {\n if (!(e instanceof R2StorageError && e.status === 412)) throw e;\n // R2 answers 412 both for an existing destination and for a source that changed\n // after it was read; only an existing destination can be this copy's result.\n let destination: Headers;\n try {\n destination = await readObjectHeaders(toKey, \"copy\");\n } catch (readError) {\n if (isObjectNotFoundError(readError)) throw e;\n throw readError;\n }\n if (holdsProvenance((name) => destination.get(`x-amz-meta-${name}`), provenance)) {\n return { key: toKey, alreadyPresent: true };\n }\n throw new ObjectAlreadyExistsError(toKey);\n }\n },\n\n async readToFile({ key, localPath }) {\n return fetchWithR2Retry({\n operation: \"readToFile\",\n bucket: args.bucket,\n key,\n seams,\n runAttempt: async () => {\n const res = await signedFetch(objectUrl(key), { method: \"GET\" });\n if (!res.ok || !res.body) {\n return { kind: \"response\" as const, response: res };\n }\n const contentType = res.headers.get(\"content-type\") ?? NEUTRAL_CONTENT_TYPE;\n // Pipe ONLY after a successful response, and open the destination in\n // truncating mode ('w') on every attempt so a partial download from a\n // prior attempt can never prefix/corrupt this one. res.body is a web\n // ReadableStream; stream it to disk so large media never sits fully in\n // memory. A transient body/network stream failure here throws and is\n // retried by the helper; a deterministic local fs error is not.\n await pipeline(\n Readable.fromWeb(res.body as Parameters<typeof Readable.fromWeb>[0]),\n createWriteStream(localPath, { flags: \"w\" }),\n );\n const { size } = await stat(localPath);\n return { kind: \"success\" as const, value: { sizeBytes: size, contentType } };\n },\n });\n },\n\n async delete(key) {\n // Intentionally one-shot: delete is never retried (see r2-retry.ts header).\n const res = await signedFetch(objectUrl(key), { method: \"DELETE\" });\n // S3 DELETE returns 204 even for missing keys; anything else non-ok is real.\n if (!res.ok && res.status !== 404) {\n throw new R2StorageError({\n operation: \"delete\",\n bucket: args.bucket,\n key,\n attempts: 1,\n status: res.status,\n responseText: await safeText(res),\n });\n }\n },\n\n async deleteCopy({ fromKey, toKey }) {\n let destination: Headers;\n try {\n destination = await readObjectHeaders(toKey, \"deleteCopy\");\n } catch (e) {\n if (isObjectNotFoundError(e)) return \"absent\";\n throw e;\n }\n if (!recordsCopyOf((name) => destination.get(`x-amz-meta-${name}`), fromKey)) return \"notThisCopy\";\n // Unconditioned: R2's DeleteObject honors no precondition header, so none is sent — one\n // it silently ignored would only look like protection. `MediaObjectStore.deleteCopy`\n // states why the gap after the check is harmless. One-shot, like `delete`.\n const res = await signedFetch(objectUrl(toKey), { method: \"DELETE\" });\n if (res.ok) return \"deleted\";\n const error = new R2StorageError({\n operation: \"deleteCopy\",\n bucket: args.bucket,\n key: toKey,\n attempts: 1,\n status: res.status,\n responseText: await safeText(res),\n });\n if (isObjectNotFoundError(error)) return \"absent\";\n throw error;\n },\n };\n}\n\nasync function safeText(res: Response): Promise<string> {\n try {\n return (await res.text()).slice(0, 500);\n } catch {\n return \"<no body>\";\n }\n}\n\n// ---------------------------------------------------------------------------\n// Firebase Storage stores — deployed (token-free) and emulator (fixed token)\n// ---------------------------------------------------------------------------\n\n/** Custom-metadata key Firebase Storage reads download tokens from. */\nconst DOWNLOAD_TOKENS_METADATA_KEY = \"firebaseStorageDownloadTokens\";\n\n/**\n * How many times `deleteCopy` checks a Firebase Storage destination before giving up. Keys\n * are write-once, so one change between the check and the delete is already rare; an object\n * still changing after that is left alone and the precondition failure is rethrown.\n */\nconst DELETE_COPY_MAX_READS = 2;\n\nexport interface CreateFirebaseStorageObjectStoreArgs {\n bucket: Bucket;\n}\n\n/** Firebase Storage store for deployed environments. Token-free (see header). */\nexport function createFirebaseStorageObjectStore(args: CreateFirebaseStorageObjectStoreArgs): MediaObjectStore {\n return createFirebaseStore(args.bucket, null);\n}\n\nexport interface CreateEmulatorObjectStoreArgs extends CreateFirebaseStorageObjectStoreArgs {\n /** Fixed token attached to every object so deterministic emulator URLs work\n * (token URLs bypass rules in the emulator). Pass the app's constant. */\n downloadToken: string;\n}\n\n/** Local dev / tests only: the Firebase Storage store plus a fixed download token. */\nexport function createFirebaseEmulatorObjectStore(args: CreateEmulatorObjectStoreArgs): MediaObjectStore {\n return createFirebaseStore(args.bucket, args.downloadToken);\n}\n\n/** `downloadToken` is the store's token policy: `null` = token-free, a string = that token on every object. */\nfunction createFirebaseStore(bucket: Bucket, downloadToken: string | null): MediaObjectStore {\n async function deleteObject(key: string): Promise<void> {\n try {\n await bucket.file(key).delete();\n } catch (e) {\n if (isObjectNotFoundError(e)) return;\n throw e;\n }\n }\n\n return {\n async putFile({ localPath, key, contentType }) {\n const { size } = await stat(localPath);\n const ct = contentType ?? getMimeFromExt(extractExt(localPath));\n // An upload replaces the object and all of its metadata, so a token-free\n // put carries no custom metadata at all.\n await bucket.upload(localPath, {\n destination: key,\n metadata: {\n contentType: ct,\n cacheControl: DEFAULT_CACHE_CONTROL,\n ...(downloadToken === null ? {} : { metadata: { [DOWNLOAD_TOKENS_METADATA_KEY]: downloadToken } }),\n },\n });\n return { key, sizeBytes: size, contentType: ct };\n },\n\n async copy({ fromKey, toKey }) {\n const [source] = await bucket.file(fromKey).getMetadata();\n const generation = requireStorageObjectGeneration(source, { bucket: bucket.name, key: fromKey });\n const provenance = copyProvenance(fromKey, generation);\n const contentHeaders: Partial<Record<CarriedContentField, string>> = {};\n for (const field of Object.values(CARRIED_CONTENT_HEADERS)) {\n const value = source[field];\n if (typeof value === \"string\" && value !== \"\") contentHeaders[field] = value;\n }\n try {\n // A rewrite's resource REPLACES the destination's metadata rather than merging with\n // the source's, so this one call commits the destination fully formed: the source's\n // content headers, the provenance, and this store's token policy — never a token the\n // source carried. The source is pinned to the generation the provenance names, so a\n // 412 can only mean the destination exists; a replaced source fails as not-found.\n await bucket.file(fromKey, { generation }).copy(bucket.file(toKey), {\n ...contentHeaders,\n metadata: downloadToken === null ? provenance : { ...provenance, [DOWNLOAD_TOKENS_METADATA_KEY]: downloadToken },\n preconditionOpts: { ifGenerationMatch: 0 },\n });\n return { key: toKey, alreadyPresent: false };\n } catch (e) {\n if (!isPreconditionFailedError(e)) throw e;\n let destinationMetadata: Record<string, unknown> | undefined;\n try {\n const [destination] = await bucket.file(toKey).getMetadata();\n destinationMetadata = destination.metadata ?? undefined;\n } catch (readError) {\n if (isObjectNotFoundError(readError)) throw e;\n throw readError;\n }\n if (holdsProvenance((name) => destinationMetadata?.[name], provenance)) {\n return { key: toKey, alreadyPresent: true };\n }\n throw new ObjectAlreadyExistsError(toKey);\n }\n },\n\n async readToFile({ key, localPath }) {\n const file = bucket.file(key);\n await file.download({ destination: localPath });\n const [meta] = await file.getMetadata();\n const { size } = await stat(localPath);\n const contentType = (meta.contentType as string | undefined) ?? NEUTRAL_CONTENT_TYPE;\n return { sizeBytes: size, contentType };\n },\n\n delete: deleteObject,\n\n async deleteCopy({ fromKey, toKey }) {\n const file = bucket.file(toKey);\n for (let read = 1; ; read++) {\n let custom: Record<string, unknown> | undefined;\n let generation: string | number | null | undefined;\n try {\n const [destination] = await file.getMetadata();\n custom = destination.metadata ?? undefined;\n generation = destination.generation;\n } catch (e) {\n if (isObjectNotFoundError(e)) return \"absent\";\n throw e;\n }\n if (!recordsCopyOf((name) => custom?.[name], fromKey)) return \"notThisCopy\";\n // The delete names the generation just checked, so it can only remove that object.\n const checked = requireStorageObjectGeneration({ generation }, { bucket: bucket.name, key: toKey });\n try {\n await file.delete({ ifGenerationMatch: checked });\n return \"deleted\";\n } catch (e) {\n if (isObjectNotFoundError(e)) return \"absent\";\n // A 412 means the object changed after it was checked: check what is there now.\n if (!isPreconditionFailedError(e) || read >= DELETE_COPY_MAX_READS) throw e;\n }\n }\n },\n };\n}\n\n/** The Cloud Storage Admin SDK's numeric `412`: a generation precondition did not hold. */\nfunction isPreconditionFailedError(e: unknown): boolean {\n return (e as { code?: unknown } | null | undefined)?.code === 412;\n}\n\n// ---------------------------------------------------------------------------\n\nfunction extractExt(localPath: string): string {\n return localPath.split(\".\").pop() || \"\";\n}\n\nexport function getMimeFromExt(ext: string): string {\n switch (ext.toLowerCase()) {\n case \"jpg\":\n case \"jpeg\":\n return \"image/jpeg\";\n case \"png\":\n return \"image/png\";\n case \"webp\":\n return \"image/webp\";\n case \"mp4\":\n return \"video/mp4\";\n case \"webm\":\n return \"video/webm\";\n case \"m4a\":\n return \"audio/mp4\";\n case \"aac\":\n return \"audio/aac\";\n case \"mp3\":\n return \"audio/mpeg\";\n default:\n return NEUTRAL_CONTENT_TYPE;\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttt-productions/media-processing-core",
3
- "version": "0.8.11",
3
+ "version": "0.8.12",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/ttt-productions/ttt-packages.git",