lean-s3 0.5.0 → 0.6.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/README.md CHANGED
@@ -137,6 +137,8 @@ See [DESIGN_DECISIONS.md](./DESIGN_DECISIONS.md) to read about why this library
137
137
  - ✅ [`PutObject`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObject.html) via `S3File.write`
138
138
  - ✅ [`HeadObject`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html) via `S3File.exists`/`S3File.stat`
139
139
  - ✅ [`ListMultipartUploads`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListMultipartUploads.html) via `.listMultipartUploads`
140
+ - ✅ [`ListParts`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListParts.html) via `.listParts`
141
+ - ✅ [`UploadPart`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_UploadPart.html) via `.uploadPart`
140
142
  - ✅ [`CompleteMultipartUpload`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_CompleteMultipartUpload.html) via `.completeMultipartUpload`
141
143
  - ✅ [`AbortMultipartUpload`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_AbortMultipartUpload.html) via `.abortMultipartUpload`
142
144
 
package/dist/index.d.ts CHANGED
@@ -20,6 +20,14 @@ declare class S3BucketEntry {
20
20
  static parse(source: any): S3BucketEntry;
21
21
  }
22
22
 
23
+ declare const __brand: unique symbol;
24
+ type Brand<B> = {
25
+ [__brand]: B;
26
+ };
27
+ type Branded<T, B> = T & Brand<B>;
28
+ type BucketName = Branded<string, "BucketName">;
29
+ type ObjectKey = Branded<string, "ObjectKey">;
30
+
23
31
  declare const write: unique symbol;
24
32
  declare const stream: unique symbol;
25
33
  declare const signedRequest: unique symbol;
