s3mini 0.7.1 → 0.8.1

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/src/S3.ts CHANGED
@@ -1,8 +1,22 @@
1
1
  'use strict';
2
2
 
3
3
  import * as C from './consts.js';
4
+ import {
5
+ hexFromBuffer,
6
+ sha256,
7
+ hmac,
8
+ uriResourceEscape,
9
+ getByteSize,
10
+ sanitizeETag,
11
+ uriEscape,
12
+ parseXml,
13
+ escapeXml,
14
+ base64FromBuffer,
15
+ extractErrCode,
16
+ S3NetworkError,
17
+ S3ServiceError,
18
+ } from './utils.js';
4
19
  import type * as IT from './types.js';
5
- import * as U from './utils.js';
6
20
 
7
21
  /**
8
22
  * S3 class for interacting with S3-compatible object storage services.
@@ -43,8 +57,8 @@ class S3mini {
43
57
  * @param {typeof fetch} [config.fetch=globalThis.fetch] - Custom fetch implementation to use for HTTP requests.
44
58
  * @throws {TypeError} Will throw an error if required parameters are missing or of incorrect type.
45
59
  */
46
- readonly accessKeyId: string;
47
- readonly secretAccessKey: string;
60
+ readonly #accessKeyId: string;
61
+ readonly #secretAccessKey: string;
48
62
  readonly endpoint: URL;
49
63
  readonly region: string;
50
64
  readonly bucketName: string;
@@ -66,8 +80,8 @@ class S3mini {
66
80
  fetch = globalThis.fetch,
67
81
  }: IT.S3Config) {
68
82
  this._validateConstructorParams(accessKeyId, secretAccessKey, endpoint);
69
- this.accessKeyId = accessKeyId;
70
- this.secretAccessKey = secretAccessKey;
83
+ this.#accessKeyId = accessKeyId;
84
+ this.#secretAccessKey = secretAccessKey;
71
85
  this.endpoint = new URL(this._ensureValidUrl(endpoint));
72
86
  this.region = region;
73
87
  this.bucketName = this._extractBucketName();
@@ -120,7 +134,7 @@ class S3mini {
120
134
  region: this.region,
121
135
  endpoint: this.endpoint.toString(),
122
136
  // Only include the first few characters of the access key, if it exists
123
- accessKeyId: this.accessKeyId ? `${this.accessKeyId.substring(0, 4)}...` : undefined,
137
+ accessKeyId: this.#accessKeyId ? `${this.#accessKeyId.substring(0, 4)}...` : undefined,
124
138
  }),
125
139
  };
126
140
 
@@ -292,14 +306,14 @@ class S3mini {
292
306
  }
293
307
  }
294
308
  const canonicalRequest = `${method}\n${url.pathname}\n${this._buildCanonicalQueryString(query)}\n${canonicalHeaders}\n\n${signedHeaders}\n${C.UNSIGNED_PAYLOAD}`;
295
- const stringToSign = `${C.AWS_ALGORITHM}\n${fullDatetime}\n${credentialScope}\n${U.hexFromBuffer(await U.sha256(canonicalRequest))}`;
309
+ const stringToSign = `${C.AWS_ALGORITHM}\n${fullDatetime}\n${credentialScope}\n${hexFromBuffer(await sha256(canonicalRequest))}`;
296
310
  if (shortDatetime !== this.signingKeyDate || !this.signingKey) {
297
311
  this.signingKeyDate = shortDatetime;
298
312
  this.signingKey = await this._getSignatureKey(shortDatetime);
299
313
  }
300
- const signature = U.hexFromBuffer(await U.hmac(this.signingKey, stringToSign));
314
+ const signature = hexFromBuffer(await hmac(this.signingKey, stringToSign));
301
315
  headers[C.HEADER_AUTHORIZATION] =
302
- `${C.AWS_ALGORITHM} Credential=${this.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
316
+ `${C.AWS_ALGORITHM} Credential=${this.#accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
303
317
  return { url: url.toString(), headers };
304
318
  }
305
319
 
@@ -335,7 +349,7 @@ class S3mini {
335
349
  ...conditionalHeaders,
336
350
  };
337
351
 
