@cloudbitmaps/gcs 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,10 @@
2
2
  import {
3
3
  ValidationError as ValidationError5,
4
4
  brandAsBackend,
5
- refuseUnknownOptions
5
+ normalizeObjectPrefix as normalizeObjectPrefix3,
6
+ prefixPart,
7
+ refuseUnknownOptions,
8
+ requireNonEmptyString
6
9
  } from "@cloudbitmaps/core/driver-kit";
7
10
  import { Storage as GcsClient } from "@google-cloud/storage";
8
11
 
@@ -11,18 +14,538 @@ function defined(values) {
11
14
  return Object.fromEntries(Object.entries(values).filter(([, v]) => v !== void 0));
12
15
  }
13
16
 
17
+ // src/read-timeout.ts
18
+ import { ValidationError } from "@cloudbitmaps/core/driver-kit";
19
+ var DEFAULT_READ_TIMEOUT_MS = 0;
20
+ var MAX_TIMER_MS = 2147483647;
21
+ function resolveReadTimeoutMs(value) {
22
+ if (value === void 0) return DEFAULT_READ_TIMEOUT_MS;
23
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0 || value > MAX_TIMER_MS) {
24
+ throw new ValidationError(
25
+ `readTimeoutMs must be a non-negative safe integer no larger than ${MAX_TIMER_MS}; got ${describe(value)}`
26
+ );
27
+ }
28
+ return value;
29
+ }
30
+ function describe(value) {
31
+ if (typeof value === "string") return JSON.stringify(value);
32
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
33
+ return String(value);
34
+ }
35
+ return value === null ? "null" : typeof value;
36
+ }
37
+ var ReadTimedOut = class extends Error {
38
+ code = "ETIMEDOUT";
39
+ constructor(read, ms, cause) {
40
+ super(`GCS ${read} timed out after ${ms} ms`, cause === void 0 ? void 0 : { cause });
41
+ this.name = "ReadTimedOut";
42
+ }
43
+ };
44
+ var Deadline = class {
45
+ constructor(ms, read) {
46
+ this.ms = ms;
47
+ this.read = read;
48
+ this.endsAt = performance.now() + ms;
49
+ }
50
+ ms;
51
+ read;
52
+ endsAt;
53
+ /** What is left of it, in milliseconds: 0 once it has passed. */
54
+ remaining() {
55
+ return Math.max(0, this.endsAt - performance.now());
56
+ }
57
+ /** The error a read fails with once this has passed; `cause` is the last attempt's own error, if one failed. */
58
+ expired(cause) {
59
+ return new ReadTimedOut(this.read, this.ms, cause);
60
+ }
61
+ };
62
+ function startDeadline(ms, read) {
63
+ return ms === 0 ? void 0 : new Deadline(ms, read);
64
+ }
65
+ function withDeadline(send, deadline) {
66
+ if (deadline === void 0) return send();
67
+ if (deadline.remaining() === 0) return Promise.reject(deadline.expired());
68
+ const request = send();
69
+ let timer;
70
+ const timedOut = new Promise((_, reject) => {
71
+ timer = setTimeout(() => reject(deadline.expired()), deadline.remaining());
72
+ });
73
+ return Promise.race([request, timedOut]).finally(() => clearTimeout(timer));
74
+ }
75
+
76
+ // src/scrub-error.ts
77
+ import { types } from "node:util";
78
+ var TRANSPORT = /* @__PURE__ */ new Set([
79
+ "request",
80
+ "agent",
81
+ "socket",
82
+ "_httpMessage",
83
+ "connection",
84
+ "req",
85
+ "res",
86
+ "config"
87
+ ]);
88
+ var RESPONSE_FIELDS = ["status", "statusText", "statusCode", "statusMessage"];
89
+ var CREDENTIAL_NAMES = [
90
+ "authorization",
91
+ "proxy-authorization",
92
+ "cookie",
93
+ "set-cookie",
94
+ "x-api-key",
95
+ "x-goog-api-key",
96
+ "x-goog-encryption-key",
97
+ "x-goog-copy-source-encryption-key",
98
+ "encryption-key",
99
+ "encryption-key-base64",
100
+ "x-goog-signature",
101
+ "x-goog-credential",
102
+ "signature",
103
+ "signature-provided",
104
+ "canonical-request",
105
+ "token",
106
+ "access-token",
107
+ "refresh-token",
108
+ "id-token",
109
+ "subject-token",
110
+ "client-secret",
111
+ "private-key",
112
+ "assertion"
113
+ ];
114
+ var fold = (name) => name.trim().replace(/:$/, "").replace(/[-_]/g, "").toLowerCase();
115
+ var CREDENTIAL_NAME = new Set(CREDENTIAL_NAMES.map(fold));
116
+ var LONGEST_NAME = 64;
117
+ var CREDENTIAL_LINES = /^((?:proxy-)?authorization|cookie|set-cookie|x-api-key|x-goog-api-key|x-goog-(?:copy-source-)?encryption-key):[^\r\n]*/gim;
118
+ var AUTHORIZATION_VALUE = /((?:proxy-)?authorization\\?["']?\s*(?::|=|%3D)\s*\\?["']?)(?:[A-Za-z][\w-]*(?:\s+|%20|\+))?[^\s&"'<>\\,;]+/gi;
119
+ var COOKIE_VALUE = /(?<![\w-])((?:set-)?cookie\\?["']?\s*:\s*\\?["']?)[^\r\n"'<>\\]+/gi;
120
+ var NAMED = "x-goog-(?:copy-source-)?encryption-key|x-goog-api-key|x-goog-signature|x-goog-credential|(?:access|refresh|id|subject)[-_]token|client[-_]secret";
121
+ var NAMED_SECRET = new RegExp(
122
+ `((?:${NAMED})\\\\?["']?\\s*(?::|=|%3D)\\s*\\\\?["']?)[^\\s&"'<>\\\\,;]+`,
123
+ "gi"
124
+ );
125
+ var BEARER = /\b(Bearer)(\s+|%20|\+)(?=[A-Za-z0-9\-._~+/]{16})[A-Za-z0-9\-._~+/]+=*/gi;
126
+ var TOKEN_TEXT = /ya29\.(?=[A-Za-z0-9_.-]{16})[A-Za-z0-9_.-]+|(?<![A-Za-z0-9_-])eyJ(?=[A-Za-z0-9_-]{8})[A-Za-z0-9_-]+\.eyJ(?=[A-Za-z0-9_-]{8})[A-Za-z0-9_-]+\.(?=[A-Za-z0-9_-]{16})[A-Za-z0-9_-]+/g;
127
+ var SIGNATURE_NAME = /Signature(?:=|%3D)/gi;
128
+ var SIGNATURE_RUN = /[A-Za-z0-9+/=_.-]*/y;
129
+ var SIGNATURE_MIN = 16;
130
+ var PEM_OPEN = /-----BEGIN [A-Z0-9 ]{0,40}PRIVATE KEY-----/y;
131
+ var PEM_CLOSE = /-----END [A-Z0-9 ]{0,40}PRIVATE KEY-----/y;
132
+ var ELEMENT = /(<([A-Za-z][\w.:-]{0,63})>)([^<]*)(<\/\2>)/g;
133
+ var CELL = /(<([A-Za-z][\w-]{0,15})\b[^<>]{0,256}>\s{0,32}([^<>\s](?:[^<>]{0,62}[^<>\s])?)\s{0,32}<\/\2\s{0,32}>(?:\s{0,32}<\/[A-Za-z][\w-]{0,15}\s{0,32}>){0,4}(?:\s{0,32}<[A-Za-z][\w-]{0,15}\b[^<>]{0,256}>){1,4})([^<]*)/g;
134
+ var REDACTED = "[redacted]";
135
+ var MAX_DEPTH = 10;
136
+ var isPlain = (value) => {
137
+ const proto = Object.getPrototypeOf(value);
138
+ return proto === Object.prototype || proto === null;
139
+ };
140
+ var isError = (value) => value instanceof Error || types.isNativeError(value);
141
+ var isCredentialName = (text) => text.length <= LONGEST_NAME && CREDENTIAL_NAME.has(fold(text));
142
+ function redactText(text) {
143
+ try {
144
+ const named = redactPrivateKeys(text).replace(CREDENTIAL_LINES, `$1: ${REDACTED}`).replace(AUTHORIZATION_VALUE, `$1${REDACTED}`).replace(COOKIE_VALUE, `$1${REDACTED}`).replace(NAMED_SECRET, `$1${REDACTED}`).replace(BEARER, `$1$2${REDACTED}`).replace(TOKEN_TEXT, REDACTED);
145
+ return redactCells(
146
+ redactSignatures(named).replace(
147
+ ELEMENT,
148
+ (whole, open, tag, inner, close) => isCredentialName(tag) && inner.trim() !== "" ? `${open}${REDACTED}${close}` : whole
149
+ )
150
+ );
151
+ } catch {
152
+ return REDACTED;
153
+ }
154
+ }
155
+ function redactPrivateKeys(text) {
156
+ let out = "";
157
+ let from = 0;
158
+ for (let at = text.indexOf("-----BEGIN "); at !== -1; at = text.indexOf("-----BEGIN ", Math.max(at + 1, from))) {
159
+ PEM_OPEN.lastIndex = at;
160
+ const open = PEM_OPEN.exec(text);
161
+ if (open === null) continue;
162
+ const body = at + open[0].length;
163
+ const end = text.indexOf("-----END", body);
164
+ let close = "";
165
+ if (end !== -1) {
166
+ PEM_CLOSE.lastIndex = end;
167
+ close = PEM_CLOSE.exec(text)?.[0] ?? "";
168
+ }
169
+ out += `${text.slice(from, body)}${REDACTED}${close}`;
170
+ from = end === -1 ? text.length : end + close.length;
171
+ }
172
+ return from === 0 ? text : out + text.slice(from);
173
+ }
174
+ function redactSignatures(text) {
175
+ let out = "";
176
+ let from = 0;
177
+ SIGNATURE_NAME.lastIndex = 0;
178
+ for (let m = SIGNATURE_NAME.exec(text); m !== null; m = SIGNATURE_NAME.exec(text)) {
179
+ const start = m.index + m[0].length;
180
+ let end = start;
181
+ for (; ; ) {
182
+ SIGNATURE_RUN.lastIndex = end;
183
+ SIGNATURE_RUN.exec(text);
184
+ end = SIGNATURE_RUN.lastIndex;
185
+ if (text[end] !== "%" || text.startsWith("%26", end)) break;
186
+ end += 1;
187
+ }
188
+ if (end - start >= SIGNATURE_MIN) {
189
+ out += `${text.slice(from, start)}${REDACTED}`;
190
+ from = end;
191
+ }
192
+ SIGNATURE_NAME.lastIndex = end;
193
+ }
194
+ return from === 0 ? text : out + text.slice(from);
195
+ }
196
+ function redactCells(text) {
197
+ let out = "";
198
+ let from = 0;
199
+ CELL.lastIndex = 0;
200
+ for (let m = CELL.exec(text); m !== null; m = CELL.exec(text)) {
201
+ const [whole, head, , name, value] = m;
202
+ if (isCredentialName(name) && value.trim() !== "") {
203
+ out += `${text.slice(from, m.index)}${head}${REDACTED}`;
204
+ from = m.index + whole.length;
205
+ } else {
206
+ CELL.lastIndex = m.index + 1;
207
+ }
208
+ }
209
+ return from === 0 ? text : out + text.slice(from);
210
+ }
211
+ var OMIT = /* @__PURE__ */ Symbol("omit");
212
+ var INDEX = /^(?:0|[1-9]\d*)$/;
213
+ var isIndex = (key) => typeof key === "string" && INDEX.test(key) && Number(key) < 2 ** 32 - 1;
214
+ function hasNamedKeys(value) {
215
+ if (!Array.isArray(value)) return Reflect.ownKeys(value).length > 0;
216
+ return Reflect.ownKeys(value).some((key) => key !== "length" && !isIndex(key));
217
+ }
218
+ function define(copy, key, value, enumerable) {
219
+ Object.defineProperty(copy, key, { value, writable: true, configurable: true, enumerable });
220
+ }
221
+ function copyOf(value, ctx, depth) {
222
+ if (typeof value === "string") {
223
+ const clean = redactText(value);
224
+ if (clean !== value) ctx.changed = true;
225
+ return clean;
226
+ }
227
+ if (typeof value === "function" || typeof value === "symbol") {
228
+ ctx.changed = true;
229
+ return OMIT;
230
+ }
231
+ if (typeof value !== "object" || value === null) return value;
232
+ if (ctx.copies.has(value)) return ctx.copies.get(value);
233
+ if (depth > MAX_DEPTH) {
234
+ ctx.changed = true;
235
+ return OMIT;
236
+ }
237
+ if (Array.isArray(value)) return copyList(value, ctx, depth);
238
+ if (value instanceof Map) return copyMap(value, ctx, depth);
239
+ if (value instanceof Set) return copySet(value, ctx, depth);
240
+ return copyRecord(value, ctx, depth);
241
+ }
242
+ function copyList(value, ctx, depth) {
243
+ const list = new Array(value.length);
244
+ ctx.copies.set(value, list);
245
+ if (hasNamedKeys(value)) ctx.changed = true;
246
+ let hidden = false;
247
+ let last = -1;
248
+ for (const key of Reflect.ownKeys(value)) {
249
+ if (!isIndex(key)) continue;
250
+ const index = Number(key);
251
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
252
+ if (descriptor === void 0 || !("value" in descriptor)) {
253
+ ctx.changed = true;
254
+ hidden = false;
255
+ continue;
256
+ }
257
+ const item = descriptor.value;
258
+ if (hidden && index === last + 1 && item != null) {
259
+ list[index] = REDACTED;
260
+ if (item !== REDACTED) ctx.changed = true;
261
+ } else {
262
+ const kept = copyOf(item, ctx, depth + 1);
263
+ if (kept === OMIT) delete list[index];
264
+ else list[index] = kept;
265
+ }
266
+ hidden = typeof item === "string" && isCredentialName(item);
267
+ last = index;
268
+ }
269
+ return list;
270
+ }
271
+ function copyMap(value, ctx, depth) {
272
+ const map = /* @__PURE__ */ new Map();
273
+ ctx.copies.set(value, map);
274
+ if (hasNamedKeys(value)) ctx.changed = true;
275
+ for (const [k, v] of value) {
276
+ const key = copyOf(k, ctx, depth + 1);
277
+ if (key === OMIT) continue;
278
+ if (typeof k === "string" && isCredentialName(k) && v != null) {
279
+ map.set(key, REDACTED);
280
+ if (v !== REDACTED) ctx.changed = true;
281
+ continue;
282
+ }
283
+ const kept = copyOf(v, ctx, depth + 1);
284
+ if (kept !== OMIT) map.set(key, kept);
285
+ }
286
+ return map;
287
+ }
288
+ function copySet(value, ctx, depth) {
289
+ const set = /* @__PURE__ */ new Set();
290
+ ctx.copies.set(value, set);
291
+ if (hasNamedKeys(value)) ctx.changed = true;
292
+ for (const v of value) {
293
+ const kept = copyOf(v, ctx, depth + 1);
294
+ if (kept !== OMIT) set.add(kept);
295
+ }
296
+ return set;
297
+ }
298
+ function copyRecord(value, ctx, depth) {
299
+ const errorLike = isError(value);
300
+ if (!errorLike && !isPlain(value)) {
301
+ ctx.changed = true;
302
+ return OMIT;
303
+ }
304
+ const copy = errorLike ? Object.setPrototypeOf(new Error(), Object.getPrototypeOf(value)) : Object.create(Object.getPrototypeOf(value));
305
+ ctx.copies.set(value, copy);
306
+ if (errorLike) delete copy.stack;
307
+ for (const key of Reflect.ownKeys(value)) {
308
+ if (typeof key === "string" && TRANSPORT.has(key)) {
309
+ ctx.changed = true;
310
+ } else if (errorLike && key === "stack") {
311
+ copyStack(value, copy, ctx, depth);
312
+ } else {
313
+ copyField(value, copy, key, ctx, depth);
314
+ }
315
+ }
316
+ if (errorLike) keepSlotFields(value, copy, ctx, depth);
317
+ redactPairs(value, copy, errorLike, ctx);
318
+ return copy;
319
+ }
320
+ var DESCRIBING = /* @__PURE__ */ new Set([
321
+ "message",
322
+ "domain",
323
+ "reason",
324
+ "location",
325
+ "locationType",
326
+ "extendedHelp"
327
+ ]);
328
+ function redactPairs(original, copy, errorLike, ctx) {
329
+ const names = Reflect.ownKeys(original).filter((key) => {
330
+ if (typeof key !== "string" || DESCRIBING.has(key)) return false;
331
+ const held = Object.getOwnPropertyDescriptor(original, key)?.value;
332
+ return typeof held === "string" && isCredentialName(held);
333
+ });
334
+ if (names.length === 0) return;
335
+ const values = errorLike ? names.filter((key) => /name$/i.test(key)).map((key) => `${key.slice(0, -4)}value`.toLowerCase()) : void 0;
336
+ for (const key of Reflect.ownKeys(copy)) {
337
+ if (typeof key !== "string" || names.includes(key) || DESCRIBING.has(key)) continue;
338
+ if (values !== void 0 && !values.includes(key.toLowerCase())) continue;
339
+ const descriptor = Object.getOwnPropertyDescriptor(copy, key);
340
+ if (descriptor?.value == null || descriptor.value === REDACTED) continue;
341
+ if (typeof descriptor.value !== "string" && typeof descriptor.value !== "object") continue;
342
+ define(copy, key, REDACTED, descriptor.enumerable ?? false);
343
+ ctx.changed = true;
344
+ }
345
+ }
346
+ function isSummary(value) {
347
+ return isPlain(value) && Reflect.ownKeys(value).every((key) => {
348
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
349
+ const held = descriptor?.value;
350
+ return typeof key === "string" && RESPONSE_FIELDS.includes(key) && (typeof held === "number" || typeof held === "string");
351
+ });
352
+ }
353
+ function copyResponse(response, ctx, depth) {
354
+ if (isSummary(response)) return copyOf(response, ctx, depth);
355
+ ctx.changed = true;
356
+ const summary = {};
357
+ for (const key of RESPONSE_FIELDS) {
358
+ let held;
359
+ try {
360
+ held = response[key];
361
+ } catch {
362
+ continue;
363
+ }
364
+ if (typeof held === "number" || typeof held === "string") {
365
+ summary[key] = copyOf(held, ctx, depth + 1);
366
+ }
367
+ }
368
+ return summary;
369
+ }
370
+ function copyStack(original, copy, ctx, depth) {
371
+ let stack;
372
+ try {
373
+ stack = original.stack;
374
+ } catch {
375
+ return;
376
+ }
377
+ const kept = copyOf(stack, ctx, depth + 1);
378
+ if (kept !== OMIT) define(copy, "stack", kept, false);
379
+ }
380
+ function copyField(original, copy, key, ctx, depth) {
381
+ const descriptor = Object.getOwnPropertyDescriptor(original, key);
382
+ if (descriptor === void 0) return;
383
+ if (!("value" in descriptor)) {
384
+ ctx.changed = true;
385
+ return;
386
+ }
387
+ const name = typeof key === "string" ? redactText(key) : key;
388
+ if (name !== key) ctx.changed = true;
389
+ const held = descriptor.value;
390
+ let kept;
391
+ if (typeof key === "string" && isCredentialName(key) && held != null) {
392
+ kept = REDACTED;
393
+ if (held !== REDACTED) ctx.changed = true;
394
+ } else if (key === "response" && typeof held === "object" && held !== null) {
395
+ kept = copyResponse(held, ctx, depth + 1);
396
+ } else {
397
+ kept = copyOf(held, ctx, depth + 1);
398
+ if (kept === OMIT) return;
399
+ }
400
+ Object.defineProperty(copy, name, {
401
+ value: kept,
402
+ writable: descriptor.writable ?? true,
403
+ configurable: true,
404
+ enumerable: descriptor.enumerable ?? false
405
+ });
406
+ }
407
+ function keepSlotFields(original, copy, ctx, depth) {
408
+ for (const key of ["name", "message", "code"]) {
409
+ if (Object.prototype.hasOwnProperty.call(original, key)) continue;
410
+ let held;
411
+ try {
412
+ held = original[key];
413
+ } catch {
414
+ continue;
415
+ }
416
+ let read = OMIT;
417
+ try {
418
+ read = copy[key];
419
+ } catch {
420
+ }
421
+ if (read === held) continue;
422
+ const kept = copyOf(held, ctx, depth + 1);
423
+ if (kept !== OMIT) define(copy, key, kept, false);
424
+ }
425
+ }
426
+ function scrubCredentials(err) {
427
+ if (typeof err !== "object" || err === null) return err;
428
+ const ctx = { copies: /* @__PURE__ */ new WeakMap(), changed: false };
429
+ const copy = copyOf(err, ctx, 0);
430
+ return ctx.changed && copy !== OMIT ? copy : err;
431
+ }
432
+
433
+ // src/lifecycle.ts
434
+ var MAX_MESSAGE = 200;
435
+ var MAX_FIELD = 64;
436
+ var MAX_CONDITION = 120;
437
+ var MAX_NAMED_RULES = 3;
438
+ var cut = (text, max) => text.length > max ? text.slice(0, max) : text;
439
+ var SAFE_ACTIONS = /* @__PURE__ */ new Set([
440
+ "SetStorageClass",
441
+ "AbortIncompleteMultipartUpload"
442
+ ]);
443
+ var NAME_SUFFIXES = [".crbm", ".reg"];
444
+ var prefixRelated = (a, b) => a.startsWith(b) || b.startsWith(a);
445
+ var suffixRelated = (a, b) => a.endsWith(b) || b.endsWith(a);
446
+ var strings = (value) => Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
447
+ var record = (value) => typeof value === "object" && value !== null ? value : {};
448
+ var NONCURRENT_ONLY = ["daysSinceNoncurrentTime", "noncurrentTimeBefore"];
449
+ var metByLiveObject = (value) => typeof value !== "number" || value < 1;
450
+ var actionOf = (rule) => {
451
+ const type = record(record(rule).action).type;
452
+ return typeof type === "string" && type !== "" ? type : void 0;
453
+ };
454
+ var isRule = (rule) => typeof rule === "object" && rule !== null;
455
+ function deletesOurs(rule, prefix) {
456
+ if (!isRule(rule)) return false;
457
+ const type = actionOf(rule);
458
+ if (type !== void 0 && SAFE_ACTIONS.has(type)) return false;
459
+ const when = record(record(rule).condition);
460
+ if (when.isLive === false) return false;
461
+ if (NONCURRENT_ONLY.some((key) => when[key] !== void 0)) return false;
462
+ if (!metByLiveObject(when.numNewerVersions)) return false;
463
+ const prefixes = strings(when.matchesPrefix);
464
+ if (prefixes.length > 0 && !prefixes.some((p) => prefixRelated(p, prefix))) return false;
465
+ const suffixes = strings(when.matchesSuffix);
466
+ return suffixes.length === 0 || suffixes.some((s) => NAME_SUFFIXES.some((ours) => suffixRelated(s, ours)));
467
+ }
468
+ function sitsInside(rule, prefix) {
469
+ const prefixes = strings(record(record(rule).condition).matchesPrefix);
470
+ return prefix !== "" && prefixes.length > 0 && !prefixes.some((p) => p !== prefix && prefix.startsWith(p));
471
+ }
472
+ function judgeMetadata(metadata, prefix) {
473
+ const rules = record(record(metadata).lifecycle).rule;
474
+ const found = (Array.isArray(rules) ? rules : []).filter(
475
+ (r) => deletesOurs(r, prefix)
476
+ );
477
+ if (found.length > 0) {
478
+ const named = found.slice(0, MAX_NAMED_RULES).map(
479
+ (r) => `${cut(actionOf(r) ?? "no action type", MAX_FIELD)} when ${cut(JSON.stringify(record(r).condition ?? {}), MAX_CONDITION)}`
480
+ ).join("; ");
481
+ const more = found.length > MAX_NAMED_RULES ? ` and ${found.length - MAX_NAMED_RULES} more` : "";
482
+ const quoted = JSON.stringify(cut(prefix, MAX_FIELD));
483
+ const narrow = `Narrow the rule with matchesPrefix to exclude ${quoted}, or restrict it to noncurrent versions.`;
484
+ 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.`;
485
+ const where = prefix === "" ? "The store has no prefix, so a rule on any prefix can reach it: give the store a prefix, or scope the rule away from its paths." : [
486
+ found.some((r) => !sitsInside(r, prefix)) ? narrow : "",
487
+ found.some((r) => sitsInside(r, prefix)) ? retarget : ""
488
+ ].filter((x) => x !== "").join(" ");
489
+ return {
490
+ status: "problem",
491
+ detail: `${found.length} bucket lifecycle rule(s) with a Delete action, or one this check does not know, can match objects under ${quoted}: ${named}${more}. The store keeps current objects it must be able to read back. ${where}`
492
+ };
493
+ }
494
+ return {
495
+ status: "ok",
496
+ detail: `no bucket lifecycle rule deletes live objects under ${JSON.stringify(cut(prefix, MAX_FIELD))}`
497
+ };
498
+ }
499
+ function unchecked(err) {
500
+ if (err instanceof ReadTimedOut) {
501
+ return {
502
+ status: "unchecked",
503
+ detail: "the read of the bucket metadata got no answer in time (readTimeoutMs); run the check again"
504
+ };
505
+ }
506
+ const scrubbed = scrubCredentials(err);
507
+ const { code, message, name } = record(scrubbed);
508
+ if (code === 403) {
509
+ return {
510
+ status: "unchecked",
511
+ detail: "the identity may not read the bucket metadata: grant storage.buckets.get on the bucket to check its lifecycle rules, or confirm by hand that no rule deletes current objects under this prefix"
512
+ };
513
+ }
514
+ const shown = typeof message === "string" ? `: ${message.slice(0, MAX_MESSAGE)}` : "";
515
+ const label = typeof name === "string" ? cut(name, MAX_FIELD) : "Error";
516
+ const status = typeof code === "number" ? ` ${code}` : "";
517
+ return {
518
+ status: "unchecked",
519
+ detail: `could not read the bucket metadata (${label}${status}${shown}); check the lifecycle rules by hand`
520
+ };
521
+ }
522
+ async function checkGcsLifecycle(client, bucket, prefix, readTimeoutMs = 0) {
523
+ try {
524
+ const [metadata] = await withDeadline(
525
+ () => client.bucket(bucket).getMetadata(),
526
+ startDeadline(readTimeoutMs, "bucket metadata read")
527
+ );
528
+ return judgeMetadata(metadata, prefix);
529
+ } catch (err) {
530
+ try {
531
+ return unchecked(err);
532
+ } catch {
533
+ return { status: "unchecked", detail: "could not read the bucket metadata" };
534
+ }
535
+ }
536
+ }
537
+
14
538
  // src/storage.ts
15
539
  import {
540
+ errorKind,
16
541
  NotFoundError,
17
542
  TransientError as TransientError2,
18
543
  ValidationError as ValidationError3,
19
544
  WriteConflictError,
20
545
  concatBytes,
21
- isNotFoundError,
22
- isValidationError,
23
- isWriteConflictError,
24
546
  normalizeObjectPrefix,
25
547
  parseGenerationFromKey,
548
+ checkDeleteOptions,
26
549
  requireBool,
27
550
  requirePositiveInt,
28
551
  segmentObjectPrefix,
@@ -37,7 +560,7 @@ import { TransientError } from "@cloudbitmaps/core/driver-kit";
37
560
 
38
561
  // src/gcs-errors.ts
39
562
  import {
40
- ValidationError,
563
+ ValidationError as ValidationError2,
41
564
  isTransportFault as isNodeTransportFault
42
565
  } from "@cloudbitmaps/core/driver-kit";
43
566
  function httpStatus(err) {
@@ -69,7 +592,7 @@ function isMissingBucket(err) {
69
592
  }
70
593
  function missingBucketError(bucket, cause) {
71
594
  return Object.assign(
72
- new ValidationError(
595
+ new ValidationError2(
73
596
  `the GCS bucket does not exist: ${bucket}; check the bucket name and the project the client is built for`
74
597
  ),
75
598
  { cause }
@@ -120,86 +643,6 @@ function transientFaultMessage(err) {
120
643
  return `transient GCS fault: ${String(e?.code ?? e?.name ?? "unknown")}`;
121
644
  }
122
645
 
123
- // src/scrub-error.ts
124
- var DROPPED = /* @__PURE__ */ new Set([
125
- "request",
126
- "agent",
127
- "socket",
128
- "_httpMessage",
129
- "connection",
130
- "req",
131
- "res",
132
- "config"
133
- ]);
134
- var CREDENTIAL_NAME = /^(?:proxy-)?authorization$|^cookie$|^set-cookie$|^x-api-key$/i;
135
- var CREDENTIAL_LINES = /^((?:proxy-)?authorization|cookie|set-cookie|x-api-key):[^\r\n]*/gim;
136
- var REDACTED = "[redacted]";
137
- var isPlain = (value) => {
138
- const proto = Object.getPrototypeOf(value);
139
- return proto === Object.prototype || proto === null;
140
- };
141
- function responseSummary(response) {
142
- const out = {};
143
- for (const key of ["status", "statusText"]) {
144
- const value = response[key];
145
- if (typeof value === "number" || typeof value === "string") out[key] = value;
146
- }
147
- return out;
148
- }
149
- function clean(value, copies) {
150
- if (typeof value === "string") return value.replace(CREDENTIAL_LINES, `$1: ${REDACTED}`);
151
- if (typeof value !== "object" || value === null) {
152
- return typeof value === "function" || typeof value === "symbol" ? void 0 : value;
153
- }
154
- if (copies.has(value)) return copies.get(value);
155
- if (Array.isArray(value)) {
156
- const list = [];
157
- copies.set(value, list);
158
- for (const item of value) list.push(clean(item, copies));
159
- return list;
160
- }
161
- const isError = value instanceof Error;
162
- if (!isError && !isPlain(value)) return void 0;
163
- const copy = Object.create(Object.getPrototypeOf(value));
164
- copies.set(value, copy);
165
- for (const key of Reflect.ownKeys(value)) {
166
- if (typeof key === "string" && DROPPED.has(key)) continue;
167
- let held;
168
- try {
169
- held = value[key];
170
- } catch {
171
- continue;
172
- }
173
- const descriptor = Object.getOwnPropertyDescriptor(value, key);
174
- let kept;
175
- if (typeof key === "string" && CREDENTIAL_NAME.test(key)) kept = held == null ? held : REDACTED;
176
- else if (key === "response" && typeof held === "object" && held !== null) {
177
- kept = responseSummary(held);
178
- } else kept = clean(held, copies);
179
- if (kept === void 0 && held !== void 0) continue;
180
- Object.defineProperty(copy, key, {
181
- value: kept,
182
- writable: true,
183
- configurable: true,
184
- enumerable: descriptor?.enumerable ?? true
185
- });
186
- }
187
- return copy;
188
- }
189
- function holdsTransport(err, seen) {
190
- if (seen.has(err)) return false;
191
- seen.add(err);
192
- if (Object.getOwnPropertyNames(err).some((key) => DROPPED.has(key) || key === "response")) {
193
- return true;
194
- }
195
- const cause = err.cause;
196
- return typeof cause === "object" && cause !== null && holdsTransport(cause, seen);
197
- }
198
- function scrubCredentials(err) {
199
- if (typeof err !== "object" || err === null) return err;
200
- return holdsTransport(err, /* @__PURE__ */ new Set()) ? clean(err, /* @__PURE__ */ new WeakMap()) : err;
201
- }
202
-
203
646
  // src/bucket-presence.ts
204
647
  var BucketPresence = class {
205
648
  constructor(storage, bucket) {
@@ -245,65 +688,6 @@ var BucketPresence = class {
245
688
  }
246
689
  };
247
690
 
248
- // src/read-timeout.ts
249
- import { ValidationError as ValidationError2 } from "@cloudbitmaps/core/driver-kit";
250
- var DEFAULT_READ_TIMEOUT_MS = 0;
251
- var MAX_TIMER_MS = 2147483647;
252
- function resolveReadTimeoutMs(value) {
253
- if (value === void 0) return DEFAULT_READ_TIMEOUT_MS;
254
- if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0 || value > MAX_TIMER_MS) {
255
- throw new ValidationError2(
256
- `readTimeoutMs must be a non-negative safe integer no larger than ${MAX_TIMER_MS}; got ${describe(value)}`
257
- );
258
- }
259
- return value;
260
- }
261
- function describe(value) {
262
- if (typeof value === "string") return JSON.stringify(value);
263
- if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
264
- return String(value);
265
- }
266
- return value === null ? "null" : typeof value;
267
- }
268
- var ReadTimedOut = class extends Error {
269
- code = "ETIMEDOUT";
270
- constructor(read, ms, cause) {
271
- super(`GCS ${read} timed out after ${ms} ms`, cause === void 0 ? void 0 : { cause });
272
- this.name = "ReadTimedOut";
273
- }
274
- };
275
- var Deadline = class {
276
- constructor(ms, read) {
277
- this.ms = ms;
278
- this.read = read;
279
- this.endsAt = performance.now() + ms;
280
- }
281
- ms;
282
- read;
283
- endsAt;
284
- /** What is left of it, in milliseconds: 0 once it has passed. */
285
- remaining() {
286
- return Math.max(0, this.endsAt - performance.now());
287
- }
288
- /** The error a read fails with once this has passed; `cause` is the last attempt's own error, if one failed. */
289
- expired(cause) {
290
- return new ReadTimedOut(this.read, this.ms, cause);
291
- }
292
- };
293
- function startDeadline(ms, read) {
294
- return ms === 0 ? void 0 : new Deadline(ms, read);
295
- }
296
- function withDeadline(send, deadline) {
297
- if (deadline === void 0) return send();
298
- if (deadline.remaining() === 0) return Promise.reject(deadline.expired());
299
- const request = send();
300
- let timer;
301
- const timedOut = new Promise((_, reject) => {
302
- timer = setTimeout(() => reject(deadline.expired()), deadline.remaining());
303
- });
304
- return Promise.race([request, timedOut]).finally(() => clearTimeout(timer));
305
- }
306
-
307
691
  // src/download-retry.ts
308
692
  var RETRIES = 3;
309
693
  var BASE_DELAY_MS = 100;
@@ -415,6 +799,10 @@ var REAL_TIME = {
415
799
  sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
416
800
  };
417
801
  var REAL_RNG = { next: () => Math.random() };
802
+ function isTypedPassThrough(err) {
803
+ const kind = errorKind(err);
804
+ return kind === "ValidationError" || kind === "WriteConflictError" || kind === "NotFoundError";
805
+ }
418
806
  var DEFAULT_UPLOAD_THRESHOLD_BYTES = 8 * 1024 * 1024;
419
807
  var GcsStorageDriver = class {
420
808
  storage;
@@ -486,7 +874,7 @@ var GcsStorageDriver = class {
486
874
  `generation already exists (write-once): ${key.segment}.${key.generation}`
487
875
  );
488
876
  }
489
- if (isValidationError(err) || isWriteConflictError(err) || isNotFoundError(err)) throw err;
877
+ if (isTypedPassThrough(err)) throw err;
490
878
  if (isNotFound(err)) await this.presence.confirm();
491
879
  throw this.mapError(err);
492
880
  }
@@ -653,7 +1041,7 @@ var GcsStorageDriver = class {
653
1041
  }
654
1042
  async delete(key, options) {
655
1043
  const objectName = storageObjectKey(this.prefix, key);
656
- const ifVersion = options?.ifVersion;
1044
+ const ifVersion = checkDeleteOptions(options);
657
1045
  const conditional = ifVersion !== void 0 && this.conditionalDelete;
658
1046
  try {
659
1047
  await this.file(objectName).delete(
@@ -707,7 +1095,7 @@ var GcsStorageDriver = class {
707
1095
  /** Map GCS read errors to the driver vocabulary; pass everything else through {@link mapError}. */
708
1096
  mapReadError(err, key) {
709
1097
  err = scrubCredentials(err);
710
- if (isValidationError(err)) return err;
1098
+ if (errorKind(err) === "ValidationError") return err;
711
1099
  if (isNotFound(err))
712
1100
  return new NotFoundError(`no such generation: ${key.segment}.${key.generation}`);
713
1101
  if (isInvalidRange(err)) {
@@ -761,6 +1149,10 @@ var GcsUploadSink = class _GcsUploadSink {
761
1149
  bufferedLen = 0;
762
1150
  stream;
763
1151
  // set once we cross the threshold → resumable mode
1152
+ /** The SDK's resumable upload under {@link stream}, which {@link abort} ends with an error: see there. */
1153
+ upload;
1154
+ /** Set by {@link abort}: the held upload starts no request from then on. */
1155
+ abandoned = false;
764
1156
  done;
765
1157
  failure;
766
1158
  static WRITE_OPTS = {
@@ -801,6 +1193,7 @@ var GcsUploadSink = class _GcsUploadSink {
801
1193
  }
802
1194
  /** Cross into resumable streaming: open the stream, flush the buffered bytes, keep only ~one threshold resident. */
803
1195
  startResumable() {
1196
+ this.holdUpload();
804
1197
  const stream = this.file.createWriteStream({ resumable: true, ...this.tagged() });
805
1198
  this.stream = stream;
806
1199
  this.done = once2(stream, "finish");
@@ -866,8 +1259,56 @@ var GcsUploadSink = class _GcsUploadSink {
866
1259
  throw err;
867
1260
  }
868
1261
  }
1262
+ /**
1263
+ * Keep the SDK's resumable upload, the writable its write stream pipes into, so {@link abort} can end its request.
1264
+ * The SDK hands it to the stream's duplex through `setWritable` in `startResumableUpload_`, a method of the `File`
1265
+ * its types declare; this wraps that method on this sink's own `File`, which no other write shares. Where the method
1266
+ * or the duplex's `setWritable` is missing, the SDK runs unchanged, nothing is kept, and an abort destroys the
1267
+ * stream alone, as it did before.
1268
+ */
1269
+ holdUpload() {
1270
+ const file = this.file;
1271
+ const start = file.startResumableUpload_;
1272
+ if (typeof start !== "function") return;
1273
+ file.startResumableUpload_ = (dup, options) => {
1274
+ const setWritable = dup.setWritable;
1275
+ if (typeof setWritable === "function") {
1276
+ dup.setWritable = (writable) => {
1277
+ this.hold(writable);
1278
+ setWritable.call(dup, writable);
1279
+ };
1280
+ }
1281
+ start.call(file, dup, options);
1282
+ };
1283
+ }
1284
+ /**
1285
+ * Keep `upload`, and let it start no request once this sink is abandoned. The SDK registers the listener that
1286
+ * aborts an upload's request only as that request starts, after the session is created, so an upload abandoned
1287
+ * before then would start its request anyway, with nothing left to end it. Each request that sends the upload's
1288
+ * bytes, its first and every retry, begins in `startUploading`, a public method of the SDK's `Upload`. On an
1289
+ * abandoned upload it resolves at once: the upload is destroyed already, and some of the SDK's callers do not
1290
+ * catch a rejection.
1291
+ */
1292
+ hold(upload) {
1293
+ this.upload = upload;
1294
+ const held = upload;
1295
+ const startUploading = held.startUploading;
1296
+ if (typeof startUploading !== "function") return;
1297
+ held.startUploading = () => this.abandoned ? Promise.resolve() : startUploading.call(held);
1298
+ }
1299
+ /**
1300
+ * End an upload that will not be committed. Destroying the write stream alone leaves the resumable upload's request
1301
+ * open: the SDK destroys its upload with no error, aborts the request only when the upload errors, and its request
1302
+ * body waits for bytes that never come, so the socket stays open and the process alive until the service drops the
1303
+ * connection. So the upload is destroyed with an error first, which aborts its request; the body was never ended, so
1304
+ * no object is finalized. An upload whose request has not started yet starts none: see {@link hold}. The error is
1305
+ * the sink's own and reaches no caller.
1306
+ */
869
1307
  async abort() {
870
- if (this.stream !== void 0 && !this.stream.destroyed) this.stream.destroy();
1308
+ this.abandoned = true;
1309
+ const abandoned = new Error("the upload was abandoned before its object was committed");
1310
+ if (this.upload !== void 0 && !this.upload.destroyed) this.upload.destroy(abandoned);
1311
+ if (this.stream !== void 0 && !this.stream.destroyed) this.stream.destroy(abandoned);
871
1312
  }
872
1313
  };
873
1314
 
@@ -1173,6 +1614,17 @@ var GcsStorage = class {
1173
1614
  registry;
1174
1615
  /** The client both halves share — built here unless one was supplied. */
1175
1616
  client;
1617
+ bucket;
1618
+ /** The name prefix every object sits under: `''`, or `trimmed/`. */
1619
+ keyPrefix;
1620
+ readTimeoutMs;
1621
+ /**
1622
+ * Builds the GCS backend over one bucket, which `bucket` names. Pass `client`, or `projectId` and `apiEndpoint` to
1623
+ * have one built, not both. `prefix` puts every object under a name prefix. Throws `ValidationError` for an unknown
1624
+ * option, no bucket, a `now` that is not a function, a client setting beside `client`, a `maxObjectBytes` or
1625
+ * `simpleUploadThresholdBytes` that is not a positive integer, a `conditionalDelete` that is not a boolean, an
1626
+ * empty or non-string `apiEndpoint` or `projectId`, or an invalid `prefix` or `readTimeoutMs`.
1627
+ */
1176
1628
  constructor(options) {
1177
1629
  let readClient;
1178
1630
  refuseUnknownOptions(
@@ -1181,7 +1633,7 @@ var GcsStorage = class {
1181
1633
  GCS_STORAGE_OPTION_KEYS,
1182
1634
  "a @google-cloud/storage client goes in `client`"
1183
1635
  );
1184
- if (typeof options.bucket !== "string" || options.bucket.length === 0) {
1636
+ if (typeof options.bucket !== "string" || options.bucket.trim().length === 0) {
1185
1637
  throw new ValidationError5("GcsStorage needs a `bucket`: the name of one, a non-empty string");
1186
1638
  }
1187
1639
  if (options.now !== void 0 && typeof options.now !== "function") {
@@ -1199,11 +1651,21 @@ var GcsStorage = class {
1199
1651
  this.client = options.client;
1200
1652
  readClient = downloadClient(options.client, true);
1201
1653
  } else {
1654
+ const apiEndpoint = requireNonEmptyString("GcsStorage: `apiEndpoint`", options.apiEndpoint);
1655
+ if (apiEndpoint !== void 0 && /^[a-z][a-z0-9+.-]*:\/*$/i.test(apiEndpoint.trim())) {
1656
+ throw new ValidationError5(
1657
+ 'GcsStorage: `apiEndpoint` must name a host, such as "http://localhost:4443" or "storage.googleapis.com"'
1658
+ );
1659
+ }
1660
+ requireNonEmptyString("GcsStorage: `projectId`", options.projectId);
1202
1661
  this.client = new GcsClient(
1203
1662
  defined({ projectId: options.projectId, apiEndpoint: options.apiEndpoint })
1204
1663
  );
1205
1664
  readClient = downloadClient(this.client, false);
1206
1665
  }
1666
+ this.bucket = options.bucket;
1667
+ this.keyPrefix = prefixPart(normalizeObjectPrefix3(options.prefix));
1668
+ this.readTimeoutMs = resolveReadTimeoutMs(options.readTimeoutMs);
1207
1669
  const shared = {
1208
1670
  storage: this.client,
1209
1671
  readStorage: readClient,
@@ -1224,6 +1686,17 @@ var GcsStorage = class {
1224
1686
  });
1225
1687
  brandAsBackend(this);
1226
1688
  }
1689
+ /**
1690
+ * Reads the bucket's metadata (one request, plus the SDK's own retries, bounded by `readTimeoutMs` when it is set) and
1691
+ * reports a lifecycle rule with a `Delete` action (or one it does not know) that applies to live
1692
+ * objects whose names can be this store's: generations and registry rows alike. Needs `storage.buckets.get`, which
1693
+ * a load never uses (`store.preflight()`'s lifecycle step does) and `roles/storage.objectAdmin` does not include: without it the answer is
1694
+ * `unchecked`, naming the permission. Never rejects; a `detail` carries no credential. Rule reading: see the
1695
+ * production guide.
1696
+ */
1697
+ checkLifecycle() {
1698
+ return checkGcsLifecycle(this.client, this.bucket, this.keyPrefix, this.readTimeoutMs);
1699
+ }
1227
1700
  };
1228
1701
  export {
1229
1702
  GcsStorage