@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/README.md +55 -15
- package/dist/backend.d.ts +64 -24
- package/dist/index.js +528 -173
- package/dist/index.js.map +4 -4
- package/package.json +4 -3
- package/dist/aws-errors.d.ts +0 -13
- package/dist/bounded-body.d.ts +0 -15
- package/dist/client-probe.d.ts +0 -36
- package/dist/defined.d.ts +0 -8
- package/dist/read-timeout.d.ts +0 -11
- package/dist/registry.d.ts +0 -118
- package/dist/s3-errors.d.ts +0 -66
- package/dist/scrub-error.d.ts +0 -12
- package/dist/send-once.d.ts +0 -23
- package/dist/socket-advisory.d.ts +0 -57
- package/dist/storage.d.ts +0 -100
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/
|
|
16
|
-
import {
|
|
17
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 = (
|
|
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((
|
|
421
|
-
if (isS3Label(
|
|
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 (
|
|
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
|
|
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 (
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|