@ttt-productions/media-processing-core 0.8.17 → 0.8.18

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.
Files changed (90) hide show
  1. package/dist/audio/audio-processor.d.ts.map +1 -1
  2. package/dist/audio/audio-processor.js +1 -2
  3. package/dist/audio/audio-processor.js.map +1 -1
  4. package/dist/audio/probe.d.ts.map +1 -1
  5. package/dist/audio/probe.js +0 -2
  6. package/dist/audio/probe.js.map +1 -1
  7. package/dist/duration/media-duration.d.ts +0 -11
  8. package/dist/duration/media-duration.d.ts.map +1 -1
  9. package/dist/duration/media-duration.js +1 -28
  10. package/dist/duration/media-duration.js.map +1 -1
  11. package/dist/image/formats.d.ts +0 -11
  12. package/dist/image/formats.d.ts.map +1 -1
  13. package/dist/image/formats.js +0 -10
  14. package/dist/image/formats.js.map +1 -1
  15. package/dist/image/image-processor.d.ts.map +1 -1
  16. package/dist/image/image-processor.js +1 -20
  17. package/dist/image/image-processor.js.map +1 -1
  18. package/dist/image/resize.d.ts +0 -14
  19. package/dist/image/resize.d.ts.map +1 -1
  20. package/dist/image/resize.js +0 -15
  21. package/dist/image/resize.js.map +1 -1
  22. package/dist/inspection/inspect-media.d.ts +0 -7
  23. package/dist/inspection/inspect-media.d.ts.map +1 -1
  24. package/dist/inspection/inspect-media.js +4 -64
  25. package/dist/inspection/inspect-media.js.map +1 -1
  26. package/dist/io/fs.d.ts.map +1 -1
  27. package/dist/io/fs.js +0 -1
  28. package/dist/io/fs.js.map +1 -1
  29. package/dist/io/local-input.d.ts +0 -20
  30. package/dist/io/local-input.d.ts.map +1 -1
  31. package/dist/io/local-input.js +0 -20
  32. package/dist/io/local-input.js.map +1 -1
  33. package/dist/io/types.d.ts +0 -3
  34. package/dist/io/types.d.ts.map +1 -1
  35. package/dist/io/types.js.map +1 -1
  36. package/dist/moderation/merge.d.ts.map +1 -1
  37. package/dist/moderation/merge.js +0 -1
  38. package/dist/moderation/merge.js.map +1 -1
  39. package/dist/moderation/types.d.ts +0 -2
  40. package/dist/moderation/types.d.ts.map +1 -1
  41. package/dist/moderation/types.js.map +1 -1
  42. package/dist/run-pipeline.d.ts +0 -17
  43. package/dist/run-pipeline.d.ts.map +1 -1
  44. package/dist/run-pipeline.js +2 -27
  45. package/dist/run-pipeline.js.map +1 -1
  46. package/dist/server/download-token.d.ts +0 -6
  47. package/dist/server/download-token.d.ts.map +1 -1
  48. package/dist/server/download-token.js +0 -10
  49. package/dist/server/download-token.js.map +1 -1
  50. package/dist/server/firebase-media-io.d.ts +1 -11
  51. package/dist/server/firebase-media-io.d.ts.map +1 -1
  52. package/dist/server/firebase-media-io.js +0 -5
  53. package/dist/server/firebase-media-io.js.map +1 -1
  54. package/dist/server/gcs-errors.d.ts +0 -8
  55. package/dist/server/gcs-errors.d.ts.map +1 -1
  56. package/dist/server/gcs-errors.js +0 -8
  57. package/dist/server/gcs-errors.js.map +1 -1
  58. package/dist/server/pending-media.d.ts +1 -32
  59. package/dist/server/pending-media.d.ts.map +1 -1
  60. package/dist/server/pending-media.js +1 -24
  61. package/dist/server/pending-media.js.map +1 -1
  62. package/dist/server/r2-retry.d.ts +0 -48
  63. package/dist/server/r2-retry.d.ts.map +1 -1
  64. package/dist/server/r2-retry.js +4 -90
  65. package/dist/server/r2-retry.js.map +1 -1
  66. package/dist/server/staged-object.d.ts +0 -44
  67. package/dist/server/staged-object.d.ts.map +1 -1
  68. package/dist/server/staged-object.js +0 -44
  69. package/dist/server/staged-object.js.map +1 -1
  70. package/dist/server/storage-ops.d.ts +2 -62
  71. package/dist/server/storage-ops.d.ts.map +1 -1
  72. package/dist/server/storage-ops.js +9 -88
  73. package/dist/server/storage-ops.js.map +1 -1
  74. package/dist/types.d.ts +0 -2
  75. package/dist/types.d.ts.map +1 -1
  76. package/dist/types.js.map +1 -1
  77. package/dist/utils/safe-path.d.ts.map +1 -1
  78. package/dist/utils/safe-path.js +1 -2
  79. package/dist/utils/safe-path.js.map +1 -1
  80. package/dist/video/ffmpeg.d.ts +0 -22
  81. package/dist/video/ffmpeg.d.ts.map +1 -1
  82. package/dist/video/ffmpeg.js +6 -21
  83. package/dist/video/ffmpeg.js.map +1 -1
  84. package/dist/video/probe.d.ts.map +1 -1
  85. package/dist/video/probe.js +0 -1
  86. package/dist/video/probe.js.map +1 -1
  87. package/dist/video/video-processor.d.ts.map +1 -1
  88. package/dist/video/video-processor.js +2 -15
  89. package/dist/video/video-processor.js.map +1 -1
  90. package/package.json +2 -2
@@ -1,71 +1,29 @@
1
- /** TOTAL attempts (initial + retries) — three totals means at most TWO delays. */
2
1
  export declare const R2_REQUEST_MAX_ATTEMPTS = 3;
3
- /** Base delay for full-jitter exponential backoff. */
4
2
  export declare const R2_RETRY_BASE_DELAY_MS = 250;
5
- /** Ceiling on the exponential backoff (before jitter). */
6
3
  export declare const R2_RETRY_MAX_BACKOFF_MS = 5000;
7
- /** Hard cap on an honored server `Retry-After` so one response can't eat the
8
- * caller's whole timeout budget. */
9
4
  export declare const R2_RETRY_AFTER_CAP_MS = 30000;
10
- /** HTTP statuses treated as transient and safe to retry. */
11
5
  export declare const R2_RETRYABLE_STATUSES: ReadonlySet<number>;
