@stowage/adapter-s3 0.0.0 → 0.2.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,2107 @@
1
+ import { StorageError, XmlSyntaxError, checkUserMetadata, decodeUserMetadataValue, encodeUserMetadataValue, errorCodeForStatus, invalidKeyReason, isStorageError, isTransientStatus, parseXml, rangeBoundsRefusal, rangeCoversWhole, rangeHeader, rangeStartRefusal, readEnvironment, uploadStream, wholeSizeOf, 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$4 = 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$4.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/answer-document.ts
323
+ /**
324
+ * The root of the document a `200` carries, read through the parser of spec 7.4. S3 may
325
+ * answer a `CopyObject` or a `DeleteObjects` with `200` and an `<Error>` in the body once
326
+ * the answer has begun, so a root of that name is the failure it reports, told with the
327
+ * provider's code and message, and not repeated (ADR 0013 lets no code into the group).
328
+ */
329
+ async function readAnswerDocument(request, response, expectedRoot) {
330
+ const requestId = response.headers.get("x-amz-request-id") ?? void 0;
331
+ const context = {
332
+ operation: request.operation,
333
+ key: request.key,
334
+ attempts: 1,
335
+ status: response.status,
336
+ requestId
337
+ };
338
+ const root = parseAnswer(request, await readBody$1(request, response, context), context);
339
+ if (root.name === "Error") throw embeddedFailure(request, context, root);
340
+ if (root.name !== expectedRoot) throw malformed$1(request, context, `a <${root.name}> where a <${expectedRoot}> belongs`);
341
+ return root;
342
+ }
343
+ /**
344
+ * The failure an `<Error>` element inside a `200` reports: a whole answer's, or one key's
345
+ * of a `DeleteObjects`. No status speaks for it, so its code decides alone.
346
+ */
347
+ function embeddedFailure(request, context, element) {
348
+ const providerCode = textOf$1(element, "Code") ?? "";
349
+ const failure = readEmbeddedFailure(providerCode, textOf$1(element, "Message") ?? `The provider failed ${request.subject}: ${providerCode}`);
350
+ return s3Error(request.bucket, {
351
+ ...context,
352
+ code: failure.code,
353
+ message: failure.message,
354
+ providerCode: providerCode === "" ? void 0 : providerCode,
355
+ retryable: failure.retryable
356
+ });
357
+ }
358
+ function textOf$1(element, name) {
359
+ return element.children.find((child) => child.name === name)?.text;
360
+ }
361
+ async function readBody$1(request, response, context) {
362
+ try {
363
+ return await response.text();
364
+ } catch (failure) {
365
+ if (failure instanceof Error && failure.name === "AbortError") throw failure;
366
+ throw s3Error(request.bucket, {
367
+ ...context,
368
+ code: "NetworkError",
369
+ message: `The answer to ${request.subject} broke while it was read: ${String(failure)}`,
370
+ retryable: true,
371
+ cause: failure
372
+ });
373
+ }
374
+ }
375
+ function parseAnswer(request, body, context) {
376
+ try {
377
+ return parseXml(body);
378
+ } catch (failure) {
379
+ if (failure instanceof XmlSyntaxError) throw malformed$1(request, context, `a document outside the XML stowage reads: ${failure.message}`, failure);
380
+ throw failure;
381
+ }
382
+ }
383
+ function malformed$1(request, context, what, cause) {
384
+ return s3Error(request.bucket, {
385
+ ...context,
386
+ code: "ProviderError",
387
+ message: `The provider answered ${request.subject} with ${what}`,
388
+ cause
389
+ });
390
+ }
391
+ //#endregion
392
+ //#region src/canonical.ts
393
+ /** What `encodeURIComponent` leaves alone and RFC 3986 counts as reserved. */
394
+ const reservedByEncodeUriComponent = /[!'()*]/g;
395
+ /** Runs of whitespace inside a header value, which SigV4 folds to one space. */
396
+ const whitespaceRun = /\s+/g;
397
+ function encodeRfc3986(value) {
398
+ return encodeURIComponent(value).replace(reservedByEncodeUriComponent, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
399
+ }
400
+ /**
401
+ * The canonical URI: the path percent-encoded segment by segment, so that a slash stays
402
+ * a slash and `#`, `%`, `?`, `+`, a space and everything above ASCII travel encoded
403
+ * (spec 7.4). S3 encodes the path once and normalizes nothing, which is why no `URL` is
404
+ * built from it: the constructor folds a `..` segment away and decodes what it was
405
+ * handed, and both would sign a path other than the one the request carries.
406
+ */
407
+ function encodePath(path) {
408
+ return path.split("/").map(encodeRfc3986).join("/");
409
+ }
410
+ /** The canonical query string: every pair encoded, sorted by name and then by value. */
411
+ function encodeQuery(query) {
412
+ return query.map(([name, value]) => [encodeRfc3986(name), encodeRfc3986(value)]).toSorted(compareFields).map(([name, value]) => `${name}=${value}`).join("&");
413
+ }
414
+ /**
415
+ * The canonical headers: names folded to lower case, values trimmed with their inner
416
+ * runs of whitespace folded to one space, fields of one name joined in the order they
417
+ * arrived, and the whole sorted by name.
418
+ */
419
+ function canonicalHeaders(fields) {
420
+ const joined = /* @__PURE__ */ new Map();
421
+ for (const [name, value] of fields) {
422
+ const folded = name.toLowerCase();
423
+ const trimmed = value.trim().replace(whitespaceRun, " ");
424
+ const held = joined.get(folded);
425
+ joined.set(folded, held === void 0 ? trimmed : `${held},${trimmed}`);
426
+ }
427
+ const sorted = [...joined].toSorted(compareFields);
428
+ return {
429
+ lines: sorted.map(([name, value]) => `${name}:${value}\n`).join(""),
430
+ names: sorted.map(([name]) => name).join(";")
431
+ };
432
+ }
433
+ function compareFields(one, other) {
434
+ if (one[0] !== other[0]) return one[0] < other[0] ? -1 : 1;
435
+ if (one[1] !== other[1]) return one[1] < other[1] ? -1 : 1;
436
+ return 0;
437
+ }
438
+ //#endregion
439
+ //#region src/range.ts
440
+ /** Refuses bounds the spec does not allow, before any request goes out (spec 4.3). */
441
+ function requireRange(bucket, range) {
442
+ const refusal = rangeBoundsRefusal(range);
443
+ if (refusal !== void 0) throw s3Error(bucket, {
444
+ ...refusal,
445
+ operation: "get",
446
+ attempts: 0
447
+ });
448
+ }
449
+ /**
450
+ * A provider that answers a ranged `GET` with `200` sent the whole object instead, which
451
+ * RFC 9110 allows. Where the range covers the object, that is the body asked for. For an
452
+ * object the range starts beyond, which is how S3 answers a range on an empty object, it is
453
+ * the refusal spec 4.3 names; for any other it is a body the caller did not ask for.
454
+ */
455
+ function wholeAnswerFailure(bucket, key, range, size) {
456
+ if (rangeCoversWhole(range, size)) return void 0;
457
+ const refusal = rangeStartRefusal(range, size, key);
458
+ if (refusal !== void 0) return s3Error(bucket, {
459
+ ...refusal,
460
+ operation: "get",
461
+ key,
462
+ attempts: 1
463
+ });
464
+ return s3Error(bucket, {
465
+ code: "ProviderError",
466
+ message: `The provider answered a range of the object under ${JSON.stringify(key)} with the whole of it`,
467
+ operation: "get",
468
+ key,
469
+ attempts: 1
470
+ });
471
+ }
472
+ //#endregion
473
+ //#region src/user-metadata.ts
474
+ const headerPrefix = "x-amz-meta-";
475
+ /**
476
+ * The header fields `userMetadata` travels in, refused before the request is signed in the
477
+ * order of spec 4.3, which reads the refusals off what the storage declares. A key outside
478
+ * ASCII is refused rather than sent, because R2 strips it on the way out and a write would
479
+ * lose it silently (ADR 0014).
480
+ */
481
+ function userMetadataHeaders(bucket, userMetadata, key, capabilities) {
482
+ const check = checkUserMetadata(userMetadata, capabilities);
483
+ if ("refusal" in check) throw s3Error(bucket, {
484
+ ...check.refusal,
485
+ operation: "put",
486
+ key,
487
+ attempts: 0
488
+ });
489
+ return {
490
+ headers: Object.entries(check.held).map(([name, value]) => [`${headerPrefix}${name}`, encodeUserMetadataValue(value)]),
491
+ held: check.held
492
+ };
493
+ }
494
+ /**
495
+ * The user metadata a `GET` or a `HEAD` response carries. AWS decodes an encoded word
496
+ * before it stores the value and encodes it again on the way out, in a form of its own
497
+ * choosing, so every form RFC 2047 allows is read and not only the one written.
498
+ */
499
+ function readUserMetadata(headers) {
500
+ const held = Object.create(null);
501
+ for (const [name, value] of headers) {
502
+ if (!name.startsWith(headerPrefix)) continue;
503
+ held[name.slice(11)] = decodeUserMetadataValue(value);
504
+ }
505
+ return Object.freeze(held);
506
+ }
507
+ //#endregion
508
+ //#region src/description.ts
509
+ const defaultContentType = "application/octet-stream";
510
+ /** The description a `GET` or a `HEAD` response carries in its headers (spec 4.4). */
511
+ function describeResponse(bucket, key, operation, response) {
512
+ return {
513
+ key,
514
+ size: sizeOf$1(bucket, key, operation, response),
515
+ lastModified: lastModifiedOf(bucket, key, operation, response),
516
+ etag: etagOf$1(response),
517
+ contentType: response.headers.get("content-type") ?? "application/octet-stream",
518
+ userMetadata: readUserMetadata(response.headers)
519
+ };
520
+ }
521
+ /**
522
+ * What `put` wrote, described from what it sent: a `PutObject` answer carries the entity
523
+ * tag and the time the provider accepted the object, and neither length nor type. Spec
524
+ * 4.4 has that time come from the provider, so an answer without one is reported rather
525
+ * than dated from this clock. `CompleteMultipartUpload` carries its entity tag in the
526
+ * body instead, and hands it in as `etag`.
527
+ */
528
+ function describeWrite(bucket, key, size, contentType, userMetadata, response, etag = etagOf$1(response)) {
529
+ const accepted = Date.parse(response.headers.get("date") ?? "");
530
+ if (Number.isNaN(accepted)) throw incomplete(bucket, key, "put", "no time it was accepted");
531
+ return {
532
+ key,
533
+ size,
534
+ lastModified: new Date(accepted),
535
+ etag,
536
+ contentType,
537
+ userMetadata
538
+ };
539
+ }
540
+ function etagOf$1(response) {
541
+ const etag = response.headers.get("etag");
542
+ if (etag === null) return void 0;
543
+ return unquotedEtag(etag);
544
+ }
545
+ function unquotedEtag(etag) {
546
+ return etag.replace(/^"|"$/gu, "");
547
+ }
548
+ function sizeOf$1(bucket, key, operation, response) {
549
+ if (response.status === 206) {
550
+ const size = wholeSizeOf(response.headers.get("content-range"));
551
+ if (size === void 0) throw incomplete(bucket, key, operation, "no size of the whole object");
552
+ return size;
553
+ }
554
+ const header = response.headers.get("content-length");
555
+ if (header === null || header.trim() === "") throw incomplete(bucket, key, operation, "no length");
556
+ const length = Number(header);
557
+ if (!Number.isInteger(length) || length < 0) throw incomplete(bucket, key, operation, "no length");
558
+ return length;
559
+ }
560
+ function lastModifiedOf(bucket, key, operation, response) {
561
+ const modified = Date.parse(response.headers.get("last-modified") ?? "");
562
+ if (Number.isNaN(modified)) throw incomplete(bucket, key, operation, "no last-modified time");
563
+ return new Date(modified);
564
+ }
565
+ function incomplete(bucket, key, operation, missing) {
566
+ return s3Error(bucket, {
567
+ code: "ProviderError",
568
+ message: `The provider described the object under ${JSON.stringify(key)} with ${missing}`,
569
+ operation,
570
+ key,
571
+ attempts: 1
572
+ });
573
+ }
574
+ //#endregion
575
+ //#region src/credentials.ts
576
+ /** The three fields spec 7.1 names, and the whole set a resolved credential may carry. */
577
+ const credentialFields = /* @__PURE__ */ new Set([
578
+ "accessKeyId",
579
+ "secretAccessKey",
580
+ "sessionToken"
581
+ ]);
582
+ const accessKeyIdVariable = "AWS_ACCESS_KEY_ID";
583
+ const secretAccessKeyVariable = "AWS_SECRET_ACCESS_KEY";
584
+ const sessionTokenVariable = "AWS_SESSION_TOKEN";
585
+ /**
586
+ * Spec 7.3: the credential is resolved before every request that is signed and nothing
587
+ * is cached between calls, so a rotation the resolver performs reaches the next request.
588
+ */
589
+ async function resolveCredentials(source, options) {
590
+ return validate(typeof source === "function" ? await source(options) : source);
591
+ }
592
+ /**
593
+ * A resolver, passed as `credentials: fromEnv` rather than called, so that a rotated
594
+ * `AWS_SESSION_TOKEN` reaches the next request. Reading the three variables again is
595
+ * what a refresh is here, so the options it is handed decide nothing (ADR 0007).
596
+ */
597
+ function fromEnv(_options) {
598
+ const accessKeyId = readEnvironment(accessKeyIdVariable);
599
+ const secretAccessKey = readEnvironment(secretAccessKeyVariable);
600
+ const sessionToken = readEnvironment(sessionTokenVariable);
601
+ if (accessKeyId === "") throw emptyVariable(accessKeyIdVariable);
602
+ if (secretAccessKey === "") throw emptyVariable(secretAccessKeyVariable);
603
+ return sessionToken === "" ? {
604
+ accessKeyId,
605
+ secretAccessKey
606
+ } : {
607
+ accessKeyId,
608
+ secretAccessKey,
609
+ sessionToken
610
+ };
611
+ }
612
+ /**
613
+ * Spec 7.3: both required fields are non-empty strings and every key of the resolved
614
+ * object is one of the three, checked before signing rather than a round trip later.
615
+ */
616
+ function validate(credentials) {
617
+ if (typeof credentials !== "object" || credentials === null) throw refusal("The resolved credential is not an object");
618
+ 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`);
619
+ requireFilled(credentials.accessKeyId, "accessKeyId");
620
+ requireFilled(credentials.secretAccessKey, "secretAccessKey");
621
+ return credentials;
622
+ }
623
+ function requireFilled(value, field) {
624
+ if (typeof value === "string" && value !== "") return;
625
+ throw refusal(`The credential field \`${field}\` is empty`);
626
+ }
627
+ function emptyVariable(name) {
628
+ return refusal(`The environment variable \`${name}\` is empty`);
629
+ }
630
+ function refusal(message) {
631
+ return s3Error("", {
632
+ code: "InvalidCredentials",
633
+ message,
634
+ operation: "credentials",
635
+ attempts: 0
636
+ });
637
+ }
638
+ //#endregion
639
+ //#region src/xml.ts
640
+ /**
641
+ * The five entities XML defines without a DTD. The core's parser keeps its table to itself,
642
+ * and `readErrorDocument` decodes a failure's message without the parser.
643
+ */
644
+ const predefinedEntities = /* @__PURE__ */ new Map([
645
+ ["amp", "&"],
646
+ ["lt", "<"],
647
+ ["gt", ">"],
648
+ ["quot", "\""],
649
+ ["apos", "'"]
650
+ ]);
651
+ const xmlEscapes = {
652
+ "&": "&amp;",
653
+ "<": "&lt;",
654
+ ">": "&gt;",
655
+ "\"": "&quot;",
656
+ "'": "&apos;"
657
+ };
658
+ /** Text as it stands inside an element of a request document stowage writes. */
659
+ function escapeXml(text) {
660
+ return text.replaceAll(/[&<>"']/gu, (character) => xmlEscapes[character] ?? character);
661
+ }
662
+ //#endregion
663
+ //#region src/error-document.ts
664
+ /**
665
+ * The `Code` and `Message` a provider answers a failed request with. The parser spec 7.4
666
+ * puts in front of a listing reads a structure; a failure needs two texts out of one flat
667
+ * element, and a body that is no error document — an HTML page from a proxy in between,
668
+ * a body the provider left empty — leaves both unset rather than failing on its way to
669
+ * reporting a failure.
670
+ */
671
+ function readErrorDocument(body) {
672
+ return {
673
+ code: textOf(body, "Code"),
674
+ message: textOf(body, "Message")
675
+ };
676
+ }
677
+ function textOf(body, element) {
678
+ const text = new RegExp(`<${element}>([^<]*)</${element}>`, "u").exec(body)?.[1];
679
+ return text === void 0 ? void 0 : decodeEntities(text);
680
+ }
681
+ function decodeEntities(text) {
682
+ return text.replaceAll(/&(#x[\da-f]+|#\d+|[a-z]+);/giu, (entity, name) => {
683
+ if (name.startsWith("#")) {
684
+ const codePoint = Number(name.startsWith("#x") ? `0x${name.slice(2)}` : name.slice(1));
685
+ return Number.isInteger(codePoint) && codePoint >= 0 && codePoint <= 1114111 ? String.fromCodePoint(codePoint) : entity;
686
+ }
687
+ return predefinedEntities.get(name.toLowerCase()) ?? entity;
688
+ });
689
+ }
690
+ //#endregion
691
+ //#region src/hash.ts
692
+ const utf8$3 = new TextEncoder();
693
+ /**
694
+ * ADR 0009: Web Crypto defines SHA-256 as one shot over a `BufferSource` and has no
695
+ * incremental form, so the adapter holds what it hashes whole.
696
+ */
697
+ async function sha256Hex(data) {
698
+ return hex(await crypto.subtle.digest("SHA-256", bytesOf$1(data)));
699
+ }
700
+ async function hmacSha256(key, data) {
701
+ const imported = await crypto.subtle.importKey("raw", key, {
702
+ name: "HMAC",
703
+ hash: "SHA-256"
704
+ }, false, ["sign"]);
705
+ return await crypto.subtle.sign("HMAC", imported, utf8$3.encode(data));
706
+ }
707
+ function hex(buffer) {
708
+ return Array.from(new Uint8Array(buffer), (byte) => byte.toString(16).padStart(2, "0")).join("");
709
+ }
710
+ function bytesOf$1(data) {
711
+ return typeof data === "string" ? utf8$3.encode(data) : data;
712
+ }
713
+ //#endregion
714
+ //#region src/sign.ts
715
+ const signingAlgorithm = "AWS4-HMAC-SHA256";
716
+ async function signRequest(request) {
717
+ const amzDate = amzDateOf(request.date);
718
+ const scope = scopeOf(request, amzDate);
719
+ const toSend = [
720
+ ...request.headers,
721
+ ["x-amz-date", amzDate],
722
+ ...sessionTokenField(request.credentials)
723
+ ];
724
+ const canonical = canonicalHeaders([...toSend, ["host", request.host]]);
725
+ const signed = await signCanonical(request, {
726
+ query: request.query,
727
+ headers: canonical,
728
+ payloadHash: request.payloadHash,
729
+ amzDate,
730
+ scope
731
+ });
732
+ const authorization = `${signingAlgorithm} Credential=${request.credentials.accessKeyId}/${scope}, SignedHeaders=${canonical.names}, Signature=${signed.signature}`;
733
+ return {
734
+ headers: [...toSend, ["authorization", authorization]],
735
+ ...signed
736
+ };
737
+ }
738
+ /**
739
+ * Spec 7.4 and ADR 0011: the payload hash a presigned URL signs, which excludes the body
740
+ * from the signature because the signer never sees it. It is written here and nowhere
741
+ * else, so a request the adapter sends itself has no way to carry it.
742
+ */
743
+ const unsignedPayload = "UNSIGNED-PAYLOAD";
744
+ /**
745
+ * SigV4 query signing: the authorization travels in the query rather than in headers, so
746
+ * that a client holding no credential can send the request. The headers it binds are
747
+ * signed and not handed back, because whoever calls the URL sends them.
748
+ */
749
+ async function presignRequest(request) {
750
+ const amzDate = amzDateOf(request.date);
751
+ const scope = scopeOf(request, amzDate);
752
+ const canonical = canonicalHeaders([...request.headers, ["host", request.host]]);
753
+ const query = [
754
+ ...request.query,
755
+ ["X-Amz-Algorithm", signingAlgorithm],
756
+ ["X-Amz-Credential", `${request.credentials.accessKeyId}/${scope}`],
757
+ ["X-Amz-Date", amzDate],
758
+ ["X-Amz-Expires", String(request.expiresIn)],
759
+ ["X-Amz-SignedHeaders", canonical.names],
760
+ ...sessionTokenParameter(request.credentials)
761
+ ];
762
+ const signed = await signCanonical(request, {
763
+ query,
764
+ headers: canonical,
765
+ payloadHash: unsignedPayload,
766
+ amzDate,
767
+ scope
768
+ });
769
+ return {
770
+ query: [...query, ["X-Amz-Signature", signed.signature]],
771
+ canonicalRequest: signed.canonicalRequest,
772
+ stringToSign: signed.stringToSign
773
+ };
774
+ }
775
+ async function signCanonical(request, input) {
776
+ const canonicalRequest = [
777
+ request.method,
778
+ encodePath(request.path),
779
+ encodeQuery(input.query),
780
+ input.headers.lines,
781
+ input.headers.names,
782
+ input.payloadHash
783
+ ].join("\n");
784
+ const stringToSign = [
785
+ signingAlgorithm,
786
+ input.amzDate,
787
+ input.scope,
788
+ await sha256Hex(canonicalRequest)
789
+ ].join("\n");
790
+ return {
791
+ canonicalRequest,
792
+ stringToSign,
793
+ signature: hex(await hmacSha256(await signingKey(request, input.amzDate.slice(0, 8)), stringToSign))
794
+ };
795
+ }
796
+ function scopeOf(request, amzDate) {
797
+ return `${amzDate.slice(0, 8)}/${request.region}/${request.service}/aws4_request`;
798
+ }
799
+ /** `20150830T123600Z`, which is what `x-amz-date` and the credential scope are written in. */
800
+ function amzDateOf(date) {
801
+ return `${date.toISOString().replaceAll(/[.:-]/gu, "").slice(0, 15)}Z`;
802
+ }
803
+ function sessionTokenField(credentials) {
804
+ if (credentials.sessionToken === void 0) return [];
805
+ return [["x-amz-security-token", credentials.sessionToken]];
806
+ }
807
+ function sessionTokenParameter(credentials) {
808
+ if (credentials.sessionToken === void 0) return [];
809
+ return [["X-Amz-Security-Token", credentials.sessionToken]];
810
+ }
811
+ async function signingKey(request, dateStamp) {
812
+ return await hmacSha256(await hmacSha256(await hmacSha256(await hmacSha256(new TextEncoder().encode(`AWS4${request.credentials.secretAccessKey}`), dateStamp), request.region), request.service), "aws4_request");
813
+ }
814
+ //#endregion
815
+ //#region src/request.ts
816
+ const emptyBody = /* @__PURE__ */ new Uint8Array(0);
817
+ /**
818
+ * Spec 4.4 reads `size` off `Content-Length`, and a provider that compresses on the offer
819
+ * `fetch` makes of its own on Node, Bun and Deno drops that header: R2 does for JSON and
820
+ * text. It is sent unsigned, because it concerns the transfer rather than what the
821
+ * provider acts on, and a hop that rewrites it would otherwise break the signature.
822
+ */
823
+ const identityEncoding = ["accept-encoding", "identity"];
824
+ const service = "s3";
825
+ /**
826
+ * One S3 request, answered by the response the provider sent or rejected with the failure
827
+ * it reported. Spec 7.4 computes the payload hash once and signs every attempt again; the
828
+ * loop of spec 7.5 lives in `@stowage/core`, as the one definition of the budget and the
829
+ * curve that a third-party adapter reads too.
830
+ */
831
+ async function send(configuration, request) {
832
+ const payloadHash = await sha256Hex(request.body ?? emptyBody);
833
+ let requestsSent = 0;
834
+ try {
835
+ return await withRetry(async () => {
836
+ try {
837
+ return await attemptWithRefresh(configuration, request, payloadHash);
838
+ } catch (failure) {
839
+ if (!isStorageError(failure)) throw failure;
840
+ requestsSent += failure.attempts;
841
+ if (request.repeatWithoutResponse === false && receivedNoResponse(failure)) throw new Unrepeated(withAttemptsMade(failure, requestsSent));
842
+ throw failure;
843
+ }
844
+ }, {
845
+ maxAttempts: configuration.maxAttempts,
846
+ signal: request.signal
847
+ });
848
+ } catch (thrown) {
849
+ if (thrown instanceof Unrepeated) throw thrown.failure;
850
+ throw thrown;
851
+ }
852
+ }
853
+ /**
854
+ * A failure carried past `withRetry`, which repeats every `retryable` `StorageError` it
855
+ * sees; the one that must not be repeated still reaches the caller as `retryable`,
856
+ * because spec 4.10 has that flag state the condition and not what stowage did about it.
857
+ */
858
+ var Unrepeated = class {
859
+ failure;
860
+ constructor(failure) {
861
+ this.failure = failure;
862
+ }
863
+ };
864
+ function receivedNoResponse(failure) {
865
+ return failure.code === "NetworkError" && failure.status === void 0;
866
+ }
867
+ /**
868
+ * One attempt, which spec 7.3 has cost a second request where the provider answered
869
+ * `Expired`: the credential is resolved again under `forceRefresh` and the request goes
870
+ * out without a delay, because no wait makes a credential fresher. `retry: false` does
871
+ * not switch that repeat off, so an attempt costs one request or two and an operation at
872
+ * most six (ADR 0013).
873
+ */
874
+ async function attemptWithRefresh(configuration, request, payloadHash) {
875
+ try {
876
+ return await attemptOnce(configuration, request, payloadHash, false, 1);
877
+ } catch (failure) {
878
+ if (!isStorageError(failure) || failure.code !== "Expired") throw failure;
879
+ return await attemptOnce(configuration, request, payloadHash, true, 2);
880
+ }
881
+ }
882
+ /** One signed request, which is the attempt CONTEXT.md names and what a repeat repeats. */
883
+ async function attemptOnce(configuration, request, payloadHash, forceRefresh, attempts) {
884
+ const path = pathOf(configuration, request.key);
885
+ const query = request.query ?? [];
886
+ const credentials = await resolveCredentials(configuration.credentials, { forceRefresh }).catch((failure) => {
887
+ throw inStorage(failure, configuration.bucket, request.operation, request.key);
888
+ });
889
+ const signed = await signRequest({
890
+ method: request.method,
891
+ host: configuration.host,
892
+ path,
893
+ query,
894
+ headers: [...request.headers ?? [], ["x-amz-content-sha256", payloadHash]],
895
+ payloadHash,
896
+ credentials,
897
+ region: configuration.region,
898
+ service,
899
+ date: /* @__PURE__ */ new Date()
900
+ });
901
+ const url = urlOf(configuration, path, query);
902
+ let response;
903
+ try {
904
+ response = await fetch(url, {
905
+ method: request.method,
906
+ headers: [...signed.headers, identityEncoding].map(([name, value]) => [name, value]),
907
+ body: request.body,
908
+ signal: request.signal
909
+ });
910
+ } catch (failure) {
911
+ throw transportFailure(configuration, request, failure, attempts);
912
+ }
913
+ if (response.ok) return response;
914
+ throw await failureOf(configuration, request, response, attempts);
915
+ }
916
+ /**
917
+ * Spec 7.1: the bucket is addressed virtual-hosted through the host, and path-style
918
+ * through the first segment of the path. The key follows as it stands; `encodePath` is
919
+ * what percent-encodes it, on the URL and in the signature alike.
920
+ */
921
+ function pathOf(configuration, key) {
922
+ const prefix = configuration.forcePathStyle ? `${configuration.basePath}/${configuration.bucket}` : configuration.basePath;
923
+ if (key === void 0) return prefix === "" ? "/" : prefix;
924
+ return `${prefix}/${key}`;
925
+ }
926
+ /**
927
+ * The URL a request is sent to, its path and query encoded as SigV4 signs them: the
928
+ * provider rebuilds the canonical request from what the URL carries, so the two may not
929
+ * differ by a single escape.
930
+ */
931
+ function urlOf(configuration, path, query) {
932
+ const search = query.length === 0 ? "" : `?${encodeQuery(query)}`;
933
+ return `${configuration.protocol}//${configuration.host}${encodePath(path)}${search}`;
934
+ }
935
+ /**
936
+ * Spec 7.9: the provider's own code decides where the table recognizes one, the status of
937
+ * spec 4.10 decides where it does not, and the status alone decides whether the condition
938
+ * is transient. `status`, `providerCode`, `requestId` and the provider's message travel
939
+ * along, which is what makes a failure traceable at the provider.
940
+ */
941
+ async function failureOf(configuration, request, response, attempts) {
942
+ const document = await readFailure(request, response);
943
+ const failure = readProviderFailure({
944
+ status: response.status,
945
+ operation: request.operation,
946
+ method: request.method,
947
+ key: request.key,
948
+ hasContinuationToken: request.query?.some(([name]) => name === "continuation-token"),
949
+ providerCode: document.code,
950
+ providerMessage: document.message,
951
+ bucketRegion: response.headers.get("x-amz-bucket-region") ?? void 0
952
+ });
953
+ return s3Error(configuration.bucket, {
954
+ code: failure.code,
955
+ message: failure.message,
956
+ operation: request.operation,
957
+ key: request.key,
958
+ attempts,
959
+ status: response.status,
960
+ providerCode: document.code,
961
+ requestId: response.headers.get("x-amz-request-id") ?? void 0,
962
+ retryable: isTransientStatus(response.status)
963
+ });
964
+ }
965
+ /**
966
+ * Spec 7.9: `HEAD` carries no body, so `stat` and `exists` report the status alone. Every
967
+ * other failed request is answered with the provider's error document, and reading it to
968
+ * the end is also what releases the connection the next attempt needs.
969
+ */
970
+ async function readFailure(request, response) {
971
+ if (request.method === "HEAD") {
972
+ await response.body?.cancel();
973
+ return {};
974
+ }
975
+ try {
976
+ return readErrorDocument(await response.text());
977
+ } catch {
978
+ return {};
979
+ }
980
+ }
981
+ function transportFailure(configuration, request, failure, attempts) {
982
+ if (failure instanceof Error && failure.name === "AbortError") return failure;
983
+ return s3Error(configuration.bucket, {
984
+ code: "NetworkError",
985
+ message: `The request received no response: ${String(failure)}`,
986
+ operation: request.operation,
987
+ key: request.key,
988
+ attempts,
989
+ retryable: true,
990
+ cause: failure
991
+ });
992
+ }
993
+ //#endregion
994
+ //#region src/copy.ts
995
+ /**
996
+ * Spec 7.8: one `CopyObject`, with the provider's refusal of a source too large for it as
997
+ * the answer rather than a fallback to `UploadPartCopy` (ADR 0016). S3's default
998
+ * directive keeps the source's content type and user metadata, which spec 4.11 asks of a
999
+ * copy. Its answer names neither the size nor the user metadata of what it wrote, so a `HEAD`
1000
+ * of the destination describes it.
1001
+ */
1002
+ async function copyObject(configuration, from, to, operation, signal) {
1003
+ const { bucket } = configuration;
1004
+ signal?.throwIfAborted();
1005
+ const response = await send(configuration, {
1006
+ method: "PUT",
1007
+ operation,
1008
+ key: to,
1009
+ headers: [["x-amz-copy-source", encodePath(`/${bucket}/${from}`)]],
1010
+ signal
1011
+ }).catch((failure) => {
1012
+ if (isStorageError(failure) && failure.providerCode === "NoSuchKey") throw inStorage(failure, bucket, operation, from);
1013
+ throw failure;
1014
+ });
1015
+ await readAnswerDocument({
1016
+ bucket,
1017
+ operation,
1018
+ key: to,
1019
+ subject: "the copy"
1020
+ }, response, "CopyObjectResult");
1021
+ return describeResponse(bucket, to, operation, await send(configuration, {
1022
+ method: "HEAD",
1023
+ operation,
1024
+ key: to,
1025
+ signal
1026
+ }));
1027
+ }
1028
+ //#endregion
1029
+ //#region src/key.ts
1030
+ /** The error the key violating the rule is reported as, or `undefined` where it holds. */
1031
+ function keyError(bucket, key, rule, operation) {
1032
+ const reason = invalidKeyReason(key, rule);
1033
+ if (reason === void 0) return void 0;
1034
+ return s3Error(bucket, {
1035
+ code: "InvalidKey",
1036
+ message: `The key ${JSON.stringify(key)} ${reason}`,
1037
+ operation,
1038
+ key,
1039
+ attempts: 0
1040
+ });
1041
+ }
1042
+ function requireKey(bucket, key, rule, operation) {
1043
+ const error = keyError(bucket, key, rule, operation);
1044
+ if (error !== void 0) throw error;
1045
+ }
1046
+ //#endregion
1047
+ //#region src/cursor.ts
1048
+ const cursorTag = "stowage-s3-1:";
1049
+ const unitDigits = 4;
1050
+ const hexPosition = new RegExp(`^(?:[\\da-f]{${unitDigits}})*$`);
1051
+ /** The provider's continuation token, as the opaque string that continues from there. */
1052
+ function encodeCursor(continuationToken) {
1053
+ return btoa(`${cursorTag}${hexOf(continuationToken)}`);
1054
+ }
1055
+ /** The token the cursor continues from, or `undefined` for one this adapter did not produce. */
1056
+ function decodeCursor(cursor) {
1057
+ let decoded;
1058
+ try {
1059
+ decoded = atob(cursor);
1060
+ } catch {
1061
+ return;
1062
+ }
1063
+ if (!decoded.startsWith(cursorTag)) return void 0;
1064
+ const position = decoded.slice(13);
1065
+ if (position.length === 0 || !hexPosition.test(position)) return void 0;
1066
+ const token = tokenOf(position);
1067
+ return token.isWellFormed() ? token : void 0;
1068
+ }
1069
+ function hexOf(token) {
1070
+ const units = [];
1071
+ for (let index = 0; index < token.length; index += 1) units.push(token.charCodeAt(index).toString(16).padStart(unitDigits, "0"));
1072
+ return units.join("");
1073
+ }
1074
+ function tokenOf(position) {
1075
+ const units = [];
1076
+ for (let index = 0; index < position.length; index += unitDigits) units.push(String.fromCharCode(Number.parseInt(position.slice(index, index + unitDigits), 16)));
1077
+ return units.join("");
1078
+ }
1079
+ //#endregion
1080
+ //#region src/listing-document.ts
1081
+ /**
1082
+ * What the provider listed, read through the parser of ADR 0003. Spec 4.6 makes an entry
1083
+ * that arrives without a key, a size or a last-modified time a `ProviderError`, and
1084
+ * spec 7.4 a document outside the subset the parser reads; both leave the page unread
1085
+ * rather than hand the caller a value made up for what was missing.
1086
+ */
1087
+ function readListingDocument(answer, body) {
1088
+ const root = parse(answer, body);
1089
+ if (root.name !== "ListBucketResult") throw malformed(answer, `a <${root.name}> where a <ListBucketResult> belongs`);
1090
+ const readName = nameReader(answer, textOf$1(root, "EncodingType"));
1091
+ return {
1092
+ objects: childrenNamed(root, "Contents").map((entry) => readEntry(answer, entry, readName)),
1093
+ prefixes: childrenNamed(root, "CommonPrefixes").map((prefix) => {
1094
+ const text = textOf$1(prefix, "Prefix");
1095
+ if (text === void 0 || text === "") throw malformed(answer, "a pseudo-directory with no prefix");
1096
+ return readName(text);
1097
+ }),
1098
+ continuationToken: continuationOf(answer, root)
1099
+ };
1100
+ }
1101
+ /**
1102
+ * Spec 7.4: under `EncodingType` `url` a key and a prefix arrive as percent-encoded UTF-8,
1103
+ * a space in them possibly as `+`, which S3 never sends for a `+` of the key itself. The
1104
+ * continuation token and the elements the answer echoes are not names this reads.
1105
+ */
1106
+ function nameReader(answer, encodingType) {
1107
+ if (encodingType !== "url") return (text) => text;
1108
+ return (text) => {
1109
+ try {
1110
+ return decodeURIComponent(text.replaceAll("+", " "));
1111
+ } catch (failure) {
1112
+ if (!(failure instanceof URIError)) throw failure;
1113
+ throw malformed(answer, `the name ${JSON.stringify(text)}, which does not decode`, failure);
1114
+ }
1115
+ };
1116
+ }
1117
+ function parse(answer, body) {
1118
+ try {
1119
+ return parseXml(body);
1120
+ } catch (failure) {
1121
+ if (failure instanceof XmlSyntaxError) throw malformed(answer, `a document outside the XML stowage reads: ${failure.message}`, failure);
1122
+ throw failure;
1123
+ }
1124
+ }
1125
+ function readEntry(answer, entry, readName) {
1126
+ const text = textOf$1(entry, "Key");
1127
+ if (text === void 0 || text === "") throw malformed(answer, "an object with no key");
1128
+ const key = readName(text);
1129
+ const size = sizeOf(textOf$1(entry, "Size"));
1130
+ if (size === void 0) throw malformed(answer, `the object under ${JSON.stringify(key)} with no size`);
1131
+ const lastModified = Date.parse(textOf$1(entry, "LastModified") ?? "");
1132
+ if (Number.isNaN(lastModified)) throw malformed(answer, `the object under ${JSON.stringify(key)} with no last-modified time`);
1133
+ return {
1134
+ key,
1135
+ size,
1136
+ lastModified: new Date(lastModified),
1137
+ etag: etagOf(textOf$1(entry, "ETag"))
1138
+ };
1139
+ }
1140
+ const decimalDigits = /^\d+$/u;
1141
+ function sizeOf(text) {
1142
+ if (text === void 0 || !decimalDigits.test(text)) return void 0;
1143
+ const size = Number(text);
1144
+ return Number.isSafeInteger(size) ? size : void 0;
1145
+ }
1146
+ function etagOf(text) {
1147
+ if (text === void 0 || text === "") return void 0;
1148
+ return unquotedEtag(text);
1149
+ }
1150
+ /**
1151
+ * The token the next page continues from. A listing the provider calls truncated and
1152
+ * hands no token for would end early without a word, so that is malformed too.
1153
+ */
1154
+ function continuationOf(answer, root) {
1155
+ const truncated = textOf$1(root, "IsTruncated");
1156
+ if (truncated === "false") return void 0;
1157
+ if (truncated !== "true") throw malformed(answer, "no word on whether the listing is complete");
1158
+ const token = textOf$1(root, "NextContinuationToken");
1159
+ if (token === void 0 || token === "") throw malformed(answer, "a listing it calls incomplete and no position to continue from");
1160
+ return token;
1161
+ }
1162
+ function childrenNamed(element, name) {
1163
+ return element.children.filter((child) => child.name === name);
1164
+ }
1165
+ function malformed(answer, what, cause) {
1166
+ return s3Error(answer.bucket, {
1167
+ code: "ProviderError",
1168
+ message: `The provider answered the listing with ${what}`,
1169
+ operation: answer.operation,
1170
+ attempts: 1,
1171
+ status: answer.status,
1172
+ requestId: answer.requestId,
1173
+ cause
1174
+ });
1175
+ }
1176
+ //#endregion
1177
+ //#region src/listing.ts
1178
+ const defaultPageSize = 1e3;
1179
+ const maxPageSize = 1e3;
1180
+ /**
1181
+ * Spec 4.6: a listing sends no request until it is read, so an option it refuses reaches
1182
+ * the caller from `page()` and from the iteration and not from `list`.
1183
+ */
1184
+ function createListing(configuration, options) {
1185
+ return {
1186
+ async page() {
1187
+ const document = await requestPage(configuration, readListRequest(configuration.bucket, options));
1188
+ return {
1189
+ objects: document.objects,
1190
+ prefixes: document.prefixes,
1191
+ cursor: document.continuationToken === void 0 ? void 0 : encodeCursor(document.continuationToken)
1192
+ };
1193
+ },
1194
+ async *[Symbol.asyncIterator]() {
1195
+ for await (const document of walkPages(configuration, readListRequest(configuration.bucket, options))) yield* document.objects;
1196
+ }
1197
+ };
1198
+ }
1199
+ /**
1200
+ * Every page of the listing from where the request starts to its end, which `list`
1201
+ * iterates and `deleteAll` deletes page by page as it arrives.
1202
+ */
1203
+ async function* walkPages(configuration, request) {
1204
+ for (let { continuationToken } = request;;) {
1205
+ const document = await requestPage(configuration, {
1206
+ ...request,
1207
+ continuationToken
1208
+ });
1209
+ if (document.continuationToken !== void 0 && document.continuationToken === continuationToken) throw s3Error(configuration.bucket, {
1210
+ code: "ProviderError",
1211
+ message: "The provider repeated the continuation token it was sent",
1212
+ operation: request.operation,
1213
+ attempts: 1
1214
+ });
1215
+ yield document;
1216
+ if (document.continuationToken === void 0) return;
1217
+ continuationToken = document.continuationToken;
1218
+ }
1219
+ }
1220
+ /** Everything spec 4.11 has `list` refuse without asking the provider. */
1221
+ function readListRequest(bucket, options) {
1222
+ requireKnownOptions(bucket, options, listOptionKeys, "list");
1223
+ const prefix = options?.prefix ?? "";
1224
+ requireKey(bucket, prefix, "prefix", "list");
1225
+ const pageSize = options?.pageSize ?? defaultPageSize;
1226
+ if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > 1e3) throw optionError$1(bucket, "pageSize", `takes a whole number from 1 to ${maxPageSize}`, "list");
1227
+ if (options?.delimiter === "") throw optionError$1(bucket, "delimiter", "takes at least one character", "list");
1228
+ const cursor = options?.cursor;
1229
+ const continuationToken = cursor === void 0 ? void 0 : decodeCursor(cursor);
1230
+ if (cursor !== void 0 && continuationToken === void 0) throw optionError$1(bucket, "cursor", "takes a cursor this storage handed out", "list");
1231
+ return {
1232
+ operation: "list",
1233
+ prefix,
1234
+ delimiter: options?.delimiter,
1235
+ pageSize,
1236
+ continuationToken,
1237
+ signal: options?.signal
1238
+ };
1239
+ }
1240
+ async function requestPage(configuration, request) {
1241
+ request.signal?.throwIfAborted();
1242
+ const query = [
1243
+ ["list-type", "2"],
1244
+ ["max-keys", String(request.pageSize)],
1245
+ ["encoding-type", "url"]
1246
+ ];
1247
+ if (request.prefix !== "") query.push(["prefix", request.prefix]);
1248
+ if (request.delimiter !== void 0) query.push(["delimiter", request.delimiter]);
1249
+ if (request.continuationToken !== void 0) query.push(["continuation-token", request.continuationToken]);
1250
+ const response = await send(configuration, {
1251
+ method: "GET",
1252
+ operation: request.operation,
1253
+ query,
1254
+ signal: request.signal
1255
+ });
1256
+ const answer = {
1257
+ bucket: configuration.bucket,
1258
+ operation: request.operation,
1259
+ status: response.status,
1260
+ requestId: response.headers.get("x-amz-request-id") ?? void 0
1261
+ };
1262
+ return readListingDocument(answer, await readBody(answer, response));
1263
+ }
1264
+ /**
1265
+ * Spec 7.5 repeats a transport failure that received no response; this one received its
1266
+ * response and broke in the body, which spec 4.5 leaves unresumed for `get` as well.
1267
+ */
1268
+ async function readBody(answer, response) {
1269
+ try {
1270
+ return await response.text();
1271
+ } catch (failure) {
1272
+ if (failure instanceof Error && failure.name === "AbortError") throw failure;
1273
+ throw s3Error(answer.bucket, {
1274
+ code: "NetworkError",
1275
+ message: `The listing broke while it was read: ${String(failure)}`,
1276
+ operation: answer.operation,
1277
+ attempts: 1,
1278
+ status: answer.status,
1279
+ requestId: answer.requestId,
1280
+ retryable: true,
1281
+ cause: failure
1282
+ });
1283
+ }
1284
+ }
1285
+ //#endregion
1286
+ //#region src/md5.ts
1287
+ /** The per-round shift amounts of RFC 1321, section 3.4. */
1288
+ const shifts = [
1289
+ 7,
1290
+ 12,
1291
+ 17,
1292
+ 22,
1293
+ 7,
1294
+ 12,
1295
+ 17,
1296
+ 22,
1297
+ 7,
1298
+ 12,
1299
+ 17,
1300
+ 22,
1301
+ 7,
1302
+ 12,
1303
+ 17,
1304
+ 22,
1305
+ 5,
1306
+ 9,
1307
+ 14,
1308
+ 20,
1309
+ 5,
1310
+ 9,
1311
+ 14,
1312
+ 20,
1313
+ 5,
1314
+ 9,
1315
+ 14,
1316
+ 20,
1317
+ 5,
1318
+ 9,
1319
+ 14,
1320
+ 20,
1321
+ 4,
1322
+ 11,
1323
+ 16,
1324
+ 23,
1325
+ 4,
1326
+ 11,
1327
+ 16,
1328
+ 23,
1329
+ 4,
1330
+ 11,
1331
+ 16,
1332
+ 23,
1333
+ 4,
1334
+ 11,
1335
+ 16,
1336
+ 23,
1337
+ 6,
1338
+ 10,
1339
+ 15,
1340
+ 21,
1341
+ 6,
1342
+ 10,
1343
+ 15,
1344
+ 21,
1345
+ 6,
1346
+ 10,
1347
+ 15,
1348
+ 21,
1349
+ 6,
1350
+ 10,
1351
+ 15,
1352
+ 21
1353
+ ];
1354
+ /** `floor(abs(sin(i + 1)) × 2^32)`, the table T of RFC 1321, section 3.4. */
1355
+ const sines = Array.from({ length: 64 }, (_, index) => Math.floor(Math.abs(Math.sin(index + 1)) * 2 ** 32) >>> 0);
1356
+ const blockBytes = 64;
1357
+ /**
1358
+ * The base64 of the MD5 digest, which is the form `Content-MD5` carries. `DeleteObjects`
1359
+ * is the one request AWS refuses without an integrity header, and ADR 0009 sends no
1360
+ * `x-amz-checksum-*`; Web Crypto offers no MD5 and `node:crypto` is not on every runtime
1361
+ * of spec 2, so the digest is computed here.
1362
+ */
1363
+ function md5Base64(message) {
1364
+ const digest = md5(message);
1365
+ return btoa(String.fromCharCode(...digest));
1366
+ }
1367
+ function md5(message) {
1368
+ const padded = pad(message);
1369
+ const words = new DataView(padded.buffer);
1370
+ const state = [
1371
+ 1732584193,
1372
+ 4023233417,
1373
+ 2562383102,
1374
+ 271733878
1375
+ ];
1376
+ for (let offset = 0; offset < padded.byteLength; offset += blockBytes) compress(state, words, offset);
1377
+ const digest = /* @__PURE__ */ new Uint8Array(16);
1378
+ const view = new DataView(digest.buffer);
1379
+ for (const [index, word] of state.entries()) view.setUint32(index * 4, word, true);
1380
+ return digest;
1381
+ }
1382
+ /** A `1` bit, zeros up to 56 bytes into a block, and the bit length as 64 bits, low first. */
1383
+ function pad(message) {
1384
+ const length = Math.ceil((message.byteLength + 9) / blockBytes) * blockBytes;
1385
+ const padded = new Uint8Array(length);
1386
+ const view = new DataView(padded.buffer);
1387
+ const bits = message.byteLength * 8;
1388
+ padded.set(message);
1389
+ padded[message.byteLength] = 128;
1390
+ view.setUint32(length - 8, bits >>> 0, true);
1391
+ view.setUint32(length - 4, Math.floor(bits / 2 ** 32), true);
1392
+ return padded;
1393
+ }
1394
+ function compress(state, words, offset) {
1395
+ let [a, b, c, d] = state;
1396
+ for (let round = 0; round < 64; round += 1) {
1397
+ let mixed;
1398
+ let word;
1399
+ if (round < 16) {
1400
+ mixed = b & c | ~b & d;
1401
+ word = round;
1402
+ } else if (round < 32) {
1403
+ mixed = d & b | ~d & c;
1404
+ word = (5 * round + 1) % 16;
1405
+ } else if (round < 48) {
1406
+ mixed = b ^ c ^ d;
1407
+ word = (3 * round + 5) % 16;
1408
+ } else {
1409
+ mixed = c ^ (b | ~d);
1410
+ word = 7 * round % 16;
1411
+ }
1412
+ const sum = a + mixed + (sines[round] ?? 0) + words.getUint32(offset + word * 4, true) >>> 0;
1413
+ const shift = shifts[round] ?? 0;
1414
+ a = d;
1415
+ d = c;
1416
+ c = b;
1417
+ b = b + (sum << shift | sum >>> 32 - shift) >>> 0;
1418
+ }
1419
+ state[0] = state[0] + a >>> 0;
1420
+ state[1] = state[1] + b >>> 0;
1421
+ state[2] = state[2] + c >>> 0;
1422
+ state[3] = state[3] + d >>> 0;
1423
+ }
1424
+ //#endregion
1425
+ //#region src/delete.ts
1426
+ /** What one `DeleteObjects` names at most, and so what spec 7.1 sends one request per. */
1427
+ const keysPerRequest = 1e3;
1428
+ /**
1429
+ * The characters of a key XML 1.0 carries neither raw nor as a reference, which is what
1430
+ * is left of those outside its `Char` once spec 4.8 refused the controls (ADR 0027).
1431
+ */
1432
+ const outsideXmlChar = /[\uFFFE\uFFFF]/u;
1433
+ /**
1434
+ * Spec 4.7 rejects the call for a failure that says nothing about the key: no response, a
1435
+ * credential the provider refuses, a bucket that is absent or lives in another region. A
1436
+ * missing key answers `204`, so `NotFound` names the bucket. What remains is what
1437
+ * `DeleteObjects` reports per key, `AccessDenied` among it.
1438
+ */
1439
+ const requestWideCodes = /* @__PURE__ */ new Set([
1440
+ "NetworkError",
1441
+ "InvalidCredentials",
1442
+ "Expired",
1443
+ "NotFound",
1444
+ "InvalidOption"
1445
+ ]);
1446
+ /**
1447
+ * Spec 7.9 files a skewed clock under `InvalidRequest`, which otherwise concerns what one
1448
+ * request asked for, and every request after it would be signed against the same clock.
1449
+ */
1450
+ const requestWideProviderCodes = /* @__PURE__ */ new Set(["RequestTimeTooSkewed"]);
1451
+ function failsTheRequestAsAWhole(failure) {
1452
+ return requestWideCodes.has(failure.code) || failure.providerCode !== void 0 && requestWideProviderCodes.has(failure.providerCode);
1453
+ }
1454
+ const utf8$2 = new TextEncoder();
1455
+ /**
1456
+ * Spec 4.7: every key is reported, the invalid ones as `InvalidKey` without being sent,
1457
+ * and a failure of a request as a whole rejects the call instead of filling the report.
1458
+ */
1459
+ async function deleteKeys(configuration, keys, call) {
1460
+ const failed = [];
1461
+ const batched = [];
1462
+ const alone = [];
1463
+ for (const key of keys) {
1464
+ const refusal = keyError(configuration.bucket, key, "addressable", call.operation);
1465
+ if (refusal !== void 0) failed.push(refusal);
1466
+ else if (outsideXmlChar.test(key)) alone.push(key);
1467
+ else batched.push(key);
1468
+ }
1469
+ for (let offset = 0; offset < batched.length; offset += keysPerRequest) {
1470
+ const slice = batched.slice(offset, offset + keysPerRequest);
1471
+ failed.push(...await deleteBatch(configuration, slice, call));
1472
+ }
1473
+ for (const key of alone) {
1474
+ const failure = await deleteAlone(configuration, key, call);
1475
+ if (failure !== void 0) failed.push(failure);
1476
+ }
1477
+ return {
1478
+ requested: keys.length,
1479
+ failed
1480
+ };
1481
+ }
1482
+ /**
1483
+ * Spec 4.11: every object below the prefix, listed a page at a time and each page deleted
1484
+ * as it arrives, so the call holds one page of keys whatever the prefix holds.
1485
+ */
1486
+ async function deleteBelow(configuration, prefix, signal) {
1487
+ const operation = "deleteAll";
1488
+ const failed = [];
1489
+ let requested = 0;
1490
+ for await (const page of walkPages(configuration, {
1491
+ operation,
1492
+ prefix,
1493
+ pageSize: maxPageSize,
1494
+ signal
1495
+ })) {
1496
+ const report = await deleteKeys(configuration, page.objects.map((entry) => entry.key), {
1497
+ operation,
1498
+ signal
1499
+ });
1500
+ requested += report.requested;
1501
+ failed.push(...report.failed);
1502
+ }
1503
+ return {
1504
+ requested,
1505
+ failed
1506
+ };
1507
+ }
1508
+ async function deleteBatch(configuration, keys, call) {
1509
+ if (keys.length === 0) return [];
1510
+ call.signal?.throwIfAborted();
1511
+ const body = utf8$2.encode(deleteDocument(keys));
1512
+ const response = await send(configuration, {
1513
+ method: "POST",
1514
+ operation: call.operation,
1515
+ query: [["delete", ""]],
1516
+ headers: [["content-type", "application/xml"], ["content-md5", md5Base64(body)]],
1517
+ body,
1518
+ signal: call.signal
1519
+ });
1520
+ const requestId = response.headers.get("x-amz-request-id") ?? void 0;
1521
+ const answered = {
1522
+ bucket: configuration.bucket,
1523
+ operation: call.operation,
1524
+ subject: "the deletion"
1525
+ };
1526
+ return (await readAnswerDocument(answered, response, "DeleteResult")).children.filter((child) => child.name === "Error").map((entry) => keyFailure(answered, entry, requestId));
1527
+ }
1528
+ /**
1529
+ * Spec 7.4: a key no `DeleteObjects` body can carry travels percent-encoded in the path,
1530
+ * where no XML parser at the provider sees it, on the budget of a request of its own.
1531
+ */
1532
+ async function deleteAlone(configuration, key, call) {
1533
+ call.signal?.throwIfAborted();
1534
+ try {
1535
+ await (await send(configuration, {
1536
+ method: "DELETE",
1537
+ operation: call.operation,
1538
+ key,
1539
+ signal: call.signal
1540
+ })).body?.cancel();
1541
+ return;
1542
+ } catch (failure) {
1543
+ if (isStorageError(failure) && !failsTheRequestAsAWhole(failure)) return failure;
1544
+ throw failure;
1545
+ }
1546
+ }
1547
+ /** `Quiet` has the provider answer with the keys it failed alone. */
1548
+ function deleteDocument(keys) {
1549
+ return `<?xml version="1.0" encoding="UTF-8"?><Delete><Quiet>true</Quiet>${keys.map((key) => `<Object><Key>${escapeXml(key)}</Key></Object>`).join("")}</Delete>`;
1550
+ }
1551
+ /**
1552
+ * One key the provider failed, told as the answer's failure is, without the `200` that
1553
+ * spoke for the whole request. Spec 4.7 has every entry carry its key, so an entry the
1554
+ * provider names no key for leaves the answer unread rather than reported keyless.
1555
+ */
1556
+ function keyFailure(request, entry, requestId) {
1557
+ const key = textOf$1(entry, "Key");
1558
+ if (key === void 0 || key === "") throw s3Error(request.bucket, {
1559
+ code: "ProviderError",
1560
+ message: `The provider answered ${request.subject} with a failed key it did not name`,
1561
+ operation: request.operation,
1562
+ attempts: 1,
1563
+ requestId
1564
+ });
1565
+ return embeddedFailure(request, {
1566
+ operation: request.operation,
1567
+ key,
1568
+ attempts: 1,
1569
+ requestId
1570
+ }, entry);
1571
+ }
1572
+ //#endregion
1573
+ //#region src/presign.ts
1574
+ /** The ceiling SigV4 query signing sets on `X-Amz-Expires`, a week in seconds. */
1575
+ const longestLifetime = 604800;
1576
+ /** Each override of spec 7.10 and the query parameter S3 answers as its response header. */
1577
+ const responseOverrides = [
1578
+ ["responseContentType", "response-content-type"],
1579
+ ["responseContentDisposition", "response-content-disposition"],
1580
+ ["responseCacheControl", "response-cache-control"],
1581
+ ["responseExpires", "response-expires"]
1582
+ ];
1583
+ /** Spec 7.10: `GetObject` on an addressable key, the overrides carried in the query. */
1584
+ async function presignGet(configuration, key, options) {
1585
+ const operation = "presignGet";
1586
+ requireKey(configuration.bucket, key, "addressable", operation);
1587
+ const given = readGroup(configuration.bucket, options, presignGetOptionKeys, operation);
1588
+ const expiresIn = readExpiresIn(configuration.bucket, given.expiresIn, operation);
1589
+ const query = [];
1590
+ for (const [option, parameter] of responseOverrides) {
1591
+ const value = given[option];
1592
+ if (value === void 0) continue;
1593
+ query.push([parameter, readText(configuration.bucket, value, option, operation)]);
1594
+ }
1595
+ return await presignedUrl(configuration, {
1596
+ method: "GET",
1597
+ operation,
1598
+ key,
1599
+ query,
1600
+ headers: [],
1601
+ expiresIn
1602
+ });
1603
+ }
1604
+ /**
1605
+ * Spec 7.10: `PutObject` on a writable key, with the content type and the content length
1606
+ * bound through signed headers. Nothing else is signed in: no user metadata and no
1607
+ * checksum, which the browser would have to match exactly for a `403` that names nothing
1608
+ * (ADR 0011). Of the two, only the content type is handed back to send beside the body
1609
+ * (spec 4.13).
1610
+ */
1611
+ async function presignPut(configuration, key, options) {
1612
+ const operation = "presignPut";
1613
+ requireKey(configuration.bucket, key, "writable", operation);
1614
+ const given = readGroup(configuration.bucket, options, presignPutOptionKeys, operation);
1615
+ const expiresIn = readExpiresIn(configuration.bucket, given.expiresIn, operation);
1616
+ const contentType = readText(configuration.bucket, given.contentType, "contentType", operation);
1617
+ const contentLength = readContentLength(configuration.bucket, given.contentLength, operation);
1618
+ return {
1619
+ url: await presignedUrl(configuration, {
1620
+ method: "PUT",
1621
+ operation,
1622
+ key,
1623
+ query: [],
1624
+ headers: [["content-type", contentType], ["content-length", contentLength]],
1625
+ expiresIn
1626
+ }),
1627
+ headers: { "content-type": contentType }
1628
+ };
1629
+ }
1630
+ /**
1631
+ * Spec 7.10: nothing is sent, so the one failure left after the options is the
1632
+ * credential's. It is resolved for every URL, as for every request (spec 7.3).
1633
+ */
1634
+ async function presignedUrl(configuration, request) {
1635
+ const credentials = await resolveCredentials(configuration.credentials, { forceRefresh: false }).catch((failure) => {
1636
+ if (isStorageError(failure) && failure.code === "InvalidCredentials") throw inStorage(failure, configuration.bucket, request.operation, request.key);
1637
+ throw s3Error(configuration.bucket, {
1638
+ code: "InvalidCredentials",
1639
+ message: "The credential resolver failed",
1640
+ operation: request.operation,
1641
+ key: request.key,
1642
+ attempts: 0,
1643
+ cause: failure
1644
+ });
1645
+ });
1646
+ const path = pathOf(configuration, request.key);
1647
+ return urlOf(configuration, path, (await presignRequest({
1648
+ method: request.method,
1649
+ host: configuration.host,
1650
+ path,
1651
+ query: request.query,
1652
+ headers: request.headers,
1653
+ credentials,
1654
+ region: configuration.region,
1655
+ service: "s3",
1656
+ date: /* @__PURE__ */ new Date(),
1657
+ expiresIn: request.expiresIn
1658
+ })).query);
1659
+ }
1660
+ /**
1661
+ * The options as a group whose keys are all known. A caller outside TypeScript may hand
1662
+ * no group at all, which reads as one holding nothing, so the required `expiresIn` is
1663
+ * what the refusal names.
1664
+ */
1665
+ function readGroup(bucket, options, known, operation) {
1666
+ if (typeof options !== "object" || options === null) return {};
1667
+ requireKnownOptions(bucket, options, known, operation);
1668
+ return { ...options };
1669
+ }
1670
+ /** ADR 0011: outside the week SigV4 allows, refused rather than signed for a `403`. */
1671
+ function readExpiresIn(bucket, value, operation) {
1672
+ if (typeof value === "number" && value >= 1 && value <= longestLifetime && Number.isInteger(value)) return value;
1673
+ throw optionError$1(bucket, "expiresIn", `takes the whole seconds 1 to ${longestLifetime}, the lifetime SigV4 allows`, operation);
1674
+ }
1675
+ function readContentLength(bucket, value, operation) {
1676
+ if (typeof value === "number" && Number.isInteger(value) && value >= 0) return BigInt(value).toString();
1677
+ throw optionError$1(bucket, "contentLength", "takes a finite, non-negative integer", operation);
1678
+ }
1679
+ function readText(bucket, value, option, operation) {
1680
+ if (typeof value === "string" && value !== "") return value;
1681
+ throw optionError$1(bucket, option, "takes a non-empty string", operation);
1682
+ }
1683
+ //#endregion
1684
+ //#region src/stored-object.ts
1685
+ /**
1686
+ * Spec 4.5: the description and the body come out of one response, the body is read
1687
+ * once, and a stream that breaks after `get` resolved errors with a `StorageError`
1688
+ * whichever of the four readers took it.
1689
+ */
1690
+ function createStoredObject(bucket, stat, response) {
1691
+ let read = false;
1692
+ const take = () => {
1693
+ if (read) throw s3Error(bucket, {
1694
+ code: "InvalidRequest",
1695
+ message: "The body of this stored object has already been read",
1696
+ operation: "get",
1697
+ key: stat.key,
1698
+ attempts: 0
1699
+ });
1700
+ read = true;
1701
+ };
1702
+ const broken = (failure) => s3Error(bucket, {
1703
+ code: "NetworkError",
1704
+ message: `The body of the object broke while it was read: ${String(failure)}`,
1705
+ operation: "get",
1706
+ key: stat.key,
1707
+ attempts: 1,
1708
+ retryable: true,
1709
+ requestId: response.headers.get("x-amz-request-id") ?? void 0,
1710
+ cause: failure
1711
+ });
1712
+ const asText = async () => {
1713
+ take();
1714
+ try {
1715
+ return await response.text();
1716
+ } catch (failure) {
1717
+ throw broken(failure);
1718
+ }
1719
+ };
1720
+ return {
1721
+ stat,
1722
+ stream() {
1723
+ let body;
1724
+ try {
1725
+ take();
1726
+ body = response.body ?? emptyStream();
1727
+ } catch (refusal) {
1728
+ return new ReadableStream({ start(controller) {
1729
+ controller.error(refusal);
1730
+ } });
1731
+ }
1732
+ return reportingFailure(body, broken);
1733
+ },
1734
+ async bytes() {
1735
+ take();
1736
+ try {
1737
+ return new Uint8Array(await response.arrayBuffer());
1738
+ } catch (failure) {
1739
+ throw broken(failure);
1740
+ }
1741
+ },
1742
+ text: asText,
1743
+ async json() {
1744
+ return JSON.parse(await asText());
1745
+ }
1746
+ };
1747
+ }
1748
+ function emptyStream() {
1749
+ return new ReadableStream({ start(controller) {
1750
+ controller.close();
1751
+ } });
1752
+ }
1753
+ /**
1754
+ * The same bytes, with a break of the response body reported as a `StorageError`. A
1755
+ * cancel reaches the response through the reader, which is what cancels the request
1756
+ * behind the stream (spec 4.5).
1757
+ */
1758
+ function reportingFailure(body, asStorageError) {
1759
+ const reader = body.getReader();
1760
+ return new ReadableStream({
1761
+ async pull(controller) {
1762
+ try {
1763
+ const { done, value } = await reader.read();
1764
+ if (done) controller.close();
1765
+ else controller.enqueue(value);
1766
+ } catch (failure) {
1767
+ controller.error(asStorageError(failure));
1768
+ }
1769
+ },
1770
+ async cancel(reason) {
1771
+ await reader.cancel(reason);
1772
+ }
1773
+ });
1774
+ }
1775
+ //#endregion
1776
+ //#region src/upload.ts
1777
+ /** The object's own headers, which a multipart upload sends with the request that starts it. */
1778
+ function objectHeaders(write) {
1779
+ return [["content-type", write.contentType], ...write.userMetadata.headers];
1780
+ }
1781
+ /** Spec 7.6: bytes the adapter holds go as one `PUT`, which it never splits. */
1782
+ async function putObject(configuration, write, bytes) {
1783
+ const response = await send(configuration, {
1784
+ method: "PUT",
1785
+ operation: "put",
1786
+ key: write.key,
1787
+ headers: objectHeaders(write),
1788
+ body: bytes,
1789
+ signal: write.signal
1790
+ });
1791
+ await response.body?.cancel();
1792
+ return describeWrite(configuration.bucket, write.key, bytes.byteLength, write.contentType, write.userMetadata.held, response);
1793
+ }
1794
+ /**
1795
+ * Spec 7.6: a stream is read into parts of `partSize`, and one that ends within the first
1796
+ * goes as the `PUT` held bytes get.
1797
+ */
1798
+ async function putStream(configuration, write, stream) {
1799
+ return await uploadStream(stream, {
1800
+ partSize: configuration.partSize,
1801
+ concurrency: configuration.concurrency,
1802
+ maxParts,
1803
+ bucket: configuration.bucket,
1804
+ provider: "s3",
1805
+ key: write.key,
1806
+ signal: write.signal
1807
+ }, {
1808
+ whole: async (bytes) => await putObject(configuration, write, bytes),
1809
+ multipart: async (sendParts) => await multipartUpload(configuration, write, sendParts)
1810
+ });
1811
+ }
1812
+ const s3Namespace = "http://s3.amazonaws.com/doc/2006-03-01/";
1813
+ /** The provider's limit on both sides, which ADR 0016 leaves no option to lift. */
1814
+ const maxParts = 1e4;
1815
+ /**
1816
+ * Spec 7.6: a multipart upload aborts itself once it failed, after the parts in flight
1817
+ * and the source stream were canceled.
1818
+ */
1819
+ async function multipartUpload(configuration, write, sendParts) {
1820
+ const uploadId = await createUpload(configuration, write);
1821
+ let sent;
1822
+ try {
1823
+ sent = await sendParts(async (index, bytes, signal) => {
1824
+ const number = index + 1;
1825
+ return {
1826
+ number,
1827
+ etag: await uploadPart(configuration, {
1828
+ ...write,
1829
+ signal
1830
+ }, uploadId, number, bytes)
1831
+ };
1832
+ });
1833
+ } catch (failure) {
1834
+ await abortUpload(configuration, write, uploadId);
1835
+ throw failure;
1836
+ }
1837
+ return await completeUpload(configuration, write, uploadId, sent.results, sent.size);
1838
+ }
1839
+ async function createUpload(configuration, write) {
1840
+ const response = await send(configuration, {
1841
+ method: "POST",
1842
+ operation: "put",
1843
+ key: write.key,
1844
+ query: [["uploads", ""]],
1845
+ headers: objectHeaders(write),
1846
+ signal: write.signal
1847
+ });
1848
+ const subject = "the start of the upload";
1849
+ const uploadId = textOf$1(await readAnswerDocument(answeredRequest(configuration, write, subject), response, "InitiateMultipartUploadResult"), "UploadId");
1850
+ if (uploadId === void 0 || uploadId === "") throw incompleteAnswer(configuration, write, response, subject, "no upload id");
1851
+ return uploadId;
1852
+ }
1853
+ async function uploadPart(configuration, write, uploadId, number, bytes) {
1854
+ const response = await send(configuration, {
1855
+ method: "PUT",
1856
+ operation: "put",
1857
+ key: write.key,
1858
+ query: [["partNumber", String(number)], ["uploadId", uploadId]],
1859
+ body: bytes,
1860
+ signal: write.signal
1861
+ });
1862
+ await response.body?.cancel();
1863
+ const etag = response.headers.get("etag");
1864
+ if (etag === null || etag === "") throw incompleteAnswer(configuration, write, response, `part ${number}`, "no entity tag");
1865
+ return etag;
1866
+ }
1867
+ async function completeUpload(configuration, write, uploadId, uploaded, size) {
1868
+ let response;
1869
+ let document;
1870
+ try {
1871
+ response = await send(configuration, {
1872
+ method: "POST",
1873
+ operation: "put",
1874
+ key: write.key,
1875
+ query: [["uploadId", uploadId]],
1876
+ headers: [["content-type", "application/xml"]],
1877
+ body: utf8$1.encode(completeDocument(uploaded)),
1878
+ signal: write.signal,
1879
+ repeatWithoutResponse: false
1880
+ });
1881
+ document = await readAnswerDocument(answeredRequest(configuration, write, "the completion of the upload"), response, "CompleteMultipartUploadResult");
1882
+ } catch (failure) {
1883
+ if (!mayHaveCommitted(failure, write.signal)) await abortUpload(configuration, write, uploadId);
1884
+ throw failure;
1885
+ }
1886
+ const etag = textOf$1(document, "ETag");
1887
+ return describeWrite(configuration.bucket, write.key, size, write.contentType, write.userMetadata.held, response, etag === void 0 ? void 0 : unquotedEtag(etag));
1888
+ }
1889
+ /**
1890
+ * Spec 7.7 leaves a completion that received no response unaborted, because an abort
1891
+ * could meet a commit still on its way. The same holds for a `200` that broke before its
1892
+ * body said how the commit went, since S3 sends that status before it has decided, and
1893
+ * for the caller's abort while the completion was out: the request may have arrived.
1894
+ * Only the provider's answer that it did not commit is aborted.
1895
+ */
1896
+ function mayHaveCommitted(failure, signal) {
1897
+ if (signal?.aborted) return true;
1898
+ return isStorageError(failure) && failure.code === "NetworkError";
1899
+ }
1900
+ /**
1901
+ * Spec 7.6: sent without a signal, because the caller's may be the one that just fired,
1902
+ * and never reported, because the failure that led here is what the caller is owed. What
1903
+ * a failed abort leaves behind is removed by the lifecycle rule of spec 7.2.
1904
+ */
1905
+ async function abortUpload(configuration, write, uploadId) {
1906
+ try {
1907
+ await (await send(configuration, {
1908
+ method: "DELETE",
1909
+ operation: "put",
1910
+ key: write.key,
1911
+ query: [["uploadId", uploadId]]
1912
+ })).body?.cancel();
1913
+ } catch {}
1914
+ }
1915
+ const utf8$1 = new TextEncoder();
1916
+ function completeDocument(uploaded) {
1917
+ const parts = uploaded.map(({ number, etag }) => `<Part><PartNumber>${number}</PartNumber><ETag>${escapeXml(etag)}</ETag></Part>`).join("");
1918
+ return `<?xml version="1.0" encoding="UTF-8"?><CompleteMultipartUpload xmlns="${s3Namespace}">${parts}</CompleteMultipartUpload>`;
1919
+ }
1920
+ function answeredRequest(configuration, write, subject) {
1921
+ return {
1922
+ bucket: configuration.bucket,
1923
+ operation: "put",
1924
+ key: write.key,
1925
+ subject
1926
+ };
1927
+ }
1928
+ function incompleteAnswer(configuration, write, response, subject, missing) {
1929
+ return s3Error(configuration.bucket, {
1930
+ code: "ProviderError",
1931
+ message: `The provider answered ${subject} with ${missing}`,
1932
+ operation: "put",
1933
+ key: write.key,
1934
+ attempts: 1,
1935
+ status: response.status,
1936
+ requestId: response.headers.get("x-amz-request-id") ?? void 0
1937
+ });
1938
+ }
1939
+ //#endregion
1940
+ //#region src/index.ts
1941
+ function s3Storage(options) {
1942
+ return new SimpleStorageServiceStorage(options);
1943
+ }
1944
+ /** Spec 7.1, in the order `capabilityNames` of spec 4.9 lists the names. */
1945
+ const s3Capabilities = Object.freeze([
1946
+ "presignedUrls",
1947
+ "rangeReads",
1948
+ "userMetadata",
1949
+ "userMetadataTokenKeys"
1950
+ ]);
1951
+ const utf8 = new TextEncoder();
1952
+ var SimpleStorageServiceStorage = class {
1953
+ provider = "s3";
1954
+ bucket;
1955
+ capabilities = s3Capabilities;
1956
+ #configuration;
1957
+ constructor(options) {
1958
+ this.#configuration = readConfiguration(options);
1959
+ this.bucket = this.#configuration.bucket;
1960
+ }
1961
+ async put(key, body, options) {
1962
+ try {
1963
+ return await this.#put(key, body, options);
1964
+ } catch (failure) {
1965
+ if (isStream(body) && !body.locked) await body.cancel(failure).catch(() => {});
1966
+ throw failure;
1967
+ }
1968
+ }
1969
+ async #put(key, body, options) {
1970
+ requireKey(this.bucket, key, "writable", "put");
1971
+ requireKnownOptions(this.bucket, options, putOptionKeys, "put");
1972
+ const write = {
1973
+ key,
1974
+ userMetadata: userMetadataHeaders(this.bucket, options?.userMetadata, key, this.capabilities),
1975
+ contentType: this.#readContentType(options?.contentType),
1976
+ signal: options?.signal
1977
+ };
1978
+ options?.signal?.throwIfAborted();
1979
+ if (isStream(body)) return await putStream(this.#configuration, write, body);
1980
+ return await putObject(this.#configuration, write, bytesOf(body));
1981
+ }
1982
+ async get(key, options) {
1983
+ requireKey(this.bucket, key, "addressable", "get");
1984
+ requireKnownOptions(this.bucket, options, getOptionKeys, "get");
1985
+ const range = options?.range;
1986
+ requireRange(this.bucket, range);
1987
+ options?.signal?.throwIfAborted();
1988
+ const response = await send(this.#configuration, {
1989
+ method: "GET",
1990
+ operation: "get",
1991
+ key,
1992
+ headers: range === void 0 ? [] : [["range", rangeHeader(range)]],
1993
+ signal: options?.signal
1994
+ });
1995
+ const stat = describeResponse(this.bucket, key, "get", response);
1996
+ const refusal = range === void 0 || response.status === 206 ? void 0 : wholeAnswerFailure(this.bucket, key, range, stat.size);
1997
+ if (refusal !== void 0) {
1998
+ await response.body?.cancel();
1999
+ throw refusal;
2000
+ }
2001
+ return createStoredObject(this.bucket, stat, response);
2002
+ }
2003
+ async stat(key, options) {
2004
+ return describeResponse(this.bucket, key, "stat", await this.#head(key, "stat", options));
2005
+ }
2006
+ async exists(key, options) {
2007
+ try {
2008
+ await this.#head(key, "exists", options);
2009
+ return true;
2010
+ } catch (failure) {
2011
+ if (isStorageError(failure) && failure.code === "NotFound") return false;
2012
+ throw failure;
2013
+ }
2014
+ }
2015
+ list(options) {
2016
+ return createListing(this.#configuration, options);
2017
+ }
2018
+ async delete(...keys) {
2019
+ return await deleteKeys(this.#configuration, keys, { operation: "delete" });
2020
+ }
2021
+ async deleteAll(prefix, options) {
2022
+ requireKey(this.bucket, prefix, "prefix", "deleteAll");
2023
+ requireKnownOptions(this.bucket, options, operationOptionKeys, "deleteAll");
2024
+ options?.signal?.throwIfAborted();
2025
+ return await deleteBelow(this.#configuration, prefix, options?.signal);
2026
+ }
2027
+ async copy(from, to, options) {
2028
+ this.#requireCopyKeys(from, to, options, "copy");
2029
+ return await copyObject(this.#configuration, from, to, "copy", options?.signal);
2030
+ }
2031
+ /**
2032
+ * Spec 4.10: `copy`, then the source deleted, each failure told as `move`'s. The
2033
+ * destination is described before the source goes, so a failing delete leaves both in
2034
+ * place and a repeated `move` is safe.
2035
+ */
2036
+ async move(from, to, options) {
2037
+ this.#requireCopyKeys(from, to, options, "move");
2038
+ const written = await copyObject(this.#configuration, from, to, "move", options?.signal);
2039
+ await (await send(this.#configuration, {
2040
+ method: "DELETE",
2041
+ operation: "move",
2042
+ key: from,
2043
+ signal: options?.signal
2044
+ })).body?.cancel();
2045
+ return written;
2046
+ }
2047
+ async presignGet(key, options) {
2048
+ return await presignGet(this.#configuration, key, options);
2049
+ }
2050
+ async presignPut(key, options) {
2051
+ return await presignPut(this.#configuration, key, options);
2052
+ }
2053
+ async #head(key, operation, options) {
2054
+ requireKey(this.bucket, key, "addressable", operation);
2055
+ requireKnownOptions(this.bucket, options, operationOptionKeys, operation);
2056
+ options?.signal?.throwIfAborted();
2057
+ return await send(this.#configuration, {
2058
+ method: "HEAD",
2059
+ operation,
2060
+ key,
2061
+ signal: options?.signal
2062
+ });
2063
+ }
2064
+ /**
2065
+ * Spec 4.8 checks both keys before acting on either, and spec 7.8 has a copy of a key
2066
+ * onto itself stop before it leaves the process; a `move` onto itself would otherwise
2067
+ * delete the one object it named.
2068
+ */
2069
+ #requireCopyKeys(from, to, options, operation) {
2070
+ requireKey(this.bucket, from, "addressable", operation);
2071
+ requireKey(this.bucket, to, "writable", operation);
2072
+ requireKnownOptions(this.bucket, options, operationOptionKeys, operation);
2073
+ if (from !== to) return;
2074
+ throw s3Error(this.bucket, {
2075
+ code: "InvalidRequest",
2076
+ message: "A copy names one key as its source and another as its destination",
2077
+ operation,
2078
+ key: from,
2079
+ attempts: 0
2080
+ });
2081
+ }
2082
+ #readContentType(contentType) {
2083
+ if (contentType === void 0) return defaultContentType;
2084
+ if (typeof contentType !== "string" || contentType === "") throw optionError$1(this.bucket, "contentType", "takes a non-empty string", "put");
2085
+ return contentType;
2086
+ }
2087
+ };
2088
+ /** Spec 4.2: a string travels as its UTF-8 bytes. */
2089
+ function bytesOf(body) {
2090
+ return typeof body === "string" ? utf8.encode(body) : heldBytes(body);
2091
+ }
2092
+ /**
2093
+ * The same bytes as a view Web Crypto takes: `BufferSource` rules out a view on a
2094
+ * `SharedArrayBuffer`, which is the one body copied rather than hashed and sent where it
2095
+ * lies. ADR 0009 already spends the memory of holding a body whole, and a copy of every
2096
+ * one of them would spend it twice.
2097
+ */
2098
+ function heldBytes(body) {
2099
+ const { buffer } = body;
2100
+ if (buffer instanceof ArrayBuffer) return new Uint8Array(buffer, body.byteOffset, body.byteLength);
2101
+ return new Uint8Array(body);
2102
+ }
2103
+ function isStream(body) {
2104
+ return typeof body !== "string" && !(body instanceof Uint8Array);
2105
+ }
2106
+ //#endregion
2107
+ export { fromEnv, s3Storage };