@cloudbitmaps/s3 0.21.0 → 0.23.0

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.
package/dist/index.js CHANGED
@@ -2,7 +2,11 @@
2
2
  import {
3
3
  ValidationError as ValidationError5,
4
4
  brandAsBackend,
5
+ normalizeObjectPrefix as normalizeObjectPrefix3,
6
+ prefixPart,
5
7
  refuseUnknownOptions,
8
+ requireBool as requireBool3,
9
+ requireNonEmptyString,
6
10
  requirePositiveInt as requirePositiveInt2
7
11
  } from "@cloudbitmaps/core/driver-kit";
8
12
  import { S3Client } from "@aws-sdk/client-s3";
@@ -12,40 +16,9 @@ function defined(values) {
12
16
  return Object.fromEntries(Object.entries(values).filter(([, v]) => v !== void 0));
13
17
  }
14
18
 
15
- // src/storage.ts
16
- import {
17
- IntegrityError,
18
- NotFoundError as NotFoundError2,
19
- TransientError as TransientError2,
20
- ValidationError as ValidationError3,
21
- WriteConflictError,
22
- concatBytes,
23
- isNotFoundError,
24
- isValidationError,
25
- isWriteConflictError,
26
- normalizeObjectPrefix,
27
- parseGenerationFromKey,
28
- requireBool,
29
- requirePositiveInt,
30
- segmentObjectPrefix,
31
- storageObjectKey,
32
- toTransient
33
- } from "@cloudbitmaps/core/driver-kit";
34
- import { createHash, randomBytes } from "node:crypto";
35
- import {
36
- AbortMultipartUploadCommand,
37
- CompleteMultipartUploadCommand,
38
- CreateMultipartUploadCommand,
39
- DeleteObjectCommand as DeleteObjectCommand2,
40
- GetObjectCommand,
41
- HeadObjectCommand,
42
- ListObjectsV2Command,
43
- PutObjectCommand as PutObjectCommand2,
44
- UploadPartCommand
45
- } from "@aws-sdk/client-s3";
46
-
47
- // src/s3-errors.ts
48
- import { ValidationError } from "@cloudbitmaps/core/driver-kit";
19
+ // src/lifecycle.ts
20
+ import { GetBucketLifecycleConfigurationCommand } from "@aws-sdk/client-s3";
21
+ import { TransientError as TransientError2 } from "@cloudbitmaps/core/driver-kit";
49
22
 
50
23
  // src/aws-errors.ts
51
24
  import { isTransportFault } from "@cloudbitmaps/core/driver-kit";
@@ -76,7 +49,422 @@ function isNetworkOrTimeout(err) {
76
49
  return NETWORK_MESSAGE.test(err?.message ?? "");
77
50
  }
78
51
 