338
- const encodedKey = key ? U.uriResourceEscape(key) : '';
352
+ const encodedKey = key ? uriResourceEscape(key) : '';
339
353
  const { url, headers: signedHeaders } = await this._sign(method, encodedKey, filteredOpts, baseHeaders);
340
354
  if (Object.keys(query).length > 0) {
341
355
  withQuery = true; // append query string to signed URL
@@ -360,7 +374,7 @@ class S3mini {
360
374
  * const cleanEtag = s3.sanitizeETag('"abc123"'); // Returns: 'abc123'
361
375
  */
362
376
  public sanitizeETag(etag: string): string {
363
- return U.sanitizeETag(etag);
377
+ return sanitizeETag(etag);
364
378
  }
365
379
 
366
380
  /**
@@ -376,7 +390,7 @@ class S3mini {
376
390
  `;
377
391
  const headers = {
378
392
  [C.HEADER_CONTENT_TYPE]: C.XML_CONTENT_TYPE,
379
- [C.HEADER_CONTENT_LENGTH]: U.getByteSize(xmlBody),
393
+ [C.HEADER_CONTENT_LENGTH]: getByteSize(xmlBody),
380
394
  };
381
395
  const res = await this._signedRequest('PUT', '', {
382
396
  body: xmlBody,
@@ -457,7 +471,7 @@ class S3mini {
457
471
  this._checkPrefix(prefix);
458
472
  this._checkOpts(opts);
459
473
 
460
- const keyPath = delimiter === '/' ? delimiter : U.uriEscape(delimiter);
474
+ const keyPath = delimiter === '/' ? delimiter : uriEscape(delimiter);
461
475
  const unlimited = !(maxKeys && maxKeys > 0);
462
476
  let remaining = unlimited ? Infinity : maxKeys;
463
477
  let token: string | undefined;
@@ -482,6 +496,47 @@ class S3mini {
482
496
  return all;
483
497
  }
484
498
 
499
+ /**
500
+ * Lists objects in the bucket with optional filtering and pagination using a continuation token.
501
+ * This method retrieves objects matching the criteria (paginated like listObjectsV2).
502
+ * @param {string} [delimiter='/'] - The delimiter to use for grouping objects.
503
+ * @param {string} [prefix=''] - The prefix to filter objects by.
504
+ * @param {number} [maxKeys] - The maximum number of keys to return. Uses a default value of 100.
505
+ * @param {string} [nextContinuationToken] - The nextContinuationToken to continue previous results. If not provided, starts from the beginning.
506
+ * @param {Record<string, unknown>} [opts={}] - Additional options for the request.
507
+ * @returns {Promise<{objects: IT.ListObject[] | null; nextContinuationToken?: string } | undefined | null>} A promise that resolves to an array of objects or null if the bucket is empty, along with nextContinuationToken if there are more reccords.
508
+ * @example
509
+ * // List all objects
510
+ * const { objects, nextContinuationToken } = await s3.listObjectsPaged();
511
+ *
512
+ * // List 200 objects with prefix
513
+ * const photos = await s3.listObjectsPaged('/', 'photos/', 200, "token...");
514
+ */
515
+ public async listObjectsPaged(
516
+ delimiter: string = '/',
517
+ prefix: string = '',
518
+ maxKeys: number = 100,
519
+ nextContinuationToken?: string,
520
+ opts: Record<string, unknown> = {},
521
+ ): Promise<{ objects: IT.ListObject[] | null; nextContinuationToken?: string } | undefined | null> {
522
+ this._checkDelimiter(delimiter);
523
+ this._checkPrefix(prefix);
524
+ this._checkOpts(opts);
525
+
526
+ const keyPath = delimiter === '/' ? delimiter : uriEscape(delimiter);
527
+ let token: string | undefined = nextContinuationToken;
528
+ const all: IT.ListObject[] = [];
529
+
530
+ const batchResult = await this._fetchObjectBatch(keyPath, prefix, maxKeys, token, opts);
531
+ if (batchResult === null) {
532
+ return null; // 404 - bucket not found
533
+ }
534
+
535
+ all.push(...batchResult.objects);
536
+ token = batchResult.continuationToken;
537
+ return { objects: all, nextContinuationToken: token };
538
+ }
539
+
485
540
  private async _fetchObjectBatch(
486
541
  keyPath: string,
487
542
  prefix: string,
@@ -546,7 +601,7 @@ class S3mini {
546
601
  objects: IT.ListObject[];
547
602
  continuationToken?: string;
548
603
  } {
549
- const raw = U.parseXml(xmlText) as Record<string, unknown>;
604
+ const raw = parseXml(xmlText) as Record<string, unknown>;
550
605
 
551
606
  if (typeof raw !== 'object' || !raw || 'error' in raw) {
552
607
  this._log('error', `${C.ERROR_PREFIX}Unexpected listObjects response shape: ${JSON.stringify(raw)}`);
@@ -562,12 +617,37 @@ class S3mini {
562
617
 
563
618
  private _extractObjectsFromResponse(response: Record<string, unknown>): IT.ListObject[] {
564
619
  const contents = response.Contents || response.contents; // S3 v2 vs v1
620
+ const commonPrefixes = response.CommonPrefixes || response.commonPrefixes;
565
621
 
566
- if (!contents) {
567
- return [];
622
+ const objects: IT.ListObject[] = [];
623
+
624
+ // Extract regular objects from Contents
625
+ if (contents) {
626
+ if (Array.isArray(contents)) {
627
+ objects.push(...(contents as IT.ListObject[]));
628
+ } else {
629
+ objects.push(contents as IT.ListObject);
630
+ }
631
+ }
632
+
633
+ // Extract directory prefixes from CommonPrefixes
634
+ if (commonPrefixes) {
635
+ const prefixList = Array.isArray(commonPrefixes) ? commonPrefixes : [commonPrefixes];
636
+ for (const item of prefixList) {
637
+ const prefix = (item as Record<string, unknown>).Prefix || (item as Record<string, unknown>).prefix;
638
+ if (typeof prefix === 'string') {
639
+ objects.push({
640
+ Key: prefix,
641
+ Size: 0,
642
+ LastModified: new Date(0),
643
+ ETag: '',
644
+ StorageClass: '',
645
+ } as IT.ListObject);
646
+ }
647
+ }
568
648
  }
569
649
 
570
- return Array.isArray(contents) ? (contents as IT.ListObject[]) : [contents as IT.ListObject];
650
+ return objects;
571
651
  }
572
652
 
573
653
  private _extractContinuationToken(response: Record<string, unknown>): string | undefined {
@@ -604,7 +684,7 @@ class S3mini {
604
684
  this._checkOpts(opts);
605
685
 
606
686
  const query = { uploads: '', ...opts };
607
- const keyPath = delimiter === '/' ? delimiter : U.uriEscape(delimiter);
687
+ const keyPath = delimiter === '/' ? delimiter : uriEscape(delimiter);
608
688
 
609
689
  const res = await this._signedRequest(method, keyPath, {
610
690
  query,
@@ -618,7 +698,7 @@ class S3mini {
618
698
  // etag: res.headers.get(C.HEADER_ETAG) ?? '',
619
699
  // };
620
700
  // }
621
- const raw = U.parseXml(await res.text()) as unknown;
701
+ const raw = parseXml(await res.text()) as unknown;
622
702
  if (typeof raw !== 'object' || raw === null) {
623
703
  throw new Error(`${C.ERROR_PREFIX}Unexpected listMultipartUploads response shape`);
624
704
  }
@@ -647,10 +727,11 @@ class S3mini {
647
727
  tolerated: [200, 404, 412, 304],
648
728
  headers: ssecHeaders ? { ...ssecHeaders } : undefined,
649
729
  });
650
- if ([404, 412, 304].includes(res.status)) {
651
- return null;
730
+ const s = res.status;
731
+ if (s === 200) {
732
+ return res.text();
652
733
  }
653
- return res.text();
734
+ return null;
654
735
  }
655
736
 
656
737
  /**
@@ -671,10 +752,10 @@ class S3mini {
671
752
  tolerated: [200, 404, 412, 304],
672
753
  headers: ssecHeaders ? { ...ssecHeaders } : undefined,
673
754
  });
674
- if ([404, 412, 304].includes(res.status)) {
675
- return null;
755
+ if (res.status === 200) {
756
+ return res;
676
757
  }
677
- return res;
758
+ return null;
678
759
  }
679
760
 
680
761
  /**
@@ -695,10 +776,10 @@ class S3mini {
695
776
  tolerated: [200, 404, 412, 304],
696
777
  headers: ssecHeaders ? { ...ssecHeaders } : undefined,
697
778
  });
698
- if ([404, 412, 304].includes(res.status)) {
699
- return null;
779
+ if (res.status === 200) {
780
+ return res.arrayBuffer();
700
781
  }
701
- return res.arrayBuffer();
782
+ return null;
702
783
  }
703
784
 
704
785
  /**
@@ -719,10 +800,10 @@ class S3mini {
719
800
  tolerated: [200, 404, 412, 304],
720
801
  headers: ssecHeaders ? { ...ssecHeaders } : undefined,
721
802
  });
722
- if ([404, 412, 304].includes(res.status)) {
723
- return null;
803
+ if (res.status === 200) {
804
+ return res.json() as Promise<T>;
724
805
  }
725
- return res.json() as Promise<T>;
806
+ return null;
726
807
  }
727
808
 
728
809
  /**
@@ -744,8 +825,8 @@ class S3mini {
744
825
  tolerated: [200, 404, 412, 304],
745
826
  headers: ssecHeaders ? { ...ssecHeaders } : undefined,
746
827
  });
747
-
748
- if ([404, 412, 304].includes(res.status)) {
828
+ const s = res.status;
829
+ if (s === 404 || s === 412 || s === 304) {
749
830
  return { etag: null, data: null };
750
831
  }
751
832
 
@@ -753,7 +834,7 @@ class S3mini {
753
834
  if (!etag) {
754
835
  throw new Error(`${C.ERROR_PREFIX}ETag not found in response headers`);
755
836
  }
756
- return { etag: U.sanitizeETag(etag), data: await res.arrayBuffer() };
837
+ return { etag: sanitizeETag(etag), data: await res.arrayBuffer() };
757
838
  } catch (err) {
758
839
  this._log('error', `Error getting object ${key} with ETag: ${String(err)}`);
759
840
  throw err;
@@ -775,12 +856,16 @@ class S3mini {
775
856
  key: string,
776
857
  wholeFile = true,
777
858
  rangeFrom = 0,
778
- rangeTo = this.requestSizeInBytes,
859
+ rangeTo?: number,
779
860
  opts: Record<string, unknown> = {},
780
861
  ssecHeaders?: IT.SSECHeaders,
781
862
  ): Promise<Response> {
782
- const rangeHdr: Record<string, string | number> = wholeFile ? {} : { range: `bytes=${rangeFrom}-${rangeTo - 1}` };
863
+ let rangeHdr: Record<string, string | number> = {};
783
864
 
865
+ if (!wholeFile) {
866
+ rangeHdr =
867
+ rangeTo === undefined ? { range: `bytes=${rangeFrom}-` } : { range: `bytes=${rangeFrom}-${rangeTo - 1}` };
868
+ }
784
869
  return this._signedRequest('GET', key, {
785
870
  query: { ...opts },
786
871
  headers: { ...rangeHdr, ...ssecHeaders },
@@ -867,7 +952,7 @@ class S3mini {
867
952
  throw new Error(`${C.ERROR_PREFIX}ETag not found in response headers`);
868
953
  }
869
954
 
870
- return U.sanitizeETag(etag);
955
+ return sanitizeETag(etag);
871
956
  }
872
957
 
873
958
  /**
@@ -897,7 +982,7 @@ class S3mini {
897
982
  return this._signedRequest('PUT', key, {
898
983
  body: this._validateData(data),
899
984
  headers: {
900
- [C.HEADER_CONTENT_LENGTH]: U.getByteSize(data),
985
+ [C.HEADER_CONTENT_LENGTH]: getByteSize(data),
901
986
  [C.HEADER_CONTENT_TYPE]: fileType,
902
987
  ...additionalHeaders,
903
988
  ...ssecHeaders,
@@ -935,7 +1020,7 @@ class S3mini {
935
1020
  headers,
936
1021
  withQuery: true,
937
1022
  });
938
- const parsed = U.parseXml(await res.text()) as Record<string, unknown>;
1023
+ const parsed = parseXml(await res.text()) as Record<string, unknown>;
939
1024
 
940
1025
  if (parsed && typeof parsed === 'object') {
941
1026
  // Check for both cases of InitiateMultipartUploadResult
@@ -990,12 +1075,12 @@ class S3mini {
990
1075
  query,
991
1076
  body,
992
1077
  headers: {
993
- [C.HEADER_CONTENT_LENGTH]: U.getByteSize(data),
1078
+ [C.HEADER_CONTENT_LENGTH]: getByteSize(data),
994
1079
  ...ssecHeaders,
995
1080
  },
996
1081
  });
997
1082
 
998
- return { partNumber, etag: U.sanitizeETag(res.headers.get('etag') || '') };
1083
+ return { partNumber, etag: sanitizeETag(res.headers.get('etag') || '') };
999
1084
  }
1000
1085
 
1001
1086
  /**
@@ -1025,7 +1110,7 @@ class S3mini {
1025
1110
  const xmlBody = this._buildCompleteMultipartUploadXml(parts);
1026
1111
  const headers = {
1027
1112
  [C.HEADER_CONTENT_TYPE]: C.XML_CONTENT_TYPE,
1028
- [C.HEADER_CONTENT_LENGTH]: U.getByteSize(xmlBody),
1113
+ [C.HEADER_CONTENT_LENGTH]: getByteSize(xmlBody),
1029
1114
  };
1030
1115
 
1031
1116
  const res = await this._signedRequest('POST', key, {
@@ -1035,7 +1120,7 @@ class S3mini {
1035
1120
  withQuery: true,
1036
1121
  });
1037
1122
 
1038
- const parsed = U.parseXml(await res.text()) as Record<string, unknown>;
1123
+ const parsed = parseXml(await res.text()) as Record<string, unknown>;
1039
1124
  if (parsed && typeof parsed === 'object') {
1040
1125
  // Check for both cases
1041
1126
  const result = parsed.completeMultipartUploadResult || parsed.CompleteMultipartUploadResult || parsed;
@@ -1048,7 +1133,7 @@ class S3mini {
1048
1133
  if (etag && typeof etag === 'string') {
1049
1134
  return {
1050
1135
  ...resultObj,
1051
- etag: U.sanitizeETag(etag),
1136
+ etag: sanitizeETag(etag),
1052
1137
  } as IT.CompleteMultipartUploadResult;
1053
1138
  }
1054
1139
 
@@ -1089,7 +1174,7 @@ class S3mini {
1089
1174
  headers,
1090
1175
  withQuery: true,
1091
1176
  });
1092
- const parsed = U.parseXml(await res.text()) as Record<string, unknown>;
1177
+ const parsed = parseXml(await res.text()) as Record<string, unknown>;
1093
1178
  if (
1094
1179
  parsed &&
1095
1180
  'error' in parsed &&
@@ -1220,7 +1305,7 @@ class S3mini {
1220
1305
  this._checkKey(sourceKey);
1221
1306
  this._checkKey(destinationKey);
1222
1307
 
1223
- const copySource = `/${this.bucketName}/${U.uriEscape(sourceKey)}`;
1308
+ const copySource = `/${this.bucketName}/${uriEscape(sourceKey)}`;
1224
1309
 
1225
1310
  return this._executeCopyOperation(destinationKey, copySource, options);
1226
1311
  }
@@ -1312,7 +1397,7 @@ class S3mini {
1312
1397
  }
1313
1398
 
1314
1399
  private _parseCopyObjectResponse(xmlText: string): IT.CopyObjectResult {
1315
- const parsed = U.parseXml(xmlText) as Record<string, unknown>;
1400
+ const parsed = parseXml(xmlText) as Record<string, unknown>;
1316
1401
  if (!parsed || typeof parsed !== 'object') {
1317
1402
  throw new Error(`${C.ERROR_PREFIX}Unexpected copyObject response format`);
1318
1403
  }
@@ -1323,7 +1408,7 @@ class S3mini {
1323
1408
  throw new Error(`${C.ERROR_PREFIX}ETag not found in copyObject response`);
1324
1409
  }
1325
1410
  return {
1326
- etag: U.sanitizeETag(etag),
1411
+ etag: sanitizeETag(etag),
1327
1412
  lastModified: lastModified ? new Date(lastModified as string) : undefined,
1328
1413
  };
1329
1414
  }
@@ -1340,13 +1425,13 @@ class S3mini {
1340
1425
  }
1341
1426
 
1342
1427
  private async _deleteObjectsProcess(keys: string[]): Promise<boolean[]> {
1343
- const objectsXml = keys.map(key => `<Object><Key>${U.escapeXml(key)}</Key></Object>`).join('');
1428
+ const objectsXml = keys.map(key => `<Object><Key>${escapeXml(key)}</Key></Object>`).join('');
1344
1429
  const xmlBody = '<Delete>' + objectsXml + '</Delete>';
1345
1430
  const query = { delete: '' };
1346
- const sha256base64 = U.base64FromBuffer(await U.sha256(xmlBody));
1431
+ const sha256base64 = base64FromBuffer(await sha256(xmlBody));
1347
1432
  const headers = {
1348
1433
  [C.HEADER_CONTENT_TYPE]: C.XML_CONTENT_TYPE,
1349
- [C.HEADER_CONTENT_LENGTH]: U.getByteSize(xmlBody),
1434
+ [C.HEADER_CONTENT_LENGTH]: getByteSize(xmlBody),
1350
1435
  [C.HEADER_AMZ_CHECKSUM_SHA256]: sha256base64,
1351
1436
  };
1352
1437
 
@@ -1356,7 +1441,7 @@ class S3mini {
1356
1441
  headers,
1357
1442
  withQuery: true,
1358
1443
  });
1359
- const parsed = U.parseXml(await res.text()) as Record<string, unknown>;
1444
+ const parsed = parseXml(await res.text()) as Record<string, unknown>;
1360
1445
  if (!parsed || typeof parsed !== 'object') {
1361
1446
  throw new Error(`${C.ERROR_PREFIX}Failed to delete objects: ${JSON.stringify(parsed)}`);
1362
1447
  }
@@ -1444,7 +1529,7 @@ class S3mini {
1444
1529
  const res = await this._fetch(url, {
1445
1530
  method,
1446
1531
  headers,
1447
- body: ['GET', 'HEAD'].includes(method) ? undefined : body,
1532
+ body: method === 'GET' || method === 'HEAD' ? undefined : body,
1448
1533
  signal: this.requestAbortTimeout ? AbortSignal.timeout(this.requestAbortTimeout) : undefined,
1449
1534
  });
1450
1535
  this._log('info', `Response status: ${res.status}, tolerated: ${toleratedStatusCodes.join(',')}`);
@@ -1454,9 +1539,9 @@ class S3mini {
1454
1539
  await this._handleErrorResponse(res);
1455
1540
  return res;
1456
1541
  } catch (err: unknown) {
1457
- const code = U.extractErrCode(err);
1542
+ const code = extractErrCode(err);
1458
1543
  if (code && ['ENOTFOUND', 'EAI_AGAIN', 'ETIMEDOUT', 'ECONNREFUSED'].includes(code)) {
1459
- throw new U.S3NetworkError(`S3 network error: ${code}`, code, err);
1544
+ throw new S3NetworkError(`S3 network error: ${code}`, code, err);
1460
1545
  }
1461
1546
  throw err;
1462
1547
  }
@@ -1466,7 +1551,7 @@ class S3mini {
1466
1551
  if (headers.get('content-type') !== 'application/xml') {
1467
1552
  return {};
1468
1553
  }
1469
- const parsedBody = U.parseXml(body);
1554
+ const parsedBody = parseXml(body);
1470
1555
  if (
1471
1556
  !parsedBody ||
1472
1557
  typeof parsedBody !== 'object' ||
@@ -1492,7 +1577,7 @@ class S3mini {
1492
1577
  'error',
1493
1578
  `${C.ERROR_PREFIX}Request failed with status ${res.status}: ${svcCode} - ${errorMessage},err body: ${errorBody}`,
1494
1579
  );
1495
- throw new U.S3ServiceError(`S3 returned ${res.status} – ${svcCode}`, res.status, svcCode, errorBody);
1580
+ throw new S3ServiceError(`S3 returned ${res.status} – ${svcCode}`, res.status, svcCode, errorBody);
1496
1581
  }
1497
1582
 
1498
1583
  private _buildCanonicalQueryString(queryParams: Record<string, unknown>): string {
@@ -1505,10 +1590,10 @@ class S3mini {
1505
1590
  .join('&');
1506
1591
  }
1507
1592
  private async _getSignatureKey(dateStamp: string): Promise<ArrayBuffer> {
1508
- const kDate = await U.hmac(`AWS4${this.secretAccessKey}`, dateStamp);
1509
- const kRegion = await U.hmac(kDate, this.region);
1510
- const kService = await U.hmac(kRegion, C.S3_SERVICE);
1511
- return await U.hmac(kService, C.AWS_REQUEST_TYPE);
1593
+ const kDate = await hmac(`AWS4${this.#secretAccessKey}`, dateStamp);
1594
+ const kRegion = await hmac(kDate, this.region);
1595
+ const kService = await hmac(kRegion, C.S3_SERVICE);
1596
+ return await hmac(kService, C.AWS_REQUEST_TYPE);
1512
1597
  }
1513
1598
  }
1514
1599
 
package/src/utils.ts CHANGED
@@ -3,7 +3,7 @@ import type { XmlValue, XmlMap, ListBucketResponse, ErrorWithCode } from './type
3
3
 
4
4
  const ENCODR = new TextEncoder();
5
5
  const chunkSize = 0x8000; // 32KB chunks
6
- const HEXS = '0123456789abcdef';
6
+ const HEX_CHARS = new Uint8Array([48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 97, 98, 99, 100, 101, 102]);
7
7
 
8
8
  export const getByteSize = (data: unknown): number => {
9
9
  if (typeof data === 'string') {
@@ -23,13 +23,15 @@ export const getByteSize = (data: unknown): number => {
23
23
  * @param {ArrayBuffer} buffer The raw bytes.
24
24
  * @returns {string} Hexadecimal string
25
25
  */
26
+
26
27
  export const hexFromBuffer = (buffer: ArrayBuffer): string => {
27
28
  const bytes = new Uint8Array(buffer);
28
- let hex = '';
29
- for (const byte of bytes) {
30
- hex += HEXS[byte >> 4]! + HEXS[byte & 0x0f]!;
29
+ const hex = new Uint8Array(bytes.length * 2);
30
+ for (let i = 0, j = 0; i < bytes.length; i++) {
31
+ hex[j++] = HEX_CHARS[bytes[i]! >> 4]!;
32
+ hex[j++] = HEX_CHARS[bytes[i]! & 0x0f]!;
31
33
  }
32
- return hex;
34
+ return String.fromCodePoint(...hex);
33
35
  };
34
36
 
35
37
  /**
@@ -158,7 +160,7 @@ export const parseXml = (input: string): XmlValue => {
158
160
  * @param c Character to encode
159
161
  * @returns Percent-encoded character
160
162
  */
161
- const encodeAsHex = (c: string): string => `%${c.charCodeAt(0).toString(16).toUpperCase()}`;
163
+ const encodeAsHex = (c: string): string => `%${(c.codePointAt(0) ?? 0).toString(16).toUpperCase()}`;
162
164
 
163
165
  /**
164
166
  * Escape a URI string using percent encoding
@@ -166,7 +168,7 @@ const encodeAsHex = (c: string): string => `%${c.charCodeAt(0).toString(16).toUp
166
168
  * @returns Escaped URI string
167
169
  */
168
170
  export const uriEscape = (uriStr: string): string => {
169
- return encodeURIComponent(uriStr).replace(/[!'()*]/g, encodeAsHex);
171
+ return encodeURIComponent(uriStr).replaceAll(/[!'()*]/g, encodeAsHex);
170
172
  };
171
173
 
172
174
  /**