12
6
  export type R2RetryableOperation = 'putFile' | 'copy' | 'readToFile' | 'deleteCopy';
13
- /** Every R2 store operation an `R2StorageError` can name; `delete` never retries. */
14
7
  export type R2Operation = R2RetryableOperation | 'delete';
15
- /** Deterministic seams for tests; production defaults are internal (see below). */
16
8
  export interface R2RetrySeams {
17
- /** Resolve after `ms` milliseconds. */
18
9
  sleep: (ms: number) => Promise<void>;
19
- /** Uniform random in [0, 1). */
20
10
  random: () => number;
21
- /** Epoch-millisecond clock — keeps HTTP-date `Retry-After` tests off wall-clock. */
22
11
  now: () => number;
23
12
  }
24
- /** Production seam defaults. */
25
13
  export declare function defaultR2RetrySeams(): R2RetrySeams;
26
- /**
27
- * True only for recognized transient transport failures. An explicit
28
- * `AbortError` and deterministic local-filesystem errors always return false —
29
- * an abort must never become another attempt.
30
- */
31
14
  export declare function isRetryableTransportError(err: unknown): boolean;
32
- /**
33
- * Parse a `Retry-After` header value into a non-negative millisecond delay.
34
- * Supports both the delta-seconds form (`"120"`) and the HTTP-date form
35
- * (`"Wed, 21 Oct 2015 07:28:00 GMT"`). Returns null when absent/unparseable.
36
- * An HTTP-date in the past clamps to 0.
37
- */
38
15
  export declare function parseRetryAfterMs(headerValue: string | null | undefined, nowMs: number): number | null;
39
- /**
40
- * Compute the pre-next-attempt delay. If the failed response carries a valid
41
- * `Retry-After`, honor it (capped, no backoff added on top). Otherwise use
42
- * full-jitter exponential backoff: `random(0, min(maxBackoff, base * 2^retryIndex))`.
43
- */
44
16
  export declare function computeR2DelayMs(params: {
45
17
  response: Response | null;
46
- /** 0-based index of the retry about to happen (0 for the first retry). */
47
18
  retryIndex: number;
48
19
  seams: R2RetrySeams;
49
20
  }): number;