52
+ // src/read-timeout.ts
53
+ import { TransientError, ValidationError } from "@cloudbitmaps/core/driver-kit";
54
+ var DEFAULT_READ_TIMEOUT_MS = 0;
55
+ var MAX_TIMER_MS = 2147483647;
56
+ function resolveReadTimeoutMs(value) {
57
+ if (value === void 0) return DEFAULT_READ_TIMEOUT_MS;
58
+ if (!Number.isSafeInteger(value) || value < 0 || value > MAX_TIMER_MS) {
59
+ throw new ValidationError(
60
+ `readTimeoutMs must be a non-negative safe integer no larger than ${MAX_TIMER_MS}; got ${describe(value)}`
61
+ );
62
+ }
63
+ return value;
64
+ }
65
+ function describe(value) {
66
+ if (typeof value === "string") return JSON.stringify(value);
67
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
68
+ return String(value);
69
+ }
70
+ return value === null ? "null" : typeof value;
71
+ }
72
+ async function timedRead(operation, timeoutMs, read) {
73
+ if (timeoutMs === 0) return read(void 0);
74
+ const controller = new AbortController();
75
+ let timer;
76
+ const timedOut = new Promise((_, reject) => {
77
+ timer = setTimeout(() => {
78
+ reject(new TransientError(`S3 ${operation} timed out after ${timeoutMs} ms`));
79
+ controller.abort();
80
+ }, timeoutMs);
81
+ });
82
+ try {
83
+ return await Promise.race([read({ abortSignal: controller.signal }), timedOut]);
84
+ } finally {
85
+ clearTimeout(timer);
86
+ }
87
+ }
88
+
89
+ // src/scrub-error.ts
90
+ import { types } from "node:util";
91
+ var TRANSPORT = /* @__PURE__ */ new Set([
92
+ "$response",
93
+ "$responseBodyText",
94
+ "socket",
95
+ "_httpMessage",
96
+ "req",
97
+ "request",
98
+ "connection",
99
+ "agent"
100
+ ]);
101
+ var PRIVATE_HEADERS = "x-amz-security-token|x-amz-s3session-token|x-amz-(?:copy-source-)?server-side-encryption-customer-key";
102
+ var CREDENTIAL_NAME = new RegExp(
103
+ `^(?:(?:proxy-)?authorization|cookie|set-cookie|x-api-key|${PRIVATE_HEADERS}|x-amz-credential|x-amz-signature|(?:copysource)?ssecustomerkey|secretaccesskey|sessiontoken|signatureprovided|canonicalrequest(?:bytes)?|token(?:-\\d+)?)$`,
104
+ "i"
105
+ );
106
+ var isCredentialName = (text) => CREDENTIAL_NAME.test(text.trim().replace(/:$/, ""));
107
+ var CREDENTIAL_LINES = new RegExp(
108
+ `^((?:proxy-)?authorization|cookie|set-cookie|x-api-key|${PRIVATE_HEADERS}):[^\\r\\n]*`,
109
+ "gim"
110
+ );
111
+ var NAMED_SECRET = new RegExp(
112
+ `((?:${PRIVATE_HEADERS})["']?\\s*(?::|=|%3D)\\s*["']?)[^\\s&"'<>\\\\,;]+`,
113
+ "gi"
114
+ );
115
+ var SIGNATURE = /(Signature(?:=|%3D))[0-9A-Fa-f]{16,}/gi;
116
+ var REDACTED = "[redacted]";
117
+ var MAX_DEPTH = 10;
118
+ var isPlain = (value) => {
119
+ const proto = Object.getPrototypeOf(value);
120
+ return proto === Object.prototype || proto === null;
121
+ };
122
+ var isError = (value) => value instanceof Error || types.isNativeError(value);
123
+ function redactText(text) {
124
+ try {
125
+ return text.replace(CREDENTIAL_LINES, `$1: ${REDACTED}`).replace(NAMED_SECRET, `$1${REDACTED}`).replace(SIGNATURE, `$1${REDACTED}`);
126
+ } catch {
127
+ return REDACTED;
128
+ }
129
+ }
130
+ var OMIT = /* @__PURE__ */ Symbol("omit");
131
+ var INDEX = /^(?:0|[1-9]\d*)$/;
132
+ function hasNamedKeys(value) {
133
+ if (!Array.isArray(value)) return Reflect.ownKeys(value).length > 0;
134
+ return Reflect.ownKeys(value).some(
135
+ (key) => key !== "length" && !(typeof key === "string" && INDEX.test(key) && Number(key) < 2 ** 32 - 1)
136
+ );
137
+ }
138
+ function define(copy, key, value, enumerable) {
139
+ Object.defineProperty(copy, key, { value, writable: true, configurable: true, enumerable });
140
+ }
141
+ function copyOf(value, ctx, depth) {
142
+ if (typeof value === "string") {
143
+ const clean = redactText(value);
144
+ if (clean !== value) ctx.changed = true;
145
+ return clean;
146
+ }
147
+ if (typeof value === "function" || typeof value === "symbol") {
148
+ ctx.changed = true;
149
+ return OMIT;
150
+ }
151
+ if (typeof value !== "object" || value === null) return value;
152
+ if (ctx.copies.has(value)) return ctx.copies.get(value);
153
+ if (depth > MAX_DEPTH) {
154
+ ctx.changed = true;
155
+ return OMIT;
156
+ }
157
+ if (Array.isArray(value)) return copyList(value, ctx, depth);
158
+ if (value instanceof Map) return copyMap(value, ctx, depth);
159
+ if (value instanceof Set) return copySet(value, ctx, depth);
160
+ return copyRecord(value, ctx, depth);
161
+ }
162
+ function copyList(value, ctx, depth) {
163
+ const list = [];
164
+ ctx.copies.set(value, list);
165
+ if (hasNamedKeys(value)) ctx.changed = true;
166
+ let hidden = false;
167
+ for (const item of value) {
168
+ if (hidden && item != null) {
169
+ list.push(REDACTED);
170
+ if (item !== REDACTED) ctx.changed = true;
171
+ } else {
172
+ const kept = copyOf(item, ctx, depth + 1);
173
+ list.push(kept === OMIT ? void 0 : kept);
174
+ }
175
+ hidden = typeof item === "string" && isCredentialName(item);
176
+ }
177
+ return list;
178
+ }
179
+ function copyMap(value, ctx, depth) {
180
+ const map = /* @__PURE__ */ new Map();
181
+ ctx.copies.set(value, map);
182
+ if (hasNamedKeys(value)) ctx.changed = true;
183
+ for (const [k, v] of value) {
184
+ const key = copyOf(k, ctx, depth + 1);
185
+ if (key === OMIT) continue;
186
+ if (typeof k === "string" && isCredentialName(k) && v != null) {
187
+ map.set(key, REDACTED);
188
+ if (v !== REDACTED) ctx.changed = true;
189
+ continue;
190
+ }
191
+ const kept = copyOf(v, ctx, depth + 1);
192
+ if (kept !== OMIT) map.set(key, kept);
193
+ }
194
+ return map;
195
+ }
196
+ function copySet(value, ctx, depth) {
197
+ const set = /* @__PURE__ */ new Set();
198
+ ctx.copies.set(value, set);
199
+ if (hasNamedKeys(value)) ctx.changed = true;
200
+ for (const v of value) {
201
+ const kept = copyOf(v, ctx, depth + 1);
202
+ if (kept !== OMIT) set.add(kept);
203
+ }
204
+ return set;
205
+ }
206
+ function copyRecord(value, ctx, depth) {
207
+ const errorLike = isError(value);
208
+ if (!errorLike && !isPlain(value)) {
209
+ ctx.changed = true;
210
+ return OMIT;
211
+ }
212
+ const copy = errorLike ? Object.setPrototypeOf(new Error(), Object.getPrototypeOf(value)) : Object.create(Object.getPrototypeOf(value));
213
+ ctx.copies.set(value, copy);
214
+ if (errorLike) delete copy.stack;
215
+ for (const key of Reflect.ownKeys(value)) {
216
+ if (typeof key === "string" && TRANSPORT.has(key)) {
217
+ ctx.changed = true;
218
+ } else if (errorLike && key === "stack") {
219
+ copyStack(value, copy, ctx, depth);
220
+ } else {
221
+ copyField(value, copy, key, ctx, depth);
222
+ }
223
+ }
224
+ if (errorLike) keepSlotFields(value, copy, ctx, depth);
225
+ redactPairs(value, copy, errorLike, ctx);
226
+ return copy;
227
+ }
228
+ function redactPairs(original, copy, errorLike, ctx) {
229
+ const names = Reflect.ownKeys(original).filter((key) => {
230
+ if (typeof key !== "string") return false;
231
+ const held = Object.getOwnPropertyDescriptor(original, key)?.value;
232
+ return typeof held === "string" && isCredentialName(held);
233
+ });
234
+ if (names.length === 0) return;
235
+ const values = errorLike ? names.filter((key) => /name$/i.test(key)).map((key) => `${key.slice(0, -4)}value`.toLowerCase()) : void 0;
236
+ for (const key of Reflect.ownKeys(copy)) {
237
+ if (typeof key !== "string" || names.includes(key)) continue;
238
+ if (values !== void 0 && !values.includes(key.toLowerCase())) continue;
239
+ const descriptor = Object.getOwnPropertyDescriptor(copy, key);
240
+ if (descriptor?.value == null || descriptor.value === REDACTED) continue;
241
+ if (typeof descriptor.value !== "string" && typeof descriptor.value !== "object") continue;
242
+ define(copy, key, REDACTED, descriptor.enumerable ?? false);
243
+ ctx.changed = true;
244
+ }
245
+ }
246
+ function copyStack(original, copy, ctx, depth) {
247
+ let stack;
248
+ try {
249
+ stack = original.stack;
250
+ } catch {
251
+ return;
252
+ }
253
+ const kept = copyOf(stack, ctx, depth + 1);
254
+ if (kept !== OMIT) define(copy, "stack", kept, false);
255
+ }
256
+ function copyField(original, copy, key, ctx, depth) {
257
+ const descriptor = Object.getOwnPropertyDescriptor(original, key);
258
+ if (descriptor === void 0) return;
259
+ if (!("value" in descriptor)) {
260
+ ctx.changed = true;
261
+ return;
262
+ }
263
+ const name = typeof key === "string" ? redactText(key) : key;
264
+ if (name !== key) ctx.changed = true;
265
+ let kept;
266
+ if (typeof key === "string" && isCredentialName(key) && descriptor.value != null) {
267
+ kept = REDACTED;
268
+ if (descriptor.value !== REDACTED) ctx.changed = true;
269
+ } else {
270
+ kept = copyOf(descriptor.value, ctx, depth + 1);
271
+ if (kept === OMIT) return;
272
+ }
273
+ Object.defineProperty(copy, name, {
274
+ value: kept,
275
+ writable: descriptor.writable ?? true,
276
+ configurable: true,
277
+ enumerable: descriptor.enumerable ?? false
278
+ });
279
+ }
280
+ function keepSlotFields(original, copy, ctx, depth) {
281
+ for (const key of ["name", "message", "code"]) {
282
+ if (Object.prototype.hasOwnProperty.call(original, key)) continue;
283
+ let held;
284
+ try {
285
+ held = original[key];
286
+ } catch {
287
+ continue;
288
+ }
289
+ let read = OMIT;
290
+ try {
291
+ read = copy[key];
292
+ } catch {
293
+ }
294
+ if (read === held) continue;
295
+ const kept = copyOf(held, ctx, depth + 1);
296
+ if (kept !== OMIT) define(copy, key, kept, false);
297
+ }
298
+ }
299
+ function scrubCredentials(err) {
300
+ if (typeof err !== "object" || err === null) return err;
301
+ const ctx = { copies: /* @__PURE__ */ new WeakMap(), changed: false };
302
+ const copy = copyOf(err, ctx, 0);
303
+ return ctx.changed && copy !== OMIT ? copy : err;
304
+ }
305
+
306
+ // src/lifecycle.ts
307
+ var MAX_MESSAGE = 200;
308
+ var MAX_FIELD = 64;
309
+ var MAX_NAMED_RULES = 3;
310
+ var READABLE_CLASSES = /* @__PURE__ */ new Set([
311
+ "STANDARD",
312
+ "STANDARD_IA",
313
+ "ONEZONE_IA",
314
+ "GLACIER_IR",
315
+ "INTELLIGENT_TIERING"
316
+ ]);
317
+ var ARCHIVE_CLASSES = /* @__PURE__ */ new Set(["GLACIER", "DEEP_ARCHIVE"]);
318
+ var INFREQUENT_CLASSES = /* @__PURE__ */ new Set([
319
+ "STANDARD_IA",
320
+ "ONEZONE_IA",
321
+ "GLACIER_IR"
322
+ ]);
323
+ var cut = (text, max) => text.length > max ? text.slice(0, max) : text;
324
+ var overlaps = (a, b) => a.startsWith(b) || b.startsWith(a);
325
+ function rulePrefix(rule) {
326
+ return rule.Filter?.Prefix ?? rule.Filter?.And?.Prefix ?? rule.Prefix ?? "";
327
+ }
328
+ var hasTagFilter = (rule) => rule.Filter?.Tag !== void 0 || (rule.Filter?.And?.Tags?.length ?? 0) > 0;
329
+ var isEnabled = (rule) => rule.Status !== "Disabled";
330
+ function threat(rule) {
331
+ if (rule.Expiration?.Days !== void 0 || rule.Expiration?.Date !== void 0) {
332
+ return "expires current objects";
333
+ }
334
+ const classes = (rule.Transitions ?? []).map((t) => t.StorageClass);
335
+ const archive = classes.find((c) => c !== void 0 && ARCHIVE_CLASSES.has(c));
336
+ if (archive !== void 0) return `moves current objects to ${archive}`;
337
+ const unknown = (rule.Transitions ?? []).find(
338
+ (t) => t.StorageClass === void 0 || !READABLE_CLASSES.has(t.StorageClass)
339
+ );
340
+ return unknown === void 0 ? void 0 : `moves current objects to ${cut(String(unknown.StorageClass ?? "no storage class"), MAX_FIELD)}, a storage class this check does not know`;
341
+ }
342
+ var label = (rule) => {
343
+ const id = rule.ID === void 0 ? "" : `${JSON.stringify(cut(rule.ID, MAX_FIELD))} `;
344
+ return `${id}(prefix ${JSON.stringify(cut(rulePrefix(rule), MAX_FIELD))})`;
345
+ };
346
+ var sitsInside = (rule, prefix) => prefix !== "" && rulePrefix(rule).startsWith(prefix);
347
+ function coversGenerations(rule, prefix) {
348
+ const own = rulePrefix(rule);
349
+ if (prefix.startsWith(own)) return true;
350
+ return !own.slice(prefix.length).startsWith("registry/");
351
+ }
352
+ var ABORT_ADVICE = "No rule aborts incomplete multipart uploads under this prefix; add one (AbortIncompleteMultipartUpload, a few days) so an upload a crashed writer abandoned stops billing for its parts.";
353
+ var INTELLIGENT_TIERING_ADVICE = "A rule moves current objects to INTELLIGENT_TIERING: if the bucket turns on its archive access tiers, an object in one needs a restore before it can be read, so leave them off for this prefix.";
354
+ var INFREQUENT_ADVICE = "A rule moves current objects to an infrequent-access class or GLACIER_IR: they stay readable, but each has a minimum storage duration and a per-GB retrieval fee that reads of the store will pay.";
355
+ function judgeRules(rules, prefix) {
356
+ const live = rules.filter((r) => isEnabled(r) && overlaps(rulePrefix(r), prefix));
357
+ const untagged = live.filter((r) => !hasTagFilter(r));
358
+ const threats = untagged.flatMap((r) => {
359
+ const how = threat(r);
360
+ return how === void 0 ? [] : [{ text: `rule ${label(r)} ${how}`, inside: sitsInside(r, prefix) }];
361
+ });
362
+ const problems = threats.map((t) => t.text);
363
+ const transitionsTo = (classes) => untagged.some(
364
+ (r) => (r.Transitions ?? []).some(
365
+ (t) => t.StorageClass !== void 0 && classes.has(t.StorageClass)
366
+ )
367
+ );
368
+ const advice = [
369
+ live.some((r) => r.AbortIncompleteMultipartUpload !== void 0 && coversGenerations(r, prefix)) ? "" : ABORT_ADVICE,
370
+ transitionsTo(/* @__PURE__ */ new Set(["INTELLIGENT_TIERING"])) ? INTELLIGENT_TIERING_ADVICE : "",
371
+ transitionsTo(INFREQUENT_CLASSES) ? INFREQUENT_ADVICE : ""
372
+ ].filter((a) => a !== "").map((a) => ` ${a}`).join("");
373
+ if (problems.length > 0) {
374
+ const named = problems.slice(0, MAX_NAMED_RULES).join("; ");
375
+ const more = problems.length > MAX_NAMED_RULES ? ` and ${problems.length - MAX_NAMED_RULES} more` : "";
376
+ const quoted = JSON.stringify(cut(prefix, MAX_FIELD));
377
+ const narrow = `Narrow the rule's prefix to exclude ${quoted}, or restrict it to noncurrent versions.`;
378
+ const retarget = `A rule that sits inside ${quoted} cannot be narrowed to exclude it: remove it or retarget it to another prefix, or restrict it to noncurrent versions.`;
379
+ const where = prefix === "" ? "The store has no prefix, so every bucket-wide or prefixed rule can reach it: give the store a prefix, or scope the rule away from its paths." : [
380
+ threats.some((t) => !t.inside) ? narrow : "",
381
+ threats.some((t) => t.inside) ? retarget : ""
382
+ ].filter((x) => x !== "").join(" ");
383
+ return {
384
+ status: "problem",
385
+ detail: `${named}${more}. The store keeps current objects it must be able to read back. ${where}${advice}`
386
+ };
387
+ }
388
+ return {
389
+ status: "ok",
390
+ detail: `no enabled lifecycle rule under ${JSON.stringify(cut(prefix, MAX_FIELD))} expires or archives current objects.${advice}`
391
+ };
392
+ }
393
+ function unchecked(err) {
394
+ if (err instanceof TransientError2) {
395
+ return {
396
+ status: "unchecked",
397
+ detail: "the read of the bucket lifecycle configuration got no answer in time (readTimeoutMs); run the check again"
398
+ };
399
+ }
400
+ const scrubbed = scrubCredentials(err);
401
+ if (errorName(scrubbed) === "AccessDenied") {
402
+ return {
403
+ status: "unchecked",
404
+ detail: "the identity may not read the bucket lifecycle configuration: grant s3:GetLifecycleConfiguration on the bucket to check it, or confirm by hand that no rule expires or archives current objects under this prefix"
405
+ };
406
+ }
407
+ const name = cut(String(errorName(scrubbed) ?? "Error"), MAX_FIELD);
408
+ const raw = scrubbed?.message;
409
+ const message = typeof raw === "string" ? `: ${raw.slice(0, MAX_MESSAGE)}` : "";
410
+ return {
411
+ status: "unchecked",
412
+ detail: `could not read the bucket lifecycle configuration (${name}${message}); check it by hand`
413
+ };
414
+ }
415
+ async function checkS3Lifecycle(client, bucket, prefix, readTimeoutMs = 0) {
416
+ try {
417
+ const out = await timedRead(
418
+ "GetBucketLifecycleConfiguration",
419
+ readTimeoutMs,
420
+ (options) => client.send(new GetBucketLifecycleConfigurationCommand({ Bucket: bucket }), options)
421
+ );
422
+ return judgeRules(out.Rules ?? [], prefix);
423
+ } catch (err) {
424
+ try {
425
+ if (errorName(err) === "NoSuchLifecycleConfiguration") {
426
+ return judgeRules([], prefix);
427
+ }
428
+ return unchecked(err);
429
+ } catch {
430
+ return { status: "unchecked", detail: "could not read the bucket lifecycle configuration" };
431
+ }
432
+ }
433
+ }
434
+
435
+ // src/storage.ts
436
+ import {
437
+ errorKind,
438
+ IntegrityError,
439
+ NotFoundError as NotFoundError2,
440
+ TransientError as TransientError3,
441
+ ValidationError as ValidationError3,
442
+ WriteConflictError,
443
+ concatBytes,
444
+ normalizeObjectPrefix,
445
+ parseGenerationFromKey,
446
+ checkDeleteOptions,
447
+ requireBool,
448
+ requirePositiveInt,
449
+ segmentObjectPrefix,
450
+ storageObjectKey,
451
+ toTransient
452
+ } from "@cloudbitmaps/core/driver-kit";
453
+ import { createHash, randomBytes } from "node:crypto";
454
+ import {
455
+ AbortMultipartUploadCommand,
456
+ CompleteMultipartUploadCommand,
457
+ CreateMultipartUploadCommand,
458
+ DeleteObjectCommand as DeleteObjectCommand2,
459
+ GetObjectCommand,
460
+ HeadObjectCommand,
461
+ ListObjectsV2Command,
462
+ PutObjectCommand as PutObjectCommand2,
463
+ UploadPartCommand
464
+ } from "@aws-sdk/client-s3";
465
+
79
466
  // src/s3-errors.ts
