lean-s3 0.2.2 → 0.3.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/S3Client.js DELETED
@@ -1,632 +0,0 @@
1
- import { request, Agent } from "undici";
2
- import { XMLParser, XMLBuilder } from "fast-xml-parser";
3
- import S3File from "./S3File.js";
4
- import S3Error from "./S3Error.js";
5
- import S3BucketEntry from "./S3BucketEntry.js";
6
- import KeyCache from "./KeyCache.js";
7
- import * as amzDate from "./AmzDate.js";
8
- import * as sign from "./sign.js";
9
- import { buildRequestUrl, getRangeHeader, prepareHeadersForSigning, } from "./url.js";
10
- import { getResponseError } from "./error.js";
11
- export const write = Symbol("write");
12
- export const stream = Symbol("stream");
13
- const xmlParser = new XMLParser();
14
- const xmlBuilder = new XMLBuilder({
15
- attributeNamePrefix: "$",
16
- ignoreAttributes: false,
17
- });
18
- /**
19
- * A configured S3 bucket instance for managing files.
20
- *
21
- * @example
22
- * ```js
23
- * // Basic bucket setup
24
- * const bucket = new S3Client({
25
- * bucket: "my-bucket",
26
- * accessKeyId: "key",
27
- * secretAccessKey: "secret"
28
- * });
29
- * // Get file instance
30
- * const file = bucket.file("image.jpg");
31
- * await file.delete();
32
- * ```
33
- */
34
- export default class S3Client {
35
- #options;
36
- #keyCache = new KeyCache();
37
- // TODO: pass options to this in client? Do we want to expose tjhe internal use of undici?
38
- #dispatcher = new Agent();
39
- /**
40
- * Create a new instance of an S3 bucket so that credentials can be managed from a single instance instead of being passed to every method.
41
- *
42
- * @param options The default options to use for the S3 client.
43
- */
44
- constructor(options) {
45
- if (!options) {
46
- throw new Error("`options` is required.");
47
- }
48
- const { accessKeyId, secretAccessKey, endpoint, region, bucket, sessionToken, } = options;
49
- if (!accessKeyId || typeof accessKeyId !== "string") {
50
- throw new Error("`accessKeyId` is required.");
51
- }
52
- if (!secretAccessKey || typeof secretAccessKey !== "string") {
53
- throw new Error("`secretAccessKey` is required.");
54
- }
55
- if (!endpoint || typeof endpoint !== "string") {
56
- throw new Error("`endpoint` is required.");
57
- }
58
- if (!region || typeof region !== "string") {
59
- throw new Error("`region` is required.");
60
- }
61
- if (!bucket || typeof bucket !== "string") {
62
- throw new Error("`bucket` is required.");
63
- }
64
- this.#options = {
65
- accessKeyId,
66
- secretAccessKey,
67
- endpoint,
68
- region,
69
- bucket,
70
- sessionToken,
71
- };
72
- }
73
- /**
74
- * Creates an S3File instance for the given path.
75
- *
76
- * @param {string} path The path to the object in the bucket. ALso known as [object key](https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html).
77
- * We recommend not using the following characters in a key name because of significant special character handling, which isn't consistent across all applications (see [AWS docs](https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html)):
78
- * - Backslash (`\\`)
79
- * - Left brace (`{`)
80
- * - Non-printable ASCII characters (128–255 decimal characters)
81
- * - Caret or circumflex (`^`)
82
- * - Right brace (`}`)
83
- * - Percent character (`%`)
84
- * - Grave accent or backtick (`\``)
85
- * - Right bracket (`]`)
86
- * - Quotation mark (`"`)
87
- * - Greater than sign (`>`)
88
- * - Left bracket (`[`)
89
- * - Tilde (`~`)
90
- * - Less than sign (`<`)
91
- * - Pound sign (`#`)
92
- * - Vertical bar or pipe (`|`)
93
- *
94
- * lean-s3 does not enforce these restrictions.
95
- *
96
- * @param {Partial<CreateFileInstanceOptions>} [options] TODO
97
- * @example
98
- * ```js
99
- * const file = client.file("image.jpg");
100
- * await file.write(imageData);
101
- *
102
- * const configFile = client.file("config.json", {
103
- * type: "application/json",
104
- * acl: "private"
105
- * });
106
- * ```
107
- */
108
- file(path, options) {
109
- // TODO: Check max path length in bytes
110
- return new S3File(this, path, undefined, undefined, undefined);
111
- }
112
- /**
113
- * Generate a presigned URL for temporary access to a file.
114
- * Useful for generating upload/download URLs without exposing credentials.
115
- * @returns The operation on {@link S3Client#presign.path} as a pre-signed URL.
116
- *
117
- * @example
118
- * ```js
119
- * const downloadUrl = client.presign("file.pdf", {
120
- * expiresIn: 3600 // 1 hour
121
- * });
122
- * ```
123
- */
124
- presign(path, { method = "GET", expiresIn = 3600, // TODO: Maybe rename this to expiresInSeconds
125
- storageClass, acl, region: regionOverride, bucket: bucketOverride, endpoint: endpointOverride, } = {}) {
126
- const now = new Date();
127
- const date = amzDate.getAmzDate(now);
128
- const options = this.#options;
129
- const region = regionOverride ?? options.region;
130
- const bucket = bucketOverride ?? options.bucket;
131
- const endpoint = endpointOverride ?? options.endpoint;
132
- const res = buildRequestUrl(endpoint, bucket, region, path);
133
- const query = buildSearchParams(`${options.accessKeyId}/${date.date}/${region}/s3/aws4_request`, date, expiresIn, "host", undefined, storageClass, options.sessionToken, acl);
134
- const dataDigest = sign.createCanonicalDataDigestHostOnly(method, res.pathname, query, res.host);
135
- const signingKey = this.#keyCache.computeIfAbsent(date, region, options.accessKeyId, options.secretAccessKey);
136
- const signature = sign.signCanonicalDataHash(signingKey, dataDigest, date, region);
137
- // See `buildSearchParams` for casing on this parameter
138
- res.search = `${query}&X-Amz-Signature=${signature}`;
139
- return res.toString();
140
- }
141
- /**
142
- * Uses [`DeleteObjects`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObjects.html) to delete multiple objects in a single request.
143
- */
144
- async deleteObjects(objects, options = {}) {
145
- const body = xmlBuilder.build({
146
- Delete: {
147
- Quiet: true,
148
- Object: objects.map(o => ({
149
- Key: typeof o === "string" ? o : o.key,
150
- })),
151
- },
152
- });
153
- const response = await this._signedRequest("POST", "", "delete=", // "=" is needed by minio for some reason
154
- body, {
155
- "content-md5": sign.md5Base64(body),
156
- }, undefined, undefined, this.#options.bucket, options.signal);
157
- if (response.statusCode === 200) {
158
- const text = await response.body.text();
159
- let res = undefined;
160
- try {
161
- // Quite mode omits all deleted elements, so it will be parsed as "", wich we need to coalasce to null/undefined
162
- res = (xmlParser.parse(text)?.DeleteResult || undefined)?.Error ?? [];
163
- }
164
- catch (cause) {
165
- // Possible according to AWS docs
166
- throw new S3Error("Unknown", "", {
167
- message: "S3 service responded with invalid XML.",
168
- cause,
169
- });
170
- }
171
- if (!res || !Array.isArray(res)) {
172
- throw new S3Error("Unknown", "", {
173
- message: "Could not process response.",
174
- });
175
- }
176
- const errors = res.map(e => ({
177
- code: e.Code,
178
- key: e.Key,
179
- message: e.Message,
180
- versionId: e.VersionId,
181
- }));
182
- return errors.length > 0 ? { errors } : null;
183
- }
184
- if (400 <= response.statusCode && response.statusCode < 500) {
185
- throw await getResponseError(response, "");
186
- }
187
- response.body.dump(); // undici docs state that we should dump the body if not used
188
- throw new Error(`Response code not implemented yet: ${response.statusCode}`);
189
- }
190
- /**
191
- * Creates a new bucket on the S3 server.
192
- *
193
- * @param name The name of the bucket to create. AWS the name according to [some rules](https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html). The most important ones are:
194
- * - Bucket names must be between `3` (min) and `63` (max) characters long.
195
- * - Bucket names can consist only of lowercase letters, numbers, periods (`.`), and hyphens (`-`).
196
- * - Bucket names must begin and end with a letter or number.
197
- * - Bucket names must not contain two adjacent periods.
198
- * - Bucket names must not be formatted as an IP address (for example, `192.168.5.4`).
199
- *
200
- * @throws {Error} If the bucket name is invalid.
201
- * @throws {S3Error} If the bucket could not be created, e.g. if it already exists.
202
- * @remarks Uses [`CreateBucket`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_CreateBucket.html)
203
- */
204
- async createBucket(name, options) {
205
- ensureValidBucketName(name);
206
- let body = undefined;
207
- if (options) {
208
- const location = options.location && (options.location.name || options.location.type)
209
- ? {
210
- Name: options.location.name ?? undefined,
211
- Type: options.location.type ?? undefined,
212
- }
213
- : undefined;
214
- const bucket = options.info && (options.info.dataRedundancy || options.info.type)
215
- ? {
216
- DataRedundancy: options.info.dataRedundancy ?? undefined,
217
- Type: options.info.type ?? undefined,
218
- }
219
- : undefined;
220
- body =
221
- location || bucket || options.locationConstraint
222
- ? xmlBuilder.build({
223
- CreateBucketConfiguration: {
224
- $xmlns: "http://s3.amazonaws.com/doc/2006-03-01/",
225
- LocationConstraint: options.locationConstraint ?? undefined,
226
- Location: location,
227
- Bucket: bucket,
228
- },
229
- })
230
- : undefined;
231
- }
232
- const additionalSignedHeaders = body
233
- ? { "content-md5": sign.md5Base64(body) }
234
- : undefined;
235
- const response = await this._signedRequest("PUT", "", undefined, body, additionalSignedHeaders, undefined, undefined, name, options?.signal);
236
- if (400 <= response.statusCode && response.statusCode < 500) {
237
- throw await getResponseError(response, "");
238
- }
239
- await response.body.dump(); // undici docs state that we should dump the body if not used
240
- if (response.statusCode === 200) {
241
- return;
242
- }
243
- throw new Error(`Response code not supported: ${response.statusCode}`);
244
- }
245
- /**
246
- * Deletes a bucket from the S3 server.
247
- * @param name The name of the bucket to delete. Same restrictions as in {@link S3Client#createBucket}.
248
- * @throws {Error} If the bucket name is invalid.
249
- * @throws {S3Error} If the bucket could not be deleted, e.g. if it is not empty.
250
- * @remarks Uses [`DeleteBucket`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteBucket.html).
251
- */
252
- async deleteBucket(name, options) {
253
- ensureValidBucketName(name);
254
- const response = await this._signedRequest("DELETE", "", undefined, undefined, undefined, undefined, undefined, name, options?.signal);
255
- if (400 <= response.statusCode && response.statusCode < 500) {
256
- throw await getResponseError(response, "");
257
- }
258
- await response.body.dump(); // undici docs state that we should dump the body if not used
259
- if (response.statusCode === 204) {
260
- return;
261
- }
262
- throw new Error(`Response code not supported: ${response.statusCode}`);
263
- }
264
- /**
265
- * Checks if a bucket exists.
266
- * @param name The name of the bucket to delete. Same restrictions as in {@link S3Client#createBucket}.
267
- * @throws {Error} If the bucket name is invalid.
268
- * @remarks Uses [`HeadBucket`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadBucket.html).
269
- */
270
- async bucketExists(name, options) {
271
- ensureValidBucketName(name);
272
- const response = await this._signedRequest("HEAD", "", undefined, undefined, undefined, undefined, undefined, name, options?.signal);
273
- if (response.statusCode !== 404 &&
274
- 400 <= response.statusCode &&
275
- response.statusCode < 500) {
276
- throw await getResponseError(response, "");
277
- }
278
- await response.body.dump(); // undici docs state that we should dump the body if not used
279
- if (response.statusCode === 200) {
280
- return true;
281
- }
282
- if (response.statusCode === 404) {
283
- return false;
284
- }
285
- throw new Error(`Response code not supported: ${response.statusCode}`);
286
- }
287
- //#region list
288
- /**
289
- * Uses [`ListObjectsV2`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html) to iterate over all keys. Pagination and continuation is handled internally.
290
- */
291
- async *listIterating(options) {
292
- // only used to get smaller pages, so we can test this properly
293
- const maxKeys = options?.internalPageSize ?? undefined;
294
- let res = undefined;
295
- let continuationToken = undefined;
296
- do {
297
- res = await this.list({
298
- ...options,
299
- maxKeys,
300
- continuationToken,
301
- });
302
- if (!res || res.contents.length === 0) {
303
- break;
304
- }
305
- yield* res.contents;
306
- continuationToken = res.nextContinuationToken;
307
- } while (continuationToken);
308
- }
309
- /**
310
- * Implements [`ListObjectsV2`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html) to iterate over all keys.
311
- */
312
- async list(options = {}) {
313
- // See `benchmark-operations.js` on why we don't use URLSearchParams but string concat
314
- // tldr: This is faster and we know the params exactly, so we can focus our encoding
315
- // ! minio requires these params to be in alphabetical order
316
- let query = "";
317
- if (typeof options.continuationToken !== "undefined") {
318
- if (typeof options.continuationToken !== "string") {
319
- throw new TypeError("`continuationToken` should be a `string`.");
320
- }
321
- query += `continuation-token=${encodeURIComponent(options.continuationToken)}&`;
322
- }
323
- query += "list-type=2";
324
- if (typeof options.maxKeys !== "undefined") {
325
- if (typeof options.maxKeys !== "number") {
326
- throw new TypeError("`maxKeys` should be a `number`.");
327
- }
328
- query += `&max-keys=${options.maxKeys}`; // no encoding needed, it's a number
329
- }
330
- // TODO: delimiter?
331
- // plan `if(a)` check, so empty strings will also not go into this branch, omitting the parameter
332
- if (options.prefix) {
333
- if (typeof options.prefix !== "string") {
334
- throw new TypeError("`prefix` should be a `string`.");
335
- }
336
- query += `&prefix=${encodeURIComponent(options.prefix)}`;
337
- }
338
- if (typeof options.startAfter !== "undefined") {
339
- if (typeof options.startAfter !== "string") {
340
- throw new TypeError("`startAfter` should be a `string`.");
341
- }
342
- query += `&start-after=${encodeURIComponent(options.startAfter)}`;
343
- }
344
- const response = await this._signedRequest("GET", "", query, undefined, undefined, undefined, undefined, options.bucket ?? this.#options.bucket, options.signal);
345
- if (response.statusCode === 200) {
346
- const text = await response.body.text();
347
- let res = undefined;
348
- try {
349
- res = xmlParser.parse(text)?.ListBucketResult;
350
- }
351
- catch (cause) {
352
- // Possible according to AWS docs
353
- throw new S3Error("Unknown", "", {
354
- message: "S3 service responded with invalid XML.",
355
- cause,
356
- });
357
- }
358
- if (!res) {
359
- throw new S3Error("Unknown", "", {
360
- message: "Could not read bucket contents.",
361
- });
362
- }
363
- // S3 is weird and doesn't return an array if there is only one item
364
- const contents = Array.isArray(res.Contents)
365
- ? (res.Contents?.map(S3BucketEntry.parse) ?? [])
366
- : res.Contents
367
- ? [res.Contents]
368
- : [];
369
- return {
370
- name: res.Name,
371
- prefix: res.Prefix,
372
- startAfter: res.StartAfter,
373
- isTruncated: res.IsTruncated,
374
- continuationToken: res.ContinuationToken,
375
- maxKeys: res.MaxKeys,
376
- keyCount: res.KeyCount,
377
- nextContinuationToken: res.NextContinuationToken,
378
- contents,
379
- };
380
- }
381
- response.body.dump(); // undici docs state that we should dump the body if not used
382
- throw new Error(`Response code not implemented yet: ${response.statusCode}`);
383
- }
384
- //#endregion
385
- /**
386
- * Do not use this. This is an internal method.
387
- * TODO: Maybe move this into a separate free function?
388
- * @internal
389
- */
390
- async _signedRequest(method, pathWithoutBucket, query, body, additionalSignedHeaders, additionalUnsignedHeaders, contentHash, bucket, signal = undefined) {
391
- const endpoint = this.#options.endpoint;
392
- const region = this.#options.region;
393
- const effectiveBucket = bucket ?? this.#options.bucket;
394
- const url = buildRequestUrl(endpoint, effectiveBucket, region, pathWithoutBucket);
395
- if (query) {
396
- url.search = query;
397
- }
398
- const now = amzDate.now();
399
- const contentHashStr = contentHash?.toString("hex") ?? sign.unsignedPayload;
400
- // Signed headers have to be sorted
401
- // To enhance sorting, we're adding all possible values somehow pre-ordered
402
- const headersToBeSigned = prepareHeadersForSigning({
403
- host: url.host,
404
- "x-amz-date": now.dateTime,
405
- "x-amz-content-sha256": contentHashStr,
406
- ...additionalSignedHeaders,
407
- });
408
- try {
409
- return await request(url, {
410
- method,
411
- signal,
412
- dispatcher: this.#dispatcher,
413
- headers: {
414
- ...headersToBeSigned,
415
- authorization: this.#getAuthorizationHeader(method, url.pathname, query ?? "", now, headersToBeSigned, region, contentHashStr, this.#options.accessKeyId, this.#options.secretAccessKey),
416
- ...additionalUnsignedHeaders,
417
- "user-agent": "lean-s3",
418
- },
419
- body,
420
- });
421
- }
422
- catch (cause) {
423
- signal?.throwIfAborted();
424
- throw new S3Error("Unknown", pathWithoutBucket, {
425
- message: "Unknown error during S3 request.",
426
- cause,
427
- });
428
- }
429
- }
430
- /**
431
- * @internal
432
- * @param {import("./index.d.ts").UndiciBodyInit} data TODO
433
- */
434
- async [write](path, data, contentType, contentLength, contentHash, rageStart, rangeEndExclusive, signal = undefined) {
435
- const bucket = this.#options.bucket;
436
- const endpoint = this.#options.endpoint;
437
- const region = this.#options.region;
438
- const url = buildRequestUrl(endpoint, bucket, region, path);
439
- const now = amzDate.now();
440
- const contentHashStr = contentHash?.toString("hex") ?? sign.unsignedPayload;
441
- // Signed headers have to be sorted
442
- // To enhance sorting, we're adding all possible values somehow pre-ordered
443
- const headersToBeSigned = prepareHeadersForSigning({
444
- "content-length": contentLength?.toString() ?? undefined,
445
- "content-type": contentType,
446
- host: url.host,
447
- range: getRangeHeader(rageStart, rangeEndExclusive),
448
- "x-amz-content-sha256": contentHashStr,
449
- "x-amz-date": now.dateTime,
450
- });
451
- /** @type {import("undici").Dispatcher.ResponseData<unknown> | undefined} */
452
- let response = undefined;
453
- try {
454
- response = await request(url, {
455
- method: "PUT",
456
- signal,
457
- dispatcher: this.#dispatcher,
458
- headers: {
459
- ...headersToBeSigned,
460
- authorization: this.#getAuthorizationHeader("PUT", url.pathname, url.search, now, headersToBeSigned, region, contentHashStr, this.#options.accessKeyId, this.#options.secretAccessKey),
461
- "user-agent": "lean-s3",
462
- },
463
- body: data,
464
- });
465
- }
466
- catch (cause) {
467
- signal?.throwIfAborted();
468
- throw new S3Error("Unknown", path, {
469
- message: "Unknown error during S3 request.",
470
- cause,
471
- });
472
- }
473
- const status = response.statusCode;
474
- if (200 <= status && status < 300) {
475
- // everything seemed to work, no need to process response body
476
- return;
477
- }
478
- throw await getResponseError(response, path);
479
- }
480
- // TODO: Support abortSignal
481
- /**
482
- * @internal
483
- */
484
- [stream](path, contentHash, rageStart, rangeEndExclusive) {
485
- const bucket = this.#options.bucket;
486
- const endpoint = this.#options.endpoint;
487
- const region = this.#options.region;
488
- const now = amzDate.now();
489
- const url = buildRequestUrl(endpoint, bucket, region, path);
490
- const range = getRangeHeader(rageStart, rangeEndExclusive);
491
- const contentHashStr = contentHash?.toString("hex") ?? sign.unsignedPayload;
492
- const headersToBeSigned = prepareHeadersForSigning({
493
- "amz-sdk-invocation-id": crypto.randomUUID(),
494
- // TODO: Maybe support retries and do "amz-sdk-request": attempt=1; max=3
495
- host: url.host,
496
- range,
497
- // Hetzner doesnt care if the x-amz-content-sha256 header is missing, R2 requires it to be present
498
- "x-amz-content-sha256": contentHashStr,
499
- "x-amz-date": now.dateTime,
500
- });
501
- const ac = new AbortController();
502
- return new ReadableStream({
503
- type: "bytes",
504
- start: controller => {
505
- const onNetworkError = (cause) => {
506
- controller.error(new S3Error("Unknown", path, {
507
- message: undefined,
508
- cause,
509
- }));
510
- };
511
- request(url, {
512
- method: "GET",
513
- signal: ac.signal,
514
- dispatcher: this.#dispatcher,
515
- headers: {
516
- ...headersToBeSigned,
517
- authorization: this.#getAuthorizationHeader("GET", url.pathname, url.search, now, headersToBeSigned, region, contentHashStr, this.#options.accessKeyId, this.#options.secretAccessKey),
518
- "user-agent": "lean-s3",
519
- },
520
- }).then(response => {
521
- const onData = controller.enqueue.bind(controller);
522
- const onClose = controller.close.bind(controller);
523
- const expectPartialResponse = range !== undefined;
524
- const status = response.statusCode;
525
- if (status === 200) {
526
- if (expectPartialResponse) {
527
- return controller.error(new S3Error("Unknown", path, {
528
- message: "Expected partial response to range request.",
529
- }));
530
- }
531
- response.body.on("data", onData);
532
- response.body.once("error", onNetworkError);
533
- response.body.once("end", onClose);
534
- return;
535
- }
536
- if (status === 206) {
537
- if (!expectPartialResponse) {
538
- return controller.error(new S3Error("Unknown", path, {
539
- message: "Received partial response but expected a full response.",
540
- }));
541
- }
542
- response.body.on("data", onData);
543
- response.body.once("error", onNetworkError);
544
- response.body.once("end", onClose);
545
- return;
546
- }
547
- if (400 <= status && status < 500) {
548
- // Some providers actually support JSON via "accept: application/json", but we cant rely on it
549
- const responseText = undefined;
550
- const ct = response.headers["content-type"];
551
- if (response.headers["content-type"] === "application/xml") {
552
- return response.body.text().then(body => {
553
- let error = undefined;
554
- try {
555
- error = xmlParser.parse(body);
556
- }
557
- catch (cause) {
558
- return controller.error(new S3Error("Unknown", path, {
559
- message: "Could not parse XML error response.",
560
- cause,
561
- }));
562
- }
563
- return controller.error(new S3Error(error.Code || "Unknown", path, {
564
- message: error.Message || undefined, // Message might be "",
565
- }));
566
- }, onNetworkError);
567
- }
568
- return controller.error(new S3Error("Unknown", path, {
569
- message: undefined,
570
- cause: responseText,
571
- }));
572
- }
573
- // TODO: Support other status codes
574
- return controller.error(new Error(`Handling for status code ${status} not implemented yet. You might want to open an issue and describe your situation.`));
575
- }, onNetworkError);
576
- },
577
- cancel(reason) {
578
- ac.abort(reason);
579
- },
580
- });
581
- }
582
- #getAuthorizationHeader(method, path, query, date, sortedSignedHeaders, region, contentHashStr, accessKeyId, secretAccessKey) {
583
- const dataDigest = sign.createCanonicalDataDigest(method, path, query, sortedSignedHeaders, contentHashStr);
584
- const signingKey = this.#keyCache.computeIfAbsent(date, region, accessKeyId, secretAccessKey);
585
- const signature = sign.signCanonicalDataHash(signingKey, dataDigest, date, region);
586
- // no encodeURIComponent because because we assume that all headers don't need escaping
587
- const signedHeadersSpec = Object.keys(sortedSignedHeaders).join(";");
588
- const credentialSpec = `${accessKeyId}/${date.date}/${region}/s3/aws4_request`;
589
- return `AWS4-HMAC-SHA256 Credential=${credentialSpec}, SignedHeaders=${signedHeadersSpec}, Signature=${signature}`;
590
- }
591
- }
592
- export function buildSearchParams(amzCredential, date, expiresIn, headerList, contentHashStr, storageClass, sessionToken, acl) {
593
- // We tried to make these query params entirely lower-cased, just like the headers
594
- // but Cloudflare R2 requires them to have this exact casing
595
- // We didn't have any issues with them being in non-alphaetical order, but as some implementations decide to require sorting
596
- // in non-pre-signed cases, we do it here as well
597
- // See `benchmark-operations.js` on why we don't use URLSearchParams but string concat
598
- let res = "";
599
- if (acl) {
600
- res += `X-Amz-Acl=${encodeURIComponent(acl)}&`;
601
- }
602
- res += "X-Amz-Algorithm=AWS4-HMAC-SHA256";
603
- if (contentHashStr) {
604
- // We assume that this is always hex-encoded, so no encoding needed
605
- res += `&X-Amz-Content-Sha256=${contentHashStr}`;
606
- }
607
- res += `&X-Amz-Credential=${encodeURIComponent(amzCredential)}`;
608
- res += `&X-Amz-Date=${date.dateTime}`; // internal dateTimes don't need encoding
609
- res += `&X-Amz-Expires=${expiresIn}`; // number -> no encoding
610
- if (sessionToken) {
611
- res += `&X-Amz-Security-Token=${encodeURIComponent(sessionToken)}`;
612
- }
613
- res += `&X-Amz-SignedHeaders=${encodeURIComponent(headerList)}`;
614
- if (storageClass) {
615
- res += `&X-Amz-Storage-Class=${storageClass}`;
616
- }
617
- return res;
618
- }
619
- function ensureValidBucketName(name) {
620
- if (name.length < 3 || name.length > 63) {
621
- throw new Error("`name` must be between 3 and 63 characters long.");
622
- }
623
- if (name.startsWith(".") || name.endsWith(".")) {
624
- throw new Error("`name` must not start or end with a period (.)");
625
- }
626
- if (!/^[a-z0-9.-]+$/.test(name)) {
627
- throw new Error("`name` can only contain lowercase letters, numbers, periods (.), and hyphens (-).");
628
- }
629
- if (name.includes("..")) {
630
- throw new Error("`name` must not contain two adjacent periods (..)");
631
- }
632
- }
package/dist/S3Error.d.ts DELETED
@@ -1,14 +0,0 @@
1
- export default class S3Error extends Error {
2
- readonly code: string;
3
- readonly path: string;
4
- readonly message: string;
5
- readonly requestId: string | undefined;
6
- readonly hostId: string | undefined;
7
- constructor(code: string, path: string, { message, requestId, hostId, cause }?: S3ErrorOptions);
8
- }
9
- export type S3ErrorOptions = {
10
- message?: string | undefined;
11
- requestId?: string | undefined;
12
- hostId?: string | undefined;
13
- cause?: unknown;
14
- };
package/dist/S3Error.js DELETED
@@ -1,15 +0,0 @@
1
- export default class S3Error extends Error {
2
- code;
3
- path;
4
- message;
5
- requestId;
6
- hostId;
7
- constructor(code, path, { message = undefined, requestId = undefined, hostId = undefined, cause = undefined, } = {}) {
8
- super(message, { cause });
9
- this.code = code;
10
- this.path = path;
11
- this.message = message ?? "Some unknown error occurred.";
12
- this.requestId = requestId;
13
- this.hostId = hostId;
14
- }
15
- }