@@ -34,6 +42,7 @@ interface S3ClientOptions {
34
42
  type OverridableS3ClientOptions = Pick<S3ClientOptions, "region" | "bucket" | "endpoint">;
35
43
  type CreateFileInstanceOptions = {};
36
44
  type DeleteObjectsOptions = {
45
+ bucket?: string;
37
46
  signal?: AbortSignal;
38
47
  };
39
48
  interface S3FilePresignOptions {
@@ -100,7 +109,7 @@ type CreateMultipartUploadOptions = {
100
109
  bucket?: string;
101
110
  signal?: AbortSignal;
102
111
  };
103
- type CreateMultipartUploadResponse = {
112
+ type CreateMultipartUploadResult = {
104
113
  bucket: string;
105
114
  key: string;
106
115
  uploadId: string;
@@ -129,7 +138,44 @@ type MultipartUploadPart = {
129
138
  partNumber: number;
130
139
  etag: string;
131
140
  };
132
- type ListObjectsResponse = {
141
+ type UploadPartOptions = {
142
+ bucket?: string;
143
+ signal?: AbortSignal;
144
+ };
145
+ type UploadPartResult = {
146
+ partNumber: number;
147
+ etag: string;
148
+ };
149
+ type ListPartsOptions = {
150
+ maxParts?: number;
151
+ partNumberMarker?: string;
152
+ bucket?: string;
153
+ signal?: AbortSignal;
154
+ };
155
+ type ListPartsResult = {
156
+ bucket: string;
157
+ key: string;
158
+ uploadId: string;
159
+ partNumberMarker?: string;
160
+ nextPartNumberMarker?: string;
161
+ maxParts?: number;
162
+ isTruncated: boolean;
163
+ parts: Array<{
164
+ checksumCRC32?: string;
165
+ checksumCRC32C?: string;
166
+ checksumCRC64NVME?: string;
167
+ checksumSHA1?: string;
168
+ checksumSHA256?: string;
169
+ etag: string;
170
+ lastModified: Date;
171
+ partNumber: number;
172
+ size: number;
173
+ }>;
174
+ storageClass?: StorageClass;
175
+ checksumAlgorithm?: ChecksumAlgorithm;
176
+ checksumType?: ChecksumType;
177
+ };
178
+ type ListObjectsResult = {
133
179
  name: string;
134
180
  prefix: string | undefined;
135
181
  startAfter: string | undefined;
@@ -226,7 +272,7 @@ declare class S3Client {
226
272
  */
227
273
  presign(path: string, { method, expiresIn, // TODO: Maybe rename this to expiresInSeconds
228
274
  storageClass, contentLength, acl, region: regionOverride, bucket: bucketOverride, endpoint: endpointOverride, }?: Partial<S3FilePresignOptions & OverridableS3ClientOptions>): string;
229
- createMultipartUpload(key: string, options?: CreateMultipartUploadOptions): Promise<CreateMultipartUploadResponse>;
275
+ createMultipartUpload(key: string, options?: CreateMultipartUploadOptions): Promise<CreateMultipartUploadResult>;
230
276
  /**
231
277
  * @remarks Uses [`ListMultipartUploads`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListMultipartUploads.html).
232
278
  * @throws {RangeError} If `options.maxKeys` is not between `1` and `1000`.
@@ -237,24 +283,28 @@ declare class S3Client {
237
283
  * @throws {RangeError} If `key` is not at least 1 character long.
238
284
  * @throws {Error} If `uploadId` is not provided.
239
285
  */
240
- abortMultipartUpload(key: string, uploadId: string, options?: AbortMultipartUploadOptions): Promise<void>;
286
+ abortMultipartUpload(path: string, uploadId: string, options?: AbortMultipartUploadOptions): Promise<void>;
241
287
  /**
242
288
  * @remarks Uses [`CompleteMultipartUpload`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_CompleteMultipartUpload.html).
243
289
  * @throws {RangeError} If `key` is not at least 1 character long.
244
290
  * @throws {Error} If `uploadId` is not provided.
245
291
  */
246
- completeMultipartUpload(key: string, uploadId: string, parts: readonly MultipartUploadPart[], options?: CompleteMultipartUploadOptions): Promise<{
247
- location: any;
248
- bucket: any;
249
- key: any;
250
- etag: any;
251
- checksumCRC32: any;
252
- checksumCRC32C: any;
253
- checksumCRC64NVME: any;
254
- checksumSHA1: any;
255
- checksumSHA256: any;
256
- checksumType: any;
257
- }>;
292
+ completeMultipartUpload(path: string, uploadId: string, parts: readonly MultipartUploadPart[], options?: CompleteMultipartUploadOptions): Promise<CompleteMultipartUploadResult>;
293
+ /**
294
+ * @remarks Uses [`UploadPart`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_UploadPart.html).
295
+ * @throws {RangeError} If `key` is not at least 1 character long.
296
+ * @throws {Error} If `uploadId` is not provided.
297
+ */
298
+ uploadPart(path: string, uploadId: string, data: UndiciBodyInit, partNumber: number, options?: UploadPartOptions): Promise<UploadPartResult>;
299
+ /**
300
+ * @remarks Uses [`ListParts`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListParts.html).
301
+ * @throws {RangeError} If `key` is not at least 1 character long.
302
+ * @throws {Error} If `uploadId` is not provided.
303
+ * @throws {TypeError} If `options.maxParts` is not a `number`.
304
+ * @throws {RangeError} If `options.maxParts` is <= 0.
305
+ * @throws {TypeError} If `options.partNumberMarker` is not a `string`.
306
+ */
307
+ listParts(path: string, uploadId: string, options?: ListPartsOptions): Promise<ListPartsResult>;
258
308
  /**
259
309
  * Creates a new bucket on the S3 server.
260
310
  *
@@ -294,7 +344,7 @@ declare class S3Client {
294
344
  *
295
345
  * @throws {RangeError} If `maxKeys` is not between `1` and `1000`.
296
346
  */
297
- list(options?: ListObjectsOptions): Promise<ListObjectsResponse>;
347
+ list(options?: ListObjectsOptions): Promise<ListObjectsResult>;
298
348
  /**
299
349
  * Uses [`DeleteObjects`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObjects.html) to delete multiple objects in a single request.
300
350
  */
@@ -306,7 +356,7 @@ declare class S3Client {
306
356
  * TODO: Maybe move this into a separate free function?
307
357
  * @internal
308
358
  */
309
- [signedRequest](method: HttpMethod, pathWithoutBucket: string, query: string | undefined, body: UndiciBodyInit | undefined, additionalSignedHeaders: Record<string, string> | undefined, additionalUnsignedHeaders: Record<string, string> | undefined, contentHash: Buffer | undefined, bucket: string | undefined, signal?: AbortSignal | undefined): Promise<Dispatcher.ResponseData<null>>;
359
+ [signedRequest](method: HttpMethod, pathWithoutBucket: ObjectKey, query: string | undefined, body: UndiciBodyInit | undefined, additionalSignedHeaders: Record<string, string> | undefined, additionalUnsignedHeaders: Record<string, string> | undefined, contentHash: Buffer | undefined, bucket: BucketName | undefined, signal?: AbortSignal | undefined): Promise<Dispatcher.ResponseData<null>>;
310
360
  /**
311
361
  * @internal
312
362
  * @param {import("./index.d.ts").UndiciBodyInit} data TODO
@@ -332,7 +382,7 @@ declare class S3File {
332
382
  /**
333
383
  * @internal
334
384
  */
335
- constructor(client: S3Client, path: string, start: number | undefined, end: number | undefined, contentType: string | undefined);
385
+ constructor(client: S3Client, path: ObjectKey, start: number | undefined, end: number | undefined, contentType: string | undefined);
336
386
  slice(start?: number | undefined, end?: number | undefined, contentType?: string | undefined): S3File;
337
387
  /**
338
388
  * Get the stat of a file in the bucket. Uses `HEAD` request to check existence.
@@ -434,4 +484,4 @@ type BucketInfo = {
434
484
  type?: string;
435
485
  };
436
486
 
437
- export { type AbortMultipartUploadOptions, type Acl, type BucketCreationOptions, type BucketDeletionOptions, type BucketExistsOptions, type BucketInfo, type BucketLocationInfo, type ByteSource, type ChecksumAlgorithm, type ChecksumType, type CompleteMultipartUploadOptions, type CompleteMultipartUploadResult, type CreateFileInstanceOptions, type CreateMultipartUploadOptions, type CreateMultipartUploadResponse, type DeleteObjectsOptions, type HttpMethod, type ListMultipartUploadsOptions, type ListMultipartUploadsResult, type ListObjectsIteratingOptions, type ListObjectsOptions, type ListObjectsResponse, type MultipartUpload, type MultipartUploadPart, type OverridableS3ClientOptions, type PresignableHttpMethod, S3BucketEntry, S3Client, type S3ClientOptions, S3Error, type S3ErrorOptions, S3File, type S3FileDeleteOptions, type S3FileExistsOptions, type S3FilePresignOptions, S3Stat, type S3StatOptions, type StorageClass, type UndiciBodyInit };
487
+ export { type AbortMultipartUploadOptions, type Acl, type BucketCreationOptions, type BucketDeletionOptions, type BucketExistsOptions, type BucketInfo, type BucketLocationInfo, type ByteSource, type ChecksumAlgorithm, type ChecksumType, type CompleteMultipartUploadOptions, type CompleteMultipartUploadResult, type CreateFileInstanceOptions, type CreateMultipartUploadOptions, type CreateMultipartUploadResult, type DeleteObjectsOptions, type HttpMethod, type ListMultipartUploadsOptions, type ListMultipartUploadsResult, type ListObjectsIteratingOptions, type ListObjectsOptions, type ListObjectsResult, type ListPartsOptions, type ListPartsResult, type MultipartUpload, type MultipartUploadPart, type OverridableS3ClientOptions, type PresignableHttpMethod, S3BucketEntry, S3Client, type S3ClientOptions, S3Error, type S3ErrorOptions, S3File, type S3FileDeleteOptions, type S3FileExistsOptions, type S3FilePresignOptions, S3Stat, type S3StatOptions, type StorageClass, type UndiciBodyInit, type UploadPartOptions, type UploadPartResult };
package/dist/index.js CHANGED
@@ -318,13 +318,41 @@ function getAuthorizationHeader(keyCache, method, path, query, date, sortedSigne
318
318
  return `AWS4-HMAC-SHA256 Credential=${credentialSpec}, SignedHeaders=${signedHeadersSpec}, Signature=${signature}`;
319
319
  }
320
320
 
321
+ // src/branded.ts
322
+ function ensureValidBucketName(name) {
323
+ if (name.length < 3 || name.length > 63) {
324
+ throw new Error("`name` must be between 3 and 63 characters long.");
325
+ }
326
+ if (name.startsWith(".") || name.endsWith(".")) {
327
+ throw new Error("`name` must not start or end with a period (.)");
328
+ }
329
+ if (!/^[a-z0-9.-]+$/.test(name)) {
330
+ throw new Error(
331
+ "`name` can only contain lowercase letters, numbers, periods (.), and hyphens (-)."
332
+ );
333
+ }
334
+ if (name.includes("..")) {
335
+ throw new Error("`name` must not contain two adjacent periods (..)");
336
+ }
337
+ return name;
338
+ }
339
+ function ensureValidPath(path) {
340
+ if (typeof path !== "string") {
341
+ throw new TypeError("`path` must be a `string`.");
342
+ }
343
+ if (path.length < 1) {
344
+ throw new RangeError("`path` must be at least 1 character long.");
345
+ }
346
+ return path;
347
+ }
348
+
321
349
  // src/S3Client.ts
322
350
  var write = Symbol("write");
323
351
  var stream = Symbol("stream");
324
352
  var signedRequest = Symbol("signedRequest");
325
353
  var xmlParser2 = new XMLParser2({
326
354
  ignoreAttributes: true,
327
- isArray: (_, jPath) => jPath === "ListMultipartUploadsResult.Upload" || jPath === "ListBucketResult.Contents" || jPath === "DeleteResult.Deleted" || jPath === "DeleteResult.Error"
355
+ isArray: (_, jPath) => jPath === "ListMultipartUploadsResult.Upload" || jPath === "ListBucketResult.Contents" || jPath === "ListPartsResult.Part" || jPath === "DeleteResult.Deleted" || jPath === "DeleteResult.Error"
328
356
  });
329
357
  var xmlBuilder = new XMLBuilder({
330
358
  attributeNamePrefix: "$",
@@ -412,7 +440,13 @@ var S3Client = class {
412
440
  * ```
413
441
  */
414
442
  file(path, _options) {
415
- return new S3File(this, path, void 0, void 0, void 0);
443
+ return new S3File(
444
+ this,
445
+ ensureValidPath(path),
446
+ void 0,
447
+ void 0,
448
+ void 0
449
+ );
416
450
  }
417
451
  /**
418
452
  * Generate a presigned URL for temporary access to a file.
@@ -489,11 +523,10 @@ var S3Client = class {
489
523
  //#region multipart uploads
490
524
  async createMultipartUpload(key, options = {}) {
491
525
  if (key.length < 1) {
492
- throw new RangeError("`key` must be at least 1 character long.");
493
526
  }
494
527
  const response = await this[signedRequest](
495
528
  "POST",
496
- key,
529
+ ensureValidPath(key),
497
530
  "uploads=",
498
531
  void 0,
499
532
  void 0,
@@ -524,28 +557,28 @@ var S3Client = class {
524
557
  let query = "uploads=";
525
558
  if (options.delimiter) {
526
559
  if (typeof options.delimiter !== "string") {
527
- throw new TypeError("`delimiter` should be a `string`.");
560
+ throw new TypeError("`delimiter` must be a `string`.");
528
561
  }
529
562
  query += `&delimiter=${encodeURIComponent(options.delimiter)}`;
530
563
  }
531
564
  if (options.keyMarker) {
532
565
  if (typeof options.keyMarker !== "string") {
533
- throw new TypeError("`keyMarker` should be a `string`.");
566
+ throw new TypeError("`keyMarker` must be a `string`.");
534
567
  }
535
568
  query += `&key-marker=${encodeURIComponent(options.keyMarker)}`;
536
569
  }
537
570
  if (typeof options.maxUploads !== "undefined") {
538
571
  if (typeof options.maxUploads !== "number") {
539
- throw new TypeError("`maxUploads` should be a `number`.");
572
+ throw new TypeError("`maxUploads` must be a `number`.");
540
573
  }
541
574
  if (options.maxUploads < 1 || options.maxUploads > 1e3) {
542
- throw new RangeError("`maxUploads` should be between 1 and 1000.");
575
+ throw new RangeError("`maxUploads` has to be between 1 and 1000.");
543
576
  }
544
577
  query += `&max-uploads=${options.maxUploads}`;
545
578
  }
546
579
  if (options.prefix) {
547
580
  if (typeof options.prefix !== "string") {
548
- throw new TypeError("`prefix` should be a `string`.");
581
+ throw new TypeError("`prefix` must be a `string`.");
549
582
  }
550
583
  query += `&prefix=${encodeURIComponent(options.prefix)}`;
551
584
  }
@@ -596,16 +629,13 @@ var S3Client = class {
596
629
  * @throws {RangeError} If `key` is not at least 1 character long.
597
630
  * @throws {Error} If `uploadId` is not provided.
598
631
  */
599
- async abortMultipartUpload(key, uploadId, options = {}) {
600
- if (key.length < 1) {
601
- throw new RangeError("`key` must be at least 1 character long.");
602
- }
632
+ async abortMultipartUpload(path, uploadId, options = {}) {
603
633
  if (!uploadId) {
604
634
  throw new Error("`uploadId` is required.");
605
635
  }
606
636
  const response = await this[signedRequest](
607
637
  "DELETE",
608
- key,
638
+ ensureValidPath(path),
609
639
  `uploadId=${encodeURIComponent(uploadId)}`,
610
640
  void 0,
611
641
  void 0,
@@ -615,7 +645,7 @@ var S3Client = class {
615
645
  options.signal
616
646
  );
617
647
  if (response.statusCode !== 204) {
618
- throw await getResponseError(response, key);
648
+ throw await getResponseError(response, path);
619
649
  }
620
650
  }
621
651
  /**
@@ -623,10 +653,7 @@ var S3Client = class {
623
653
  * @throws {RangeError} If `key` is not at least 1 character long.
624
654
  * @throws {Error} If `uploadId` is not provided.
625
655
  */
626
- async completeMultipartUpload(key, uploadId, parts, options = {}) {
627
- if (key.length < 1) {
628
- throw new RangeError("`key` must be at least 1 character long.");
629
- }
656
+ async completeMultipartUpload(path, uploadId, parts, options = {}) {
630
657
  if (!uploadId) {
631
658
  throw new Error("`uploadId` is required.");
632
659
  }
@@ -640,7 +667,7 @@ var S3Client = class {
640
667
  });
641
668
  const response = await this[signedRequest](
642
669
  "POST",
643
- key,
670
+ ensureValidPath(path),
644
671
  `uploadId=${encodeURIComponent(uploadId)}`,
645
672
  body,
646
673
  void 0,
@@ -650,7 +677,7 @@ var S3Client = class {
650
677
  options.signal
651
678
  );
652
679
  if (response.statusCode !== 200) {
653
- throw await getResponseError(response, key);
680
+ throw await getResponseError(response, path);
654
681
  }
655
682
  const text = await response.body.text();
656
683
  const res = ensureParsedXml(text).CompleteMultipartUploadResult ?? {};
@@ -667,6 +694,109 @@ var S3Client = class {
667
694
  checksumType: res.ChecksumType || void 0
668
695
  };
669
696
  }
697
+ /**
698
+ * @remarks Uses [`UploadPart`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_UploadPart.html).
699
+ * @throws {RangeError} If `key` is not at least 1 character long.
700
+ * @throws {Error} If `uploadId` is not provided.
701
+ */
702
+ async uploadPart(path, uploadId, data, partNumber, options = {}) {
703
+ if (!uploadId) {
704
+ throw new Error("`uploadId` is required.");
705
+ }
706
+ if (!data) {
707
+ throw new Error("`data` is required.");
708
+ }
709
+ if (typeof partNumber !== "number" || partNumber <= 0) {
710
+ throw new Error("`partNumber` has to be a `number` which is >= 1.");
711
+ }
712
+ const response = await this[signedRequest](
713
+ "PUT",
714
+ ensureValidPath(path),
715
+ `partNumber=${partNumber}&uploadId=${encodeURIComponent(uploadId)}`,
716
+ data,
717
+ void 0,
718
+ void 0,
719
+ void 0,
720
+ ensureValidBucketName(options.bucket ?? this.#options.bucket),
721
+ options.signal
722
+ );
723
+ if (response.statusCode === 200) {
724
+ await response.body.dump();
725
+ const etag = response.headers.etag;
726
+ if (typeof etag !== "string" || etag.length === 0) {
727
+ throw new S3Error("Unknown", "", {
728
+ message: "Response did not contain an etag."
729
+ });
730
+ }
731
+ return {
732
+ partNumber,
733
+ etag
734
+ };
735
+ }
736
+ throw await getResponseError(response, "");
737
+ }
738
+ /**
739
+ * @remarks Uses [`ListParts`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListParts.html).
740
+ * @throws {RangeError} If `key` is not at least 1 character long.
741
+ * @throws {Error} If `uploadId` is not provided.
742
+ * @throws {TypeError} If `options.maxParts` is not a `number`.
743
+ * @throws {RangeError} If `options.maxParts` is <= 0.
744
+ * @throws {TypeError} If `options.partNumberMarker` is not a `string`.
745
+ */
746
+ async listParts(path, uploadId, options = {}) {
747
+ let query = "";
748
+ if (options.maxParts) {
749
+ if (typeof options.maxParts !== "number") {
750
+ throw new TypeError("`maxParts` must be a `number`.");
751
+ }
752
+ if (options.maxParts <= 0) {
753
+ throw new RangeError("`maxParts` must be >= 1.");
754
+ }
755
+ query += `&max-parts=${options.maxParts}`;
756
+ }
757
+ if (options.partNumberMarker) {
758
+ if (typeof options.partNumberMarker !== "string") {
759
+ throw new TypeError("`partNumberMarker` must be a `string`.");
760
+ }
761
+ query += `&part-number-marker=${encodeURIComponent(options.partNumberMarker)}`;
762
+ }
763
+ query += `&uploadId=${encodeURIComponent(uploadId)}`;
764
+ const response = await this[signedRequest](
765
+ "GET",
766
+ ensureValidPath(path),
767
+ // We always have a leading &, so we can slice the leading & away (this way, we have less conditionals on the hot path); see benchmark-operations.js
768
+ query.substring(1),
769
+ void 0,
770
+ void 0,
771
+ void 0,
772
+ void 0,
773
+ ensureValidBucketName(options.bucket ?? this.#options.bucket),
774
+ options?.signal
775
+ );
776
+ if (response.statusCode === 200) {
777
+ const text = await response.body.text();
778
+ const root = ensureParsedXml(text).ListPartsResult ?? {};
779
+ return {
780
+ bucket: root.Bucket,
781
+ key: root.Key,
782
+ uploadId: root.UploadId,
783
+ partNumberMarker: root.PartNumberMarker ?? void 0,
784
+ nextPartNumberMarker: root.NextPartNumberMarker ?? void 0,
785
+ maxParts: root.MaxParts ?? 1e3,
786
+ isTruncated: root.IsTruncated ?? false,
787
+ parts: (
788
+ // biome-ignore lint/suspicious/noExplicitAny: parsing code
789
+ root.Part?.map((part) => ({
790
+ etag: part.ETag,
791
+ lastModified: part.LastModified ? new Date(part.LastModified) : void 0,
792
+ partNumber: part.PartNumber ?? void 0,
793
+ size: part.Size ?? void 0
794
+ })) ?? []
795
+ )
796
+ };
797
+ }
798
+ throw await getResponseError(response, path);
799
+ }
670
800
  //#endregion
671
801
  //#region bucket operations
672
802
  /**
@@ -812,29 +942,29 @@ var S3Client = class {
812
942
  let query = "";
813
943
  if (typeof options.continuationToken !== "undefined") {
814
944
  if (typeof options.continuationToken !== "string") {
815
- throw new TypeError("`continuationToken` should be a `string`.");
945
+ throw new TypeError("`continuationToken` must be a `string`.");
816
946
  }
817
947
  query += `continuation-token=${encodeURIComponent(options.continuationToken)}&`;
818
948
  }
819
949
  query += "list-type=2";
820
950
  if (typeof options.maxKeys !== "undefined") {
821
951
  if (typeof options.maxKeys !== "number") {
822
- throw new TypeError("`maxKeys` should be a `number`.");
952
+ throw new TypeError("`maxKeys` must be a `number`.");
823
953
  }
824
954
  if (options.maxKeys < 1 || options.maxKeys > 1e3) {
825
- throw new RangeError("`maxKeys` should be between 1 and 1000.");
955
+ throw new RangeError("`maxKeys` has to be between 1 and 1000.");
826
956
  }
827
957
  query += `&max-keys=${options.maxKeys}`;
828
958
  }
829
959
  if (options.prefix) {
830
960
  if (typeof options.prefix !== "string") {
831
- throw new TypeError("`prefix` should be a `string`.");
961
+ throw new TypeError("`prefix` must be a `string`.");
832
962
  }
833
963
  query += `&prefix=${encodeURIComponent(options.prefix)}`;
834
964
  }
835
965
  if (typeof options.startAfter !== "undefined") {
836
966
  if (typeof options.startAfter !== "string") {
837
- throw new TypeError("`startAfter` should be a `string`.");
967
+ throw new TypeError("`startAfter` must be a `string`.");
838
968
  }
839
969
  query += `&start-after=${encodeURIComponent(options.startAfter)}`;
840
970
  }
@@ -846,7 +976,7 @@ var S3Client = class {
846
976
  void 0,
847
977
  void 0,
848
978
  void 0,
849
- options.bucket ?? this.#options.bucket,
979
+ ensureValidBucketName(options.bucket ?? this.#options.bucket),
850
980
  options.signal
851
981
  );
852
982
  if (response.statusCode !== 200) {
@@ -898,7 +1028,7 @@ var S3Client = class {
898
1028
  },
899
1029
  void 0,
900
1030
  void 0,
901
- this.#options.bucket,
1031
+ ensureValidBucketName(options.bucket ?? this.#options.bucket),
902
1032
  options.signal
903
1033
  );
904
1034
  if (response.statusCode === 200) {
@@ -1193,23 +1323,6 @@ function buildSearchParams(amzCredential, date, expiresIn, headerList, contentHa
1193
1323
  }
1194
1324
  return res;
1195
1325
  }
1196
- function ensureValidBucketName(name) {
1197
- if (name.length < 3 || name.length > 63) {
1198
- throw new Error("`name` must be between 3 and 63 characters long.");
1199
- }
1200
- if (name.startsWith(".") || name.endsWith(".")) {
1201
- throw new Error("`name` must not start or end with a period (.)");
1202
- }
1203
- if (!/^[a-z0-9.-]+$/.test(name)) {
1204
- throw new Error(
1205
- "`name` can only contain lowercase letters, numbers, periods (.), and hyphens (-)."
1206
- );
1207
- }
1208
- if (name.includes("..")) {
1209
- throw new Error("`name` must not contain two adjacent periods (..)");
1210
- }
1211
- return name;
1212
- }
1213
1326
  function ensureParsedXml(text) {
1214
1327
  try {
1215
1328
  const r = xmlParser2.parse(text);
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "lean-s3",
3
3
  "author": "Niklas Mollenhauer",
4
4
  "license": "MIT",
5
- "version": "0.5.0",
5
+ "version": "0.6.1",
6
6
  "description": "A server-side S3 API for the regular user.",
7
7
  "keywords": [
8
8
  "s3",
@@ -44,14 +44,15 @@
44
44
  },
45
45
  "dependencies": {
46
46
  "fast-xml-parser": "^5.2.5",
47
+ "i": "^0.3.7",
47
48
  "undici": "^7.10.0"
48
49
  },
49
50
  "devDependencies": {
50
- "@biomejs/biome": "^2.0.0",
51
+ "@biomejs/biome": "2.0.0",
51
52
  "@testcontainers/localstack": "^11.0.3",
52
53
  "@testcontainers/minio": "^11.0.3",
53
54
  "@types/node": "^24.0.3",
54
- "@typescript/native-preview": "^7.0.0-dev.20250619.1",
55
+ "@typescript/native-preview": "^7.0.0-dev.20250620.1",
55
56
  "expect": "^30.0.2",
56
57
  "lefthook": "^1.11.14",
57
58
  "tsup": "^8.5.0",