467
+ import { ValidationError as ValidationError2 } from "@cloudbitmaps/core/driver-kit";
80
468
  function isPreconditionFailed(err) {
81
469
  return errorName(err) === "PreconditionFailed" || httpStatus(err) === 412;
82
470
  }
@@ -84,7 +472,8 @@ function isConditionalConflict(err) {
84
472
  return isPreconditionFailed(err) || errorName(err) === "ConditionalRequestConflict" || httpStatus(err) === 409;
85
473
  }
86
474
  function isThrottle(err) {
87
- return errorName(err) === "SlowDown" || httpStatus(err) === 503;
475
+ const status = httpStatus(err);
476
+ return errorName(err) === "SlowDown" || status === 503 || status === 429;
88
477
  }
89
478
  function isNoSuchUpload(err) {
90
479
  return errorName(err) === "NoSuchUpload";
@@ -95,7 +484,7 @@ function isMissingBucket(err) {
95
484
  function asMissingBucket(err, bucket) {
96
485
  if (!isMissingBucket(err)) return err;
97
486
  return Object.assign(
98
- new ValidationError(
487
+ new ValidationError2(
99
488
  `the S3 bucket does not exist: ${bucket}; check the bucket name, and the region and endpoint of the client`
100
489
  ),
101
490
  { cause: err }
@@ -115,7 +504,7 @@ function isClockSkewCorrected(err) {
115
504
  }
116
505
  function isTransient(err) {
117
506
  if (isConditionalConflict(err) || isNotFound(err) || isInvalidRange(err)) return false;
118
- return errorName(err) === "SlowDown" || isServerSide(err) || isNetworkOrTimeout(err) || isSdkRetryable(err) || isClockSkewCorrected(err);
507
+ return isThrottle(err) || isServerSide(err) || isNetworkOrTimeout(err) || isSdkRetryable(err) || isClockSkewCorrected(err);
119
508
  }
120
509
  function transientFaultMessage(err) {
121
510
  return `transient S3 fault: ${errorName(err) ?? "unknown"}`;
@@ -136,43 +525,6 @@ function totalFromContentRange(contentRange) {
136
525
  return Number.isSafeInteger(total) ? total : void 0;
137
526
  }
138
527
 
139
- // src/read-timeout.ts
140
- import { TransientError, ValidationError as ValidationError2 } from "@cloudbitmaps/core/driver-kit";
141
- var DEFAULT_READ_TIMEOUT_MS = 0;
142
- var MAX_TIMER_MS = 2147483647;
143
- function resolveReadTimeoutMs(value) {
144
- if (value === void 0) return DEFAULT_READ_TIMEOUT_MS;
145
- if (!Number.isSafeInteger(value) || value < 0 || value > MAX_TIMER_MS) {
146
- throw new ValidationError2(
147
- `readTimeoutMs must be a non-negative safe integer no larger than ${MAX_TIMER_MS}; got ${describe(value)}`
148
- );
149
- }
150
- return value;
151
- }
152
- function describe(value) {
153
- if (typeof value === "string") return JSON.stringify(value);
154
- if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
155
- return String(value);
156
- }
157
- return value === null ? "null" : typeof value;
158
- }
159
- async function timedRead(operation, timeoutMs, read) {
160
- if (timeoutMs === 0) return read(void 0);
161
- const controller = new AbortController();
162
- let timer;
163
- const timedOut = new Promise((_, reject) => {
164
- timer = setTimeout(() => {
165
- reject(new TransientError(`S3 ${operation} timed out after ${timeoutMs} ms`));
166
- controller.abort();
167
- }, timeoutMs);
168
- });
169
- try {
170
- return await Promise.race([read({ abortSignal: controller.signal }), timedOut]);
171
- } finally {
172
- clearTimeout(timer);
173
- }
174
- }
175
-
176
528
  // src/send-once.ts
177
529
  var NO_RETRY = {
178
530
  name: "retryMiddleware",
@@ -186,7 +538,7 @@ function sendOnce(client, command) {
186
538
  }
187
539
 
188
540
  // src/bounded-body.ts
189
- import { NotFoundError } from "@cloudbitmaps/core";
541
+ import { NotFoundError } from "@cloudbitmaps/core/driver-kit";
190
542
  function destroyBody(body) {
191
543
  body?.destroy?.();
192
544
  }
@@ -224,89 +576,6 @@ async function readBounded(body, maxBytes, oversize, advertised) {
224
576
  return out;
225
577
  }
226
578
 
227
- // src/scrub-error.ts
228
- var TRANSPORT = /* @__PURE__ */ new Set([
229
- "$response",
230
- "socket",
231
- "_httpMessage",
232
- "req",
233
- "request",
234
- "connection",
235
- "agent"
236
- ]);
237
- var CREDENTIAL_NAME = /^(?:proxy-)?authorization$|^cookie$|^set-cookie$|^x-api-key$|^x-amz-security-token$|^x-amz-credential$|^x-amz-signature$/i;
238
- var CREDENTIAL_LINE = new RegExp(
239
- "^((?:proxy-)?authorization|cookie|set-cookie|x-api-key|x-amz-security-token):[^\\r\\n]*",
240
- "im"
241
- );
242
- var CREDENTIAL_LINES = new RegExp(CREDENTIAL_LINE.source, "gim");
243
- var REDACTED = "[redacted]";
244
- var MAX_DEPTH = 6;
245
- var isPlain = (value) => {
246
- const proto = Object.getPrototypeOf(value);
247
- return proto === Object.prototype || proto === null || Array.isArray(value);
248
- };
249
- function redact(node, seen, depth) {
250
- if (seen.has(node) || depth > MAX_DEPTH) return;
251
- seen.add(node);
252
- if (Array.isArray(node)) {
253
- for (let i = 0; i + 1 < node.length; i++) {
254
- const item = node[i];
255
- if (typeof item === "string" && CREDENTIAL_NAME.test(item)) node[i + 1] = REDACTED;
256
- }
257
- }
258
- for (const key of Reflect.ownKeys(node)) {
259
- try {
260
- const record = node;
261
- const value = record[key];
262
- if (typeof key === "string" && CREDENTIAL_NAME.test(key) && value != null) {
263
- record[key] = REDACTED;
264
- } else if (typeof value === "string" && CREDENTIAL_LINE.test(value)) {
265
- record[key] = value.replace(CREDENTIAL_LINES, `$1: ${REDACTED}`);
266
- } else if (typeof value === "object" && value !== null && isPlain(value)) {
267
- redact(value, seen, depth + 1);
268
- }
269
- } catch {
270
- }
271
- }
272
- }
273
- function copyOf(err, depth) {
274
- const copy = Object.create(Object.getPrototypeOf(err));
275
- for (const key of Reflect.ownKeys(err)) {
276
- if (typeof key === "string" && TRANSPORT.has(key)) continue;
277
- const descriptor = Object.getOwnPropertyDescriptor(err, key);
278
- if (descriptor === void 0) continue;
279
- if (key === "cause" && "value" in descriptor) {
280
- const cause = descriptor.value;
281
- if (typeof cause === "object" && cause !== null && depth < MAX_DEPTH) {
282
- descriptor.value = copyOf(cause, depth + 1);
283
- }
284
- }
285
- if (key === "stack") {
286
- Object.defineProperty(copy, key, {
287
- value: err.stack,
288
- writable: true,
289
- configurable: true,
290
- enumerable: false
291
- });
292
- continue;
293
- }
294
- Object.defineProperty(copy, key, descriptor);
295
- }
296
- return copy;
297
- }
298
- function holdsTransport(err, depth) {
299
- if (Object.getOwnPropertyNames(err).some((key) => TRANSPORT.has(key))) return true;
300
- const cause = err.cause;
301
- return typeof cause === "object" && cause !== null && depth < MAX_DEPTH ? holdsTransport(cause, depth + 1) : false;
302
- }
303
- function scrubCredentials(err) {
304
- if (typeof err !== "object" || err === null) return err;
305
- const safe = holdsTransport(err, 0) ? copyOf(err, 0) : err;
306
- redact(safe, /* @__PURE__ */ new WeakSet(), 0);
307
- return safe;
308
- }
309
-
310
579
  // src/client-probe.ts
311
580
  import { DeleteObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
312
581
  var PROBE_KEY = "cloudbitmaps-probe";
@@ -410,15 +679,15 @@ var S3_LABELS = /* @__PURE__ */ new Set([
410
679
  var S3_DASH_REGION_LABEL = /^s3-(fips-)?[a-z]{2}(-gov)?-[a-z]+-\d$/;
411
680
  var S3_EXPRESS_LABEL = /^s3express-[a-z0-9-]+$/;
412
681
  var REGION_LABEL = /^[a-z]{2}(-[a-z]+)+-\d+$/;
413
- var isS3Label = (label) => S3_LABELS.has(label) || S3_DASH_REGION_LABEL.test(label) || S3_EXPRESS_LABEL.test(label);
682
+ var isS3Label = (label2) => S3_LABELS.has(label2) || S3_DASH_REGION_LABEL.test(label2) || S3_EXPRESS_LABEL.test(label2);
414
683
  function isAwsS3Host(hostname) {
415
684
  const host = hostname.toLowerCase().replace(/\.$/, "");
416
685
  const domain = AWS_DOMAINS.find((d) => host.endsWith(d));
417
686
  if (domain === void 0) return false;
418
687
  const labels = host.slice(0, -domain.length).split(".");
419
688
  let at = -1;
420
- labels.forEach((label, i) => {
421
- if (isS3Label(label)) at = i;
689
+ labels.forEach((label2, i) => {
690
+ if (isS3Label(label2)) at = i;
422
691
  });
423
692
  if (at === -1) return false;
424
693
  const rest = labels.slice(at + 1);
@@ -440,6 +709,10 @@ var THROTTLE_BASE_DELAY_MS = 500;
440
709
  var REAL_TIME = {
441
710
  sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
442
711
  };
712
+ function isTypedPassThrough(err) {
713
+ const kind = errorKind(err);
714
+ return kind === "ValidationError" || kind === "WriteConflictError" || kind === "NotFoundError";
715
+ }
443
716
  var REAL_RNG = { next: () => Math.random() };
444
717
  var S3StorageDriver = class {
445
718
  client;
@@ -516,9 +789,7 @@ var S3StorageDriver = class {
516
789
  `generation already exists (write-once): ${key.segment}.${key.generation}`
517
790
  );
518
791
  }
519
- if (isValidationError(err) || isWriteConflictError(err) || isNotFoundError(err)) {
520
- throw err;
521
- }
792
+ if (isTypedPassThrough(err)) throw err;
522
793
  throw this.mapError(err);
523
794
  }
524
795
  }
@@ -649,7 +920,7 @@ var S3StorageDriver = class {
649
920
  }
650
921
  async delete(key, options) {
651
922
  const objectKey = storageObjectKey(this.prefix, key);
652
- const ifVersion = options?.ifVersion;
923
+ const ifVersion = checkDeleteOptions(options);
653
924
  if (ifVersion !== void 0 && this.explicitConditionalDelete !== false) await this.settle();
654
925
  const conditional = ifVersion !== void 0 && appliesDeleteIfMatch(this.explicitConditionalDelete, this.facts);
655
926
  try {
@@ -680,7 +951,7 @@ var S3StorageDriver = class {
680
951
  await this.head(key, objectKey);
681
952
  return true;
682
953
  } catch (err) {
683
- if (isNotFoundError(err)) return false;
954
+ if (errorKind(err) === "NotFoundError") return false;
684
955
  throw err;
685
956
  }
686
957
  }
@@ -719,7 +990,7 @@ var S3StorageDriver = class {
719
990
  /** Map S3 read errors to the driver vocabulary; pass everything else through {@link mapError}. */
720
991
  mapReadError(err, key) {
721
992
  err = scrubCredentials(err);
722
- if (isValidationError(err)) return err;
993
+ if (errorKind(err) === "ValidationError") return err;
723
994
  if (isNotFound(err)) {
724
995
  return new NotFoundError2(`no such generation: ${key.segment}.${key.generation}`);
725
996
  }
@@ -793,7 +1064,7 @@ var S3MultipartSink = class {
793
1064
  })
794
1065
  );
795
1066
  if (res2.UploadId === void 0) {
796
- throw new TransientError2("S3 CreateMultipartUpload returned no UploadId");
1067
+ throw new TransientError3("S3 CreateMultipartUpload returned no UploadId");
797
1068
  }
798
1069
  this.uploadId = res2.UploadId;
799
1070
  }
@@ -881,7 +1152,7 @@ var S3MultipartSink = class {
881
1152
  const stored = await this.storedObject();
882
1153
  if (stored?.id === this.writeId) return;
883
1154
  if (stored === void 0 && !isPreconditionFailed(err)) {
884
- throw new TransientError2(
1155
+ throw new TransientError3(
885
1156
  `S3 ${operation}: ${this.objectKey} is not stored, and the first send may still be applying`,
886
1157
  { cause: err }
887
1158
  );
@@ -891,7 +1162,7 @@ var S3MultipartSink = class {
891
1162
  }
892
1163
  if (!isThrottle(err)) throw err;
893
1164
  if (resent >= THROTTLE_RESENDS) {
894
- throw new TransientError2(
1165
+ throw new TransientError3(
895
1166
  `S3 ${operation} was throttled on each of its ${resent + 1} sends; the object may or may not exist`,
896
1167
  { cause: err }
897
1168
  );
@@ -936,8 +1207,8 @@ var S3MultipartSink = class {
936
1207
 
937
1208
  // src/registry.ts
938
1209
  import {
1210
+ errorKind as errorKind2,
939
1211
  IntegrityError as IntegrityError2,
940
- isIntegrityError,
941
1212
  MAX_ROW_BYTES,
942
1213
  ObjectStoreRegistry,
943
1214
  ValidationError as ValidationError4,
@@ -1021,7 +1292,7 @@ var S3RegistryStore = class {
1021
1292
  return { bytes, version: res.ETag ?? "" };
1022
1293
  } catch (err) {
1023
1294
  destroyBody(res.Body);
1024
- throw isIntegrityError(err) ? err : mapError(err, this.bucket);
1295
+ throw errorKind2(err) === "IntegrityError" ? err : mapError(err, this.bucket);
1025
1296
  }
1026
1297
  });
1027
1298
  }
@@ -1200,6 +1471,48 @@ function emit(sink, event) {
1200
1471
  }
1201
1472
 
1202
1473
  // src/backend.ts
1474
+ function checkCredentials(credentials) {
1475
+ if (credentials === void 0) return;
1476
+ if (typeof credentials === "function") {
1477
+ throw new ValidationError5(
1478
+ "S3Storage: `credentials` must be an object of `accessKeyId` and `secretAccessKey`, not a provider function; pass a provider on an S3Client given as `client`"
1479
+ );
1480
+ }
1481
+ const keys = [
1482
+ "accessKeyId",
1483
+ "secretAccessKey",
1484
+ "sessionToken",
1485
+ "$source",
1486
+ "credentialScope",
1487
+ "accountId",
1488
+ "expiration"
1489
+ ];
1490
+ refuseUnknownOptions("S3Storage credentials", credentials, keys);
1491
+ const given = credentials;
1492
+ const needText = (key, value) => {
1493
+ if (typeof value !== "string" || value.trim().length === 0) {
1494
+ throw new ValidationError5(
1495
+ `S3Storage: \`credentials.${key}\` must be a non-empty string; got ${typeof value === "string" ? "an empty string" : value === null || value === void 0 ? String(value) : `a ${typeof value}`}`
1496
+ );
1497
+ }
1498
+ };
1499
+ needText("accessKeyId", given.accessKeyId);
1500
+ needText("secretAccessKey", given.secretAccessKey);
1501
+ if (given.sessionToken !== void 0) needText("sessionToken", given.sessionToken);
1502
+ }
1503
+ function checkEndpointUrl(endpoint) {
1504
+ let url;
1505
+ try {
1506
+ url = new URL(endpoint);
1507
+ } catch {
1508
+ url = void 0;
1509
+ }
1510
+ if (url === void 0 || !/^https?:$/.test(url.protocol) || url.hostname.length === 0) {
1511
+ throw new ValidationError5(
1512
+ 'S3Storage: `endpoint` must be an http or https URL such as "http://localhost:9000"'
1513
+ );
1514
+ }
1515
+ }
1203
1516
  var S3_STORAGE_OPTION_KEYS = [
1204
1517
  "bucket",
1205
1518
  "prefix",
@@ -1247,6 +1560,18 @@ var S3Storage = class {
1247
1560
  /** The client both halves share — built here unless one was supplied. */
1248
1561
  client;
1249
1562
  sockets;
1563
+ bucket;
1564
+ /** The key prefix every object sits under: `''`, or `trimmed/`. */
1565
+ keyPrefix;
1566
+ readTimeoutMs;
1567
+ /**
1568
+ * Builds the S3 backend over one bucket, which `bucket` names. Pass `client`, or `region`, `endpoint`, `pathStyle`,
1569
+ * `credentials` and `maxSockets` to have one built, not both. `prefix` puts every object under a key prefix. Throws
1570
+ * `ValidationError` for an unknown option, no bucket, a `now` that is not a function, a client setting beside
1571
+ * `client`, a `maxSockets` (with no `client`) that is not a positive integer, a `partBytes` or `maxObjectBytes` that
1572
+ * is not a positive integer, a `conditionalDelete` that is not a boolean, a `region` or `endpoint` that is empty or not a string, a `pathStyle` that
1573
+ * is not a boolean, `credentials` that are not complete, or an invalid `prefix` or `readTimeoutMs`.
1574
+ */
1250
1575
  constructor(options) {
1251
1576
  refuseUnknownOptions(
1252
1577
  "S3Storage",
@@ -1254,7 +1579,7 @@ var S3Storage = class {
1254
1579
  S3_STORAGE_OPTION_KEYS,
1255
1580
  "an S3 client goes in `client`"
1256
1581
  );
1257
- if (typeof options.bucket !== "string" || options.bucket.length === 0) {
1582
+ if (typeof options.bucket !== "string" || options.bucket.trim().length === 0) {
1258
1583
  throw new ValidationError5("S3Storage needs a `bucket`: the name of one, a non-empty string");
1259
1584
  }
1260
1585
  if (options.now !== void 0 && typeof options.now !== "function") {
@@ -1269,9 +1594,24 @@ var S3Storage = class {
1269
1594
  `S3Storage takes \`client\` OR ${CLIENT_SETTINGS.map((k) => `\`${k}\``).join(" / ")}, not both \u2014 got \`client\` with ${ignored.map((k) => `\`${k}\``).join(", ")}; the \`client\` already carries them, so configure them on the client, or drop \`client\``
1270
1595
  );
1271
1596
  }
1597
+ if (typeof options.client.send !== "function") {
1598
+ throw new ValidationError5(
1599
+ "S3Storage: `client` must be an S3Client from @aws-sdk/client-s3 (an object with `send`)"
1600
+ );
1601
+ }
1272
1602
  this.client = options.client;
1273
1603
  } else {
1274
1604
  const maxSockets = requirePositiveInt2("maxSockets", options.maxSockets) ?? DEFAULT_MAX_SOCKETS;
1605
+ const region = options.region;
1606
+ if (region !== void 0 && (typeof region !== "string" || region.trim().length === 0)) {
1607
+ throw new ValidationError5(
1608
+ "S3Storage's `region` must be a non-empty string such as 'us-east-1'"
1609
+ );
1610
+ }
1611
+ const endpoint = requireNonEmptyString("S3Storage: `endpoint`", options.endpoint);
1612
+ if (endpoint !== void 0) checkEndpointUrl(endpoint);
1613
+ requireBool3("S3Storage: `pathStyle`", options.pathStyle);
1614
+ checkCredentials(options.credentials);
1275
1615
  this.client = new S3Client(
1276
1616
  defined({
1277
1617
  credentials: options.credentials,
@@ -1282,6 +1622,9 @@ var S3Storage = class {
1282
1622
  );
1283
1623
  limitSockets(this.client, maxSockets);
1284
1624
  }
1625
+ this.bucket = options.bucket;
1626
+ this.keyPrefix = prefixPart(normalizeObjectPrefix3(options.prefix));
1627
+ this.readTimeoutMs = resolveReadTimeoutMs(options.readTimeoutMs);
1285
1628
  this.sockets = new SocketAdvisory(this.client, options.bucket);
1286
1629
  const shared = {
1287
1630
  client: this.client,
@@ -1310,6 +1653,18 @@ var S3Storage = class {
1310
1653
  attachMetrics(sink) {
1311
1654
  this.sockets.attach(sink);
1312
1655
  }
1656
+ /**
1657
+ * Reads the bucket's lifecycle configuration (one `GetBucketLifecycleConfiguration` request, plus the SDK's own
1658
+ * retries, bounded by `readTimeoutMs` when it is set) and reports a rule that would expire, or move to GLACIER,
1659
+ * DEEP_ARCHIVE or a class it does not know, the current objects under this store's prefix, generations and registry
1660
+ * rows alike. Needs `s3:GetLifecycleConfiguration`, which a load never uses (`store.preflight()`'s
1661
+ * lifecycle step does): without it the
1662
+ * answer is `unchecked`, naming the permission. A missing rule that aborts incomplete multipart uploads is advice in
1663
+ * `detail`, not a problem. Never rejects; a `detail` carries no credential. Rule reading: see the production guide.
1664
+ */
1665
+ checkLifecycle() {
1666
+ return checkS3Lifecycle(this.client, this.bucket, this.keyPrefix, this.readTimeoutMs);
1667
+ }
1313
1668
  };
1314
1669
  export {
1315
1670
  S3Storage