lean-s3 0.6.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -141,6 +141,9 @@ See [DESIGN_DECISIONS.md](./DESIGN_DECISIONS.md) to read about why this library
141
141
  - ✅ [`UploadPart`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_UploadPart.html) via `.uploadPart`
142
142
  - ✅ [`CompleteMultipartUpload`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_CompleteMultipartUpload.html) via `.completeMultipartUpload`
143
143
  - ✅ [`AbortMultipartUpload`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_AbortMultipartUpload.html) via `.abortMultipartUpload`
144
+ - ✅ [`PutBucketCors`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketCors.html) via `.putBucketCors`
145
+ - ✅ [`GetBucketCors`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketCors.html) via `.getBucketCors`
146
+ - ✅ [`DeleteBucketCors`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteBucketCors.html) via `.deleteBucketCors`
144
147
 
145
148
  ## Example Configurations
146
149
  ### Hetzner Object Storage
package/dist/index.d.ts CHANGED
@@ -39,8 +39,11 @@ interface S3ClientOptions {
39
39
  secretAccessKey: string;
40
40
  sessionToken?: string;
41
41
  }
42
- type OverridableS3ClientOptions = Pick<S3ClientOptions, "region" | "bucket" | "endpoint">;
43
- type CreateFileInstanceOptions = {};
42
+ type OverridableS3ClientOptions = Partial<Pick<S3ClientOptions, "region" | "bucket" | "endpoint">>;
43
+ type CreateFileInstanceOptions = {
44
+ /** Content-Type of the file. */
45
+ type?: string;
46
+ };
44
47
  type DeleteObjectsOptions = {
45
48
  bucket?: string;
46
49
  signal?: AbortSignal;
@@ -55,13 +58,15 @@ type DeleteObjectsError = {
55
58
  versionId: string;
56
59
  };
57
60
  interface S3FilePresignOptions {
58
- contentHash: Buffer;
61
+ contentHash?: Buffer;
59
62
  /** Seconds. */
60
- expiresIn: number;
61
- method: PresignableHttpMethod;
62
- contentLength: number;
63
- storageClass: StorageClass;
64
- acl: Acl;
63
+ expiresIn?: number;
64
+ method?: PresignableHttpMethod;
65
+ contentLength?: number;
66
+ storageClass?: StorageClass;
67
+ acl?: Acl;
68
+ /** `Content-Type` of the file. */
69
+ type?: string;
65
70
  }
66
71
  type ListObjectsOptions = {
67
72
  bucket?: string;
@@ -208,6 +213,35 @@ type BucketDeletionOptions = {
208
213
  type BucketExistsOptions = {
209
214
  signal?: AbortSignal;
210
215
  };
216
+ type BucketCorsRules = readonly BucketCorsRule[];
217
+ type BucketCorsRule = {
218
+ allowedMethods: readonly HttpMethod[];
219
+ /** One or more origins you want customers to be able to access the bucket from. */
220
+ allowedOrigins: readonly string[];
221
+ /** Headers that are specified in the `Access-Control-Request-Headers` header. These headers are allowed in a preflight `OPTIONS` request. */
222
+ allowedHeaders?: readonly string[];
223
+ /** One or more headers in the response that you want customers to be able to access from their applications. */
224
+ exposeHeaders?: readonly string[];
225
+ /** Unique identifier for the rule. The value cannot be longer than 255 characters. */
226
+ id?: string;
227
+ /** The time in seconds that your browser is to cache the preflight response for the specified resource. */
228
+ maxAgeSeconds?: number;
229
+ };
230
+ type PutBucketCorsOptions = {
231
+ bucket?: string;
232
+ signal?: AbortSignal;
233
+ };
234
+ type DeleteBucketCorsOptions = {
235
+ bucket?: string;
236
+ signal?: AbortSignal;
237
+ };
238
+ type GetBucketCorsOptions = {
239
+ bucket?: string;
240
+ signal?: AbortSignal;
241
+ };
242
+ type GetBucketCorsResult = {
243
+ rules: BucketCorsRule[];
244
+ };
211
245
  /**
212
246
  * A configured S3 bucket instance for managing files.
213
247
  *
@@ -255,7 +289,6 @@ declare class S3Client {
255
289
  *
256
290
  * lean-s3 does not enforce these restrictions.
257
291
  *
258
- * @param {Partial<CreateFileInstanceOptions>} [_options] TODO
259
292
  * @example
260
293
  * ```js
261
294
  * const file = client.file("image.jpg");
@@ -263,11 +296,10 @@ declare class S3Client {
263
296
  *
264
297
  * const configFile = client.file("config.json", {
265
298
  * type: "application/json",
266
- * acl: "private"
267
299
  * });
268
300
  * ```
269
301
  */
270
- file(path: string, _options?: Partial<CreateFileInstanceOptions>): S3File;
302
+ file(path: string, options?: CreateFileInstanceOptions): S3File;
271
303
  /**
272
304
  * Generate a presigned URL for temporary access to a file.
273
305
  * Useful for generating upload/download URLs without exposing credentials.
@@ -281,7 +313,7 @@ declare class S3Client {
281
313
  * ```
282
314
  */
283
315
  presign(path: string, { method, expiresIn, // TODO: Maybe rename this to expiresInSeconds
284
- storageClass, contentLength, acl, region: regionOverride, bucket: bucketOverride, endpoint: endpointOverride, }?: Partial<S3FilePresignOptions & OverridableS3ClientOptions>): string;
316
+ storageClass, contentLength, type, acl, region: regionOverride, bucket: bucketOverride, endpoint: endpointOverride, }?: S3FilePresignOptions & OverridableS3ClientOptions): string;
285
317
  createMultipartUpload(key: string, options?: CreateMultipartUploadOptions): Promise<CreateMultipartUploadResult>;
286
318
  /**
287
319
  * @remarks Uses [`ListMultipartUploads`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListMultipartUploads.html).
@@ -345,6 +377,18 @@ declare class S3Client {
345
377
  * @remarks Uses [`HeadBucket`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadBucket.html).
346
378
  */
347
379
  bucketExists(name: string, options?: BucketExistsOptions): Promise<boolean>;
380
+ /**
381
+ * @remarks Uses [`PutBucketCors`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketCors.html).
382
+ */
383
+ putBucketCors(rules: BucketCorsRules, options?: PutBucketCorsOptions): Promise<void>;
384
+ /**
385
+ * @remarks Uses [`GetBucketCors`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketCors.html).
386
+ */
387
+ getBucketCors(options?: GetBucketCorsOptions): Promise<GetBucketCorsResult>;
388
+ /**
389
+ * @remarks Uses [`DeleteBucketCors`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteBucketCors.html).
390
+ */
391
+ deleteBucketCors(options?: DeleteBucketCorsOptions): Promise<void>;
348
392
  /**
349
393
  * Uses [`ListObjectsV2`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html) to iterate over all keys. Pagination and continuation is handled internally.
350
394
  */
@@ -385,6 +429,10 @@ declare class S3Stat {
385
429
  static tryParseFromHeaders(headers: Record<string, string | string[] | undefined>): S3Stat | undefined;
386
430
  }
387
431
 
432
+ type S3FileWriteOptions = {
433
+ /** Content-Type of the file. */
434
+ type?: string;
435
+ };
388
436
  declare class S3File {
389
437
  #private;
390
438
  /** @internal */
@@ -397,20 +445,20 @@ declare class S3File {
397
445
  * @throws {S3Error} If the file does not exist or the server has some other issues.
398
446
  * @throws {Error} If the server returns an invalid response.
399
447
  */
400
- stat(options?: Partial<S3StatOptions>): Promise<S3Stat>;
448
+ stat(options?: S3StatOptions): Promise<S3Stat>;
401
449
  /**
402
450
  * Check if a file exists in the bucket. Uses `HEAD` request to check existence.
403
451
  *
404
452
  * @remarks Uses [`HeadObject`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html).
405
453
  */
406
- exists(options?: Partial<S3FileExistsOptions>): Promise<boolean>;
454
+ exists(options?: S3FileExistsOptions): Promise<boolean>;
407
455
  /**
408
456
  * Delete a file from the bucket.
409
457
  *
410
458
  * @remarks - Uses [`DeleteObject`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObject.html).
411
459
  * - `versionId` not supported.
412
460
  *
413
- * @param {Partial<S3FileDeleteOptions>} [options]
461
+ * @param {S3FileDeleteOptions} [options]
414
462
  *
415
463
  * @example
416
464
  * ```js
@@ -426,7 +474,7 @@ declare class S3File {
426
474
  * }
427
475
  * ```
428
476
  */
429
- delete(options?: Partial<S3FileDeleteOptions>): Promise<void>;
477
+ delete(options?: S3FileDeleteOptions): Promise<void>;
430
478
  toString(): string;
431
479
  json(): Promise<unknown>;
432
480
  bytes(): Promise<Uint8Array>;
@@ -437,18 +485,19 @@ declare class S3File {
437
485
  stream(): ReadableStream<Uint8Array>;
438
486
  /**
439
487
  * @param {ByteSource} data
488
+ * @param {S3FileWriteOptions} [options.type] Defaults to the Content-Type that was used to create the {@link S3File} instance.
440
489
  * @returns {Promise<void>}
441
490
  */
442
- write(data: ByteSource): Promise<void>;
491
+ write(data: ByteSource, options?: S3FileWriteOptions): Promise<void>;
443
492
  }
444
493
  interface S3FileDeleteOptions extends OverridableS3ClientOptions {
445
- signal: AbortSignal;
494
+ signal?: AbortSignal;
446
495
  }
447
496
  interface S3StatOptions extends OverridableS3ClientOptions {
448
- signal: AbortSignal;
497
+ signal?: AbortSignal;
449
498
  }
450
499
  interface S3FileExistsOptions extends OverridableS3ClientOptions {
451
- signal: AbortSignal;
500
+ signal?: AbortSignal;
452
501
  }
453
502
 
454
503
  declare class S3Error extends Error {
@@ -490,4 +539,4 @@ type BucketInfo = {
490
539
  type?: string;
491
540
  };
492
541
 
493
- 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 DeleteObjectsError, type DeleteObjectsOptions, type DeleteObjectsResult, 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 };
542
+ export { type AbortMultipartUploadOptions, type Acl, type BucketCorsRule, type BucketCorsRules, 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 DeleteBucketCorsOptions, type DeleteObjectsError, type DeleteObjectsOptions, type DeleteObjectsResult, type GetBucketCorsOptions, type GetBucketCorsResult, type HttpMethod, type ListMultipartUploadsOptions, type ListMultipartUploadsResult, type ListObjectsIteratingOptions, type ListObjectsOptions, type ListObjectsResult, type ListPartsOptions, type ListPartsResult, type MultipartUpload, type MultipartUploadPart, type OverridableS3ClientOptions, type PresignableHttpMethod, type PutBucketCorsOptions, S3BucketEntry, S3Client, type S3ClientOptions, S3Error, type S3ErrorOptions, S3File, type S3FileDeleteOptions, type S3FileExistsOptions, type S3FilePresignOptions, type S3FileWriteOptions, S3Stat, type S3StatOptions, type StorageClass, type UndiciBodyInit, type UploadPartOptions, type UploadPartResult };
package/dist/index.js CHANGED
@@ -404,6 +404,20 @@ var S3Client = class {
404
404
  sessionToken
405
405
  };
406
406
  }
407
+ // Maybe future API
408
+ /*
409
+ cors = {
410
+ get: () => {
411
+ // TODO: GetBucketCors
412
+ },
413
+ set: () => {
414
+ // TODO: PutBucketCors
415
+ },
416
+ delete: () => {
417
+ // TODO: DeleteBucketCors
418
+ },
419
+ };
420
+ */
407
421
  /**
408
422
  * Creates an S3File instance for the given path.
409
423
  *
@@ -427,7 +441,6 @@ var S3Client = class {
427
441
  *
428
442
  * lean-s3 does not enforce these restrictions.
429
443
  *
430
- * @param {Partial<CreateFileInstanceOptions>} [_options] TODO
431
444
  * @example
432
445
  * ```js
433
446
  * const file = client.file("image.jpg");
@@ -435,17 +448,16 @@ var S3Client = class {
435
448
  *
436
449
  * const configFile = client.file("config.json", {
437
450
  * type: "application/json",
438
- * acl: "private"
439
451
  * });
440
452
  * ```
441
453
  */
442
- file(path, _options) {
454
+ file(path, options = {}) {
443
455
  return new S3File(
444
456
  this,
445
457
  ensureValidPath(path),
446
458
  void 0,
447
459
  void 0,
448
- void 0
460
+ options.type ?? void 0
449
461
  );
450
462
  }
451
463
  /**
@@ -466,6 +478,7 @@ var S3Client = class {
466
478
  // TODO: Maybe rename this to expiresInSeconds
467
479
  storageClass,
468
480
  contentLength,
481
+ type,
469
482
  acl,
470
483
  region: regionOverride,
471
484
  bucket: bucketOverride,
@@ -487,17 +500,21 @@ var S3Client = class {
487
500
  `${options.accessKeyId}/${date.date}/${region}/s3/aws4_request`,
488
501
  date,
489
502
  expiresIn,
490
- typeof contentLength === "number" ? "content-length;host" : "host",
503
+ typeof contentLength === "number" || typeof type === "string" ? typeof contentLength === "number" && typeof type === "string" ? "content-length;content-type;host" : typeof contentLength === "number" ? "content-length;host" : typeof type === "string" ? "content-type;host" : "" : "host",
491
504
  unsignedPayload,
492
505
  storageClass,
493
506
  options.sessionToken,
494
507
  acl
495
508
  );
496
- const dataDigest = typeof contentLength === "number" ? createCanonicalDataDigest(
509
+ const dataDigest = typeof contentLength === "number" || typeof type === "string" ? createCanonicalDataDigest(
497
510
  method,
498
511
  res.pathname,
499
512
  query,
500
- { "content-length": String(contentLength), host: res.host },
513
+ typeof contentLength === "number" && typeof type === "string" ? {
514
+ "content-length": String(contentLength),
515
+ "content-type": type,
516
+ host: res.host
517
+ } : typeof contentLength === "number" ? { "content-length": String(contentLength), host: res.host } : typeof type === "string" ? { "content-type": type, host: res.host } : {},
501
518
  unsignedPayload
502
519
  ) : createCanonicalDataDigestHostOnly(
503
520
  method,
@@ -912,6 +929,90 @@ var S3Client = class {
912
929
  }
913
930
  throw new Error(`Response code not supported: ${response.statusCode}`);
914
931
  }
932
+ //#region bucket cors
933
+ /**
934
+ * @remarks Uses [`PutBucketCors`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketCors.html).
935
+ */
936
+ async putBucketCors(rules, options = {}) {
937
+ const body = xmlBuilder.build({
938
+ CORSConfiguration: {
939
+ CORSRule: rules.map((r) => ({
940
+ AllowedOrigin: r.allowedOrigins,
941
+ AllowedMethod: r.allowedMethods,
942
+ ExposeHeader: r.exposeHeaders,
943
+ ID: r.id ?? void 0,
944
+ MaxAgeSeconds: r.maxAgeSeconds ?? void 0
945
+ }))
946
+ }
947
+ });
948
+ const response = await this[signedRequest](
949
+ "PUT",
950
+ "",
951
+ "cors=",
952
+ // "=" is needed by minio for some reason
953
+ body,
954
+ {
955
+ "content-md5": md5Base64(body)
956
+ },
957
+ void 0,
958
+ void 0,
959
+ ensureValidBucketName(options.bucket ?? this.#options.bucket),
960
+ options.signal
961
+ );
962
+ if (response.statusCode === 200) {
963
+ response.body.dump();
964
+ return;
965
+ }
966
+ if (400 <= response.statusCode && response.statusCode < 500) {
967
+ throw await getResponseError(response, "");
968
+ }
969
+ throw new Error(
970
+ `Response code not implemented yet: ${response.statusCode}`
971
+ );
972
+ }
973
+ /**
974
+ * @remarks Uses [`GetBucketCors`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketCors.html).
975
+ */
976
+ async getBucketCors(options = {}) {
977
+ const response = await this[signedRequest](
978
+ "GET",
979
+ "",
980
+ "cors=",
981
+ // "=" is needed by minio for some reason
982
+ void 0,
983
+ void 0,
984
+ void 0,
985
+ void 0,
986
+ ensureValidBucketName(options.bucket ?? this.#options.bucket),
987
+ options.signal
988
+ );
989
+ if (response.statusCode !== 200) {
990
+ response.body.dump();
991
+ throw fromStatusCode(response.statusCode, "");
992
+ }
993
+ throw new Error("Not implemented");
994
+ }
995
+ /**
996
+ * @remarks Uses [`DeleteBucketCors`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteBucketCors.html).
997
+ */
998
+ async deleteBucketCors(options = {}) {
999
+ const response = await this[signedRequest](
1000
+ "DELETE",
1001
+ "",
1002
+ "cors=",
1003
+ // "=" is needed by minio for some reason
1004
+ void 0,
1005
+ void 0,
1006
+ void 0,
1007
+ void 0,
1008
+ ensureValidBucketName(options.bucket ?? this.#options.bucket),
1009
+ options.signal
1010
+ );
1011
+ if (response.statusCode !== 204) {
1012
+ response.body.dump();
1013
+ throw fromStatusCode(response.statusCode, "");
1014
+ }
1015
+ }
915
1016
  //#endregion
916
1017
  //#region list objects
917
1018
  /**
@@ -1449,7 +1550,7 @@ var S3File = class _S3File {
1449
1550
  * @remarks - Uses [`DeleteObject`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObject.html).
1450
1551
  * - `versionId` not supported.
1451
1552
  *
1452
- * @param {Partial<S3FileDeleteOptions>} [options]
1553
+ * @param {S3FileDeleteOptions} [options]
1453
1554
  *
1454
1555
  * @example
1455
1556
  * ```js
@@ -1540,15 +1641,16 @@ var S3File = class _S3File {
1540
1641
  }
1541
1642
  /**
1542
1643
  * @param {ByteSource} data
1644
+ * @param {S3FileWriteOptions} [options.type] Defaults to the Content-Type that was used to create the {@link S3File} instance.
1543
1645
  * @returns {Promise<void>}
1544
1646
  */
1545
- async write(data) {
1647
+ async write(data, options = {}) {
1546
1648
  const signal = void 0;
1547
1649
  const [bytes, length, hash] = await this.#transformData(data);
1548
1650
  return await this.#client[write](
1549
1651
  this.#path,
1550
1652
  bytes,
1551
- this.#contentType,
1653
+ options.type ?? this.#contentType,
1552
1654
  length,
1553
1655
  hash,
1554
1656
  this.#start,
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.6.2",
5
+ "version": "0.7.0",
6
6
  "description": "A server-side S3 API for the regular user.",
7
7
  "keywords": [
8
8
  "s3",
@@ -44,19 +44,19 @@
44
44
  },
45
45
  "dependencies": {
46
46
  "fast-xml-parser": "^5.2.5",
47
- "undici": "^7.10.0"
47
+ "undici": "^7.11.0"
48
48
  },
49
49
  "devDependencies": {
50
- "@biomejs/biome": "2.0.5",
51
- "@testcontainers/localstack": "^11.0.3",
52
- "@testcontainers/minio": "^11.0.3",
53
- "@types/node": "^24.0.4",
54
- "@typescript/native-preview": "^7.0.0-dev.20250626.1",
55
- "expect": "^30.0.3",
56
- "lefthook": "^1.11.14",
50
+ "@biomejs/biome": "2.0.6",
51
+ "@testcontainers/localstack": "^11.2.0",
52
+ "@testcontainers/minio": "^11.2.0",
53
+ "@types/node": "^24.0.10",
54
+ "@typescript/native-preview": "^7.0.0-dev.20250705.1",
55
+ "expect": "^30.0.4",
56
+ "lefthook": "^1.11.16",
57
57
  "tsup": "^8.5.0",
58
58
  "tsx": "^4.20.3",
59
- "typedoc": "^0.28.5"
59
+ "typedoc": "^0.28.7"
60
60
  },
61
61
  "engines": {
62
62
  "node": "^20.19.2 || ^22.16.0 || ^24.2.0"