@stowage/adapter-s3 0.0.0 → 0.1.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 ADDED
@@ -0,0 +1,2483 @@
1
+ import { StorageError, errorCodeForStatus, invalidKeyReason, isStorageError, isTransientStatus, withRetry } from "@stowage/core";
2
+ //#region src/storage-error.ts
3
+ function s3Error(bucket, fields) {
4
+ return new StorageError({
5
+ ...fields,
6
+ bucket,
7
+ provider: "s3"
8
+ });
9
+ }
10
+ /**
11
+ * The same failure told against the storage it happened in. A credential resolver is
12
+ * written outside the adapter and knows neither bucket nor operation, so the error it
13
+ * throws arrives without them and is re-issued here rather than reaching a caller with
14
+ * the placeholders it was built from (spec 4.10).
15
+ */
16
+ function inStorage(failure, bucket, operation, key) {
17
+ if (!isStorageError(failure)) return failure;
18
+ return s3Error(bucket, {
19
+ ...fieldsOf(failure),
20
+ operation,
21
+ key: key ?? failure.key
22
+ });
23
+ }
24
+ /** The same failure, counting the attempts made where the one that failed does not know. */
25
+ function withAttemptsMade(failure, attempts) {
26
+ return s3Error(failure.bucket, {
27
+ ...fieldsOf(failure),
28
+ attempts
29
+ });
30
+ }
31
+ /**
32
+ * Every field of `StorageErrorFields` is named below, so a field added to that type has
33
+ * to be added here too or it is dropped on the way through.
34
+ */
35
+ function fieldsOf(failure) {
36
+ return {
37
+ code: failure.code,
38
+ message: failure.message,
39
+ operation: failure.operation,
40
+ key: failure.key,
41
+ attempts: failure.attempts,
42
+ status: failure.status,
43
+ providerCode: failure.providerCode,
44
+ requestId: failure.requestId,
45
+ retryable: failure.retryable,
46
+ capability: failure.capability,
47
+ cause: failure.cause
48
+ };
49
+ }
50
+ //#endregion
51
+ //#region src/options.ts
52
+ const operationOptionKeys = ["signal"];
53
+ const putOptionKeys = [
54
+ ...operationOptionKeys,
55
+ "contentType",
56
+ "userMetadata"
57
+ ];
58
+ const getOptionKeys = [...operationOptionKeys, "range"];
59
+ const presignGetOptionKeys = [
60
+ "expiresIn",
61
+ "responseContentType",
62
+ "responseContentDisposition",
63
+ "responseCacheControl",
64
+ "responseExpires"
65
+ ];
66
+ const presignPutOptionKeys = [
67
+ "expiresIn",
68
+ "contentType",
69
+ "contentLength"
70
+ ];
71
+ const listOptionKeys = [
72
+ ...operationOptionKeys,
73
+ "prefix",
74
+ "delimiter",
75
+ "pageSize",
76
+ "cursor"
77
+ ];
78
+ /**
79
+ * Refuses an option key the spec does not list (spec 4.3). TypeScript catches one at the
80
+ * call site; this catches the rest.
81
+ */
82
+ function requireKnownOptions(bucket, options, known, operation) {
83
+ if (options === void 0) return;
84
+ for (const key of Object.keys(options)) {
85
+ if (known.includes(key)) continue;
86
+ throw optionError$1(bucket, key, "is not one this storage takes", operation);
87
+ }
88
+ }
89
+ function optionError$1(bucket, option, expectation, operation) {
90
+ return s3Error(bucket, {
91
+ code: "InvalidOption",
92
+ message: `The option \`${option}\` ${expectation}`,
93
+ operation,
94
+ attempts: 0
95
+ });
96
+ }
97
+ //#endregion
98
+ //#region src/configuration.ts
99
+ const adapterOptionKeys = [
100
+ "bucket",
101
+ "region",
102
+ "endpoint",
103
+ "forcePathStyle",
104
+ "credentials",
105
+ "retry",
106
+ "multipart"
107
+ ];
108
+ const retryOptionKeys = ["maxAttempts"];
109
+ const multipartOptionKeys = ["partSize", "concurrency"];
110
+ const mebibyte = 1048576;
111
+ const defaultMaxAttempts = 3;
112
+ const defaultPartSize = 8 * mebibyte;
113
+ const defaultConcurrency = 4;
114
+ const maxAttemptsRange = {
115
+ least: 1,
116
+ most: 3
117
+ };
118
+ const partSizeRange = {
119
+ least: 5 * mebibyte,
120
+ most: 5120 * mebibyte
121
+ };
122
+ const concurrencyRange = {
123
+ least: 1,
124
+ most: 16
125
+ };
126
+ /**
127
+ * IPv4 loopback is the whole `127.0.0.0/8` block and IPv6 loopback the single `::1`.
128
+ * `localhost` stands beside them because RFC 6761 binds the name to one of the two, which
129
+ * is what makes it an address spec 7.1 accepts rather than a host that might be anywhere.
130
+ */
131
+ const loopbackHosts = /^(?:localhost|127(?:\.\d{1,3}){3}|\[::1\])$/u;
132
+ /**
133
+ * Spec 7.1: every option is validated where the storage is constructed, an unknown key
134
+ * and a value outside its range are `InvalidOption` naming the key, and no value is
135
+ * clamped onto the range it missed.
136
+ */
137
+ function readConfiguration(options) {
138
+ const bucket = typeof options.bucket === "string" ? options.bucket : "";
139
+ requireKnownOptions(bucket, options, adapterOptionKeys, "s3Storage");
140
+ requireFilled$1(bucket, options.bucket, "bucket");
141
+ requireFilled$1(bucket, options.region, "region");
142
+ if (options.credentials === void 0) throw optionError(bucket, "credentials", "is required: v0.1 sends no unsigned request");
143
+ const forcePathStyle = readFlag(bucket, options.forcePathStyle, "forcePathStyle");
144
+ const endpoint = readEndpoint(bucket, options, forcePathStyle);
145
+ return {
146
+ bucket: options.bucket,
147
+ region: options.region,
148
+ ...endpoint,
149
+ forcePathStyle,
150
+ credentials: options.credentials,
151
+ maxAttempts: readMaxAttempts(bucket, options.retry),
152
+ ...readMultipart(bucket, options.multipart)
153
+ };
154
+ }
155
+ /**
156
+ * Spec 7.1: no endpoint addresses AWS S3, a configured one is an absolute URL without
157
+ * userinfo, query and fragment, and `http:` is accepted for a loopback host alone.
158
+ * Addressing is virtual-hosted unless `forcePathStyle` moves the bucket into the path.
159
+ */
160
+ function readEndpoint(bucket, options, forcePathStyle) {
161
+ if (options.endpoint === void 0) return {
162
+ protocol: "https:",
163
+ host: hostFor(`s3.${options.region}.amazonaws.com`, options.bucket, forcePathStyle),
164
+ basePath: ""
165
+ };
166
+ if (typeof options.endpoint !== "string") throw optionError(bucket, "endpoint", "is no absolute URL");
167
+ const parsed = URL.parse(options.endpoint);
168
+ if (parsed === null) throw optionError(bucket, "endpoint", "is no absolute URL");
169
+ if (parsed.username !== "" || parsed.password !== "") throw optionError(bucket, "endpoint", "carries userinfo");
170
+ if (parsed.search !== "") throw optionError(bucket, "endpoint", "carries a query");
171
+ if (parsed.hash !== "") throw optionError(bucket, "endpoint", "carries a fragment");
172
+ if (parsed.protocol !== "https:" && !isLoopbackHttp(parsed)) throw optionError(bucket, "endpoint", "is neither `https:` nor `http:` to a loopback host");
173
+ return {
174
+ protocol: parsed.protocol,
175
+ host: hostFor(parsed.host, options.bucket, forcePathStyle),
176
+ basePath: parsed.pathname === "/" ? "" : parsed.pathname.replace(/\/$/u, "")
177
+ };
178
+ }
179
+ function isLoopbackHttp(endpoint) {
180
+ return endpoint.protocol === "http:" && loopbackHosts.test(endpoint.hostname);
181
+ }
182
+ function hostFor(host, bucket, forcePathStyle) {
183
+ return forcePathStyle ? host : `${bucket}.${host}`;
184
+ }
185
+ function readMaxAttempts(bucket, retry) {
186
+ if (retry === false) return 1;
187
+ if (retry === void 0) return defaultMaxAttempts;
188
+ requireGroup(bucket, retry, "retry");
189
+ requireKnownOptions(bucket, retry, retryOptionKeys, "s3Storage");
190
+ return readInRange(bucket, retry.maxAttempts, "maxAttempts", maxAttemptsRange, defaultMaxAttempts);
191
+ }
192
+ function readMultipart(bucket, multipart) {
193
+ if (multipart !== void 0) {
194
+ requireGroup(bucket, multipart, "multipart");
195
+ requireKnownOptions(bucket, multipart, multipartOptionKeys, "s3Storage");
196
+ }
197
+ return {
198
+ partSize: readInRange(bucket, multipart?.partSize, "partSize", partSizeRange, defaultPartSize),
199
+ concurrency: readInRange(bucket, multipart?.concurrency, "concurrency", concurrencyRange, defaultConcurrency)
200
+ };
201
+ }
202
+ function readInRange(bucket, value, option, range, fallback) {
203
+ if (value === void 0) return fallback;
204
+ if (!Number.isInteger(value) || value < range.least || value > range.most) throw optionError(bucket, option, `takes the integers ${range.least} to ${range.most}`);
205
+ return value;
206
+ }
207
+ function readFlag(bucket, value, option) {
208
+ if (value === void 0) return false;
209
+ if (typeof value !== "boolean") throw optionError(bucket, option, "takes a boolean");
210
+ return value;
211
+ }
212
+ /**
213
+ * A group of options is an object. Without this, `Object.keys` reads `retry: true` as a
214
+ * group with no key and hands back the default, and `retry: null` throws a `TypeError`
215
+ * where spec 7.1 asks for `InvalidOption`.
216
+ */
217
+ function requireGroup(bucket, group, option) {
218
+ if (typeof group === "object" && group !== null) return;
219
+ throw optionError(bucket, option, "takes a group of options");
220
+ }
221
+ function requireFilled$1(bucket, value, option) {
222
+ if (typeof value === "string" && value !== "") return;
223
+ throw optionError(bucket, option, "is empty");
224
+ }
225
+ function optionError(bucket, option, expectation) {
226
+ return optionError$1(bucket, option, expectation, "s3Storage");
227
+ }
228
+ //#endregion
229
+ //#region src/provider-code.ts
230
+ /**
231
+ * The table of spec 7.9, holding both vendors' strings: a code recognized here decides
232
+ * the error code alone, and an unrecognized one falls to the status mapping of spec 4.10.
233
+ * Whether the condition is transient is never read from here — ADR 0013 decides that by
234
+ * the status, so that an unknown `5xx` is treated no worse than one stowage has heard of.
235
+ */
236
+ const providerCodes = /* @__PURE__ */ new Map([
237
+ ["NoSuchKey", "NotFound"],
238
+ ["NoSuchBucket", "NotFound"],
239
+ ["AccessDenied", "AccessDenied"],
240
+ ["InvalidAccessKeyId", "InvalidCredentials"],
241
+ ["SignatureDoesNotMatch", "InvalidCredentials"],
242
+ ["Unauthorized", "InvalidCredentials"],
243
+ ["ExpiredToken", "Expired"],
244
+ ["ExpiredRequest", "Expired"],
245
+ ["RequestTimeTooSkewed", "InvalidRequest"],
246
+ ["InvalidRange", "InvalidRequest"],
247
+ ["InvalidRequest", "InvalidRequest"],
248
+ ["InvalidArgument", "InvalidRequest"],
249
+ ["MetadataTooLarge", "InvalidRequest"],
250
+ ["EntityTooLarge", "InvalidRequest"],
251
+ ["EntityTooSmall", "InvalidRequest"],
252
+ ["InvalidPart", "InvalidRequest"],
253
+ ["InvalidPartOrder", "InvalidRequest"],
254
+ ["BadDigest", "InvalidRequest"],
255
+ ["MalformedXML", "InvalidRequest"],
256
+ ["InvalidDigest", "InvalidRequest"],
257
+ ["InvalidObjectName", "InvalidKey"],
258
+ ["KeyTooLongError", "InvalidKey"],
259
+ ["NoSuchUpload", "ProviderError"],
260
+ ["SlowDown", "ProviderError"],
261
+ ["TooManyRequests", "ProviderError"],
262
+ ["ServiceUnavailable", "ProviderError"],
263
+ ["InternalError", "ProviderError"],
264
+ ["RequestTimeout", "ProviderError"]
265
+ ]);
266
+ const permanentRedirect = 301;
267
+ const badRequest = 400;
268
+ /** The longest key AWS S3 and R2 hold, in UTF-8 bytes, above which they answer `KeyTooLongError`. */
269
+ const longestHeldKey = 1024;
270
+ const utf8$5 = new TextEncoder();
271
+ /**
272
+ * What the provider's answer means, decided by its own code where the table recognizes
273
+ * one and by the status where it does not. The message is the provider's word for word
274
+ * (spec 4.10), except where spec 7.9 has the failure name the option that is wrong: a
275
+ * caller can act on `region` and on `cursor`, and cannot on a message about either.
276
+ */
277
+ function readProviderFailure(answer) {
278
+ const said = answer.providerMessage ?? `The provider answered ${answer.status} to \`${answer.method}\``;
279
+ if (answer.status === permanentRedirect || answer.providerCode === "PermanentRedirect") return {
280
+ code: "InvalidOption",
281
+ message: `The option \`region\` is not the bucket's${answer.bucketRegion === void 0 ? "" : `, which is \`${answer.bucketRegion}\``}: ${said}`
282
+ };
283
+ if (answer.method === "HEAD" && answer.status === badRequest && answer.key !== void 0 && utf8$5.encode(answer.key).byteLength > longestHeldKey) return {
284
+ code: "InvalidKey",
285
+ message: `The key is longer than the ${longestHeldKey} bytes the provider holds: ${said}`
286
+ };
287
+ if (answer.providerCode === "InvalidArgument" && answer.operation === "list" && answer.hasContinuationToken) return {
288
+ code: "InvalidOption",
289
+ message: `The option \`cursor\` is not one the provider continued from: ${said}`
290
+ };
291
+ return {
292
+ code: (answer.providerCode === void 0 ? void 0 : providerCodes.get(answer.providerCode)) ?? errorCodeForStatus(answer.status) ?? "ProviderError",
293
+ message: said
294
+ };
295
+ }
296
+ /**
297
+ * The codes of the table a provider answers with a transient status: the five the table
298
+ * leaves to the status, as spec 7.9 notes beside them.
299
+ */
300
+ const transientProviderCodes = /* @__PURE__ */ new Set([
301
+ "SlowDown",
302
+ "TooManyRequests",
303
+ "ServiceUnavailable",
304
+ "InternalError",
305
+ "RequestTimeout"
306
+ ]);
307
+ /**
308
+ * A failure the provider reported inside a `200`, such as one key of a `DeleteObjects` or
309
+ * a `CopyObject` that failed after the answer began. No status speaks for it, so the code
310
+ * decides alone; whether it is transient is read off the code, since spec 4.7 has the
311
+ * entry carry `retryable` for a caller who repeats it. Nothing here is repeated: ADR 0013
312
+ * lets no provider code into the retry group.
313
+ */
314
+ function readEmbeddedFailure(providerCode, providerMessage) {
315
+ return {
316
+ code: providerCodes.get(providerCode) ?? "ProviderError",
317
+ message: providerMessage,
318
+ retryable: transientProviderCodes.has(providerCode)
319
+ };
320
+ }
321
+ //#endregion
322
+ //#region src/xml.ts
323
+ /** A document outside the subset ADR 0003 reads, reported rather than read around. */
324
+ var XmlSyntaxError = class extends Error {
325
+ name = "XmlSyntaxError";
326
+ };
327
+ /**
328
+ * The root element of an XML document of the subset S3 answers a listing with: elements,
329
+ * attributes, text and comments under one optional declaration.
330
+ */
331
+ function parseXml(document) {
332
+ const scanner = new Scanner(document);
333
+ scanner.skipDeclaration();
334
+ scanner.skipMisc();
335
+ const root = scanner.readElement();
336
+ scanner.skipMisc();
337
+ scanner.requireEnd();
338
+ return root;
339
+ }
340
+ const namePattern = /[:A-Z_a-z\u00C0-\uFFFF][:A-Z_a-z\u00C0-\uFFFF.\d-]*/uy;
341
+ const whitespacePattern = /[ \t\r\n]*/y;
342
+ const entityPattern = /&(?:#x([\da-fA-F]+)|#(\d+)|([A-Za-z]\w*));/uy;
343
+ /** `Char` of XML 1.0, section 2.2: what a character reference may name. */
344
+ function isXmlCharacter(codePoint) {
345
+ return codePoint === 9 || codePoint === 10 || codePoint === 13 || codePoint >= 32 && codePoint <= 55295 || codePoint >= 57344 && codePoint <= 65533 || codePoint >= 65536 && codePoint <= 1114111;
346
+ }
347
+ /** The five entities XML defines without a DTD, which is every one a document here has. */
348
+ const predefinedEntities = /* @__PURE__ */ new Map([
349
+ ["amp", "&"],
350
+ ["lt", "<"],
351
+ ["gt", ">"],
352
+ ["quot", "\""],
353
+ ["apos", "'"]
354
+ ]);
355
+ const xmlEscapes = {
356
+ "&": "&amp;",
357
+ "<": "&lt;",
358
+ ">": "&gt;",
359
+ "\"": "&quot;",
360
+ "'": "&apos;"
361
+ };
362
+ /** Text as it stands inside an element of a request document stowage writes. */
363
+ function escapeXml(text) {
364
+ return text.replaceAll(/[&<>"']/gu, (character) => xmlEscapes[character] ?? character);
365
+ }
366
+ var Scanner = class {
367
+ #document;
368
+ #position = 0;
369
+ constructor(document) {
370
+ this.#document = document;
371
+ }
372
+ skipDeclaration() {
373
+ if (!this.#startsWith("<?xml")) return;
374
+ this.#position = this.#positionPast("?>", "The XML declaration is never closed");
375
+ }
376
+ /** Whitespace and comments, which are all that may stand beside the root element. */
377
+ skipMisc() {
378
+ for (;;) {
379
+ this.#skipWhitespace();
380
+ if (this.#startsWith("<!DOCTYPE")) throw this.#error("A DTD is refused");
381
+ if (!this.#startsWith("<!--")) return;
382
+ this.#skipComment();
383
+ }
384
+ }
385
+ requireEnd() {
386
+ if (this.#position < this.#document.length) throw this.#error("Something follows the root element");
387
+ }
388
+ readElement() {
389
+ this.#expect("<");
390
+ const name = this.#readName();
391
+ if (this.#readStartTagEnd() === "empty") return {
392
+ name,
393
+ children: [],
394
+ text: ""
395
+ };
396
+ const children = [];
397
+ let text = "";
398
+ for (;;) {
399
+ if (this.#position >= this.#document.length) throw this.#error(`The element <${name}> is never closed`);
400
+ if (this.#startsWith("</")) {
401
+ this.#position += 2;
402
+ this.#closeElement(name);
403
+ return {
404
+ name,
405
+ children,
406
+ text
407
+ };
408
+ }
409
+ if (this.#startsWith("<!--")) this.#skipComment();
410
+ else if (this.#startsWith("<![CDATA[")) throw this.#error("A CDATA section is refused");
411
+ else if (this.#startsWith("<")) children.push(this.readElement());
412
+ else text += this.#readText();
413
+ }
414
+ }
415
+ #closeElement(name) {
416
+ const closing = this.#readName();
417
+ if (closing !== name) throw this.#error(`The element <${name}> is closed by </${closing}>`);
418
+ this.#skipWhitespace();
419
+ this.#expect(">");
420
+ }
421
+ #readStartTagEnd() {
422
+ for (;;) {
423
+ const before = this.#position;
424
+ this.#skipWhitespace();
425
+ if (this.#startsWith("/>")) {
426
+ this.#position += 2;
427
+ return "empty";
428
+ }
429
+ if (this.#startsWith(">")) {
430
+ this.#position += 1;
431
+ return "open";
432
+ }
433
+ if (this.#position === before) throw this.#error("An attribute follows no whitespace");
434
+ this.#readName();
435
+ this.#skipWhitespace();
436
+ this.#expect("=");
437
+ this.#skipWhitespace();
438
+ this.#skipQuoted();
439
+ }
440
+ }
441
+ #skipQuoted() {
442
+ const quote = this.#document[this.#position];
443
+ if (quote !== "\"" && quote !== "'") throw this.#error("An attribute value is not quoted");
444
+ this.#position = this.#positionPast(quote, "An attribute value is never closed", 1);
445
+ }
446
+ /** Text up to the next markup, every `&` in it the start of an entity it decodes. */
447
+ #readText() {
448
+ const markup = this.#document.indexOf("<", this.#position);
449
+ const end = markup === -1 ? this.#document.length : markup;
450
+ let text = "";
451
+ while (this.#position < end) {
452
+ const ampersand = this.#document.indexOf("&", this.#position);
453
+ if (ampersand === -1 || ampersand >= end) {
454
+ text += this.#document.slice(this.#position, end);
455
+ this.#position = end;
456
+ break;
457
+ }
458
+ text += this.#document.slice(this.#position, ampersand);
459
+ this.#position = ampersand;
460
+ text += this.#readEntity();
461
+ }
462
+ return text;
463
+ }
464
+ #readEntity() {
465
+ entityPattern.lastIndex = this.#position;
466
+ const found = entityPattern.exec(this.#document);
467
+ if (found === null) throw this.#error("An `&` starts no entity");
468
+ const [entity, hex, decimal, name] = found;
469
+ if (name !== void 0) {
470
+ const character = predefinedEntities.get(name);
471
+ if (character === void 0) throw this.#error(`The entity ${entity} is not defined`);
472
+ this.#position = entityPattern.lastIndex;
473
+ return character;
474
+ }
475
+ const codePoint = hex === void 0 ? Number(decimal) : Number.parseInt(hex, 16);
476
+ if (!isXmlCharacter(codePoint)) throw this.#error(`The reference ${entity} names no character XML carries`);
477
+ this.#position = entityPattern.lastIndex;
478
+ return String.fromCodePoint(codePoint);
479
+ }
480
+ #skipComment() {
481
+ this.#position = this.#positionPast("-->", "A comment is never closed", 4);
482
+ }
483
+ #readName() {
484
+ namePattern.lastIndex = this.#position;
485
+ const found = namePattern.exec(this.#document);
486
+ if (found === null) throw this.#error("A name is missing where one belongs");
487
+ this.#position = namePattern.lastIndex;
488
+ return found[0];
489
+ }
490
+ #skipWhitespace() {
491
+ whitespacePattern.lastIndex = this.#position;
492
+ whitespacePattern.exec(this.#document);
493
+ this.#position = whitespacePattern.lastIndex;
494
+ }
495
+ #expect(literal) {
496
+ if (!this.#startsWith(literal)) throw this.#error(`\`${literal}\` is missing`);
497
+ this.#position += literal.length;
498
+ }
499
+ #positionPast(terminator, unterminated, offset = 0) {
500
+ const end = this.#document.indexOf(terminator, this.#position + offset);
501
+ if (end === -1) throw this.#error(unterminated);
502
+ return end + terminator.length;
503
+ }
504
+ #startsWith(literal) {
505
+ return this.#document.startsWith(literal, this.#position);
506
+ }
507
+ #error(message) {
508
+ return new XmlSyntaxError(`${message} (at character ${this.#position})`);
509
+ }
510
+ };
511
+ //#endregion
512
+ //#region src/answer-document.ts
513
+ /**
514
+ * The root of the document a `200` carries, read through the parser of spec 7.4. S3 may
515
+ * answer a `CopyObject` or a `DeleteObjects` with `200` and an `<Error>` in the body once
516
+ * the answer has begun, so a root of that name is the failure it reports, told with the
517
+ * provider's code and message, and not repeated (ADR 0013 lets no code into the group).
518
+ */
519
+ async function readAnswerDocument(request, response, expectedRoot) {
520
+ const requestId = response.headers.get("x-amz-request-id") ?? void 0;
521
+ const context = {
522
+ operation: request.operation,
523
+ key: request.key,
524
+ attempts: 1,
525
+ status: response.status,
526
+ requestId
527
+ };
528
+ const root = parseAnswer(request, await readBody$1(request, response, context), context);
529
+ if (root.name === "Error") throw embeddedFailure(request, context, root);
530
+ if (root.name !== expectedRoot) throw malformed$1(request, context, `a <${root.name}> where a <${expectedRoot}> belongs`);
531
+ return root;
532
+ }
533
+ /**
534
+ * The failure an `<Error>` element inside a `200` reports: a whole answer's, or one key's
535
+ * of a `DeleteObjects`. No status speaks for it, so its code decides alone.
536
+ */
537
+ function embeddedFailure(request, context, element) {
538
+ const providerCode = textOf$1(element, "Code") ?? "";
539
+ const failure = readEmbeddedFailure(providerCode, textOf$1(element, "Message") ?? `The provider failed ${request.subject}: ${providerCode}`);
540
+ return s3Error(request.bucket, {
541
+ ...context,
542
+ code: failure.code,
543
+ message: failure.message,
544
+ providerCode: providerCode === "" ? void 0 : providerCode,
545
+ retryable: failure.retryable
546
+ });
547
+ }
548
+ function textOf$1(element, name) {
549
+ return element.children.find((child) => child.name === name)?.text;
550
+ }
551
+ async function readBody$1(request, response, context) {
552
+ try {
553
+ return await response.text();
554
+ } catch (failure) {
555
+ if (failure instanceof Error && failure.name === "AbortError") throw failure;
556
+ throw s3Error(request.bucket, {
557
+ ...context,
558
+ code: "NetworkError",
559
+ message: `The answer to ${request.subject} broke while it was read: ${String(failure)}`,
560
+ retryable: true,
561
+ cause: failure
562
+ });
563
+ }
564
+ }
565
+ function parseAnswer(request, body, context) {
566
+ try {
567
+ return parseXml(body);
568
+ } catch (failure) {
569
+ if (failure instanceof XmlSyntaxError) throw malformed$1(request, context, `a document outside the XML stowage reads: ${failure.message}`, failure);
570
+ throw failure;
571
+ }
572
+ }
573
+ function malformed$1(request, context, what, cause) {
574
+ return s3Error(request.bucket, {
575
+ ...context,
576
+ code: "ProviderError",
577
+ message: `The provider answered ${request.subject} with ${what}`,
578
+ cause
579
+ });
580
+ }
581
+ //#endregion
582
+ //#region src/canonical.ts
583
+ /** What `encodeURIComponent` leaves alone and RFC 3986 counts as reserved. */
584
+ const reservedByEncodeUriComponent = /[!'()*]/g;
585
+ /** Runs of whitespace inside a header value, which SigV4 folds to one space. */
586
+ const whitespaceRun = /\s+/g;
587
+ function encodeRfc3986(value) {
588
+ return encodeURIComponent(value).replace(reservedByEncodeUriComponent, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
589
+ }
590
+ /**
591
+ * The canonical URI: the path percent-encoded segment by segment, so that a slash stays
592
+ * a slash and `#`, `%`, `?`, `+`, a space and everything above ASCII travel encoded
593
+ * (spec 7.4). S3 encodes the path once and normalizes nothing, which is why no `URL` is
594
+ * built from it: the constructor folds a `..` segment away and decodes what it was
595
+ * handed, and both would sign a path other than the one the request carries.
596
+ */
597
+ function encodePath(path) {
598
+ return path.split("/").map(encodeRfc3986).join("/");
599
+ }
600
+ /** The canonical query string: every pair encoded, sorted by name and then by value. */
601
+ function encodeQuery(query) {
602
+ return query.map(([name, value]) => [encodeRfc3986(name), encodeRfc3986(value)]).toSorted(compareFields).map(([name, value]) => `${name}=${value}`).join("&");
603
+ }
604
+ /**
605
+ * The canonical headers: names folded to lower case, values trimmed with their inner
606
+ * runs of whitespace folded to one space, fields of one name joined in the order they
607
+ * arrived, and the whole sorted by name.
608
+ */
609
+ function canonicalHeaders(fields) {
610
+ const joined = /* @__PURE__ */ new Map();
611
+ for (const [name, value] of fields) {
612
+ const folded = name.toLowerCase();
613
+ const trimmed = value.trim().replace(whitespaceRun, " ");
614
+ const held = joined.get(folded);
615
+ joined.set(folded, held === void 0 ? trimmed : `${held},${trimmed}`);
616
+ }
617
+ const sorted = [...joined].toSorted(compareFields);
618
+ return {
619
+ lines: sorted.map(([name, value]) => `${name}:${value}\n`).join(""),
620
+ names: sorted.map(([name]) => name).join(";")
621
+ };
622
+ }
623
+ function compareFields(one, other) {
624
+ if (one[0] !== other[0]) return one[0] < other[0] ? -1 : 1;
625
+ if (one[1] !== other[1]) return one[1] < other[1] ? -1 : 1;
626
+ return 0;
627
+ }
628
+ //#endregion
629
+ //#region src/range.ts
630
+ /** Refuses bounds the spec does not allow, before any request goes out (spec 4.3). */
631
+ function requireRange(bucket, range) {
632
+ if (range === void 0) return;
633
+ const { start, end } = range;
634
+ if (!isOffset(start) || end !== void 0 && (!isOffset(end) || end < start)) throw optionError$1(bucket, "range", "takes two whole numbers from zero up, `start` at most `end`", "get");
635
+ }
636
+ /** The `Range` field of RFC 9110, both ends inclusive as `ByteRange` is. */
637
+ function rangeHeader(range) {
638
+ return `bytes=${range.start}-${range.end ?? ""}`;
639
+ }
640
+ const contentRange = /^bytes (\d+)-(\d+)\/(\d+)$/u;
641
+ /**
642
+ * The size of the whole object out of a `206`'s `Content-Range`, which is what spec 4.4
643
+ * has a ranged `get` report rather than the length of the range.
644
+ */
645
+ function wholeSizeOf(response) {
646
+ const found = contentRange.exec(response.headers.get("content-range")?.trim() ?? "");
647
+ const size = Number(found?.[3]);
648
+ return Number.isSafeInteger(size) ? size : void 0;
649
+ }
650
+ /**
651
+ * A provider that answers a ranged `GET` with `200` sent the whole object instead, which
652
+ * RFC 9110 allows. Where the range covers the object, clipped as spec 4.3 clips it, that
653
+ * is the body asked for. For an object the range starts beyond, which is how S3 answers a
654
+ * range on an empty object, it is the refusal spec 4.3 names; for any other it is a body
655
+ * the caller did not ask for.
656
+ */
657
+ function wholeAnswerFailure(bucket, key, range, size) {
658
+ if (range.start === 0 && size > 0 && (range.end === void 0 || range.end >= size - 1)) return;
659
+ if (range.start >= size) return s3Error(bucket, {
660
+ code: "InvalidRequest",
661
+ message: `The range starts beyond the ${size} bytes under the key ${JSON.stringify(key)}`,
662
+ operation: "get",
663
+ key,
664
+ attempts: 1
665
+ });
666
+ return s3Error(bucket, {
667
+ code: "ProviderError",
668
+ message: `The provider answered a range of the object under ${JSON.stringify(key)} with the whole of it`,
669
+ operation: "get",
670
+ key,
671
+ attempts: 1
672
+ });
673
+ }
674
+ function isOffset(value) {
675
+ return Number.isInteger(value) && value >= 0;
676
+ }
677
+ //#endregion
678
+ //#region src/user-metadata.ts
679
+ const headerPrefix = "x-amz-meta-";
680
+ /** Spec 4.3 bounds the set at 2 KB of the header bytes it costs once it is encoded. */
681
+ const headerByteLimit = 2048;
682
+ /** The characters RFC 9110 allows in a field name, which is what a user metadata key is. */
683
+ const httpToken = /^[!#$%&'*+.^_`|~\dA-Za-z-]+$/u;
684
+ /**
685
+ * What survives a header field as it stands: printable ASCII with no space at either end,
686
+ * which HTTP trims, and no `=?`, which the reader would take for the start of an encoded
687
+ * word.
688
+ */
689
+ const travelsAsWritten = /^(?:[\x21-\x7e](?:[\x20-\x7e]*[\x21-\x7e])?)?$/u;
690
+ const encodedWordStart = "=?UTF-8?B?";
691
+ const encodedWordEnd = "?=";
692
+ /**
693
+ * RFC 2047 bounds an encoded word at 75 characters; 45 bytes are 60 characters of base64,
694
+ * which leaves room for the 12 the word's frame costs.
695
+ */
696
+ const bytesPerEncodedWord = 45;
697
+ const utf8$4 = new TextEncoder();
698
+ /**
699
+ * The header fields `userMetadata` travels in, refused with `InvalidRequest` before the
700
+ * request is signed where a key is no ASCII HTTP token or the whole set is above 2 KB of
701
+ * encoded header bytes (spec 4.3). A key outside ASCII is refused rather than sent,
702
+ * because R2 strips it on the way out and a write would lose it silently (ADR 0014).
703
+ */
704
+ function userMetadataHeaders(bucket, userMetadata, key) {
705
+ const headers = [];
706
+ const held = Object.create(null);
707
+ let headerBytes = 0;
708
+ for (const [name, value] of Object.entries(userMetadata ?? {})) {
709
+ if (!httpToken.test(name)) throw refusal$1(bucket, `The user metadata key ${JSON.stringify(name)} is no ASCII HTTP token`, key);
710
+ const folded = name.toLowerCase();
711
+ if (folded in held) throw refusal$1(bucket, `The user metadata key ${JSON.stringify(folded)} is given more than once`, key);
712
+ const encoded = encodeValue(value);
713
+ held[folded] = value;
714
+ headers.push([`${headerPrefix}${folded}`, encoded]);
715
+ headerBytes += folded.length + encoded.length;
716
+ }
717
+ if (headerBytes > headerByteLimit) throw refusal$1(bucket, `The user metadata is ${headerBytes} encoded header bytes, above the limit of ${headerByteLimit}`, key);
718
+ return {
719
+ headers,
720
+ held: Object.freeze(held)
721
+ };
722
+ }
723
+ /**
724
+ * The user metadata a `GET` or a `HEAD` response carries. AWS decodes an encoded word
725
+ * before it stores the value and encodes it again on the way out, in a form of its own
726
+ * choosing, so every form RFC 2047 allows is read and not only the one written.
727
+ */
728
+ function readUserMetadata(headers) {
729
+ const held = Object.create(null);
730
+ for (const [name, value] of headers) {
731
+ if (!name.startsWith(headerPrefix)) continue;
732
+ held[name.slice(11)] = decodeValue(value);
733
+ }
734
+ return Object.freeze(held);
735
+ }
736
+ function encodeValue(value) {
737
+ if (travelsAsWritten.test(value) && !value.includes("=?")) return value;
738
+ return utf8PiecesOf(value).map((piece) => `${encodedWordStart}${btoa(String.fromCharCode(...piece))}${encodedWordEnd}`).join(" ");
739
+ }
740
+ /** The UTF-8 bytes in pieces of at most one encoded word, each ending on a character. */
741
+ function utf8PiecesOf(value) {
742
+ const pieces = [];
743
+ let pending = [];
744
+ for (const character of value) {
745
+ const bytes = utf8$4.encode(character);
746
+ if (pending.length + bytes.length > bytesPerEncodedWord) {
747
+ pieces.push(Uint8Array.from(pending));
748
+ pending = [];
749
+ }
750
+ pending.push(...bytes);
751
+ }
752
+ pieces.push(Uint8Array.from(pending));
753
+ return pieces;
754
+ }
755
+ const encodedWord = /=\?([^?\s]+)\?([BbQq])\?([^?\s]*)\?=/gu;
756
+ const whitespaceOnly = /^[ \t]+$/u;
757
+ const hexPair = /^[\dA-Fa-f]{2}$/u;
758
+ function decodeValue(value) {
759
+ let decoded = "";
760
+ let last = 0;
761
+ let previousWasEncoded = false;
762
+ for (const match of value.matchAll(encodedWord)) {
763
+ const between = value.slice(last, match.index);
764
+ const word = decodeWord(match[1] ?? "", match[2] ?? "", match[3] ?? "");
765
+ if (!(previousWasEncoded && word !== void 0 && whitespaceOnly.test(between))) decoded += between;
766
+ decoded += word ?? match[0];
767
+ previousWasEncoded = word !== void 0;
768
+ last = match.index + match[0].length;
769
+ }
770
+ return decoded + value.slice(last);
771
+ }
772
+ /** The text of one encoded word, or `undefined` where it names what cannot be read. */
773
+ function decodeWord(charset, encoding, text) {
774
+ const bytes = encoding.toUpperCase() === "B" ? base64Bytes(text) : quotedBytes(text);
775
+ if (bytes === void 0) return void 0;
776
+ try {
777
+ return new TextDecoder(charset, { fatal: true }).decode(bytes);
778
+ } catch {
779
+ return;
780
+ }
781
+ }
782
+ function base64Bytes(text) {
783
+ try {
784
+ return Uint8Array.from(atob(text), (character) => character.charCodeAt(0));
785
+ } catch {
786
+ return;
787
+ }
788
+ }
789
+ /** The `Q` encoding: `_` is a space and `=XX` one byte in hex. */
790
+ function quotedBytes(text) {
791
+ const bytes = [];
792
+ for (let index = 0; index < text.length; index += 1) {
793
+ const character = text[index];
794
+ if (character === "_") bytes.push(32);
795
+ else if (character === "=") {
796
+ const pair = text.slice(index + 1, index + 3);
797
+ if (!hexPair.test(pair)) return void 0;
798
+ bytes.push(Number.parseInt(pair, 16));
799
+ index += 2;
800
+ } else bytes.push(text.charCodeAt(index));
801
+ }
802
+ return Uint8Array.from(bytes);
803
+ }
804
+ function refusal$1(bucket, message, key) {
805
+ return s3Error(bucket, {
806
+ code: "InvalidRequest",
807
+ message,
808
+ operation: "put",
809
+ key,
810
+ attempts: 0
811
+ });
812
+ }
813
+ //#endregion
814
+ //#region src/description.ts
815
+ const defaultContentType = "application/octet-stream";
816
+ /** The description a `GET` or a `HEAD` response carries in its headers (spec 4.4). */
817
+ function describeResponse(bucket, key, operation, response) {
818
+ return {
819
+ key,
820
+ size: sizeOf$1(bucket, key, operation, response),
821
+ lastModified: lastModifiedOf(bucket, key, operation, response),
822
+ etag: etagOf$1(response),
823
+ contentType: response.headers.get("content-type") ?? "application/octet-stream",
824
+ userMetadata: readUserMetadata(response.headers)
825
+ };
826
+ }
827
+ /**
828
+ * What `put` wrote, described from what it sent: a `PutObject` answer carries the entity
829
+ * tag and the time the provider accepted the object, and neither length nor type. Spec
830
+ * 4.4 has that time come from the provider, so an answer without one is reported rather
831
+ * than dated from this clock. `CompleteMultipartUpload` carries its entity tag in the
832
+ * body instead, and hands it in as `etag`.
833
+ */
834
+ function describeWrite(bucket, key, size, contentType, userMetadata, response, etag = etagOf$1(response)) {
835
+ const accepted = Date.parse(response.headers.get("date") ?? "");
836
+ if (Number.isNaN(accepted)) throw incomplete(bucket, key, "put", "no time it was accepted");
837
+ return {
838
+ key,
839
+ size,
840
+ lastModified: new Date(accepted),
841
+ etag,
842
+ contentType,
843
+ userMetadata
844
+ };
845
+ }
846
+ function etagOf$1(response) {
847
+ const etag = response.headers.get("etag");
848
+ if (etag === null) return void 0;
849
+ return unquotedEtag(etag);
850
+ }
851
+ function unquotedEtag(etag) {
852
+ return etag.replace(/^"|"$/gu, "");
853
+ }
854
+ function sizeOf$1(bucket, key, operation, response) {
855
+ if (response.status === 206) {
856
+ const size = wholeSizeOf(response);
857
+ if (size === void 0) throw incomplete(bucket, key, operation, "no size of the whole object");
858
+ return size;
859
+ }
860
+ const header = response.headers.get("content-length");
861
+ if (header === null || header.trim() === "") throw incomplete(bucket, key, operation, "no length");
862
+ const length = Number(header);
863
+ if (!Number.isInteger(length) || length < 0) throw incomplete(bucket, key, operation, "no length");
864
+ return length;
865
+ }
866
+ function lastModifiedOf(bucket, key, operation, response) {
867
+ const modified = Date.parse(response.headers.get("last-modified") ?? "");
868
+ if (Number.isNaN(modified)) throw incomplete(bucket, key, operation, "no last-modified time");
869
+ return new Date(modified);
870
+ }
871
+ function incomplete(bucket, key, operation, missing) {
872
+ return s3Error(bucket, {
873
+ code: "ProviderError",
874
+ message: `The provider described the object under ${JSON.stringify(key)} with ${missing}`,
875
+ operation,
876
+ key,
877
+ attempts: 1
878
+ });
879
+ }
880
+ //#endregion
881
+ //#region src/credentials.ts
882
+ /** The three fields spec 7.1 names, and the whole set a resolved credential may carry. */
883
+ const credentialFields = /* @__PURE__ */ new Set([
884
+ "accessKeyId",
885
+ "secretAccessKey",
886
+ "sessionToken"
887
+ ]);
888
+ const accessKeyIdVariable = "AWS_ACCESS_KEY_ID";
889
+ const secretAccessKeyVariable = "AWS_SECRET_ACCESS_KEY";
890
+ const sessionTokenVariable = "AWS_SESSION_TOKEN";
891
+ /**
892
+ * Spec 7.3: the credential is resolved before every request that is signed and nothing
893
+ * is cached between calls, so a rotation the resolver performs reaches the next request.
894
+ */
895
+ async function resolveCredentials(source, options) {
896
+ return validate(typeof source === "function" ? await source(options) : source);
897
+ }
898
+ /**
899
+ * A resolver, passed as `credentials: fromEnv` rather than called, so that a rotated
900
+ * `AWS_SESSION_TOKEN` reaches the next request. Reading the three variables again is
901
+ * what a refresh is here, so the options it is handed decide nothing (ADR 0007).
902
+ */
903
+ function fromEnv(_options) {
904
+ const accessKeyId = readEnvironment(accessKeyIdVariable);
905
+ const secretAccessKey = readEnvironment(secretAccessKeyVariable);
906
+ const sessionToken = readEnvironment(sessionTokenVariable);
907
+ if (accessKeyId === "") throw emptyVariable(accessKeyIdVariable);
908
+ if (secretAccessKey === "") throw emptyVariable(secretAccessKeyVariable);
909
+ return sessionToken === "" ? {
910
+ accessKeyId,
911
+ secretAccessKey
912
+ } : {
913
+ accessKeyId,
914
+ secretAccessKey,
915
+ sessionToken
916
+ };
917
+ }
918
+ /**
919
+ * ADR 0007: `process.env` is the one route through Node, Bun, Deno's compatibility layer
920
+ * and a Worker under `nodejs_compat`. A Worker without it has no `process` at all, and
921
+ * Deno without `--allow-env` throws `NotCapable` rather than answering `undefined`, so
922
+ * both leave the value empty. The three names are read one at a time, because
923
+ * enumerating `process.env` needs the unscoped permission in Deno.
924
+ */
925
+ function readEnvironment(name) {
926
+ try {
927
+ if (typeof process === "undefined") return "";
928
+ return process?.env?.[name] ?? "";
929
+ } catch {
930
+ return "";
931
+ }
932
+ }
933
+ /**
934
+ * Spec 7.3: both required fields are non-empty strings and every key of the resolved
935
+ * object is one of the three, checked before signing rather than a round trip later.
936
+ */
937
+ function validate(credentials) {
938
+ if (typeof credentials !== "object" || credentials === null) throw refusal("The resolved credential is not an object");
939
+ for (const field of Object.keys(credentials)) if (!credentialFields.has(field)) throw refusal(`The credential field \`${field}\` is not one of the three S3 takes`);
940
+ requireFilled(credentials.accessKeyId, "accessKeyId");
941
+ requireFilled(credentials.secretAccessKey, "secretAccessKey");
942
+ return credentials;
943
+ }
944
+ function requireFilled(value, field) {
945
+ if (typeof value === "string" && value !== "") return;
946
+ throw refusal(`The credential field \`${field}\` is empty`);
947
+ }
948
+ function emptyVariable(name) {
949
+ return refusal(`The environment variable \`${name}\` is empty`);
950
+ }
951
+ function refusal(message) {
952
+ return s3Error("", {
953
+ code: "InvalidCredentials",
954
+ message,
955
+ operation: "credentials",
956
+ attempts: 0
957
+ });
958
+ }
959
+ //#endregion
960
+ //#region src/error-document.ts
961
+ /**
962
+ * The `Code` and `Message` a provider answers a failed request with. The parser spec 7.4
963
+ * puts in front of a listing reads a structure; a failure needs two texts out of one flat
964
+ * element, and a body that is no error document — an HTML page from a proxy in between,
965
+ * a body the provider left empty — leaves both unset rather than failing on its way to
966
+ * reporting a failure.
967
+ */
968
+ function readErrorDocument(body) {
969
+ return {
970
+ code: textOf(body, "Code"),
971
+ message: textOf(body, "Message")
972
+ };
973
+ }
974
+ function textOf(body, element) {
975
+ const text = new RegExp(`<${element}>([^<]*)</${element}>`, "u").exec(body)?.[1];
976
+ return text === void 0 ? void 0 : decodeEntities(text);
977
+ }
978
+ function decodeEntities(text) {
979
+ return text.replaceAll(/&(#x[\da-f]+|#\d+|[a-z]+);/giu, (entity, name) => {
980
+ if (name.startsWith("#")) {
981
+ const codePoint = Number(name.startsWith("#x") ? `0x${name.slice(2)}` : name.slice(1));
982
+ return Number.isInteger(codePoint) && codePoint >= 0 && codePoint <= 1114111 ? String.fromCodePoint(codePoint) : entity;
983
+ }
984
+ return predefinedEntities.get(name.toLowerCase()) ?? entity;
985
+ });
986
+ }
987
+ //#endregion
988
+ //#region src/hash.ts
989
+ const utf8$3 = new TextEncoder();
990
+ /**
991
+ * ADR 0009: Web Crypto defines SHA-256 as one shot over a `BufferSource` and has no
992
+ * incremental form, so the adapter holds what it hashes whole.
993
+ */
994
+ async function sha256Hex(data) {
995
+ return hex(await crypto.subtle.digest("SHA-256", bytesOf$1(data)));
996
+ }
997
+ async function hmacSha256(key, data) {
998
+ const imported = await crypto.subtle.importKey("raw", key, {
999
+ name: "HMAC",
1000
+ hash: "SHA-256"
1001
+ }, false, ["sign"]);
1002
+ return await crypto.subtle.sign("HMAC", imported, utf8$3.encode(data));
1003
+ }
1004
+ function hex(buffer) {
1005
+ return Array.from(new Uint8Array(buffer), (byte) => byte.toString(16).padStart(2, "0")).join("");
1006
+ }
1007
+ function bytesOf$1(data) {
1008
+ return typeof data === "string" ? utf8$3.encode(data) : data;
1009
+ }
1010
+ //#endregion
1011
+ //#region src/sign.ts
1012
+ const signingAlgorithm = "AWS4-HMAC-SHA256";
1013
+ async function signRequest(request) {
1014
+ const amzDate = amzDateOf(request.date);
1015
+ const scope = scopeOf(request, amzDate);
1016
+ const toSend = [
1017
+ ...request.headers,
1018
+ ["x-amz-date", amzDate],
1019
+ ...sessionTokenField(request.credentials)
1020
+ ];
1021
+ const canonical = canonicalHeaders([...toSend, ["host", request.host]]);
1022
+ const signed = await signCanonical(request, {
1023
+ query: request.query,
1024
+ headers: canonical,
1025
+ payloadHash: request.payloadHash,
1026
+ amzDate,
1027
+ scope
1028
+ });
1029
+ const authorization = `${signingAlgorithm} Credential=${request.credentials.accessKeyId}/${scope}, SignedHeaders=${canonical.names}, Signature=${signed.signature}`;
1030
+ return {
1031
+ headers: [...toSend, ["authorization", authorization]],
1032
+ ...signed
1033
+ };
1034
+ }
1035
+ /**
1036
+ * Spec 7.4 and ADR 0011: the payload hash a presigned URL signs, which excludes the body
1037
+ * from the signature because the signer never sees it. It is written here and nowhere
1038
+ * else, so a request the adapter sends itself has no way to carry it.
1039
+ */
1040
+ const unsignedPayload = "UNSIGNED-PAYLOAD";
1041
+ /**
1042
+ * SigV4 query signing: the authorization travels in the query rather than in headers, so
1043
+ * that a client holding no credential can send the request. The headers it binds are
1044
+ * signed and not handed back, because whoever calls the URL sends them.
1045
+ */
1046
+ async function presignRequest(request) {
1047
+ const amzDate = amzDateOf(request.date);
1048
+ const scope = scopeOf(request, amzDate);
1049
+ const canonical = canonicalHeaders([...request.headers, ["host", request.host]]);
1050
+ const query = [
1051
+ ...request.query,
1052
+ ["X-Amz-Algorithm", signingAlgorithm],
1053
+ ["X-Amz-Credential", `${request.credentials.accessKeyId}/${scope}`],
1054
+ ["X-Amz-Date", amzDate],
1055
+ ["X-Amz-Expires", String(request.expiresIn)],
1056
+ ["X-Amz-SignedHeaders", canonical.names],
1057
+ ...sessionTokenParameter(request.credentials)
1058
+ ];
1059
+ const signed = await signCanonical(request, {
1060
+ query,
1061
+ headers: canonical,
1062
+ payloadHash: unsignedPayload,
1063
+ amzDate,
1064
+ scope
1065
+ });
1066
+ return {
1067
+ query: [...query, ["X-Amz-Signature", signed.signature]],
1068
+ canonicalRequest: signed.canonicalRequest,
1069
+ stringToSign: signed.stringToSign
1070
+ };
1071
+ }
1072
+ async function signCanonical(request, input) {
1073
+ const canonicalRequest = [
1074
+ request.method,
1075
+ encodePath(request.path),
1076
+ encodeQuery(input.query),
1077
+ input.headers.lines,
1078
+ input.headers.names,
1079
+ input.payloadHash
1080
+ ].join("\n");
1081
+ const stringToSign = [
1082
+ signingAlgorithm,
1083
+ input.amzDate,
1084
+ input.scope,
1085
+ await sha256Hex(canonicalRequest)
1086
+ ].join("\n");
1087
+ return {
1088
+ canonicalRequest,
1089
+ stringToSign,
1090
+ signature: hex(await hmacSha256(await signingKey(request, input.amzDate.slice(0, 8)), stringToSign))
1091
+ };
1092
+ }
1093
+ function scopeOf(request, amzDate) {
1094
+ return `${amzDate.slice(0, 8)}/${request.region}/${request.service}/aws4_request`;
1095
+ }
1096
+ /** `20150830T123600Z`, which is what `x-amz-date` and the credential scope are written in. */
1097
+ function amzDateOf(date) {
1098
+ return `${date.toISOString().replaceAll(/[.:-]/gu, "").slice(0, 15)}Z`;
1099
+ }
1100
+ function sessionTokenField(credentials) {
1101
+ if (credentials.sessionToken === void 0) return [];
1102
+ return [["x-amz-security-token", credentials.sessionToken]];
1103
+ }
1104
+ function sessionTokenParameter(credentials) {
1105
+ if (credentials.sessionToken === void 0) return [];
1106
+ return [["X-Amz-Security-Token", credentials.sessionToken]];
1107
+ }
1108
+ async function signingKey(request, dateStamp) {
1109
+ return await hmacSha256(await hmacSha256(await hmacSha256(await hmacSha256(new TextEncoder().encode(`AWS4${request.credentials.secretAccessKey}`), dateStamp), request.region), request.service), "aws4_request");
1110
+ }
1111
+ //#endregion
1112
+ //#region src/request.ts
1113
+ const emptyBody = /* @__PURE__ */ new Uint8Array(0);
1114
+ /**
1115
+ * Spec 4.4 reads `size` off `Content-Length`, and a provider that compresses on the offer
1116
+ * `fetch` makes of its own on Node, Bun and Deno drops that header: R2 does for JSON and
1117
+ * text. It is sent unsigned, because it concerns the transfer rather than what the
1118
+ * provider acts on, and a hop that rewrites it would otherwise break the signature.
1119
+ */
1120
+ const identityEncoding = ["accept-encoding", "identity"];
1121
+ const service = "s3";
1122
+ /**
1123
+ * One S3 request, answered by the response the provider sent or rejected with the failure
1124
+ * it reported. Spec 7.4 computes the payload hash once and signs every attempt again; the
1125
+ * loop of spec 7.5 lives in `@stowage/core`, as the one definition of the budget and the
1126
+ * curve that a third-party adapter reads too.
1127
+ */
1128
+ async function send(configuration, request) {
1129
+ const payloadHash = await sha256Hex(request.body ?? emptyBody);
1130
+ let requestsSent = 0;
1131
+ try {
1132
+ return await withRetry(async () => {
1133
+ try {
1134
+ return await attemptWithRefresh(configuration, request, payloadHash);
1135
+ } catch (failure) {
1136
+ if (!isStorageError(failure)) throw failure;
1137
+ requestsSent += failure.attempts;
1138
+ if (request.repeatWithoutResponse === false && receivedNoResponse(failure)) throw new Unrepeated(withAttemptsMade(failure, requestsSent));
1139
+ throw failure;
1140
+ }
1141
+ }, {
1142
+ maxAttempts: configuration.maxAttempts,
1143
+ signal: request.signal
1144
+ });
1145
+ } catch (thrown) {
1146
+ if (thrown instanceof Unrepeated) throw thrown.failure;
1147
+ throw thrown;
1148
+ }
1149
+ }
1150
+ /**
1151
+ * A failure carried past `withRetry`, which repeats every `retryable` `StorageError` it
1152
+ * sees; the one that must not be repeated still reaches the caller as `retryable`,
1153
+ * because spec 4.10 has that flag state the condition and not what stowage did about it.
1154
+ */
1155
+ var Unrepeated = class {
1156
+ failure;
1157
+ constructor(failure) {
1158
+ this.failure = failure;
1159
+ }
1160
+ };
1161
+ function receivedNoResponse(failure) {
1162
+ return failure.code === "NetworkError" && failure.status === void 0;
1163
+ }
1164
+ /**
1165
+ * One attempt, which spec 7.3 has cost a second request where the provider answered
1166
+ * `Expired`: the credential is resolved again under `forceRefresh` and the request goes
1167
+ * out without a delay, because no wait makes a credential fresher. `retry: false` does
1168
+ * not switch that repeat off, so an attempt costs one request or two and an operation at
1169
+ * most six (ADR 0013).
1170
+ */
1171
+ async function attemptWithRefresh(configuration, request, payloadHash) {
1172
+ try {
1173
+ return await attemptOnce(configuration, request, payloadHash, false, 1);
1174
+ } catch (failure) {
1175
+ if (!isStorageError(failure) || failure.code !== "Expired") throw failure;
1176
+ return await attemptOnce(configuration, request, payloadHash, true, 2);
1177
+ }
1178
+ }
1179
+ /** One signed request, which is the attempt CONTEXT.md names and what a repeat repeats. */
1180
+ async function attemptOnce(configuration, request, payloadHash, forceRefresh, attempts) {
1181
+ const path = pathOf(configuration, request.key);
1182
+ const query = request.query ?? [];
1183
+ const credentials = await resolveCredentials(configuration.credentials, { forceRefresh }).catch((failure) => {
1184
+ throw inStorage(failure, configuration.bucket, request.operation, request.key);
1185
+ });
1186
+ const signed = await signRequest({
1187
+ method: request.method,
1188
+ host: configuration.host,
1189
+ path,
1190
+ query,
1191
+ headers: [...request.headers ?? [], ["x-amz-content-sha256", payloadHash]],
1192
+ payloadHash,
1193
+ credentials,
1194
+ region: configuration.region,
1195
+ service,
1196
+ date: /* @__PURE__ */ new Date()
1197
+ });
1198
+ const url = urlOf(configuration, path, query);
1199
+ let response;
1200
+ try {
1201
+ response = await fetch(url, {
1202
+ method: request.method,
1203
+ headers: [...signed.headers, identityEncoding].map(([name, value]) => [name, value]),
1204
+ body: request.body,
1205
+ signal: request.signal
1206
+ });
1207
+ } catch (failure) {
1208
+ throw transportFailure(configuration, request, failure, attempts);
1209
+ }
1210
+ if (response.ok) return response;
1211
+ throw await failureOf(configuration, request, response, attempts);
1212
+ }
1213
+ /**
1214
+ * Spec 7.1: the bucket is addressed virtual-hosted through the host, and path-style
1215
+ * through the first segment of the path. The key follows as it stands; `encodePath` is
1216
+ * what percent-encodes it, on the URL and in the signature alike.
1217
+ */
1218
+ function pathOf(configuration, key) {
1219
+ const prefix = configuration.forcePathStyle ? `${configuration.basePath}/${configuration.bucket}` : configuration.basePath;
1220
+ if (key === void 0) return prefix === "" ? "/" : prefix;
1221
+ return `${prefix}/${key}`;
1222
+ }
1223
+ /**
1224
+ * The URL a request is sent to, its path and query encoded as SigV4 signs them: the
1225
+ * provider rebuilds the canonical request from what the URL carries, so the two may not
1226
+ * differ by a single escape.
1227
+ */
1228
+ function urlOf(configuration, path, query) {
1229
+ const search = query.length === 0 ? "" : `?${encodeQuery(query)}`;
1230
+ return `${configuration.protocol}//${configuration.host}${encodePath(path)}${search}`;
1231
+ }
1232
+ /**
1233
+ * Spec 7.9: the provider's own code decides where the table recognizes one, the status of
1234
+ * spec 4.10 decides where it does not, and the status alone decides whether the condition
1235
+ * is transient. `status`, `providerCode`, `requestId` and the provider's message travel
1236
+ * along, which is what makes a failure traceable at the provider.
1237
+ */
1238
+ async function failureOf(configuration, request, response, attempts) {
1239
+ const document = await readFailure(request, response);
1240
+ const failure = readProviderFailure({
1241
+ status: response.status,
1242
+ operation: request.operation,
1243
+ method: request.method,
1244
+ key: request.key,
1245
+ hasContinuationToken: request.query?.some(([name]) => name === "continuation-token"),
1246
+ providerCode: document.code,
1247
+ providerMessage: document.message,
1248
+ bucketRegion: response.headers.get("x-amz-bucket-region") ?? void 0
1249
+ });
1250
+ return s3Error(configuration.bucket, {
1251
+ code: failure.code,
1252
+ message: failure.message,
1253
+ operation: request.operation,
1254
+ key: request.key,
1255
+ attempts,
1256
+ status: response.status,
1257
+ providerCode: document.code,
1258
+ requestId: response.headers.get("x-amz-request-id") ?? void 0,
1259
+ retryable: isTransientStatus(response.status)
1260
+ });
1261
+ }
1262
+ /**
1263
+ * Spec 7.9: `HEAD` carries no body, so `stat` and `exists` report the status alone. Every
1264
+ * other failed request is answered with the provider's error document, and reading it to
1265
+ * the end is also what releases the connection the next attempt needs.
1266
+ */
1267
+ async function readFailure(request, response) {
1268
+ if (request.method === "HEAD") {
1269
+ await response.body?.cancel();
1270
+ return {};
1271
+ }
1272
+ try {
1273
+ return readErrorDocument(await response.text());
1274
+ } catch {
1275
+ return {};
1276
+ }
1277
+ }
1278
+ function transportFailure(configuration, request, failure, attempts) {
1279
+ if (failure instanceof Error && failure.name === "AbortError") return failure;
1280
+ return s3Error(configuration.bucket, {
1281
+ code: "NetworkError",
1282
+ message: `The request received no response: ${String(failure)}`,
1283
+ operation: request.operation,
1284
+ key: request.key,
1285
+ attempts,
1286
+ retryable: true,
1287
+ cause: failure
1288
+ });
1289
+ }
1290
+ //#endregion
1291
+ //#region src/copy.ts
1292
+ /**
1293
+ * Spec 7.8: one `CopyObject`, with the provider's refusal of a source too large for it as
1294
+ * the answer rather than a fallback to `UploadPartCopy` (ADR 0016). S3's default
1295
+ * directive keeps the source's content type and user metadata, which spec 4.11 asks of a
1296
+ * copy. Its answer names neither the size nor the user metadata of what it wrote, so a `HEAD`
1297
+ * of the destination describes it.
1298
+ */
1299
+ async function copyObject(configuration, from, to, operation, signal) {
1300
+ const { bucket } = configuration;
1301
+ signal?.throwIfAborted();
1302
+ const response = await send(configuration, {
1303
+ method: "PUT",
1304
+ operation,
1305
+ key: to,
1306
+ headers: [["x-amz-copy-source", encodePath(`/${bucket}/${from}`)]],
1307
+ signal
1308
+ }).catch((failure) => {
1309
+ if (isStorageError(failure) && failure.providerCode === "NoSuchKey") throw inStorage(failure, bucket, operation, from);
1310
+ throw failure;
1311
+ });
1312
+ await readAnswerDocument({
1313
+ bucket,
1314
+ operation,
1315
+ key: to,
1316
+ subject: "the copy"
1317
+ }, response, "CopyObjectResult");
1318
+ return describeResponse(bucket, to, operation, await send(configuration, {
1319
+ method: "HEAD",
1320
+ operation,
1321
+ key: to,
1322
+ signal
1323
+ }));
1324
+ }
1325
+ //#endregion
1326
+ //#region src/key.ts
1327
+ /** The error the key violating the rule is reported as, or `undefined` where it holds. */
1328
+ function keyError(bucket, key, rule, operation) {
1329
+ const reason = invalidKeyReason(key, rule);
1330
+ if (reason === void 0) return void 0;
1331
+ return s3Error(bucket, {
1332
+ code: "InvalidKey",
1333
+ message: `The key ${JSON.stringify(key)} ${reason}`,
1334
+ operation,
1335
+ key,
1336
+ attempts: 0
1337
+ });
1338
+ }
1339
+ function requireKey(bucket, key, rule, operation) {
1340
+ const error = keyError(bucket, key, rule, operation);
1341
+ if (error !== void 0) throw error;
1342
+ }
1343
+ //#endregion
1344
+ //#region src/cursor.ts
1345
+ const cursorTag = "stowage-s3-1:";
1346
+ const unitDigits = 4;
1347
+ const hexPosition = new RegExp(`^(?:[\\da-f]{${unitDigits}})*$`);
1348
+ /** The provider's continuation token, as the opaque string that continues from there. */
1349
+ function encodeCursor(continuationToken) {
1350
+ return btoa(`${cursorTag}${hexOf(continuationToken)}`);
1351
+ }
1352
+ /** The token the cursor continues from, or `undefined` for one this adapter did not produce. */
1353
+ function decodeCursor(cursor) {
1354
+ let decoded;
1355
+ try {
1356
+ decoded = atob(cursor);
1357
+ } catch {
1358
+ return;
1359
+ }
1360
+ if (!decoded.startsWith(cursorTag)) return void 0;
1361
+ const position = decoded.slice(13);
1362
+ return position.length > 0 && hexPosition.test(position) ? tokenOf(position) : void 0;
1363
+ }
1364
+ function hexOf(token) {
1365
+ const units = [];
1366
+ for (let index = 0; index < token.length; index += 1) units.push(token.charCodeAt(index).toString(16).padStart(unitDigits, "0"));
1367
+ return units.join("");
1368
+ }
1369
+ function tokenOf(position) {
1370
+ const units = [];
1371
+ for (let index = 0; index < position.length; index += unitDigits) units.push(String.fromCharCode(Number.parseInt(position.slice(index, index + unitDigits), 16)));
1372
+ return units.join("");
1373
+ }
1374
+ //#endregion
1375
+ //#region src/listing-document.ts
1376
+ /**
1377
+ * What the provider listed, read through the parser of ADR 0003. Spec 4.6 makes an entry
1378
+ * that arrives without a key, a size or a last-modified time a `ProviderError`, and
1379
+ * spec 7.4 a document outside the subset the parser reads; both leave the page unread
1380
+ * rather than hand the caller a value made up for what was missing.
1381
+ */
1382
+ function readListingDocument(answer, body) {
1383
+ const root = parse(answer, body);
1384
+ if (root.name !== "ListBucketResult") throw malformed(answer, `a <${root.name}> where a <ListBucketResult> belongs`);
1385
+ return {
1386
+ objects: childrenNamed(root, "Contents").map((entry) => readEntry(answer, entry)),
1387
+ prefixes: childrenNamed(root, "CommonPrefixes").map((prefix) => {
1388
+ const text = textOf$1(prefix, "Prefix");
1389
+ if (text === void 0 || text === "") throw malformed(answer, "a pseudo-directory with no prefix");
1390
+ return text;
1391
+ }),
1392
+ continuationToken: continuationOf(answer, root)
1393
+ };
1394
+ }
1395
+ function parse(answer, body) {
1396
+ try {
1397
+ return parseXml(body);
1398
+ } catch (failure) {
1399
+ if (failure instanceof XmlSyntaxError) throw malformed(answer, `a document outside the XML stowage reads: ${failure.message}`, failure);
1400
+ throw failure;
1401
+ }
1402
+ }
1403
+ function readEntry(answer, entry) {
1404
+ const key = textOf$1(entry, "Key");
1405
+ if (key === void 0 || key === "") throw malformed(answer, "an object with no key");
1406
+ const size = sizeOf(textOf$1(entry, "Size"));
1407
+ if (size === void 0) throw malformed(answer, `the object under ${JSON.stringify(key)} with no size`);
1408
+ const lastModified = Date.parse(textOf$1(entry, "LastModified") ?? "");
1409
+ if (Number.isNaN(lastModified)) throw malformed(answer, `the object under ${JSON.stringify(key)} with no last-modified time`);
1410
+ return {
1411
+ key,
1412
+ size,
1413
+ lastModified: new Date(lastModified),
1414
+ etag: etagOf(textOf$1(entry, "ETag"))
1415
+ };
1416
+ }
1417
+ const decimalDigits = /^\d+$/u;
1418
+ function sizeOf(text) {
1419
+ if (text === void 0 || !decimalDigits.test(text)) return void 0;
1420
+ const size = Number(text);
1421
+ return Number.isSafeInteger(size) ? size : void 0;
1422
+ }
1423
+ function etagOf(text) {
1424
+ if (text === void 0 || text === "") return void 0;
1425
+ return unquotedEtag(text);
1426
+ }
1427
+ /**
1428
+ * The token the next page continues from. A listing the provider calls truncated and
1429
+ * hands no token for would end early without a word, so that is malformed too.
1430
+ */
1431
+ function continuationOf(answer, root) {
1432
+ const truncated = textOf$1(root, "IsTruncated");
1433
+ if (truncated === "false") return void 0;
1434
+ if (truncated !== "true") throw malformed(answer, "no word on whether the listing is complete");
1435
+ const token = textOf$1(root, "NextContinuationToken");
1436
+ if (token === void 0 || token === "") throw malformed(answer, "a listing it calls incomplete and no position to continue from");
1437
+ return token;
1438
+ }
1439
+ function childrenNamed(element, name) {
1440
+ return element.children.filter((child) => child.name === name);
1441
+ }
1442
+ function malformed(answer, what, cause) {
1443
+ return s3Error(answer.bucket, {
1444
+ code: "ProviderError",
1445
+ message: `The provider answered the listing with ${what}`,
1446
+ operation: answer.operation,
1447
+ attempts: 1,
1448
+ status: answer.status,
1449
+ requestId: answer.requestId,
1450
+ cause
1451
+ });
1452
+ }
1453
+ //#endregion
1454
+ //#region src/listing.ts
1455
+ const defaultPageSize = 1e3;
1456
+ const maxPageSize = 1e3;
1457
+ /**
1458
+ * Spec 4.6: a listing sends no request until it is read, so an option it refuses reaches
1459
+ * the caller from `page()` and from the iteration and not from `list`.
1460
+ */
1461
+ function createListing(configuration, options) {
1462
+ return {
1463
+ async page() {
1464
+ const document = await requestPage(configuration, readListRequest(configuration.bucket, options));
1465
+ return {
1466
+ objects: document.objects,
1467
+ prefixes: document.prefixes,
1468
+ cursor: document.continuationToken === void 0 ? void 0 : encodeCursor(document.continuationToken)
1469
+ };
1470
+ },
1471
+ async *[Symbol.asyncIterator]() {
1472
+ for await (const document of walkPages(configuration, readListRequest(configuration.bucket, options))) yield* document.objects;
1473
+ }
1474
+ };
1475
+ }
1476
+ /**
1477
+ * Every page of the listing from where the request starts to its end, which `list`
1478
+ * iterates and `deleteAll` deletes page by page as it arrives.
1479
+ */
1480
+ async function* walkPages(configuration, request) {
1481
+ for (let { continuationToken } = request;;) {
1482
+ const document = await requestPage(configuration, {
1483
+ ...request,
1484
+ continuationToken
1485
+ });
1486
+ if (document.continuationToken !== void 0 && document.continuationToken === continuationToken) throw s3Error(configuration.bucket, {
1487
+ code: "ProviderError",
1488
+ message: "The provider repeated the continuation token it was sent",
1489
+ operation: request.operation,
1490
+ attempts: 1
1491
+ });
1492
+ yield document;
1493
+ if (document.continuationToken === void 0) return;
1494
+ continuationToken = document.continuationToken;
1495
+ }
1496
+ }
1497
+ /** Everything spec 4.11 has `list` refuse without asking the provider. */
1498
+ function readListRequest(bucket, options) {
1499
+ requireKnownOptions(bucket, options, listOptionKeys, "list");
1500
+ const prefix = options?.prefix ?? "";
1501
+ requireKey(bucket, prefix, "prefix", "list");
1502
+ const pageSize = options?.pageSize ?? defaultPageSize;
1503
+ if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > 1e3) throw optionError$1(bucket, "pageSize", `takes a whole number from 1 to ${maxPageSize}`, "list");
1504
+ if (options?.delimiter === "") throw optionError$1(bucket, "delimiter", "takes at least one character", "list");
1505
+ const cursor = options?.cursor;
1506
+ const continuationToken = cursor === void 0 ? void 0 : decodeCursor(cursor);
1507
+ if (cursor !== void 0 && continuationToken === void 0) throw optionError$1(bucket, "cursor", "takes a cursor this storage handed out", "list");
1508
+ return {
1509
+ operation: "list",
1510
+ prefix,
1511
+ delimiter: options?.delimiter,
1512
+ pageSize,
1513
+ continuationToken,
1514
+ signal: options?.signal
1515
+ };
1516
+ }
1517
+ async function requestPage(configuration, request) {
1518
+ request.signal?.throwIfAborted();
1519
+ const query = [["list-type", "2"], ["max-keys", String(request.pageSize)]];
1520
+ if (request.prefix !== "") query.push(["prefix", request.prefix]);
1521
+ if (request.delimiter !== void 0) query.push(["delimiter", request.delimiter]);
1522
+ if (request.continuationToken !== void 0) query.push(["continuation-token", request.continuationToken]);
1523
+ const response = await send(configuration, {
1524
+ method: "GET",
1525
+ operation: request.operation,
1526
+ query,
1527
+ signal: request.signal
1528
+ });
1529
+ const answer = {
1530
+ bucket: configuration.bucket,
1531
+ operation: request.operation,
1532
+ status: response.status,
1533
+ requestId: response.headers.get("x-amz-request-id") ?? void 0
1534
+ };
1535
+ return readListingDocument(answer, await readBody(answer, response));
1536
+ }
1537
+ /**
1538
+ * Spec 7.5 repeats a transport failure that received no response; this one received its
1539
+ * response and broke in the body, which spec 4.5 leaves unresumed for `get` as well.
1540
+ */
1541
+ async function readBody(answer, response) {
1542
+ try {
1543
+ return await response.text();
1544
+ } catch (failure) {
1545
+ if (failure instanceof Error && failure.name === "AbortError") throw failure;
1546
+ throw s3Error(answer.bucket, {
1547
+ code: "NetworkError",
1548
+ message: `The listing broke while it was read: ${String(failure)}`,
1549
+ operation: answer.operation,
1550
+ attempts: 1,
1551
+ status: answer.status,
1552
+ requestId: answer.requestId,
1553
+ retryable: true,
1554
+ cause: failure
1555
+ });
1556
+ }
1557
+ }
1558
+ //#endregion
1559
+ //#region src/md5.ts
1560
+ /** The per-round shift amounts of RFC 1321, section 3.4. */
1561
+ const shifts = [
1562
+ 7,
1563
+ 12,
1564
+ 17,
1565
+ 22,
1566
+ 7,
1567
+ 12,
1568
+ 17,
1569
+ 22,
1570
+ 7,
1571
+ 12,
1572
+ 17,
1573
+ 22,
1574
+ 7,
1575
+ 12,
1576
+ 17,
1577
+ 22,
1578
+ 5,
1579
+ 9,
1580
+ 14,
1581
+ 20,
1582
+ 5,
1583
+ 9,
1584
+ 14,
1585
+ 20,
1586
+ 5,
1587
+ 9,
1588
+ 14,
1589
+ 20,
1590
+ 5,
1591
+ 9,
1592
+ 14,
1593
+ 20,
1594
+ 4,
1595
+ 11,
1596
+ 16,
1597
+ 23,
1598
+ 4,
1599
+ 11,
1600
+ 16,
1601
+ 23,
1602
+ 4,
1603
+ 11,
1604
+ 16,
1605
+ 23,
1606
+ 4,
1607
+ 11,
1608
+ 16,
1609
+ 23,
1610
+ 6,
1611
+ 10,
1612
+ 15,
1613
+ 21,
1614
+ 6,
1615
+ 10,
1616
+ 15,
1617
+ 21,
1618
+ 6,
1619
+ 10,
1620
+ 15,
1621
+ 21,
1622
+ 6,
1623
+ 10,
1624
+ 15,
1625
+ 21
1626
+ ];
1627
+ /** `floor(abs(sin(i + 1)) × 2^32)`, the table T of RFC 1321, section 3.4. */
1628
+ const sines = Array.from({ length: 64 }, (_, index) => Math.floor(Math.abs(Math.sin(index + 1)) * 2 ** 32) >>> 0);
1629
+ const blockBytes = 64;
1630
+ /**
1631
+ * The base64 of the MD5 digest, which is the form `Content-MD5` carries. `DeleteObjects`
1632
+ * is the one request AWS refuses without an integrity header, and ADR 0009 sends no
1633
+ * `x-amz-checksum-*`; Web Crypto offers no MD5 and `node:crypto` is not on every runtime
1634
+ * of spec 2, so the digest is computed here.
1635
+ */
1636
+ function md5Base64(message) {
1637
+ const digest = md5(message);
1638
+ return btoa(String.fromCharCode(...digest));
1639
+ }
1640
+ function md5(message) {
1641
+ const padded = pad(message);
1642
+ const words = new DataView(padded.buffer);
1643
+ const state = [
1644
+ 1732584193,
1645
+ 4023233417,
1646
+ 2562383102,
1647
+ 271733878
1648
+ ];
1649
+ for (let offset = 0; offset < padded.byteLength; offset += blockBytes) compress(state, words, offset);
1650
+ const digest = /* @__PURE__ */ new Uint8Array(16);
1651
+ const view = new DataView(digest.buffer);
1652
+ for (const [index, word] of state.entries()) view.setUint32(index * 4, word, true);
1653
+ return digest;
1654
+ }
1655
+ /** A `1` bit, zeros up to 56 bytes into a block, and the bit length as 64 bits, low first. */
1656
+ function pad(message) {
1657
+ const length = Math.ceil((message.byteLength + 9) / blockBytes) * blockBytes;
1658
+ const padded = new Uint8Array(length);
1659
+ const view = new DataView(padded.buffer);
1660
+ const bits = message.byteLength * 8;
1661
+ padded.set(message);
1662
+ padded[message.byteLength] = 128;
1663
+ view.setUint32(length - 8, bits >>> 0, true);
1664
+ view.setUint32(length - 4, Math.floor(bits / 2 ** 32), true);
1665
+ return padded;
1666
+ }
1667
+ function compress(state, words, offset) {
1668
+ let [a, b, c, d] = state;
1669
+ for (let round = 0; round < 64; round += 1) {
1670
+ let mixed;
1671
+ let word;
1672
+ if (round < 16) {
1673
+ mixed = b & c | ~b & d;
1674
+ word = round;
1675
+ } else if (round < 32) {
1676
+ mixed = d & b | ~d & c;
1677
+ word = (5 * round + 1) % 16;
1678
+ } else if (round < 48) {
1679
+ mixed = b ^ c ^ d;
1680
+ word = (3 * round + 5) % 16;
1681
+ } else {
1682
+ mixed = c ^ (b | ~d);
1683
+ word = 7 * round % 16;
1684
+ }
1685
+ const sum = a + mixed + (sines[round] ?? 0) + words.getUint32(offset + word * 4, true) >>> 0;
1686
+ const shift = shifts[round] ?? 0;
1687
+ a = d;
1688
+ d = c;
1689
+ c = b;
1690
+ b = b + (sum << shift | sum >>> 32 - shift) >>> 0;
1691
+ }
1692
+ state[0] = state[0] + a >>> 0;
1693
+ state[1] = state[1] + b >>> 0;
1694
+ state[2] = state[2] + c >>> 0;
1695
+ state[3] = state[3] + d >>> 0;
1696
+ }
1697
+ //#endregion
1698
+ //#region src/delete.ts
1699
+ /** What one `DeleteObjects` names at most, and so what spec 4.1 sends one request per. */
1700
+ const keysPerRequest = 1e3;
1701
+ const utf8$2 = new TextEncoder();
1702
+ /**
1703
+ * Spec 4.7: every key is reported, the invalid ones as `InvalidKey` without being sent,
1704
+ * and a failure of a request as a whole rejects the call instead of filling the report.
1705
+ */
1706
+ async function deleteKeys(configuration, keys, batch) {
1707
+ const failed = [];
1708
+ const sendable = [];
1709
+ for (const key of keys) {
1710
+ const refusal = refusalOf(configuration.bucket, key, batch.operation);
1711
+ if (refusal === void 0) sendable.push(key);
1712
+ else failed.push(refusal);
1713
+ }
1714
+ for (let offset = 0; offset < sendable.length; offset += keysPerRequest) {
1715
+ const slice = sendable.slice(offset, offset + keysPerRequest);
1716
+ failed.push(...await deleteBatch(configuration, slice, batch));
1717
+ }
1718
+ return {
1719
+ requested: keys.length,
1720
+ failed
1721
+ };
1722
+ }
1723
+ /**
1724
+ * Spec 4.11: every object below the prefix, listed a page at a time and each page deleted
1725
+ * as it arrives, so the call holds one page of keys whatever the prefix holds.
1726
+ */
1727
+ async function deleteBelow(configuration, prefix, signal) {
1728
+ const operation = "deleteAll";
1729
+ const failed = [];
1730
+ let requested = 0;
1731
+ for await (const page of walkPages(configuration, {
1732
+ operation,
1733
+ prefix,
1734
+ pageSize: maxPageSize,
1735
+ signal
1736
+ })) {
1737
+ const report = await deleteKeys(configuration, page.objects.map((entry) => entry.key), {
1738
+ operation,
1739
+ signal
1740
+ });
1741
+ requested += report.requested;
1742
+ failed.push(...report.failed);
1743
+ }
1744
+ return {
1745
+ requested,
1746
+ failed
1747
+ };
1748
+ }
1749
+ function refusalOf(bucket, key, operation) {
1750
+ const invalid = keyError(bucket, key, "addressable", operation);
1751
+ if (invalid !== void 0) return invalid;
1752
+ if (key.isWellFormed()) return void 0;
1753
+ return s3Error(bucket, {
1754
+ code: "InvalidKey",
1755
+ message: `The key ${JSON.stringify(key)} holds a lone surrogate, which has no UTF-8 form`,
1756
+ operation,
1757
+ key,
1758
+ attempts: 0
1759
+ });
1760
+ }
1761
+ async function deleteBatch(configuration, keys, batch) {
1762
+ if (keys.length === 0) return [];
1763
+ batch.signal?.throwIfAborted();
1764
+ const body = utf8$2.encode(deleteDocument(keys));
1765
+ const response = await send(configuration, {
1766
+ method: "POST",
1767
+ operation: batch.operation,
1768
+ query: [["delete", ""]],
1769
+ headers: [["content-type", "application/xml"], ["content-md5", md5Base64(body)]],
1770
+ body,
1771
+ signal: batch.signal
1772
+ });
1773
+ const requestId = response.headers.get("x-amz-request-id") ?? void 0;
1774
+ const answered = {
1775
+ bucket: configuration.bucket,
1776
+ operation: batch.operation,
1777
+ subject: "the deletion"
1778
+ };
1779
+ return (await readAnswerDocument(answered, response, "DeleteResult")).children.filter((child) => child.name === "Error").map((entry) => keyFailure(answered, entry, requestId));
1780
+ }
1781
+ /** `Quiet` has the provider answer with the keys it failed alone. */
1782
+ function deleteDocument(keys) {
1783
+ return `<?xml version="1.0" encoding="UTF-8"?><Delete><Quiet>true</Quiet>${keys.map((key) => `<Object><Key>${escapeXml(key)}</Key></Object>`).join("")}</Delete>`;
1784
+ }
1785
+ /**
1786
+ * One key the provider failed, told as the answer's failure is, without the `200` that
1787
+ * spoke for the whole request. Spec 4.7 has every entry carry its key, so an entry the
1788
+ * provider names no key for leaves the answer unread rather than reported keyless.
1789
+ */
1790
+ function keyFailure(request, entry, requestId) {
1791
+ const key = textOf$1(entry, "Key");
1792
+ if (key === void 0 || key === "") throw s3Error(request.bucket, {
1793
+ code: "ProviderError",
1794
+ message: `The provider answered ${request.subject} with a failed key it did not name`,
1795
+ operation: request.operation,
1796
+ attempts: 1,
1797
+ requestId
1798
+ });
1799
+ return embeddedFailure(request, {
1800
+ operation: request.operation,
1801
+ key,
1802
+ attempts: 1,
1803
+ requestId
1804
+ }, entry);
1805
+ }
1806
+ //#endregion
1807
+ //#region src/presign.ts
1808
+ /** The ceiling SigV4 query signing sets on `X-Amz-Expires`, a week in seconds. */
1809
+ const longestLifetime = 604800;
1810
+ /** Each override of spec 7.10 and the query parameter S3 answers as its response header. */
1811
+ const responseOverrides = [
1812
+ ["responseContentType", "response-content-type"],
1813
+ ["responseContentDisposition", "response-content-disposition"],
1814
+ ["responseCacheControl", "response-cache-control"],
1815
+ ["responseExpires", "response-expires"]
1816
+ ];
1817
+ /** Spec 7.10: `GetObject` on an addressable key, the overrides carried in the query. */
1818
+ async function presignGet(configuration, key, options) {
1819
+ const operation = "presignGet";
1820
+ requireKey(configuration.bucket, key, "addressable", operation);
1821
+ const given = readGroup(configuration.bucket, options, presignGetOptionKeys, operation);
1822
+ const expiresIn = readExpiresIn(configuration.bucket, given.expiresIn, operation);
1823
+ const query = [];
1824
+ for (const [option, parameter] of responseOverrides) {
1825
+ const value = given[option];
1826
+ if (value === void 0) continue;
1827
+ query.push([parameter, readText(configuration.bucket, value, option, operation)]);
1828
+ }
1829
+ return await presignedUrl(configuration, {
1830
+ method: "GET",
1831
+ operation,
1832
+ key,
1833
+ query,
1834
+ headers: [],
1835
+ expiresIn
1836
+ });
1837
+ }
1838
+ /**
1839
+ * Spec 7.10: `PutObject` on a writable key, with the content type and the content length
1840
+ * bound through signed headers. Nothing else is signed in: no user metadata and no
1841
+ * checksum, which the browser would have to match exactly for a `403` that names nothing
1842
+ * (ADR 0011).
1843
+ */
1844
+ async function presignPut(configuration, key, options) {
1845
+ const operation = "presignPut";
1846
+ requireKey(configuration.bucket, key, "writable", operation);
1847
+ const given = readGroup(configuration.bucket, options, presignPutOptionKeys, operation);
1848
+ const expiresIn = readExpiresIn(configuration.bucket, given.expiresIn, operation);
1849
+ const contentType = readText(configuration.bucket, given.contentType, "contentType", operation);
1850
+ const contentLength = readContentLength(configuration.bucket, given.contentLength, operation);
1851
+ return await presignedUrl(configuration, {
1852
+ method: "PUT",
1853
+ operation,
1854
+ key,
1855
+ query: [],
1856
+ headers: [["content-type", contentType], ["content-length", contentLength]],
1857
+ expiresIn
1858
+ });
1859
+ }
1860
+ /**
1861
+ * Spec 7.10: nothing is sent, so the one failure left after the options is the
1862
+ * credential's. It is resolved for every URL, as for every request (spec 7.3).
1863
+ */
1864
+ async function presignedUrl(configuration, request) {
1865
+ const credentials = await resolveCredentials(configuration.credentials, { forceRefresh: false }).catch((failure) => {
1866
+ if (isStorageError(failure) && failure.code === "InvalidCredentials") throw inStorage(failure, configuration.bucket, request.operation, request.key);
1867
+ throw s3Error(configuration.bucket, {
1868
+ code: "InvalidCredentials",
1869
+ message: "The credential resolver failed",
1870
+ operation: request.operation,
1871
+ key: request.key,
1872
+ attempts: 0,
1873
+ cause: failure
1874
+ });
1875
+ });
1876
+ const path = pathOf(configuration, request.key);
1877
+ return urlOf(configuration, path, (await presignRequest({
1878
+ method: request.method,
1879
+ host: configuration.host,
1880
+ path,
1881
+ query: request.query,
1882
+ headers: request.headers,
1883
+ credentials,
1884
+ region: configuration.region,
1885
+ service: "s3",
1886
+ date: /* @__PURE__ */ new Date(),
1887
+ expiresIn: request.expiresIn
1888
+ })).query);
1889
+ }
1890
+ /**
1891
+ * The options as a group whose keys are all known. A caller outside TypeScript may hand
1892
+ * no group at all, which reads as one holding nothing, so the required `expiresIn` is
1893
+ * what the refusal names.
1894
+ */
1895
+ function readGroup(bucket, options, known, operation) {
1896
+ if (typeof options !== "object" || options === null) return {};
1897
+ requireKnownOptions(bucket, options, known, operation);
1898
+ return { ...options };
1899
+ }
1900
+ /** ADR 0011: outside the week SigV4 allows, refused rather than signed for a `403`. */
1901
+ function readExpiresIn(bucket, value, operation) {
1902
+ if (typeof value === "number" && value >= 1 && value <= longestLifetime && Number.isInteger(value)) return value;
1903
+ throw optionError$1(bucket, "expiresIn", `takes the whole seconds 1 to ${longestLifetime}, the lifetime SigV4 allows`, operation);
1904
+ }
1905
+ function readContentLength(bucket, value, operation) {
1906
+ if (typeof value === "number" && Number.isInteger(value) && value >= 0) return BigInt(value).toString();
1907
+ throw optionError$1(bucket, "contentLength", "takes a finite, non-negative integer", operation);
1908
+ }
1909
+ function readText(bucket, value, option, operation) {
1910
+ if (typeof value === "string" && value !== "") return value;
1911
+ throw optionError$1(bucket, option, "takes a non-empty string", operation);
1912
+ }
1913
+ //#endregion
1914
+ //#region src/stored-object.ts
1915
+ /**
1916
+ * Spec 4.5: the description and the body come out of one response, the body is read
1917
+ * once, and a stream that breaks after `get` resolved errors with a `StorageError`
1918
+ * whichever of the four readers took it.
1919
+ */
1920
+ function createStoredObject(bucket, stat, response) {
1921
+ let read = false;
1922
+ const take = () => {
1923
+ if (read) throw s3Error(bucket, {
1924
+ code: "InvalidRequest",
1925
+ message: "The body of this stored object has already been read",
1926
+ operation: "get",
1927
+ key: stat.key,
1928
+ attempts: 0
1929
+ });
1930
+ read = true;
1931
+ };
1932
+ const broken = (failure) => s3Error(bucket, {
1933
+ code: "NetworkError",
1934
+ message: `The body of the object broke while it was read: ${String(failure)}`,
1935
+ operation: "get",
1936
+ key: stat.key,
1937
+ attempts: 1,
1938
+ retryable: true,
1939
+ requestId: response.headers.get("x-amz-request-id") ?? void 0,
1940
+ cause: failure
1941
+ });
1942
+ const asText = async () => {
1943
+ take();
1944
+ try {
1945
+ return await response.text();
1946
+ } catch (failure) {
1947
+ throw broken(failure);
1948
+ }
1949
+ };
1950
+ return {
1951
+ stat,
1952
+ stream() {
1953
+ let body;
1954
+ try {
1955
+ take();
1956
+ body = response.body ?? emptyStream();
1957
+ } catch (refusal) {
1958
+ return new ReadableStream({ start(controller) {
1959
+ controller.error(refusal);
1960
+ } });
1961
+ }
1962
+ return reportingFailure(body, broken);
1963
+ },
1964
+ async bytes() {
1965
+ take();
1966
+ try {
1967
+ return new Uint8Array(await response.arrayBuffer());
1968
+ } catch (failure) {
1969
+ throw broken(failure);
1970
+ }
1971
+ },
1972
+ text: asText,
1973
+ async json() {
1974
+ return JSON.parse(await asText());
1975
+ }
1976
+ };
1977
+ }
1978
+ function emptyStream() {
1979
+ return new ReadableStream({ start(controller) {
1980
+ controller.close();
1981
+ } });
1982
+ }
1983
+ /**
1984
+ * The same bytes, with a break of the response body reported as a `StorageError`. A
1985
+ * cancel reaches the response through the reader, which is what cancels the request
1986
+ * behind the stream (spec 4.5).
1987
+ */
1988
+ function reportingFailure(body, asStorageError) {
1989
+ const reader = body.getReader();
1990
+ return new ReadableStream({
1991
+ async pull(controller) {
1992
+ try {
1993
+ const { done, value } = await reader.read();
1994
+ if (done) controller.close();
1995
+ else controller.enqueue(value);
1996
+ } catch (failure) {
1997
+ controller.error(asStorageError(failure));
1998
+ }
1999
+ },
2000
+ async cancel(reason) {
2001
+ await reader.cancel(reason);
2002
+ }
2003
+ });
2004
+ }
2005
+ //#endregion
2006
+ //#region src/part-reader.ts
2007
+ /** Where a buffer starts before the stream has shown whether it fills a part. */
2008
+ const initialCapacity = 65536;
2009
+ /**
2010
+ * A stream read into parts of one size (spec 7.6). A part is known to be the last only
2011
+ * once the stream has ended behind it, so a full part looks one chunk ahead; that chunk
2012
+ * is the stream's own and becomes the start of the next part.
2013
+ *
2014
+ * A part handed back through `recycle` lends its buffer to a later one. Spec 7.6 bounds
2015
+ * an upload at `partSize × concurrency` of buffers, and a fresh buffer per part would
2016
+ * keep the settled ones alive until the collector noticed them.
2017
+ *
2018
+ * Until a part has filled, the buffer grows by doubling, because ADR 0009 has a body
2019
+ * shorter than a part allocate only what it needs. Once one has, the stream is known to
2020
+ * go as a multipart upload, and every buffer after it starts at the full part size.
2021
+ */
2022
+ var PartReader = class {
2023
+ #reader;
2024
+ #partSize;
2025
+ #signal;
2026
+ #cancelOnAbort = () => {
2027
+ this.#reader.cancel(this.#signal?.reason).catch(() => {});
2028
+ };
2029
+ #free = [];
2030
+ #ahead;
2031
+ #filledOnce = false;
2032
+ constructor(stream, partSize, signal) {
2033
+ this.#reader = stream.getReader();
2034
+ this.#partSize = partSize;
2035
+ this.#signal = signal;
2036
+ signal?.addEventListener("abort", this.#cancelOnAbort, { once: true });
2037
+ }
2038
+ async next() {
2039
+ let bytes = this.#freshBuffer();
2040
+ let filled = 0;
2041
+ while (filled < this.#partSize) {
2042
+ const chunk = this.#ahead ?? await this.#read();
2043
+ this.#ahead = void 0;
2044
+ if (chunk === void 0) return {
2045
+ bytes: bytes.subarray(0, filled),
2046
+ last: true
2047
+ };
2048
+ const taken = Math.min(chunk.byteLength, this.#partSize - filled);
2049
+ if (filled + taken > bytes.byteLength) bytes = this.#grown(bytes, filled + taken);
2050
+ bytes.set(chunk.subarray(0, taken), filled);
2051
+ filled += taken;
2052
+ if (taken < chunk.byteLength) this.#ahead = chunk.subarray(taken);
2053
+ }
2054
+ this.#filledOnce = true;
2055
+ this.#ahead ??= await this.#read();
2056
+ return {
2057
+ bytes,
2058
+ last: this.#ahead === void 0
2059
+ };
2060
+ }
2061
+ /** The part's request settled, so nothing reads its bytes any more. */
2062
+ recycle(part) {
2063
+ if (part.bytes.buffer.byteLength === this.#partSize) this.#free.push(part.bytes.buffer);
2064
+ }
2065
+ /** Spec 4.2 leaves the stream canceled where `put` rejects before reading it to its end. */
2066
+ async cancel(reason) {
2067
+ await this.#reader.cancel(reason).catch(() => {});
2068
+ }
2069
+ release() {
2070
+ this.#signal?.removeEventListener("abort", this.#cancelOnAbort);
2071
+ this.#reader.releaseLock();
2072
+ }
2073
+ #freshBuffer() {
2074
+ const pooled = this.#free.pop();
2075
+ if (pooled !== void 0) return new Uint8Array(pooled);
2076
+ return new Uint8Array(this.#filledOnce ? this.#partSize : Math.min(this.#partSize, initialCapacity));
2077
+ }
2078
+ #grown(bytes, needed) {
2079
+ let capacity = bytes.byteLength;
2080
+ while (capacity < needed) capacity *= 2;
2081
+ const larger = new Uint8Array(Math.min(capacity, this.#partSize));
2082
+ larger.set(bytes);
2083
+ return larger;
2084
+ }
2085
+ async #read() {
2086
+ for (;;) {
2087
+ const { done, value } = await this.#reader.read();
2088
+ this.#signal?.throwIfAborted();
2089
+ if (done) return void 0;
2090
+ if (value.byteLength > 0) return value;
2091
+ }
2092
+ }
2093
+ };
2094
+ //#endregion
2095
+ //#region src/upload.ts
2096
+ /** The object's own headers, which a multipart upload sends with the request that starts it. */
2097
+ function objectHeaders(write) {
2098
+ return [["content-type", write.contentType], ...write.userMetadata.headers];
2099
+ }
2100
+ /** Spec 7.6: bytes the adapter holds go as one `PUT`, which it never splits. */
2101
+ async function putObject(configuration, write, bytes) {
2102
+ const response = await send(configuration, {
2103
+ method: "PUT",
2104
+ operation: "put",
2105
+ key: write.key,
2106
+ headers: objectHeaders(write),
2107
+ body: bytes,
2108
+ signal: write.signal
2109
+ });
2110
+ await response.body?.cancel();
2111
+ return describeWrite(configuration.bucket, write.key, bytes.byteLength, write.contentType, write.userMetadata.held, response);
2112
+ }
2113
+ /**
2114
+ * Spec 7.6: a stream is read into parts of `partSize`, and one that ends within the first
2115
+ * goes as the `PUT` held bytes get.
2116
+ */
2117
+ async function uploadStream(configuration, write, stream) {
2118
+ const parts = new PartReader(stream, configuration.partSize, write.signal);
2119
+ try {
2120
+ const first = await parts.next();
2121
+ if (first.last) return await putObject(configuration, write, first.bytes);
2122
+ return await multipartUpload(configuration, write, parts, first);
2123
+ } catch (failure) {
2124
+ await parts.cancel(failure);
2125
+ throw failure;
2126
+ } finally {
2127
+ parts.release();
2128
+ }
2129
+ }
2130
+ const s3Namespace = "http://s3.amazonaws.com/doc/2006-03-01/";
2131
+ /** The provider's limit on both sides, which ADR 0016 leaves no option to lift. */
2132
+ const maxParts = 1e4;
2133
+ /**
2134
+ * Spec 7.6: a multipart upload aborts itself once it failed, after the parts in flight
2135
+ * and the source stream were canceled.
2136
+ */
2137
+ async function multipartUpload(configuration, write, parts, first) {
2138
+ const uploadId = await createUpload(configuration, write);
2139
+ let sent;
2140
+ try {
2141
+ sent = await sendParts(configuration, write, uploadId, parts, first);
2142
+ } catch (failure) {
2143
+ await parts.cancel(failure);
2144
+ await abortUpload(configuration, write, uploadId);
2145
+ throw failure;
2146
+ }
2147
+ return await completeUpload(configuration, write, uploadId, sent);
2148
+ }
2149
+ /**
2150
+ * Spec 7.6: `concurrency` parts in flight, and the next part read only once one of them
2151
+ * settled, so the part buffers never outnumber the parts in flight. The first failure
2152
+ * stops the parts still in flight, and is what the upload rejects with once they settled.
2153
+ */
2154
+ async function sendParts(configuration, write, uploadId, parts, first) {
2155
+ const stop = new AbortController();
2156
+ const partWrite = {
2157
+ ...write,
2158
+ signal: write.signal === void 0 ? stop.signal : AbortSignal.any([write.signal, stop.signal])
2159
+ };
2160
+ const inFlight = /* @__PURE__ */ new Set();
2161
+ const uploaded = [];
2162
+ let failure;
2163
+ let size = 0;
2164
+ const fail = (reason) => {
2165
+ failure ??= { reason };
2166
+ stop.abort();
2167
+ parts.cancel(reason);
2168
+ };
2169
+ const sendPart = async (number, part) => {
2170
+ try {
2171
+ const etag = await uploadPart(configuration, partWrite, uploadId, number, part);
2172
+ uploaded.push({
2173
+ number,
2174
+ etag
2175
+ });
2176
+ } catch (reason) {
2177
+ fail(reason);
2178
+ } finally {
2179
+ parts.recycle(part);
2180
+ }
2181
+ };
2182
+ try {
2183
+ for (let number = 1, part = first; !stop.signal.aborted; number += 1) {
2184
+ if (number === maxParts && !part.last) throw tooManyParts(configuration, write);
2185
+ const sending = sendPart(number, part);
2186
+ inFlight.add(sending);
2187
+ sending.finally(() => inFlight.delete(sending));
2188
+ size += part.bytes.byteLength;
2189
+ if (part.last) break;
2190
+ while (inFlight.size >= configuration.concurrency) await Promise.race(inFlight);
2191
+ part = await parts.next();
2192
+ }
2193
+ } catch (reason) {
2194
+ fail(reason);
2195
+ }
2196
+ await Promise.all(inFlight);
2197
+ if (failure !== void 0) throw failure.reason;
2198
+ return {
2199
+ uploaded: uploaded.toSorted((one, other) => one.number - other.number),
2200
+ size
2201
+ };
2202
+ }
2203
+ async function createUpload(configuration, write) {
2204
+ const response = await send(configuration, {
2205
+ method: "POST",
2206
+ operation: "put",
2207
+ key: write.key,
2208
+ query: [["uploads", ""]],
2209
+ headers: objectHeaders(write),
2210
+ signal: write.signal
2211
+ });
2212
+ const subject = "the start of the upload";
2213
+ const uploadId = textOf$1(await readAnswerDocument(answeredRequest(configuration, write, subject), response, "InitiateMultipartUploadResult"), "UploadId");
2214
+ if (uploadId === void 0 || uploadId === "") throw incompleteAnswer(configuration, write, response, subject, "no upload id");
2215
+ return uploadId;
2216
+ }
2217
+ async function uploadPart(configuration, write, uploadId, number, part) {
2218
+ const response = await send(configuration, {
2219
+ method: "PUT",
2220
+ operation: "put",
2221
+ key: write.key,
2222
+ query: [["partNumber", String(number)], ["uploadId", uploadId]],
2223
+ body: part.bytes,
2224
+ signal: write.signal
2225
+ });
2226
+ await response.body?.cancel();
2227
+ const etag = response.headers.get("etag");
2228
+ if (etag === null || etag === "") throw incompleteAnswer(configuration, write, response, `part ${number}`, "no entity tag");
2229
+ return etag;
2230
+ }
2231
+ async function completeUpload(configuration, write, uploadId, { uploaded, size }) {
2232
+ let response;
2233
+ let document;
2234
+ try {
2235
+ response = await send(configuration, {
2236
+ method: "POST",
2237
+ operation: "put",
2238
+ key: write.key,
2239
+ query: [["uploadId", uploadId]],
2240
+ headers: [["content-type", "application/xml"]],
2241
+ body: utf8$1.encode(completeDocument(uploaded)),
2242
+ signal: write.signal,
2243
+ repeatWithoutResponse: false
2244
+ });
2245
+ document = await readAnswerDocument(answeredRequest(configuration, write, "the completion of the upload"), response, "CompleteMultipartUploadResult");
2246
+ } catch (failure) {
2247
+ if (!mayHaveCommitted(failure, write.signal)) await abortUpload(configuration, write, uploadId);
2248
+ throw failure;
2249
+ }
2250
+ const etag = textOf$1(document, "ETag");
2251
+ return describeWrite(configuration.bucket, write.key, size, write.contentType, write.userMetadata.held, response, etag === void 0 ? void 0 : unquotedEtag(etag));
2252
+ }
2253
+ /**
2254
+ * Spec 7.7 leaves a completion that received no response unaborted, because an abort
2255
+ * could meet a commit still on its way. The same holds for a `200` that broke before its
2256
+ * body said how the commit went, since S3 sends that status before it has decided, and
2257
+ * for the caller's abort while the completion was out: the request may have arrived.
2258
+ * Only the provider's answer that it did not commit is aborted.
2259
+ */
2260
+ function mayHaveCommitted(failure, signal) {
2261
+ if (signal?.aborted) return true;
2262
+ return isStorageError(failure) && failure.code === "NetworkError";
2263
+ }
2264
+ /**
2265
+ * Spec 7.6: sent without a signal, because the caller's may be the one that just fired,
2266
+ * and never reported, because the failure that led here is what the caller is owed. What
2267
+ * a failed abort leaves behind is removed by the lifecycle rule of spec 7.2.
2268
+ */
2269
+ async function abortUpload(configuration, write, uploadId) {
2270
+ try {
2271
+ await (await send(configuration, {
2272
+ method: "DELETE",
2273
+ operation: "put",
2274
+ key: write.key,
2275
+ query: [["uploadId", uploadId]]
2276
+ })).body?.cancel();
2277
+ } catch {}
2278
+ }
2279
+ const utf8$1 = new TextEncoder();
2280
+ function completeDocument(uploaded) {
2281
+ const parts = uploaded.map(({ number, etag }) => `<Part><PartNumber>${number}</PartNumber><ETag>${escapeXml(etag)}</ETag></Part>`).join("");
2282
+ return `<?xml version="1.0" encoding="UTF-8"?><CompleteMultipartUpload xmlns="${s3Namespace}">${parts}</CompleteMultipartUpload>`;
2283
+ }
2284
+ function answeredRequest(configuration, write, subject) {
2285
+ return {
2286
+ bucket: configuration.bucket,
2287
+ operation: "put",
2288
+ key: write.key,
2289
+ subject
2290
+ };
2291
+ }
2292
+ /**
2293
+ * Spec 7.6: known once the last part the provider takes is full and the stream goes on.
2294
+ * ADR 0016 fixes the part size before the first part, so the way past it is a larger one.
2295
+ */
2296
+ function tooManyParts(configuration, write) {
2297
+ return s3Error(configuration.bucket, {
2298
+ code: "InvalidRequest",
2299
+ message: `The stream needs more than ${maxParts} parts of the configured \`partSize\` of ${configuration.partSize} bytes; a larger \`multipart.partSize\` carries it`,
2300
+ operation: "put",
2301
+ key: write.key,
2302
+ attempts: 0
2303
+ });
2304
+ }
2305
+ function incompleteAnswer(configuration, write, response, subject, missing) {
2306
+ return s3Error(configuration.bucket, {
2307
+ code: "ProviderError",
2308
+ message: `The provider answered ${subject} with ${missing}`,
2309
+ operation: "put",
2310
+ key: write.key,
2311
+ attempts: 1,
2312
+ status: response.status,
2313
+ requestId: response.headers.get("x-amz-request-id") ?? void 0
2314
+ });
2315
+ }
2316
+ //#endregion
2317
+ //#region src/index.ts
2318
+ function s3Storage(options) {
2319
+ return new SimpleStorageServiceStorage(options);
2320
+ }
2321
+ /** Spec 7.1, in the order `capabilityNames` of spec 4.9 lists the names. */
2322
+ const s3Capabilities = Object.freeze([
2323
+ "presignedUrls",
2324
+ "rangeReads",
2325
+ "userMetadata"
2326
+ ]);
2327
+ const utf8 = new TextEncoder();
2328
+ var SimpleStorageServiceStorage = class {
2329
+ provider = "s3";
2330
+ bucket;
2331
+ capabilities = s3Capabilities;
2332
+ #configuration;
2333
+ constructor(options) {
2334
+ this.#configuration = readConfiguration(options);
2335
+ this.bucket = this.#configuration.bucket;
2336
+ }
2337
+ async put(key, body, options) {
2338
+ try {
2339
+ return await this.#put(key, body, options);
2340
+ } catch (failure) {
2341
+ if (isStream(body) && !body.locked) await body.cancel(failure).catch(() => {});
2342
+ throw failure;
2343
+ }
2344
+ }
2345
+ async #put(key, body, options) {
2346
+ requireKey(this.bucket, key, "writable", "put");
2347
+ requireKnownOptions(this.bucket, options, putOptionKeys, "put");
2348
+ const write = {
2349
+ key,
2350
+ userMetadata: userMetadataHeaders(this.bucket, options?.userMetadata, key),
2351
+ contentType: this.#readContentType(options?.contentType),
2352
+ signal: options?.signal
2353
+ };
2354
+ options?.signal?.throwIfAborted();
2355
+ if (isStream(body)) return await uploadStream(this.#configuration, write, body);
2356
+ return await putObject(this.#configuration, write, bytesOf(body));
2357
+ }
2358
+ async get(key, options) {
2359
+ requireKey(this.bucket, key, "addressable", "get");
2360
+ requireKnownOptions(this.bucket, options, getOptionKeys, "get");
2361
+ const range = options?.range;
2362
+ requireRange(this.bucket, range);
2363
+ options?.signal?.throwIfAborted();
2364
+ const response = await send(this.#configuration, {
2365
+ method: "GET",
2366
+ operation: "get",
2367
+ key,
2368
+ headers: range === void 0 ? [] : [["range", rangeHeader(range)]],
2369
+ signal: options?.signal
2370
+ });
2371
+ const stat = describeResponse(this.bucket, key, "get", response);
2372
+ const refusal = range === void 0 || response.status === 206 ? void 0 : wholeAnswerFailure(this.bucket, key, range, stat.size);
2373
+ if (refusal !== void 0) {
2374
+ await response.body?.cancel();
2375
+ throw refusal;
2376
+ }
2377
+ return createStoredObject(this.bucket, stat, response);
2378
+ }
2379
+ async stat(key, options) {
2380
+ return describeResponse(this.bucket, key, "stat", await this.#head(key, "stat", options));
2381
+ }
2382
+ async exists(key, options) {
2383
+ try {
2384
+ await this.#head(key, "exists", options);
2385
+ return true;
2386
+ } catch (failure) {
2387
+ if (isStorageError(failure) && failure.code === "NotFound") return false;
2388
+ throw failure;
2389
+ }
2390
+ }
2391
+ list(options) {
2392
+ return createListing(this.#configuration, options);
2393
+ }
2394
+ async delete(...keys) {
2395
+ return await deleteKeys(this.#configuration, keys, { operation: "delete" });
2396
+ }
2397
+ async deleteAll(prefix, options) {
2398
+ requireKey(this.bucket, prefix, "prefix", "deleteAll");
2399
+ requireKnownOptions(this.bucket, options, operationOptionKeys, "deleteAll");
2400
+ options?.signal?.throwIfAborted();
2401
+ return await deleteBelow(this.#configuration, prefix, options?.signal);
2402
+ }
2403
+ async copy(from, to, options) {
2404
+ this.#requireCopyKeys(from, to, options, "copy");
2405
+ return await copyObject(this.#configuration, from, to, "copy", options?.signal);
2406
+ }
2407
+ /**
2408
+ * Spec 4.10: `copy`, then the source deleted, each failure told as `move`'s. The
2409
+ * destination is described before the source goes, so a failing delete leaves both in
2410
+ * place and a repeated `move` is safe.
2411
+ */
2412
+ async move(from, to, options) {
2413
+ this.#requireCopyKeys(from, to, options, "move");
2414
+ const written = await copyObject(this.#configuration, from, to, "move", options?.signal);
2415
+ await (await send(this.#configuration, {
2416
+ method: "DELETE",
2417
+ operation: "move",
2418
+ key: from,
2419
+ signal: options?.signal
2420
+ })).body?.cancel();
2421
+ return written;
2422
+ }
2423
+ async presignGet(key, options) {
2424
+ return await presignGet(this.#configuration, key, options);
2425
+ }
2426
+ async presignPut(key, options) {
2427
+ return await presignPut(this.#configuration, key, options);
2428
+ }
2429
+ async #head(key, operation, options) {
2430
+ requireKey(this.bucket, key, "addressable", operation);
2431
+ requireKnownOptions(this.bucket, options, operationOptionKeys, operation);
2432
+ options?.signal?.throwIfAborted();
2433
+ return await send(this.#configuration, {
2434
+ method: "HEAD",
2435
+ operation,
2436
+ key,
2437
+ signal: options?.signal
2438
+ });
2439
+ }
2440
+ /**
2441
+ * Spec 4.8 checks both keys before acting on either, and spec 7.8 has a copy of a key
2442
+ * onto itself stop before it leaves the process; a `move` onto itself would otherwise
2443
+ * delete the one object it named.
2444
+ */
2445
+ #requireCopyKeys(from, to, options, operation) {
2446
+ requireKey(this.bucket, from, "addressable", operation);
2447
+ requireKey(this.bucket, to, "writable", operation);
2448
+ requireKnownOptions(this.bucket, options, operationOptionKeys, operation);
2449
+ if (from !== to) return;
2450
+ throw s3Error(this.bucket, {
2451
+ code: "InvalidRequest",
2452
+ message: "A copy names one key as its source and another as its destination",
2453
+ operation,
2454
+ key: from,
2455
+ attempts: 0
2456
+ });
2457
+ }
2458
+ #readContentType(contentType) {
2459
+ if (contentType === void 0) return defaultContentType;
2460
+ if (typeof contentType !== "string" || contentType === "") throw optionError$1(this.bucket, "contentType", "takes a non-empty string", "put");
2461
+ return contentType;
2462
+ }
2463
+ };
2464
+ /** Spec 4.2: a string travels as its UTF-8 bytes. */
2465
+ function bytesOf(body) {
2466
+ return typeof body === "string" ? utf8.encode(body) : heldBytes(body);
2467
+ }
2468
+ /**
2469
+ * The same bytes as a view Web Crypto takes: `BufferSource` rules out a view on a
2470
+ * `SharedArrayBuffer`, which is the one body copied rather than hashed and sent where it
2471
+ * lies. ADR 0009 already spends the memory of holding a body whole, and a copy of every
2472
+ * one of them would spend it twice.
2473
+ */
2474
+ function heldBytes(body) {
2475
+ const { buffer } = body;
2476
+ if (buffer instanceof ArrayBuffer) return new Uint8Array(buffer, body.byteOffset, body.byteLength);
2477
+ return new Uint8Array(body);
2478
+ }
2479
+ function isStream(body) {
2480
+ return typeof body !== "string" && !(body instanceof Uint8Array);
2481
+ }
2482
+ //#endregion
2483
+ export { fromEnv, s3Storage };