lean-s3 0.7.3 → 0.7.5

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
@@ -181,3 +181,13 @@ const client = new S3Client({
181
181
  ```
182
182
 
183
183
  Popular S3 provider missing? Open an issue or file a PR!
184
+
185
+ ## Tested On
186
+ To ensure compability across various providers and self-hosted services, all tests are run on:
187
+ - Amazon AWS S3
188
+ - Hetzner Object Storage
189
+ - Cloudflare R2
190
+ - Garage
191
+ - Minio
192
+ - LocalStack
193
+ - Ceph
package/dist/index.d.ts CHANGED
@@ -27,10 +27,13 @@ type Brand<B> = {
27
27
  type Branded<T, B> = T & Brand<B>;
28
28
  type BucketName = Branded<string, "BucketName">;
29
29
  type ObjectKey = Branded<string, "ObjectKey">;
30
+ type Endpoint = Branded<string, "Endpoint">;
31
+ type Region = Branded<string, "Region">;
30
32
 
31
- declare const write: unique symbol;
32
- declare const stream: unique symbol;
33
- declare const signedRequest: unique symbol;
33
+ declare const kWrite: unique symbol;
34
+ declare const kStream: unique symbol;
35
+ declare const kSignedRequest: unique symbol;
36
+ declare const kGetEffectiveParams: unique symbol;
34
37
  interface S3ClientOptions {
35
38
  bucket: string;
36
39
  region: string;
@@ -223,6 +226,8 @@ type ListObjectsResult = {
223
226
  contents: readonly S3BucketEntry[];
224
227
  };
225
228
  type BucketCreationOptions = {
229
+ endpoint?: string;
230
+ region?: string;
226
231
  locationConstraint?: string;
227
232
  location?: BucketLocationInfo;
228
233
  info?: BucketInfo;
@@ -287,6 +292,8 @@ declare class S3Client {
287
292
  * @param options The default options to use for the S3 client.
288
293
  */
289
294
  constructor(options: S3ClientOptions);
295
+ /** @internal */
296
+ [kGetEffectiveParams](options: OverridableS3ClientOptions): [region: Region, endpoint: Endpoint, bucket: BucketName];
290
297
  /**
291
298
  * Creates an S3File instance for the given path.
292
299
  *
@@ -346,7 +353,7 @@ declare class S3Client {
346
353
  * });
347
354
  * ```
348
355
  */
349
- presign(path: string, optio2ns?: S3FilePresignOptions): string;
356
+ presign(path: string, options?: S3FilePresignOptions): string;
350
357
  createMultipartUpload(key: string, options?: CreateMultipartUploadOptions): Promise<CreateMultipartUploadResult>;
351
358
  /**
352
359
  * @remarks Uses [`ListMultipartUploads`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListMultipartUploads.html).
@@ -441,16 +448,16 @@ declare class S3Client {
441
448
  * TODO: Maybe move this into a separate free function?
442
449
  * @internal
443
450
  */
444
- [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>>;
451
+ [kSignedRequest](region: Region, endpoint: Endpoint, bucket: BucketName, method: HttpMethod, pathWithoutBucket: ObjectKey, query: string | undefined, body: UndiciBodyInit | undefined, additionalSignedHeaders: Record<string, string> | undefined, additionalUnsignedHeaders: Record<string, string> | undefined, contentHash: Buffer | undefined, signal: AbortSignal | undefined): Promise<Dispatcher.ResponseData<null>>;
445
452
  /**
446
453
  * @internal
447
454
  * @param {import("./index.d.ts").UndiciBodyInit} data TODO
448
455
  */
449
- [write](path: string, data: UndiciBodyInit, contentType: string, contentLength: number | undefined, contentHash: Buffer | undefined, rageStart: number | undefined, rangeEndExclusive: number | undefined, signal?: AbortSignal | undefined): Promise<void>;
456
+ [kWrite](path: ObjectKey, data: UndiciBodyInit, contentType: string, contentLength: number | undefined, contentHash: Buffer | undefined, rageStart: number | undefined, rangeEndExclusive: number | undefined, signal?: AbortSignal | undefined): Promise<void>;
450
457
  /**
451
458
  * @internal
452
459
  */
453
- [stream](path: string, contentHash: Buffer | undefined, rageStart: number | undefined, rangeEndExclusive: number | undefined): stream_web.ReadableStream<Uint8Array<ArrayBufferLike>>;
460
+ [kStream](path: ObjectKey, contentHash: Buffer | undefined, rageStart: number | undefined, rangeEndExclusive: number | undefined): stream_web.ReadableStream<Uint8Array<ArrayBufferLike>>;
454
461
  }
455
462
 
456
463
  declare class S3Stat {
@@ -470,7 +477,19 @@ declare class S3File {
470
477
  #private;
471
478
  /** @internal */
472
479
  constructor(client: S3Client, path: ObjectKey, start: number | undefined, end: number | undefined, contentType: string | undefined);
473
- slice(start?: number | undefined, end?: number | undefined, contentType?: string | undefined): S3File;
480
+ /**
481
+ * Creates and returns a new {@link S3File} containing a subset of this {@link S3File} data.
482
+ * @param start The starting index.
483
+ * @param end The ending index, exclusive.
484
+ */
485
+ slice(start?: number, end?: number): S3File;
486
+ /**
487
+ * Creates and returns a new {@link S3File} containing a subset of this {@link S3File} data.
488
+ * @param start The starting index.
489
+ * @param end The ending index, exclusive.
490
+ * @param contentType The content-type for the new {@link S3File}.
491
+ */
492
+ slice(start?: number, end?: number, contentType?: string): S3File;
474
493
  /**
475
494
  * Get the stat of a file in the bucket. Uses `HEAD` request to check existence.
476
495
  *
package/dist/index.js CHANGED
@@ -319,32 +319,75 @@ function getAuthorizationHeader(keyCache, method, path, query, date, sortedSigne
319
319
  }
320
320
 
321
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.");
322
+ function ensureValidBucketName(bucket) {
323
+ if (typeof bucket !== "string") {
324
+ throw new TypeError("`bucket` is required and must be a `string`.");
325
325
  }
326
- if (name.startsWith(".") || name.endsWith(".")) {
327
- throw new Error("`name` must not start or end with a period (.)");
326
+ if (bucket.length < 3 || bucket.length > 63) {
327
+ throw new Error("`bucket` must be between 3 and 63 characters long.");
328
328
  }
329
- if (!/^[a-z0-9.-]+$/.test(name)) {
329
+ if (bucket.startsWith(".") || bucket.endsWith(".")) {
330
+ throw new Error("`bucket` must not start or end with a period (.)");
331
+ }
332
+ if (!/^[a-z0-9.-]+$/.test(bucket)) {
330
333
  throw new Error(
331
- "`name` can only contain lowercase letters, numbers, periods (.), and hyphens (-)."
334
+ "`bucket` can only contain lowercase letters, numbers, periods (.), and hyphens (-)."
332
335
  );
333
336
  }
334
- if (name.includes("..")) {
335
- throw new Error("`name` must not contain two adjacent periods (..)");
337
+ if (bucket.includes("..")) {
338
+ throw new Error("`bucket` must not contain two adjacent periods (..)");
339
+ }
340
+ return bucket;
341
+ }
342
+ function ensureValidAccessKeyId(accessKeyId) {
343
+ if (typeof accessKeyId !== "string") {
344
+ throw new TypeError("`AccessKeyId` is required and must be a `string`.");
345
+ }
346
+ if (accessKeyId.length < 1) {
347
+ throw new RangeError("`AccessKeyId` must be at least 1 character long.");
348
+ }
349
+ return accessKeyId;
350
+ }
351
+ function ensureValidSecretAccessKey(secretAccessKey) {
352
+ if (typeof secretAccessKey !== "string") {
353
+ throw new TypeError(
354
+ "`SecretAccessKey` is required and must be a `string`."
355
+ );
356
+ }
357
+ if (secretAccessKey.length < 1) {
358
+ throw new RangeError(
359
+ "`SecretAccessKey` must be at least 1 character long."
360
+ );
336
361
  }
337
- return name;
362
+ return secretAccessKey;
338
363
  }
339
364
  function ensureValidPath(path) {
340
365
  if (typeof path !== "string") {
341
- throw new TypeError("`path` must be a `string`.");
366
+ throw new TypeError("`path` is required and must be a `string`.");
342
367
  }
343
368
  if (path.length < 1) {
344
369
  throw new RangeError("`path` must be at least 1 character long.");
345
370
  }
346
371
  return path;
347
372
  }
373
+ function ensureValidEndpoint(endpoint) {
374
+ if (typeof endpoint !== "string") {
375
+ throw new TypeError("`endpoint` is required and must be a `string`.");
376
+ }
377
+ if (endpoint.length < 1) {
378
+ throw new RangeError("`endpoint` must be at least 1 character long.");
379
+ }
380
+ return endpoint;
381
+ }
382
+ function ensureValidRegion(region) {
383
+ if (typeof region !== "string") {
384
+ throw new TypeError("`region` is required and must be a `string`.");
385
+ }
386
+ if (region.length < 1) {
387
+ throw new RangeError("`region` must be at least 1 character long.");
388
+ }
389
+ return region;
390
+ }
348
391
 
349
392
  // src/assertNever.ts
350
393
  function assertNever(v) {
@@ -373,9 +416,10 @@ function encodeURIComponentExtended(value) {
373
416
  }
374
417
 
375
418
  // src/S3Client.ts
376
- var write = Symbol("write");
377
- var stream = Symbol("stream");
378
- var signedRequest = Symbol("signedRequest");
419
+ var kWrite = Symbol("kWrite");
420
+ var kStream = Symbol("kStream");
421
+ var kSignedRequest = Symbol("kSignedRequest");
422
+ var kGetEffectiveParams = Symbol("kGetEffectiveParams");
379
423
  var xmlParser2 = new XMLParser2({
380
424
  ignoreAttributes: true,
381
425
  isArray: (_, jPath) => jPath === "ListMultipartUploadsResult.Upload" || jPath === "ListBucketResult.Contents" || jPath === "ListPartsResult.Part" || jPath === "DeleteResult.Deleted" || jPath === "DeleteResult.Error"
@@ -387,7 +431,7 @@ var xmlBuilder = new XMLBuilder({
387
431
  var S3Client = class {
388
432
  #options;
389
433
  #keyCache = new KeyCache();
390
- // TODO: pass options to this in client? Do we want to expose tjhe internal use of undici?
434
+ // TODO: pass options to this in client? Do we want to expose the internal use of undici?
391
435
  #dispatcher = new Agent();
392
436
  /**
393
437
  * 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.
@@ -398,52 +442,23 @@ var S3Client = class {
398
442
  if (!options) {
399
443
  throw new Error("`options` is required.");
400
444
  }
401
- const {
402
- accessKeyId,
403
- secretAccessKey,
404
- endpoint,
405
- region,
406
- bucket,
407
- sessionToken
408
- } = options;
409
- if (!accessKeyId || typeof accessKeyId !== "string") {
410
- throw new Error("`accessKeyId` is required.");
411
- }
412
- if (!secretAccessKey || typeof secretAccessKey !== "string") {
413
- throw new Error("`secretAccessKey` is required.");
414
- }
415
- if (!endpoint || typeof endpoint !== "string") {
416
- throw new Error("`endpoint` is required.");
417
- }
418
- if (!region || typeof region !== "string") {
419
- throw new Error("`region` is required.");
420
- }
421
- if (!bucket || typeof bucket !== "string") {
422
- throw new Error("`bucket` is required.");
423
- }
424
445
  this.#options = {
425
- accessKeyId,
426
- secretAccessKey,
427
- endpoint,
428
- region,
429
- bucket,
430
- sessionToken
446
+ accessKeyId: ensureValidAccessKeyId(options.accessKeyId),
447
+ secretAccessKey: ensureValidSecretAccessKey(options.secretAccessKey),
448
+ endpoint: ensureValidEndpoint(options.endpoint),
449
+ region: ensureValidRegion(options.region),
450
+ bucket: ensureValidBucketName(options.bucket),
451
+ sessionToken: options.sessionToken
431
452
  };
432
453
  }
433
- // Maybe future API
434
- /*
435
- cors = {
436
- get: () => {
437
- // TODO: GetBucketCors
438
- },
439
- set: () => {
440
- // TODO: PutBucketCors
441
- },
442
- delete: () => {
443
- // TODO: DeleteBucketCors
444
- },
445
- };
446
- */
454
+ /** @internal */
455
+ [kGetEffectiveParams](options) {
456
+ return [
457
+ options.region ? ensureValidRegion(options.region) : this.#options.region,
458
+ options.endpoint ? ensureValidEndpoint(options.endpoint) : this.#options.endpoint,
459
+ options.bucket ? ensureValidBucketName(options.bucket) : this.#options.bucket
460
+ ];
461
+ }
447
462
  /**
448
463
  * Creates an S3File instance for the given path.
449
464
  *
@@ -511,33 +526,36 @@ var S3Client = class {
511
526
  * });
512
527
  * ```
513
528
  */
514
- presign(path, optio2ns = {}) {
515
- const contentLength = optio2ns.contentLength ?? void 0;
529
+ presign(path, options = {}) {
530
+ const contentLength = options.contentLength ?? void 0;
516
531
  if (typeof contentLength === "number") {
517
532
  if (contentLength < 0) {
518
533
  throw new RangeError("`contentLength` must be >= 0.");
519
534
  }
520
535
  }
521
- const method = optio2ns.method ?? "GET";
522
- const contentType = optio2ns.type ?? void 0;
523
- const region = optio2ns.region ?? this.#options.region;
524
- const bucket = optio2ns.bucket ?? this.#options.bucket;
525
- const endpoint = optio2ns.endpoint ?? this.#options.endpoint;
526
- const responseOptions = optio2ns.response;
536
+ const method = options.method ?? "GET";
537
+ const contentType = options.type ?? void 0;
538
+ const [region, endpoint, bucket] = this[kGetEffectiveParams](options);
539
+ const responseOptions = options.response;
527
540
  const contentDisposition = responseOptions?.contentDisposition;
528
541
  const responseContentDisposition = contentDisposition ? getContentDispositionHeader(contentDisposition) : void 0;
529
- const res = buildRequestUrl(endpoint, bucket, region, path);
542
+ const res = buildRequestUrl(
543
+ endpoint,
544
+ bucket,
545
+ region,
546
+ ensureValidPath(path)
547
+ );
530
548
  const now2 = /* @__PURE__ */ new Date();
531
549
  const date = getAmzDate(now2);
532
550
  const query = buildSearchParams(
533
551
  `${this.#options.accessKeyId}/${date.date}/${region}/s3/aws4_request`,
534
552
  date,
535
- optio2ns.expiresIn ?? 3600,
553
+ options.expiresIn ?? 3600,
536
554
  typeof contentLength === "number" || typeof contentType === "string" ? typeof contentLength === "number" && typeof contentType === "string" ? "content-length;content-type;host" : typeof contentLength === "number" ? "content-length;host" : typeof contentType === "string" ? "content-type;host" : "" : "host",
537
555
  unsignedPayload,
538
- optio2ns.storageClass,
556
+ options.storageClass,
539
557
  this.#options.sessionToken,
540
- optio2ns.acl,
558
+ options.acl,
541
559
  responseContentDisposition
542
560
  );
543
561
  const dataDigest = typeof contentLength === "number" || typeof contentType === "string" ? createCanonicalDataDigest(
@@ -573,9 +591,10 @@ var S3Client = class {
573
591
  }
574
592
  //#region multipart uploads
575
593
  async createMultipartUpload(key, options = {}) {
576
- if (key.length < 1) {
577
- }
578
- const response = await this[signedRequest](
594
+ const response = await this[kSignedRequest](
595
+ this.#options.region,
596
+ this.#options.endpoint,
597
+ options.bucket ? ensureValidBucketName(options.bucket) : this.#options.bucket,
579
598
  "POST",
580
599
  ensureValidPath(key),
581
600
  "uploads=",
@@ -583,7 +602,6 @@ var S3Client = class {
583
602
  void 0,
584
603
  void 0,
585
604
  void 0,
586
- ensureValidBucketName(options.bucket ?? this.#options.bucket),
587
605
  options.signal
588
606
  );
589
607
  if (response.statusCode !== 200) {
@@ -602,9 +620,6 @@ var S3Client = class {
602
620
  * @throws {RangeError} If `options.maxKeys` is not between `1` and `1000`.
603
621
  */
604
622
  async listMultipartUploads(options = {}) {
605
- const bucket = ensureValidBucketName(
606
- options.bucket ?? this.#options.bucket
607
- );
608
623
  let query = "uploads=";
609
624
  if (options.delimiter) {
610
625
  if (typeof options.delimiter !== "string") {
@@ -633,7 +648,10 @@ var S3Client = class {
633
648
  }
634
649
  query += `&prefix=${encodeURIComponent(options.prefix)}`;
635
650
  }
636
- const response = await this[signedRequest](
651
+ const response = await this[kSignedRequest](
652
+ this.#options.region,
653
+ this.#options.endpoint,
654
+ options.bucket ? ensureValidBucketName(options.bucket) : this.#options.bucket,
637
655
  "GET",
638
656
  "",
639
657
  query,
@@ -641,7 +659,6 @@ var S3Client = class {
641
659
  void 0,
642
660
  void 0,
643
661
  void 0,
644
- bucket,
645
662
  options.signal
646
663
  );
647
664
  if (response.statusCode !== 200) {
@@ -684,7 +701,10 @@ var S3Client = class {
684
701
  if (!uploadId) {
685
702
  throw new Error("`uploadId` is required.");
686
703
  }
687
- const response = await this[signedRequest](
704
+ const response = await this[kSignedRequest](
705
+ this.#options.region,
706
+ this.#options.endpoint,
707
+ options.bucket ? ensureValidBucketName(options.bucket) : this.#options.bucket,
688
708
  "DELETE",
689
709
  ensureValidPath(path),
690
710
  `uploadId=${encodeURIComponent(uploadId)}`,
@@ -692,10 +712,9 @@ var S3Client = class {
692
712
  void 0,
693
713
  void 0,
694
714
  void 0,
695
- ensureValidBucketName(options.bucket ?? this.#options.bucket),
696
715
  options.signal
697
716
  );
698
- if (response.statusCode !== 204) {
717
+ if (response.statusCode !== 204 && response.statusCode !== 200) {
699
718
  throw await getResponseError(response, path);
700
719
  }
701
720
  }
@@ -716,7 +735,10 @@ var S3Client = class {
716
735
  }))
717
736
  }
718
737
  });
719
- const response = await this[signedRequest](
738
+ const response = await this[kSignedRequest](
739
+ this.#options.region,
740
+ this.#options.endpoint,
741
+ options.bucket ? ensureValidBucketName(options.bucket) : this.#options.bucket,
720
742
  "POST",
721
743
  ensureValidPath(path),
722
744
  `uploadId=${encodeURIComponent(uploadId)}`,
@@ -724,7 +746,6 @@ var S3Client = class {
724
746
  void 0,
725
747
  void 0,
726
748
  void 0,
727
- ensureValidBucketName(options.bucket ?? this.#options.bucket),
728
749
  options.signal
729
750
  );
730
751
  if (response.statusCode !== 200) {
@@ -760,7 +781,10 @@ var S3Client = class {
760
781
  if (typeof partNumber !== "number" || partNumber <= 0) {
761
782
  throw new Error("`partNumber` has to be a `number` which is >= 1.");
762
783
  }
763
- const response = await this[signedRequest](
784
+ const response = await this[kSignedRequest](
785
+ this.#options.region,
786
+ this.#options.endpoint,
787
+ options.bucket ? ensureValidBucketName(options.bucket) : this.#options.bucket,
764
788
  "PUT",
765
789
  ensureValidPath(path),
766
790
  `partNumber=${partNumber}&uploadId=${encodeURIComponent(uploadId)}`,
@@ -768,7 +792,6 @@ var S3Client = class {
768
792
  void 0,
769
793
  void 0,
770
794
  void 0,
771
- ensureValidBucketName(options.bucket ?? this.#options.bucket),
772
795
  options.signal
773
796
  );
774
797
  if (response.statusCode === 200) {
@@ -812,7 +835,10 @@ var S3Client = class {
812
835
  query += `&part-number-marker=${encodeURIComponent(options.partNumberMarker)}`;
813
836
  }
814
837
  query += `&uploadId=${encodeURIComponent(uploadId)}`;
815
- const response = await this[signedRequest](
838
+ const response = await this[kSignedRequest](
839
+ this.#options.region,
840
+ this.#options.endpoint,
841
+ options.bucket ? ensureValidBucketName(options.bucket) : this.#options.bucket,
816
842
  "GET",
817
843
  ensureValidPath(path),
818
844
  // 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
@@ -821,7 +847,6 @@ var S3Client = class {
821
847
  void 0,
822
848
  void 0,
823
849
  void 0,
824
- ensureValidBucketName(options.bucket ?? this.#options.bucket),
825
850
  options?.signal
826
851
  );
827
852
  if (response.statusCode === 200) {
@@ -864,7 +889,7 @@ var S3Client = class {
864
889
  * @throws {S3Error} If the bucket could not be created, e.g. if it already exists.
865
890
  * @remarks Uses [`CreateBucket`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_CreateBucket.html)
866
891
  */
867
- async createBucket(name, options) {
892
+ async createBucket(name, options = {}) {
868
893
  let body;
869
894
  if (options) {
870
895
  const location = options.location && (options.location.name || options.location.type) ? {
@@ -885,7 +910,10 @@ var S3Client = class {
885
910
  }) : void 0;
886
911
  }
887
912
  const additionalSignedHeaders = body ? { "content-md5": md5Base64(body) } : void 0;
888
- const response = await this[signedRequest](
913
+ const response = await this[kSignedRequest](
914
+ options.region ? ensureValidRegion(options.region) : this.#options.region,
915
+ options.endpoint ? ensureValidEndpoint(options.endpoint) : this.#options.endpoint,
916
+ ensureValidBucketName(name),
889
917
  "PUT",
890
918
  "",
891
919
  void 0,
@@ -893,8 +921,7 @@ var S3Client = class {
893
921
  additionalSignedHeaders,
894
922
  void 0,
895
923
  void 0,
896
- ensureValidBucketName(name),
897
- options?.signal
924
+ options.signal
898
925
  );
899
926
  if (400 <= response.statusCode && response.statusCode < 500) {
900
927
  throw await getResponseError(response, "");
@@ -913,7 +940,10 @@ var S3Client = class {
913
940
  * @remarks Uses [`DeleteBucket`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteBucket.html).
914
941
  */
915
942
  async deleteBucket(name, options) {
916
- const response = await this[signedRequest](
943
+ const response = await this[kSignedRequest](
944
+ this.#options.region,
945
+ this.#options.endpoint,
946
+ ensureValidBucketName(name),
917
947
  "DELETE",
918
948
  "",
919
949
  void 0,
@@ -921,7 +951,6 @@ var S3Client = class {
921
951
  void 0,
922
952
  void 0,
923
953
  void 0,
924
- ensureValidBucketName(name),
925
954
  options?.signal
926
955
  );
927
956
  if (400 <= response.statusCode && response.statusCode < 500) {
@@ -940,7 +969,10 @@ var S3Client = class {
940
969
  * @remarks Uses [`HeadBucket`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadBucket.html).
941
970
  */
942
971
  async bucketExists(name, options) {
943
- const response = await this[signedRequest](
972
+ const response = await this[kSignedRequest](
973
+ this.#options.region,
974
+ this.#options.endpoint,
975
+ ensureValidBucketName(name),
944
976
  "HEAD",
945
977
  "",
946
978
  void 0,
@@ -948,7 +980,6 @@ var S3Client = class {
948
980
  void 0,
949
981
  void 0,
950
982
  void 0,
951
- ensureValidBucketName(name),
952
983
  options?.signal
953
984
  );
954
985
  if (response.statusCode !== 404 && 400 <= response.statusCode && response.statusCode < 500) {
@@ -979,7 +1010,10 @@ var S3Client = class {
979
1010
  }))
980
1011
  }
981
1012
  });
982
- const response = await this[signedRequest](
1013
+ const response = await this[kSignedRequest](
1014
+ this.#options.region,
1015
+ this.#options.endpoint,
1016
+ options.bucket ? ensureValidBucketName(options.bucket) : this.#options.bucket,
983
1017
  "PUT",
984
1018
  "",
985
1019
  "cors=",
@@ -990,7 +1024,6 @@ var S3Client = class {
990
1024
  },
991
1025
  void 0,
992
1026
  void 0,
993
- ensureValidBucketName(options.bucket ?? this.#options.bucket),
994
1027
  options.signal
995
1028
  );
996
1029
  if (response.statusCode === 200) {
@@ -1008,7 +1041,10 @@ var S3Client = class {
1008
1041
  * @remarks Uses [`GetBucketCors`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketCors.html).
1009
1042
  */
1010
1043
  async getBucketCors(options = {}) {
1011
- const response = await this[signedRequest](
1044
+ const response = await this[kSignedRequest](
1045
+ this.#options.region,
1046
+ this.#options.endpoint,
1047
+ options.bucket ? ensureValidBucketName(options.bucket) : this.#options.bucket,
1012
1048
  "GET",
1013
1049
  "",
1014
1050
  "cors=",
@@ -1017,7 +1053,6 @@ var S3Client = class {
1017
1053
  void 0,
1018
1054
  void 0,
1019
1055
  void 0,
1020
- ensureValidBucketName(options.bucket ?? this.#options.bucket),
1021
1056
  options.signal
1022
1057
  );
1023
1058
  if (response.statusCode !== 200) {
@@ -1030,7 +1065,10 @@ var S3Client = class {
1030
1065
  * @remarks Uses [`DeleteBucketCors`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteBucketCors.html).
1031
1066
  */
1032
1067
  async deleteBucketCors(options = {}) {
1033
- const response = await this[signedRequest](
1068
+ const response = await this[kSignedRequest](
1069
+ this.#options.region,
1070
+ this.#options.endpoint,
1071
+ options.bucket ? ensureValidBucketName(options.bucket) : this.#options.bucket,
1034
1072
  "DELETE",
1035
1073
  "",
1036
1074
  "cors=",
@@ -1039,7 +1077,6 @@ var S3Client = class {
1039
1077
  void 0,
1040
1078
  void 0,
1041
1079
  void 0,
1042
- ensureValidBucketName(options.bucket ?? this.#options.bucket),
1043
1080
  options.signal
1044
1081
  );
1045
1082
  if (response.statusCode !== 204) {
@@ -1109,7 +1146,10 @@ var S3Client = class {
1109
1146
  }
1110
1147
  query += `&start-after=${encodeURIComponent(options.startAfter)}`;
1111
1148
  }
1112
- const response = await this[signedRequest](
1149
+ const response = await this[kSignedRequest](
1150
+ ensureValidRegion(this.#options.region),
1151
+ ensureValidEndpoint(this.#options.endpoint),
1152
+ options.bucket ? ensureValidBucketName(options.bucket) : this.#options.bucket,
1113
1153
  "GET",
1114
1154
  "",
1115
1155
  query,
@@ -1117,7 +1157,6 @@ var S3Client = class {
1117
1157
  void 0,
1118
1158
  void 0,
1119
1159
  void 0,
1120
- ensureValidBucketName(options.bucket ?? this.#options.bucket),
1121
1160
  options.signal
1122
1161
  );
1123
1162
  if (response.statusCode !== 200) {
@@ -1158,7 +1197,10 @@ var S3Client = class {
1158
1197
  }))
1159
1198
  }
1160
1199
  });
1161
- const response = await this[signedRequest](
1200
+ const response = await this[kSignedRequest](
1201
+ this.#options.region,
1202
+ this.#options.endpoint,
1203
+ options.bucket ? ensureValidBucketName(options.bucket) : this.#options.bucket,
1162
1204
  "POST",
1163
1205
  "",
1164
1206
  "delete=",
@@ -1169,7 +1211,6 @@ var S3Client = class {
1169
1211
  },
1170
1212
  void 0,
1171
1213
  void 0,
1172
- ensureValidBucketName(options.bucket ?? this.#options.bucket),
1173
1214
  options.signal
1174
1215
  );
1175
1216
  if (response.statusCode === 200) {
@@ -1207,16 +1248,8 @@ var S3Client = class {
1207
1248
  * TODO: Maybe move this into a separate free function?
1208
1249
  * @internal
1209
1250
  */
1210
- async [signedRequest](method, pathWithoutBucket, query, body, additionalSignedHeaders, additionalUnsignedHeaders, contentHash, bucket, signal = void 0) {
1211
- const endpoint = this.#options.endpoint;
1212
- const region = this.#options.region;
1213
- const effectiveBucket = bucket ?? this.#options.bucket;
1214
- const url = buildRequestUrl(
1215
- endpoint,
1216
- effectiveBucket,
1217
- region,
1218
- pathWithoutBucket
1219
- );
1251
+ async [kSignedRequest](region, endpoint, bucket, method, pathWithoutBucket, query, body, additionalSignedHeaders, additionalUnsignedHeaders, contentHash, signal) {
1252
+ const url = buildRequestUrl(endpoint, bucket, region, pathWithoutBucket);
1220
1253
  if (query) {
1221
1254
  url.search = query;
1222
1255
  }
@@ -1264,7 +1297,7 @@ var S3Client = class {
1264
1297
  * @internal
1265
1298
  * @param {import("./index.d.ts").UndiciBodyInit} data TODO
1266
1299
  */
1267
- async [write](path, data, contentType, contentLength, contentHash, rageStart, rangeEndExclusive, signal = void 0) {
1300
+ async [kWrite](path, data, contentType, contentLength, contentHash, rageStart, rangeEndExclusive, signal = void 0) {
1268
1301
  const bucket = this.#options.bucket;
1269
1302
  const endpoint = this.#options.endpoint;
1270
1303
  const region = this.#options.region;
@@ -1320,7 +1353,7 @@ var S3Client = class {
1320
1353
  /**
1321
1354
  * @internal
1322
1355
  */
1323
- [stream](path, contentHash, rageStart, rangeEndExclusive) {
1356
+ [kStream](path, contentHash, rageStart, rangeEndExclusive) {
1324
1357
  const bucket = this.#options.bucket;
1325
1358
  const endpoint = this.#options.endpoint;
1326
1359
  const region = this.#options.region;
@@ -1505,7 +1538,12 @@ var S3File = class _S3File {
1505
1538
  this.#end = end;
1506
1539
  this.#contentType = contentType ?? "application/octet-stream";
1507
1540
  }
1508
- // TODO: slice overloads
1541
+ /**
1542
+ * Creates and returns a new {@link S3File} containing a subset of this {@link S3File} data.
1543
+ * @param start The starting index.
1544
+ * @param end The ending index, exclusive.
1545
+ * @param contentType The content-type for the new {@link S3File}.
1546
+ */
1509
1547
  slice(start, end, contentType) {
1510
1548
  return new _S3File(
1511
1549
  this.#client,
@@ -1523,7 +1561,11 @@ var S3File = class _S3File {
1523
1561
  * @throws {Error} If the server returns an invalid response.
1524
1562
  */
1525
1563
  async stat(options = {}) {
1526
- const response = await this.#client[signedRequest](
1564
+ const [region, endpoint, bucket] = this.#client[kGetEffectiveParams](options);
1565
+ const response = await this.#client[kSignedRequest](
1566
+ region,
1567
+ endpoint,
1568
+ bucket,
1527
1569
  "HEAD",
1528
1570
  this.#path,
1529
1571
  void 0,
@@ -1531,7 +1573,6 @@ var S3File = class _S3File {
1531
1573
  void 0,
1532
1574
  void 0,
1533
1575
  void 0,
1534
- void 0,
1535
1576
  options.signal
1536
1577
  );
1537
1578
  response.body.dump();
@@ -1554,7 +1595,11 @@ var S3File = class _S3File {
1554
1595
  * @remarks Uses [`HeadObject`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html).
1555
1596
  */
1556
1597
  async exists(options = {}) {
1557
- const response = await this.#client[signedRequest](
1598
+ const [region, endpoint, bucket] = this.#client[kGetEffectiveParams](options);
1599
+ const response = await this.#client[kSignedRequest](
1600
+ region,
1601
+ endpoint,
1602
+ bucket,
1558
1603
  "HEAD",
1559
1604
  this.#path,
1560
1605
  void 0,
@@ -1562,7 +1607,6 @@ var S3File = class _S3File {
1562
1607
  void 0,
1563
1608
  void 0,
1564
1609
  void 0,
1565
- void 0,
1566
1610
  options.signal
1567
1611
  );
1568
1612
  response.body.dump();
@@ -1599,7 +1643,11 @@ var S3File = class _S3File {
1599
1643
  * ```
1600
1644
  */
1601
1645
  async delete(options = {}) {
1602
- const response = await this.#client[signedRequest](
1646
+ const [region, endpoint, bucket] = this.#client[kGetEffectiveParams](options);
1647
+ const response = await this.#client[kSignedRequest](
1648
+ region,
1649
+ endpoint,
1650
+ bucket,
1603
1651
  "DELETE",
1604
1652
  this.#path,
1605
1653
  void 0,
@@ -1607,7 +1655,6 @@ var S3File = class _S3File {
1607
1655
  void 0,
1608
1656
  void 0,
1609
1657
  void 0,
1610
- void 0,
1611
1658
  options.signal
1612
1659
  );
1613
1660
  if (response.statusCode === 204) {
@@ -1638,7 +1685,7 @@ var S3File = class _S3File {
1638
1685
  }
1639
1686
  /** @returns {ReadableStream<Uint8Array>} */
1640
1687
  stream() {
1641
- return this.#client[stream](this.#path, void 0, this.#start, this.#end);
1688
+ return this.#client[kStream](this.#path, void 0, this.#start, this.#end);
1642
1689
  }
1643
1690
  async #transformData(data) {
1644
1691
  if (typeof data === "string") {
@@ -1681,7 +1728,7 @@ var S3File = class _S3File {
1681
1728
  async write(data, options = {}) {
1682
1729
  const signal = void 0;
1683
1730
  const [bytes, length, hash] = await this.#transformData(data);
1684
- return await this.#client[write](
1731
+ return await this.#client[kWrite](
1685
1732
  this.#path,
1686
1733
  bytes,
1687
1734
  options.type ?? this.#contentType,
@@ -1692,15 +1739,6 @@ var S3File = class _S3File {
1692
1739
  signal
1693
1740
  );
1694
1741
  }
1695
- /*
1696
- // Future API?
1697
- setTags(): Promise<void> {
1698
- throw new Error("Not implemented");
1699
- }
1700
- getTags(): Promise<unknown> {
1701
- throw new Error("Not implemented");
1702
- }
1703
- */
1704
1742
  };
1705
1743
  export {
1706
1744
  S3BucketEntry,
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.7.3",
5
+ "version": "0.7.5",
6
6
  "description": "A server-side S3 API for the regular user.",
7
7
  "keywords": [
8
8
  "s3",
@@ -34,8 +34,8 @@
34
34
  "type": "module",
35
35
  "scripts": {
36
36
  "build": "tsup",
37
- "test": "tsgo && tsx --test src/*.test.ts",
38
- "test:integration": "tsgo && tsx --test src/test.integration.ts",
37
+ "test": "tsgo && tsx --test src/*.test.ts src/test/*.test.ts",
38
+ "test:integration": "tsgo && tsx --test src/test/test.integration.ts",
39
39
  "ci": "biome ci ./src",
40
40
  "docs": "typedoc",
41
41
  "lint": "biome lint ./src",
@@ -47,13 +47,14 @@
47
47
  "undici": "^7.11.0"
48
48
  },
49
49
  "devDependencies": {
50
- "@biomejs/biome": "2.0.6",
51
- "@testcontainers/localstack": "^11.2.0",
52
- "@testcontainers/minio": "^11.2.0",
50
+ "@biomejs/biome": "2.1.1",
51
+ "@testcontainers/localstack": "^11.2.1",
52
+ "@testcontainers/minio": "^11.2.1",
53
53
  "@types/node": "^24.0.10",
54
- "@typescript/native-preview": "^7.0.0-dev.20250705.1",
54
+ "@typescript/native-preview": "^7.0.0-dev.20250708.1",
55
55
  "expect": "^30.0.4",
56
- "lefthook": "^1.11.16",
56
+ "lefthook": "^1.12.0",
57
+ "testcontainers": "^11.2.1",
57
58
  "tsup": "^8.5.0",
58
59
  "tsx": "^4.20.3",
59
60
  "typedoc": "^0.28.7"