lean-s3 0.7.4 → 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
@@ -226,6 +226,8 @@ type ListObjectsResult = {
226
226
  contents: readonly S3BucketEntry[];
227
227
  };
228
228
  type BucketCreationOptions = {
229
+ endpoint?: string;
230
+ region?: string;
229
231
  locationConstraint?: string;
230
232
  location?: BucketLocationInfo;
231
233
  info?: BucketInfo;
@@ -290,7 +292,8 @@ declare class S3Client {
290
292
  * @param options The default options to use for the S3 client.
291
293
  */
292
294
  constructor(options: S3ClientOptions);
293
- [kGetEffectiveParams](region: string | undefined | null, endpoint: string | undefined | null, bucket: string | undefined | null): [region: Region, endpoint: Endpoint, bucket: BucketName];
295
+ /** @internal */
296
+ [kGetEffectiveParams](options: OverridableS3ClientOptions): [region: Region, endpoint: Endpoint, bucket: BucketName];
294
297
  /**
295
298
  * Creates an S3File instance for the given path.
296
299
  *
@@ -445,7 +448,7 @@ declare class S3Client {
445
448
  * TODO: Maybe move this into a separate free function?
446
449
  * @internal
447
450
  */
448
- [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>>;
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>>;
449
452
  /**
450
453
  * @internal
451
454
  * @param {import("./index.d.ts").UndiciBodyInit} data TODO
package/dist/index.js CHANGED
@@ -319,29 +319,51 @@ function getAuthorizationHeader(keyCache, method, path, query, date, sortedSigne
319
319
  }
320
320
 
321
321
  // src/branded.ts
322
- function ensureValidBucketName(name) {
323
- if (typeof name !== "string") {
324
- throw new TypeError("`name` must be a `string`.");
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.length < 3 || name.length > 63) {
327
- throw new Error("`name` must be between 3 and 63 characters long.");
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 (name.startsWith(".") || name.endsWith(".")) {
330
- throw new Error("`name` must not start or end with a period (.)");
329
+ if (bucket.startsWith(".") || bucket.endsWith(".")) {
330
+ throw new Error("`bucket` must not start or end with a period (.)");
331
331
  }
332
- if (!/^[a-z0-9.-]+$/.test(name)) {
332
+ if (!/^[a-z0-9.-]+$/.test(bucket)) {
333
333
  throw new Error(
334
- "`name` can only contain lowercase letters, numbers, periods (.), and hyphens (-)."
334
+ "`bucket` can only contain lowercase letters, numbers, periods (.), and hyphens (-)."
335
335
  );
336
336
  }
337
- if (name.includes("..")) {
338
- 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
339
  }
340
- return name;
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
+ );
361
+ }
362
+ return secretAccessKey;
341
363
  }
342
364
  function ensureValidPath(path) {
343
365
  if (typeof path !== "string") {
344
- throw new TypeError("`path` must be a `string`.");
366
+ throw new TypeError("`path` is required and must be a `string`.");
345
367
  }
346
368
  if (path.length < 1) {
347
369
  throw new RangeError("`path` must be at least 1 character long.");
@@ -350,7 +372,7 @@ function ensureValidPath(path) {
350
372
  }
351
373
  function ensureValidEndpoint(endpoint) {
352
374
  if (typeof endpoint !== "string") {
353
- throw new TypeError("`endpoint` must be a `string`.");
375
+ throw new TypeError("`endpoint` is required and must be a `string`.");
354
376
  }
355
377
  if (endpoint.length < 1) {
356
378
  throw new RangeError("`endpoint` must be at least 1 character long.");
@@ -359,7 +381,7 @@ function ensureValidEndpoint(endpoint) {
359
381
  }
360
382
  function ensureValidRegion(region) {
361
383
  if (typeof region !== "string") {
362
- throw new TypeError("`region` must be a `string`.");
384
+ throw new TypeError("`region` is required and must be a `string`.");
363
385
  }
364
386
  if (region.length < 1) {
365
387
  throw new RangeError("`region` must be at least 1 character long.");
@@ -420,43 +442,21 @@ var S3Client = class {
420
442
  if (!options) {
421
443
  throw new Error("`options` is required.");
422
444
  }
423
- const {
424
- accessKeyId,
425
- secretAccessKey,
426
- endpoint,
427
- region,
428
- bucket,
429
- sessionToken
430
- } = options;
431
- if (!accessKeyId || typeof accessKeyId !== "string") {
432
- throw new Error("`accessKeyId` is required.");
433
- }
434
- if (!secretAccessKey || typeof secretAccessKey !== "string") {
435
- throw new Error("`secretAccessKey` is required.");
436
- }
437
- if (!endpoint || typeof endpoint !== "string") {
438
- throw new Error("`endpoint` is required.");
439
- }
440
- if (!region || typeof region !== "string") {
441
- throw new Error("`region` is required.");
442
- }
443
- if (!bucket || typeof bucket !== "string") {
444
- throw new Error("`bucket` is required.");
445
- }
446
445
  this.#options = {
447
- accessKeyId,
448
- secretAccessKey,
446
+ accessKeyId: ensureValidAccessKeyId(options.accessKeyId),
447
+ secretAccessKey: ensureValidSecretAccessKey(options.secretAccessKey),
449
448
  endpoint: ensureValidEndpoint(options.endpoint),
450
449
  region: ensureValidRegion(options.region),
451
450
  bucket: ensureValidBucketName(options.bucket),
452
- sessionToken
451
+ sessionToken: options.sessionToken
453
452
  };
454
453
  }
455
- [kGetEffectiveParams](region, endpoint, bucket) {
454
+ /** @internal */
455
+ [kGetEffectiveParams](options) {
456
456
  return [
457
- ensureValidRegion(region ?? this.#options.region),
458
- ensureValidEndpoint(endpoint ?? this.#options.endpoint),
459
- ensureValidBucketName(bucket ?? this.#options.bucket)
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
460
  ];
461
461
  }
462
462
  /**
@@ -535,11 +535,7 @@ var S3Client = class {
535
535
  }
536
536
  const method = options.method ?? "GET";
537
537
  const contentType = options.type ?? void 0;
538
- const [region, endpoint, bucket] = this[kGetEffectiveParams](
539
- options.region,
540
- options.endpoint,
541
- options.bucket
542
- );
538
+ const [region, endpoint, bucket] = this[kGetEffectiveParams](options);
543
539
  const responseOptions = options.response;
544
540
  const contentDisposition = responseOptions?.contentDisposition;
545
541
  const responseContentDisposition = contentDisposition ? getContentDispositionHeader(contentDisposition) : void 0;
@@ -595,8 +591,6 @@ var S3Client = class {
595
591
  }
596
592
  //#region multipart uploads
597
593
  async createMultipartUpload(key, options = {}) {
598
- if (key.length < 1) {
599
- }
600
594
  const response = await this[kSignedRequest](
601
595
  this.#options.region,
602
596
  this.#options.endpoint,
@@ -895,7 +889,7 @@ var S3Client = class {
895
889
  * @throws {S3Error} If the bucket could not be created, e.g. if it already exists.
896
890
  * @remarks Uses [`CreateBucket`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_CreateBucket.html)
897
891
  */
898
- async createBucket(name, options) {
892
+ async createBucket(name, options = {}) {
899
893
  let body;
900
894
  if (options) {
901
895
  const location = options.location && (options.location.name || options.location.type) ? {
@@ -917,8 +911,8 @@ var S3Client = class {
917
911
  }
918
912
  const additionalSignedHeaders = body ? { "content-md5": md5Base64(body) } : void 0;
919
913
  const response = await this[kSignedRequest](
920
- this.#options.region,
921
- this.#options.endpoint,
914
+ options.region ? ensureValidRegion(options.region) : this.#options.region,
915
+ options.endpoint ? ensureValidEndpoint(options.endpoint) : this.#options.endpoint,
922
916
  ensureValidBucketName(name),
923
917
  "PUT",
924
918
  "",
@@ -927,7 +921,7 @@ var S3Client = class {
927
921
  additionalSignedHeaders,
928
922
  void 0,
929
923
  void 0,
930
- options?.signal
924
+ options.signal
931
925
  );
932
926
  if (400 <= response.statusCode && response.statusCode < 500) {
933
927
  throw await getResponseError(response, "");
@@ -1254,7 +1248,7 @@ var S3Client = class {
1254
1248
  * TODO: Maybe move this into a separate free function?
1255
1249
  * @internal
1256
1250
  */
1257
- async [kSignedRequest](region, endpoint, bucket, method, pathWithoutBucket, query, body, additionalSignedHeaders, additionalUnsignedHeaders, contentHash, signal = void 0) {
1251
+ async [kSignedRequest](region, endpoint, bucket, method, pathWithoutBucket, query, body, additionalSignedHeaders, additionalUnsignedHeaders, contentHash, signal) {
1258
1252
  const url = buildRequestUrl(endpoint, bucket, region, pathWithoutBucket);
1259
1253
  if (query) {
1260
1254
  url.search = query;
@@ -1567,11 +1561,7 @@ var S3File = class _S3File {
1567
1561
  * @throws {Error} If the server returns an invalid response.
1568
1562
  */
1569
1563
  async stat(options = {}) {
1570
- const [region, endpoint, bucket] = this.#client[kGetEffectiveParams](
1571
- options.region,
1572
- options.endpoint,
1573
- options.bucket
1574
- );
1564
+ const [region, endpoint, bucket] = this.#client[kGetEffectiveParams](options);
1575
1565
  const response = await this.#client[kSignedRequest](
1576
1566
  region,
1577
1567
  endpoint,
@@ -1605,11 +1595,7 @@ var S3File = class _S3File {
1605
1595
  * @remarks Uses [`HeadObject`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html).
1606
1596
  */
1607
1597
  async exists(options = {}) {
1608
- const [region, endpoint, bucket] = this.#client[kGetEffectiveParams](
1609
- options.region,
1610
- options.endpoint,
1611
- options.bucket
1612
- );
1598
+ const [region, endpoint, bucket] = this.#client[kGetEffectiveParams](options);
1613
1599
  const response = await this.#client[kSignedRequest](
1614
1600
  region,
1615
1601
  endpoint,
@@ -1657,11 +1643,7 @@ var S3File = class _S3File {
1657
1643
  * ```
1658
1644
  */
1659
1645
  async delete(options = {}) {
1660
- const [region, endpoint, bucket] = this.#client[kGetEffectiveParams](
1661
- options.region,
1662
- options.endpoint,
1663
- options.bucket
1664
- );
1646
+ const [region, endpoint, bucket] = this.#client[kGetEffectiveParams](options);
1665
1647
  const response = await this.#client[kSignedRequest](
1666
1648
  region,
1667
1649
  endpoint,
@@ -1757,15 +1739,6 @@ var S3File = class _S3File {
1757
1739
  signal
1758
1740
  );
1759
1741
  }
1760
- /*
1761
- // Future API?
1762
- setTags(): Promise<void> {
1763
- throw new Error("Not implemented");
1764
- }
1765
- getTags(): Promise<unknown> {
1766
- throw new Error("Not implemented");
1767
- }
1768
- */
1769
1742
  };
1770
1743
  export {
1771
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.4",
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",