50
- /**
51
- * Thrown when an R2 operation fails: a retried operation exhausts its attempts or
52
- * hits a non-retryable response, or a one-shot delete fails. Carries
53
- * operation/bucket/key/attempt/status context, the S3 error code parsed from the
54
- * response body, and the bounded body itself as properties — never credentials,
55
- * authorization headers, or full signed URLs. Callers classify it from those
56
- * properties, never from the message.
57
- *
58
- * The message names only the operation, the attempts, the status, and the S3 error
59
- * code. The bucket, the key, and the raw body — which can echo either — stay out of
60
- * it, so an escaping error never carries an object key to monitoring.
61
- */
62
21
  export declare class R2StorageError extends Error {
63
22
  readonly operation: R2Operation;
64
23
  readonly bucket: string;
65
24
  readonly key: string;
66
25
  readonly attempts: number;
67
26
  readonly status?: number;
68
- /** The S3 `<Code>` of the response body (`NoSuchKey`, `PreconditionFailed`, …), when it had one. */
69
27
  readonly s3ErrorCode?: string;
70
28
  readonly responseText?: string;
71
29
  constructor(params: {
@@ -85,12 +43,6 @@ export type R2AttemptResult<T> = {
85
43
  kind: 'response';
86
44
  response: Response;
87
45
  };
88
- /**
89
- * Drive up to `R2_REQUEST_MAX_ATTEMPTS` attempts of a single storage operation.
90
- * `runAttempt` builds a FRESH signed request each call and returns either a
91
- * success value or the raw `Response` for the driver to classify. Thrown errors
92
- * are classified: only recognized transient transport failures are retried.
93
- */
94
46
  export declare function fetchWithR2Retry<T>(params: {
95
47
  operation: R2RetryableOperation;
96
48
  bucket: string;
@@ -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,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,CAC/B,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACtC,KAAK,EAAE,MAAM,GACZ,MAAM,GAAG,IAAI,CAiBf;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;CAkBF;AAWD,MAAM,MAAM,eAAe,CAAC,CAAC,IAC3B;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC;AAyB3E;;;;;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":"AAAA,eAAO,MAAM,uBAAuB,IAAI,CAAC;AACzC,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAC1C,eAAO,MAAM,uBAAuB,OAAQ,CAAC;AAC7C,eAAO,MAAM,qBAAqB,QAAS,CAAC;AAE5C,eAAO,MAAM,qBAAqB,EAAE,WAAW,CAAC,MAAM,CAA2C,CAAC;AAElG,MAAM,MAAM,oBAAoB,GAAG,SAAS,GAAG,MAAM,GAAG,YAAY,GAAG,YAAY,CAAC;AAEpF,MAAM,MAAM,WAAW,GAAG,oBAAoB,GAAG,QAAQ,CAAC;AAE1D,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,MAAM,EAAE,MAAM,MAAM,CAAC;IACrB,GAAG,EAAE,MAAM,MAAM,CAAC;CACnB;AAED,wBAAgB,mBAAmB,IAAI,YAAY,CAMlD;AA8CD,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAmB/D;AAED,wBAAgB,iBAAiB,CAC/B,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACtC,KAAK,EAAE,MAAM,GACZ,MAAM,GAAG,IAAI,CAef;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE;IACvC,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,YAAY,CAAC;CACrB,GAAG,MAAM,CAYT;AAED,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,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;CAkBF;AAMD,MAAM,MAAM,eAAe,CAAC,CAAC,IAC3B;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC;AAqB3E,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,CA+Bb"}
@@ -1,29 +1,8 @@
1
- // Bounded transient-retry policy for the R2 object store.
2
- //
3
- // PRIVATE to the server folder. The server entry (`server/index.ts`) forwards
4
- // only the `R2StorageError` type, so callers can recognize an exhausted storage
5
- // operation; the retry helper itself is never public.
6
- //
7
- // The policy applies to the idempotent-against-a-deterministic-key requests
8
- // only (putFile, copy, readToFile, and deleteCopy's read of the destination).
9
- // Every DELETE request is intentionally one-shot and never routes through here.
10
- // Signing/config failures, local filesystem errors, and deterministic HTTP 4xx
11
- // responses are never retried.
12
- // ---------------------------------------------------------------------------
13
- // Fixed internal policy — never exposed as tunables on the public factory.
14
- // ---------------------------------------------------------------------------
15
- /** TOTAL attempts (initial + retries) — three totals means at most TWO delays. */
16
1
  export const R2_REQUEST_MAX_ATTEMPTS = 3;
17
- /** Base delay for full-jitter exponential backoff. */
18
2
  export const R2_RETRY_BASE_DELAY_MS = 250;
19
- /** Ceiling on the exponential backoff (before jitter). */
20
3
  export const R2_RETRY_MAX_BACKOFF_MS = 5_000;
21
- /** Hard cap on an honored server `Retry-After` so one response can't eat the
22
- * caller's whole timeout budget. */
23
4
  export const R2_RETRY_AFTER_CAP_MS = 30_000;
24
- /** HTTP statuses treated as transient and safe to retry. */
25
5
  export const R2_RETRYABLE_STATUSES = new Set([408, 429, 500, 502, 503, 504]);
26
- /** Production seam defaults. */
27
6
  export function defaultR2RetrySeams() {
28
7
  return {
29
8
  sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
@@ -31,11 +10,7 @@ export function defaultR2RetrySeams() {
31
10
  now: Date.now,
32
11
  };
33
12
  }
34
- // ---------------------------------------------------------------------------
35
- // Transient-error classification
36
- // ---------------------------------------------------------------------------
37
- // Recognized transient transport failure codes (undici/fetch surface these on
38
- // the error or its `.cause`). A broken connection to R2 lands here.
13
+ // undici/fetch surface these codes on the error or its `.cause`.
39
14
  const RETRYABLE_NETWORK_ERROR_CODES = new Set([
40
15
  'ECONNRESET',
41
16
  'ECONNREFUSED',
@@ -53,9 +28,6 @@ const RETRYABLE_NETWORK_ERROR_CODES = new Set([
53
28
  'UND_ERR_BODY_TIMEOUT',
54
29
  'UND_ERR_SOCKET',
55
30
  ]);
56
- // Deterministic local filesystem errors are NEVER retried (disk full, perms,
57
- // missing dir, etc.). This guard wins over any transport classification so a
58
- // local write failure during readToFile piping can't be mistaken for transient.
59
31
  const LOCAL_FS_ERROR_CODES = new Set([
60
32
  'ENOSPC',
61
33
  'EACCES',
@@ -80,71 +52,45 @@ function codeOf(err) {
80
52
  }
81
53
  return undefined;
82
54
  }
83
- /**
84
- * True only for recognized transient transport failures. An explicit
85
- * `AbortError` and deterministic local-filesystem errors always return false —
86
- * an abort must never become another attempt.
87
- */
88
55
  export function isRetryableTransportError(err) {
89
56
  if (!err || typeof err !== 'object')
90
57
  return false;
91
58
  const e = err;
92
- // NEVER convert an explicit abort into another attempt.
93
59
  if (e.name === 'AbortError')
94
60
  return false;
95
61
  const selfCode = codeOf(err);
96
62
  const causeCode = codeOf(e.cause);
97
- // Deterministic local filesystem errors are not transient — decided first so a
98
- // local write error can never be reclassified as a transport failure.
99
63
  if (selfCode && LOCAL_FS_ERROR_CODES.has(selfCode))
100
64
  return false;
101
65
  if (causeCode && LOCAL_FS_ERROR_CODES.has(causeCode))
102
66
  return false;
103
- // Recognized transient network codes on the error or its cause.
104
67
  if (selfCode && RETRYABLE_NETWORK_ERROR_CODES.has(selfCode))
105
68
  return true;
106
69
  if (causeCode && RETRYABLE_NETWORK_ERROR_CODES.has(causeCode))
107
70
  return true;
108
- // fetch()/undici surface transport failures as `TypeError: fetch failed`
109
- // (often with an unrecognized or absent cause) — treat those as transient.
71
+ // fetch()/undici surface transport failures as `TypeError: fetch failed`, often with no recognizable cause.
110
72
  if (err instanceof TypeError || e.name === 'TypeError')
111
73
  return true;
112
74
  return false;
113
75
  }
114
- // ---------------------------------------------------------------------------
115
- // Retry-After parsing + delay computation
116
- // ---------------------------------------------------------------------------
117
- /**
118
- * Parse a `Retry-After` header value into a non-negative millisecond delay.
119
- * Supports both the delta-seconds form (`"120"`) and the HTTP-date form
120
- * (`"Wed, 21 Oct 2015 07:28:00 GMT"`). Returns null when absent/unparseable.
121
- * An HTTP-date in the past clamps to 0.
122
- */
123
76
  export function parseRetryAfterMs(headerValue, nowMs) {
124
77
  if (headerValue == null)
125
78
  return null;
126
79
  const trimmed = headerValue.trim();
127
80
  if (trimmed === '')
128
81
  return null;
129
- // delta-seconds form
130
82
  if (/^\d+$/.test(trimmed)) {
131
83
  const seconds = Number(trimmed);
132
84
  if (!Number.isFinite(seconds) || seconds < 0)
133
85
  return null;
134
86
  return seconds * 1000;
135
87
  }
136
- // HTTP-date form
137
88
  const dateMs = Date.parse(trimmed);
138
89
  if (Number.isNaN(dateMs))
139
90
  return null;
140
91
  const delta = dateMs - nowMs;
141
92
  return delta > 0 ? delta : 0;
142
93
  }
143
- /**
144
- * Compute the pre-next-attempt delay. If the failed response carries a valid
145
- * `Retry-After`, honor it (capped, no backoff added on top). Otherwise use
146
- * full-jitter exponential backoff: `random(0, min(maxBackoff, base * 2^retryIndex))`.
147
- */
148
94
  export function computeR2DelayMs(params) {
149
95
  const { response, retryIndex, seams } = params;
150
96
  if (response) {
@@ -156,28 +102,12 @@ export function computeR2DelayMs(params) {
156
102
  const ceiling = Math.min(R2_RETRY_MAX_BACKOFF_MS, R2_RETRY_BASE_DELAY_MS * 2 ** retryIndex);
157
103
  return Math.floor(seams.random() * ceiling);
158
104
  }
159
- // ---------------------------------------------------------------------------
160
- // Exhaustion error
161
- // ---------------------------------------------------------------------------
162
- /**
163
- * Thrown when an R2 operation fails: a retried operation exhausts its attempts or
164
- * hits a non-retryable response, or a one-shot delete fails. Carries
165
- * operation/bucket/key/attempt/status context, the S3 error code parsed from the
166
- * response body, and the bounded body itself as properties — never credentials,
167
- * authorization headers, or full signed URLs. Callers classify it from those
168
- * properties, never from the message.
169
- *
170
- * The message names only the operation, the attempts, the status, and the S3 error
171
- * code. The bucket, the key, and the raw body — which can echo either — stay out of
172
- * it, so an escaping error never carries an object key to monitoring.
173
- */
174
105
  export class R2StorageError extends Error {
175
106
  operation;
176
107
  bucket;
177
108
  key;
178
109
  attempts;
179
110
  status;
180
- /** The S3 `<Code>` of the response body (`NoSuchKey`, `PreconditionFailed`, …), when it had one. */
181
111
  s3ErrorCode;
182
112
  responseText;
183
113
  constructor(params) {
@@ -196,13 +126,10 @@ export class R2StorageError extends Error {
196
126
  this.responseText = params.responseText;
197
127
  }
198
128
  }
199
- /** The `<Code>` of an S3 XML error body; only a plain alphanumeric token is taken. */
200
129
  function parseS3ErrorCode(responseText) {
201
130
  return responseText ? /<Code>([A-Za-z0-9]{1,64})<\/Code>/.exec(responseText)?.[1] : undefined;
202
131
  }
203
- // Strip ASCII control characters (0x00-0x1F, 0x7F) and bound length. Done with a
204
- // code-point filter rather than a control-character regex (which `no-control-regex`
205
- // forbids and which would embed literal control bytes in source).
132
+ // A code-point filter, not a control-character regex, which `no-control-regex` forbids.
206
133
  function sanitizeBodyText(text) {
207
134
  let out = '';
208
135
  for (const ch of text.slice(0, 1000)) {
@@ -211,9 +138,7 @@ function sanitizeBodyText(text) {
211
138
  }
212
139
  return out.trim().slice(0, 500);
213
140
  }
214
- // Bounded, sanitized read of a failed response body. Also drains the body so
215
- // the underlying socket can be reused before the next attempt. R2 error bodies
216
- // are short XML and never contain our credentials; sanitize defensively regardless.
141
+ // Draining the body lets the socket be reused before the next attempt.
217
142
  async function safeReadBoundedBody(res) {
218
143
  try {
219
144
  return sanitizeBodyText(await res.text());
@@ -222,12 +147,6 @@ async function safeReadBoundedBody(res) {
222
147
  return '<no body>';
223
148
  }
224
149
  }
225
- /**
226
- * Drive up to `R2_REQUEST_MAX_ATTEMPTS` attempts of a single storage operation.
227
- * `runAttempt` builds a FRESH signed request each call and returns either a
228
- * success value or the raw `Response` for the driver to classify. Thrown errors
229
- * are classified: only recognized transient transport failures are retried.
230
- */
231
150
  export async function fetchWithR2Retry(params) {
232
151
  const { operation, bucket, key, seams, runAttempt } = params;
233
152
  for (let attempt = 1; attempt <= R2_REQUEST_MAX_ATTEMPTS; attempt++) {
@@ -236,8 +155,6 @@ export async function fetchWithR2Retry(params) {
236
155
  result = await runAttempt(attempt);
237
156
  }
238
157
  catch (err) {
239
- // Signing failures, explicit aborts, and deterministic local-fs errors are
240
- // never retried; only recognized transient transport failures are.
241
158
  if (!isRetryableTransportError(err))
242
159
  throw err;
243
160
  if (attempt >= R2_REQUEST_MAX_ATTEMPTS) {
@@ -250,16 +167,13 @@ export async function fetchWithR2Retry(params) {
250
167
  return result.value;
251
168
  const response = result.response;
252
169
  const status = response.status;
253
- // Consume the failed body before the next attempt.
254
170
  const responseText = await safeReadBoundedBody(response);
255
171
  if (R2_RETRYABLE_STATUSES.has(status) && attempt < R2_REQUEST_MAX_ATTEMPTS) {
256
172
  await seams.sleep(computeR2DelayMs({ response, retryIndex: attempt - 1, seams }));
257
173
  continue;
258
174
  }
259
- // Non-retryable status, or a retryable status with attempts exhausted.
260
175
  throw new R2StorageError({ operation, bucket, key, attempts: attempt, status, responseText });
261
176
  }
262
- // Unreachable: the loop always returns or throws.
263
177
  throw new R2StorageError({ operation, bucket, key, attempts: R2_REQUEST_MAX_ATTEMPTS });
264
178
  }
265
179
  //# sourceMappingURL=r2-retry.js.map
@@ -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 to the server folder. The server entry (`server/index.ts`) forwards\n// only the `R2StorageError` type, so callers can recognize an exhausted storage\n// operation; the retry helper itself is never public.\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(\n headerValue: string | null | undefined,\n nowMs: number,\n): 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]\n .filter(Boolean)\n .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 } | { 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":["export const R2_REQUEST_MAX_ATTEMPTS = 3;\nexport const R2_RETRY_BASE_DELAY_MS = 250;\nexport const R2_RETRY_MAX_BACKOFF_MS = 5_000;\nexport const R2_RETRY_AFTER_CAP_MS = 30_000;\n\nexport const R2_RETRYABLE_STATUSES: ReadonlySet<number> = new Set([408, 429, 500, 502, 503, 504]);\n\nexport type R2RetryableOperation = 'putFile' | 'copy' | 'readToFile' | 'deleteCopy';\n\nexport type R2Operation = R2RetryableOperation | 'delete';\n\nexport interface R2RetrySeams {\n sleep: (ms: number) => Promise<void>;\n random: () => number;\n now: () => number;\n}\n\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// undici/fetch surface these codes on the error or its `.cause`.\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\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\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 if (e.name === 'AbortError') return false;\n\n const selfCode = codeOf(err);\n const causeCode = codeOf(e.cause);\n\n if (selfCode && LOCAL_FS_ERROR_CODES.has(selfCode)) return false;\n if (causeCode && LOCAL_FS_ERROR_CODES.has(causeCode)) return false;\n\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`, often with no recognizable cause.\n if (err instanceof TypeError || e.name === 'TypeError') return true;\n\n return false;\n}\n\nexport function parseRetryAfterMs(\n headerValue: string | null | undefined,\n nowMs: number,\n): number | null {\n if (headerValue == null) return null;\n const trimmed = headerValue.trim();\n if (trimmed === '') return null;\n\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 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\nexport function computeR2DelayMs(params: {\n response: Response | null;\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\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 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]\n .filter(Boolean)\n .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\nfunction parseS3ErrorCode(responseText: string | undefined): string | undefined {\n return responseText ? /<Code>([A-Za-z0-9]{1,64})<\\/Code>/.exec(responseText)?.[1] : undefined;\n}\n\nexport type R2AttemptResult<T> =\n { kind: 'success'; value: T } | { kind: 'response'; response: Response };\n\n// A code-point filter, not a control-character regex, which `no-control-regex` forbids.\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// Draining the body lets the socket be reused before the next attempt.\nasync function safeReadBoundedBody(res: Response): Promise<string> {\n try {\n return sanitizeBodyText(await res.text());\n } catch {\n return '<no body>';\n }\n}\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 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 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 throw new R2StorageError({ operation, bucket, key, attempts: attempt, status, responseText });\n }\n\n throw new R2StorageError({ operation, bucket, key, attempts: R2_REQUEST_MAX_ATTEMPTS });\n}\n"]}
@@ -1,5 +1,4 @@
1
1
  import type { Bucket } from './storage-ops.js';
2
- /** The stored-object metadata fields these reads use (the Admin SDK returns more). */
3
2
  export interface StagedObjectMetadata {
4
3
  contentType?: string;
5
4
  size?: string | number;
@@ -12,43 +11,17 @@ export type StagedUploadMetadataRead = {
12
11
  } | {
13
12
  found: false;
14
13
  };
15
- /**
16
- * Read a staged object's stored metadata (`contentType`, `size`, `generation`, …) for
17
- * `verifyStagedUploadMetadata`. A missing object is `{ found: false }`; any other read
18
- * failure throws.
19
- */
20
14
  export declare function readStagedUploadMetadata(bucket: Bucket, storagePath: string): Promise<StagedUploadMetadataRead>;
21
- /**
22
- * The staged object's generation from metadata the caller already fetched, as a string
23
- * (GCS generations exceed 2^53) — the one normalization `readStagedObjectGeneration`
24
- * applies, for a processor that already holds the object's metadata and must not read
25
- * it again. `undefined` when the metadata carries no generation (the emulator may not):
26
- * absent, `null`, or an empty string, which names no generation.
27
- */
28
15
  export declare function stagedObjectGenerationFromMetadata(metadata: {
29
16
  generation?: string | number | null;
30
17
  }): string | undefined;
31
- /** The stored-object metadata field an `UnreadableStorageObjectMetadataError` names. */
32
18
  export type StorageObjectMetadataField = 'generation' | 'size' | 'etag' | 'lastModified';
33
- /**
34
- * A storage object whose metadata lacks a field its backend always reports: a Cloud Storage
35
- * generation or byte size (GCS, the Storage emulator, and a Storage finalize event carry
36
- * both), or an R2 object's ETag, last-modified time, or size. This is an anomaly to surface,
37
- * never to paper over with a placeholder. The bucket and key are PROPERTIES, never message
38
- * text: an escaping error reaches monitoring, and an object key must not.
39
- */
40
19
  export declare class UnreadableStorageObjectMetadataError extends Error {
41
20
  readonly bucket: string;
42
21
  readonly key: string;
43
22
  readonly field: StorageObjectMetadataField;
44
23
  constructor(bucket: string, key: string, field: StorageObjectMetadataField);
45
24
  }
46
- /**
47
- * The object's generation from metadata the caller already holds, for a site that cannot
48
- * proceed without it (a hold key, an evidence record, a pinned read). Throws
49
- * `UnreadableStorageObjectMetadataError` (field `generation`) when the metadata names none —
50
- * the same normalization as `stagedObjectGenerationFromMetadata`, with no fallback value.
51
- */
52
25
  export declare function requireStorageObjectGeneration(metadata: {
53
26
  generation?: string | number | null;
54
27
  }, object: {
@@ -56,25 +29,8 @@ export declare function requireStorageObjectGeneration(metadata: {
56
29
  key: string;
57
30
  }): string;
58
31
  export interface ReadStagedObjectGenerationOptions {
59
- /** Called when the generation cannot be read (the caller logs it); reads then stay unpinned. */
60
32
  onReadFailure?: (error: unknown) => void;
61
33
  }
62
- /**
63
- * The staged object's current generation, as a string (GCS generations exceed 2^53).
64
- *
65
- * This is the one value that pins every later read of the staged bytes — the processing
66
- * download, a moderation scan, a hash — to the SAME immutable object, so "processed
67
- * bytes == scanned bytes == hashed bytes" holds even if the staging path were
68
- * overwritten (a pinned read resolves exactly the captured generation). Returns
69
- * `undefined` when no path is given, the object reports no generation (the emulator may
70
- * not), or the read fails — best effort: callers fall back to unpinned reads, and a
71
- * create-only staging rule stays the primary defense.
72
- */
73
34
  export declare function readStagedObjectGeneration(bucket: Bucket, storagePath: string | undefined, options?: ReadStagedObjectGenerationOptions): Promise<string | undefined>;
74
- /**
75
- * A `gs://` URI pinned to a captured generation (`gs://bucket/path#<generation>`), so a
76
- * service reading by URI (a moderation scan) reads the same bytes the processor did.
77
- * Without a generation it is the bare URI.
78
- */
79
35
  export declare function pinnedGcsUri(gcsUri: string, generation: string | undefined): string;
80
36
  //# sourceMappingURL=staged-object.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"staged-object.d.ts","sourceRoot":"","sources":["../../src/server/staged-object.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAE/C,sFAAsF;AACtF,MAAM,WAAW,oBAAoB;IACnC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC7B,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;CAC1B;AAED,MAAM,MAAM,wBAAwB,GAClC;IAAE,KAAK,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,oBAAoB,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,KAAK,CAAA;CAAE,CAAC;AAErE;;;;GAIG;AACH,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,wBAAwB,CAAC,CAQnC;AAED;;;;;;GAMG;AACH,wBAAgB,kCAAkC,CAAC,QAAQ,EAAE;IAC3D,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;CACrC,GAAG,MAAM,GAAG,SAAS,CAKrB;AAED,wFAAwF;AACxF,MAAM,MAAM,0BAA0B,GAAG,YAAY,GAAG,MAAM,GAAG,MAAM,GAAG,cAAc,CAAC;AAEzF;;;;;;GAMG;AACH,qBAAa,oCAAqC,SAAQ,KAAK;aAE3C,MAAM,EAAE,MAAM;aACd,GAAG,EAAE,MAAM;aACX,KAAK,EAAE,0BAA0B;gBAFjC,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,0BAA0B;CAKpD;AAED;;;;;GAKG;AACH,wBAAgB,8BAA8B,CAC5C,QAAQ,EAAE;IAAE,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;CAAE,EACjD,MAAM,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GACtC,MAAM,CAMR;AAED,MAAM,WAAW,iCAAiC;IAChD,gGAAgG;IAChG,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CAC1C;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,0BAA0B,CAC9C,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,OAAO,GAAE,iCAAsC,GAC9C,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAS7B;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAEnF"}
1
+ {"version":3,"file":"staged-object.d.ts","sourceRoot":"","sources":["../../src/server/staged-object.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAE/C,MAAM,WAAW,oBAAoB;IACnC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC7B,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;CAC1B;AAED,MAAM,MAAM,wBAAwB,GAClC;IAAE,KAAK,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,oBAAoB,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,KAAK,CAAA;CAAE,CAAC;AAErE,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,wBAAwB,CAAC,CAQnC;AAED,wBAAgB,kCAAkC,CAAC,QAAQ,EAAE;IAC3D,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;CACrC,GAAG,MAAM,GAAG,SAAS,CAKrB;AAED,MAAM,MAAM,0BAA0B,GAAG,YAAY,GAAG,MAAM,GAAG,MAAM,GAAG,cAAc,CAAC;AAEzF,qBAAa,oCAAqC,SAAQ,KAAK;aAE3C,MAAM,EAAE,MAAM;aACd,GAAG,EAAE,MAAM;aACX,KAAK,EAAE,0BAA0B;gBAFjC,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,0BAA0B;CAKpD;AAED,wBAAgB,8BAA8B,CAC5C,QAAQ,EAAE;IAAE,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;CAAE,EACjD,MAAM,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GACtC,MAAM,CAMR;AAED,MAAM,WAAW,iCAAiC;IAChD,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CAC1C;AAED,wBAAsB,0BAA0B,CAC9C,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,OAAO,GAAE,iCAAsC,GAC9C,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAS7B;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAEnF"}
@@ -1,12 +1,4 @@
1
- // Admin-SDK reads of a STAGED upload object (the client-written staging copy a
2
- // processor reads from). The accept-time verdict over the metadata is the pure
3
- // `verifyStagedUploadMetadata` in `@ttt-productions/media-schemas`.
4
1
  import { isObjectNotFoundError } from './gcs-errors.js';
5
- /**
6
- * Read a staged object's stored metadata (`contentType`, `size`, `generation`, …) for
7
- * `verifyStagedUploadMetadata`. A missing object is `{ found: false }`; any other read
8
- * failure throws.
9
- */
10
2
  export async function readStagedUploadMetadata(bucket, storagePath) {
11
3
  try {
12
4
  const [metadata] = await bucket.file(storagePath).getMetadata();
@@ -18,13 +10,6 @@ export async function readStagedUploadMetadata(bucket, storagePath) {
18
10
  throw e;
19
11
  }
20
12
  }
21
- /**
22
- * The staged object's generation from metadata the caller already fetched, as a string
23
- * (GCS generations exceed 2^53) — the one normalization `readStagedObjectGeneration`
24
- * applies, for a processor that already holds the object's metadata and must not read
25
- * it again. `undefined` when the metadata carries no generation (the emulator may not):
26
- * absent, `null`, or an empty string, which names no generation.
27
- */
28
13
  export function stagedObjectGenerationFromMetadata(metadata) {
29
14
  const { generation } = metadata;
30
15
  if (generation === undefined || generation === null)
@@ -32,13 +17,6 @@ export function stagedObjectGenerationFromMetadata(metadata) {
32
17
  const value = String(generation);
33
18
  return value === '' ? undefined : value;
34
19
  }
35
- /**
36
- * A storage object whose metadata lacks a field its backend always reports: a Cloud Storage
37
- * generation or byte size (GCS, the Storage emulator, and a Storage finalize event carry
38
- * both), or an R2 object's ETag, last-modified time, or size. This is an anomaly to surface,
39
- * never to paper over with a placeholder. The bucket and key are PROPERTIES, never message
40
- * text: an escaping error reaches monitoring, and an object key must not.
41
- */
42
20
  export class UnreadableStorageObjectMetadataError extends Error {
43
21
  bucket;
44
22
  key;
@@ -51,12 +29,6 @@ export class UnreadableStorageObjectMetadataError extends Error {
51
29
  this.name = 'UnreadableStorageObjectMetadataError';
52
30
  }
53
31
  }
54
- /**
55
- * The object's generation from metadata the caller already holds, for a site that cannot
56
- * proceed without it (a hold key, an evidence record, a pinned read). Throws
57
- * `UnreadableStorageObjectMetadataError` (field `generation`) when the metadata names none —
58
- * the same normalization as `stagedObjectGenerationFromMetadata`, with no fallback value.
59
- */
60
32
  export function requireStorageObjectGeneration(metadata, object) {
61
33
  const generation = stagedObjectGenerationFromMetadata(metadata);
62
34
  if (generation === undefined) {
@@ -64,17 +36,6 @@ export function requireStorageObjectGeneration(metadata, object) {
64
36
  }
65
37
  return generation;
66
38
  }
67
- /**
68
- * The staged object's current generation, as a string (GCS generations exceed 2^53).
69
- *
70
- * This is the one value that pins every later read of the staged bytes — the processing
71
- * download, a moderation scan, a hash — to the SAME immutable object, so "processed
72
- * bytes == scanned bytes == hashed bytes" holds even if the staging path were
73
- * overwritten (a pinned read resolves exactly the captured generation). Returns
74
- * `undefined` when no path is given, the object reports no generation (the emulator may
75
- * not), or the read fails — best effort: callers fall back to unpinned reads, and a
76
- * create-only staging rule stays the primary defense.
77
- */
78
39
  export async function readStagedObjectGeneration(bucket, storagePath, options = {}) {
79
40
  if (!storagePath)
80
41
  return undefined;
@@ -87,11 +48,6 @@ export async function readStagedObjectGeneration(bucket, storagePath, options =
87
48
  return undefined;
88
49
  }
89
50
  }
90
- /**
91
- * A `gs://` URI pinned to a captured generation (`gs://bucket/path#<generation>`), so a
92
- * service reading by URI (a moderation scan) reads the same bytes the processor did.
93
- * Without a generation it is the bare URI.
94
- */
95
51
  export function pinnedGcsUri(gcsUri, generation) {
96
52
  return generation ? `${gcsUri}#${generation}` : gcsUri;
97
53
  }
@@ -1 +1 @@
1
- {"version":3,"file":"staged-object.js","sourceRoot":"","sources":["../../src/server/staged-object.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,+EAA+E;AAC/E,oEAAoE;AAEpE,OAAO,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAcxD;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAC5C,MAAc,EACd,WAAmB;IAEnB,IAAI,CAAC;QACH,MAAM,CAAC,QAAQ,CAAC,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,WAAW,EAAE,CAAC;QAChE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;IACnC,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,IAAI,qBAAqB,CAAC,CAAC,CAAC;YAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;QACtD,MAAM,CAAC,CAAC;IACV,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,kCAAkC,CAAC,QAElD;IACC,MAAM,EAAE,UAAU,EAAE,GAAG,QAAQ,CAAC;IAChC,IAAI,UAAU,KAAK,SAAS,IAAI,UAAU,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IACtE,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC;IACjC,OAAO,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC;AAC1C,CAAC;AAKD;;;;;;GAMG;AACH,MAAM,OAAO,oCAAqC,SAAQ,KAAK;IAE3C;IACA;IACA;IAHlB,YACkB,MAAc,EACd,GAAW,EACX,KAAiC;QAEjD,KAAK,CAAC,2CAA2C,KAAK,EAAE,CAAC,CAAC;QAJ1C,WAAM,GAAN,MAAM,CAAQ;QACd,QAAG,GAAH,GAAG,CAAQ;QACX,UAAK,GAAL,KAAK,CAA4B;QAGjD,IAAI,CAAC,IAAI,GAAG,sCAAsC,CAAC;IACrD,CAAC;CACF;AAED;;;;;GAKG;AACH,MAAM,UAAU,8BAA8B,CAC5C,QAAiD,EACjD,MAAuC;IAEvC,MAAM,UAAU,GAAG,kCAAkC,CAAC,QAAQ,CAAC,CAAC;IAChE,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;QAC7B,MAAM,IAAI,oCAAoC,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;IAC1F,CAAC;IACD,OAAO,UAAU,CAAC;AACpB,CAAC;AAOD;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,0BAA0B,CAC9C,MAAc,EACd,WAA+B,EAC/B,UAA6C,EAAE;IAE/C,IAAI,CAAC,WAAW;QAAE,OAAO,SAAS,CAAC;IACnC,IAAI,CAAC;QACH,MAAM,CAAC,QAAQ,CAAC,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,WAAW,EAAE,CAAC;QAChE,OAAO,kCAAkC,CAAC,QAAQ,CAAC,CAAC;IACtD,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,CAAC;QAC3B,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,MAAc,EAAE,UAA8B;IACzE,OAAO,UAAU,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI,UAAU,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC;AACzD,CAAC","sourcesContent":["// Admin-SDK reads of a STAGED upload object (the client-written staging copy a\n// processor reads from). The accept-time verdict over the metadata is the pure\n// `verifyStagedUploadMetadata` in `@ttt-productions/media-schemas`.\n\nimport { isObjectNotFoundError } from './gcs-errors.js';\nimport type { Bucket } from './storage-ops.js';\n\n/** The stored-object metadata fields these reads use (the Admin SDK returns more). */\nexport interface StagedObjectMetadata {\n contentType?: string;\n size?: string | number;\n generation?: string | number;\n [field: string]: unknown;\n}\n\nexport type StagedUploadMetadataRead =\n { found: true; metadata: StagedObjectMetadata } | { found: false };\n\n/**\n * Read a staged object's stored metadata (`contentType`, `size`, `generation`, …) for\n * `verifyStagedUploadMetadata`. A missing object is `{ found: false }`; any other read\n * failure throws.\n */\nexport async function readStagedUploadMetadata(\n bucket: Bucket,\n storagePath: string,\n): Promise<StagedUploadMetadataRead> {\n try {\n const [metadata] = await bucket.file(storagePath).getMetadata();\n return { found: true, metadata };\n } catch (e) {\n if (isObjectNotFoundError(e)) return { found: false };\n throw e;\n }\n}\n\n/**\n * The staged object's generation from metadata the caller already fetched, as a string\n * (GCS generations exceed 2^53) — the one normalization `readStagedObjectGeneration`\n * applies, for a processor that already holds the object's metadata and must not read\n * it again. `undefined` when the metadata carries no generation (the emulator may not):\n * absent, `null`, or an empty string, which names no generation.\n */\nexport function stagedObjectGenerationFromMetadata(metadata: {\n generation?: string | number | null;\n}): string | undefined {\n const { generation } = metadata;\n if (generation === undefined || generation === null) return undefined;\n const value = String(generation);\n return value === '' ? undefined : value;\n}\n\n/** The stored-object metadata field an `UnreadableStorageObjectMetadataError` names. */\nexport type StorageObjectMetadataField = 'generation' | 'size' | 'etag' | 'lastModified';\n\n/**\n * A storage object whose metadata lacks a field its backend always reports: a Cloud Storage\n * generation or byte size (GCS, the Storage emulator, and a Storage finalize event carry\n * both), or an R2 object's ETag, last-modified time, or size. This is an anomaly to surface,\n * never to paper over with a placeholder. The bucket and key are PROPERTIES, never message\n * text: an escaping error reaches monitoring, and an object key must not.\n */\nexport class UnreadableStorageObjectMetadataError extends Error {\n constructor(\n public readonly bucket: string,\n public readonly key: string,\n public readonly field: StorageObjectMetadataField,\n ) {\n super(`storage object metadata has no readable ${field}`);\n this.name = 'UnreadableStorageObjectMetadataError';\n }\n}\n\n/**\n * The object's generation from metadata the caller already holds, for a site that cannot\n * proceed without it (a hold key, an evidence record, a pinned read). Throws\n * `UnreadableStorageObjectMetadataError` (field `generation`) when the metadata names none —\n * the same normalization as `stagedObjectGenerationFromMetadata`, with no fallback value.\n */\nexport function requireStorageObjectGeneration(\n metadata: { generation?: string | number | null },\n object: { bucket: string; key: string },\n): string {\n const generation = stagedObjectGenerationFromMetadata(metadata);\n if (generation === undefined) {\n throw new UnreadableStorageObjectMetadataError(object.bucket, object.key, 'generation');\n }\n return generation;\n}\n\nexport interface ReadStagedObjectGenerationOptions {\n /** Called when the generation cannot be read (the caller logs it); reads then stay unpinned. */\n onReadFailure?: (error: unknown) => void;\n}\n\n/**\n * The staged object's current generation, as a string (GCS generations exceed 2^53).\n *\n * This is the one value that pins every later read of the staged bytes — the processing\n * download, a moderation scan, a hash — to the SAME immutable object, so \"processed\n * bytes == scanned bytes == hashed bytes\" holds even if the staging path were\n * overwritten (a pinned read resolves exactly the captured generation). Returns\n * `undefined` when no path is given, the object reports no generation (the emulator may\n * not), or the read fails — best effort: callers fall back to unpinned reads, and a\n * create-only staging rule stays the primary defense.\n */\nexport async function readStagedObjectGeneration(\n bucket: Bucket,\n storagePath: string | undefined,\n options: ReadStagedObjectGenerationOptions = {},\n): Promise<string | undefined> {\n if (!storagePath) return undefined;\n try {\n const [metadata] = await bucket.file(storagePath).getMetadata();\n return stagedObjectGenerationFromMetadata(metadata);\n } catch (e) {\n options.onReadFailure?.(e);\n return undefined;\n }\n}\n\n/**\n * A `gs://` URI pinned to a captured generation (`gs://bucket/path#<generation>`), so a\n * service reading by URI (a moderation scan) reads the same bytes the processor did.\n * Without a generation it is the bare URI.\n */\nexport function pinnedGcsUri(gcsUri: string, generation: string | undefined): string {\n return generation ? `${gcsUri}#${generation}` : gcsUri;\n}\n"]}
1
+ {"version":3,"file":"staged-object.js","sourceRoot":"","sources":["../../src/server/staged-object.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAaxD,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAC5C,MAAc,EACd,WAAmB;IAEnB,IAAI,CAAC;QACH,MAAM,CAAC,QAAQ,CAAC,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,WAAW,EAAE,CAAC;QAChE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;IACnC,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,IAAI,qBAAqB,CAAC,CAAC,CAAC;YAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;QACtD,MAAM,CAAC,CAAC;IACV,CAAC;AACH,CAAC;AAED,MAAM,UAAU,kCAAkC,CAAC,QAElD;IACC,MAAM,EAAE,UAAU,EAAE,GAAG,QAAQ,CAAC;IAChC,IAAI,UAAU,KAAK,SAAS,IAAI,UAAU,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IACtE,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC;IACjC,OAAO,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC;AAC1C,CAAC;AAID,MAAM,OAAO,oCAAqC,SAAQ,KAAK;IAE3C;IACA;IACA;IAHlB,YACkB,MAAc,EACd,GAAW,EACX,KAAiC;QAEjD,KAAK,CAAC,2CAA2C,KAAK,EAAE,CAAC,CAAC;QAJ1C,WAAM,GAAN,MAAM,CAAQ;QACd,QAAG,GAAH,GAAG,CAAQ;QACX,UAAK,GAAL,KAAK,CAA4B;QAGjD,IAAI,CAAC,IAAI,GAAG,sCAAsC,CAAC;IACrD,CAAC;CACF;AAED,MAAM,UAAU,8BAA8B,CAC5C,QAAiD,EACjD,MAAuC;IAEvC,MAAM,UAAU,GAAG,kCAAkC,CAAC,QAAQ,CAAC,CAAC;IAChE,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;QAC7B,MAAM,IAAI,oCAAoC,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;IAC1F,CAAC;IACD,OAAO,UAAU,CAAC;AACpB,CAAC;AAMD,MAAM,CAAC,KAAK,UAAU,0BAA0B,CAC9C,MAAc,EACd,WAA+B,EAC/B,UAA6C,EAAE;IAE/C,IAAI,CAAC,WAAW;QAAE,OAAO,SAAS,CAAC;IACnC,IAAI,CAAC;QACH,MAAM,CAAC,QAAQ,CAAC,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,WAAW,EAAE,CAAC;QAChE,OAAO,kCAAkC,CAAC,QAAQ,CAAC,CAAC;IACtD,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,CAAC;QAC3B,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,MAAc,EAAE,UAA8B;IACzE,OAAO,UAAU,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI,UAAU,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC;AACzD,CAAC","sourcesContent":["import { isObjectNotFoundError } from './gcs-errors.js';\nimport type { Bucket } from './storage-ops.js';\n\nexport interface StagedObjectMetadata {\n contentType?: string;\n size?: string | number;\n generation?: string | number;\n [field: string]: unknown;\n}\n\nexport type StagedUploadMetadataRead =\n { found: true; metadata: StagedObjectMetadata } | { found: false };\n\nexport async function readStagedUploadMetadata(\n bucket: Bucket,\n storagePath: string,\n): Promise<StagedUploadMetadataRead> {\n try {\n const [metadata] = await bucket.file(storagePath).getMetadata();\n return { found: true, metadata };\n } catch (e) {\n if (isObjectNotFoundError(e)) return { found: false };\n throw e;\n }\n}\n\nexport function stagedObjectGenerationFromMetadata(metadata: {\n generation?: string | number | null;\n}): string | undefined {\n const { generation } = metadata;\n if (generation === undefined || generation === null) return undefined;\n const value = String(generation);\n return value === '' ? undefined : value;\n}\n\nexport type StorageObjectMetadataField = 'generation' | 'size' | 'etag' | 'lastModified';\n\nexport class UnreadableStorageObjectMetadataError extends Error {\n constructor(\n public readonly bucket: string,\n public readonly key: string,\n public readonly field: StorageObjectMetadataField,\n ) {\n super(`storage object metadata has no readable ${field}`);\n this.name = 'UnreadableStorageObjectMetadataError';\n }\n}\n\nexport function requireStorageObjectGeneration(\n metadata: { generation?: string | number | null },\n object: { bucket: string; key: string },\n): string {\n const generation = stagedObjectGenerationFromMetadata(metadata);\n if (generation === undefined) {\n throw new UnreadableStorageObjectMetadataError(object.bucket, object.key, 'generation');\n }\n return generation;\n}\n\nexport interface ReadStagedObjectGenerationOptions {\n onReadFailure?: (error: unknown) => void;\n}\n\nexport async function readStagedObjectGeneration(\n bucket: Bucket,\n storagePath: string | undefined,\n options: ReadStagedObjectGenerationOptions = {},\n): Promise<string | undefined> {\n if (!storagePath) return undefined;\n try {\n const [metadata] = await bucket.file(storagePath).getMetadata();\n return stagedObjectGenerationFromMetadata(metadata);\n } catch (e) {\n options.onReadFailure?.(e);\n return undefined;\n }\n}\n\nexport function pinnedGcsUri(gcsUri: string, generation: string | undefined): string {\n return generation ? `${gcsUri}#${generation}` : gcsUri;\n}\n"]}