lean-s3 0.6.1 → 0.6.3

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
@@ -45,6 +45,15 @@ type DeleteObjectsOptions = {
45
45
  bucket?: string;
46
46
  signal?: AbortSignal;
47
47
  };
48
+ type DeleteObjectsResult = {
49
+ errors: DeleteObjectsError[];
50
+ };
51
+ type DeleteObjectsError = {
52
+ code: string;
53
+ key: string;
54
+ message: string;
55
+ versionId: string;
56
+ };
48
57
  interface S3FilePresignOptions {
49
58
  contentHash: Buffer;
50
59
  /** Seconds. */
@@ -58,6 +67,7 @@ type ListObjectsOptions = {
58
67
  bucket?: string;
59
68
  prefix?: string;
60
69
  maxKeys?: number;
70
+ delimiter?: string;
61
71
  startAfter?: string;
62
72
  continuationToken?: string;
63
73
  signal?: AbortSignal;
@@ -198,6 +208,35 @@ type BucketDeletionOptions = {
198
208
  type BucketExistsOptions = {
199
209
  signal?: AbortSignal;
200
210
  };
211
+ type BucketCorsRules = readonly BucketCorsRule[];
212
+ type BucketCorsRule = {
213
+ allowedMethods: readonly HttpMethod[];
214
+ /** One or more origins you want customers to be able to access the bucket from. */
215
+ allowedOrigins: readonly string[];
216
+ /** Headers that are specified in the `Access-Control-Request-Headers` header. These headers are allowed in a preflight `OPTIONS` request. */
217
+ allowedHeaders?: readonly string[];
218
+ /** One or more headers in the response that you want customers to be able to access from their applications. */
219
+ exposeHeaders?: readonly string[];
220
+ /** Unique identifier for the rule. The value cannot be longer than 255 characters. */
221
+ id?: string;
222
+ /** The time in seconds that your browser is to cache the preflight response for the specified resource. */
223
+ maxAgeSeconds?: number;
224
+ };
225
+ type PutBucketCorsOptions = {
226
+ bucket?: string;
227
+ signal?: AbortSignal;
228
+ };
229
+ type DeleteBucketCorsOptions = {
230
+ bucket?: string;
231
+ signal?: AbortSignal;
232
+ };
233
+ type GetBucketCorsOptions = {
234
+ bucket?: string;
235
+ signal?: AbortSignal;
236
+ };
237
+ type GetBucketCorsResult = {
238
+ rules: BucketCorsRule[];
239
+ };
201
240
  /**
202
241
  * A configured S3 bucket instance for managing files.
203
242
  *
@@ -222,6 +261,11 @@ declare class S3Client {
222
261
  * @param options The default options to use for the S3 client.
223
262
  */
224
263
  constructor(options: S3ClientOptions);
264
+ cors: {
265
+ get: () => void;
266
+ set: () => void;
267
+ delete: () => void;
268
+ };
225
269
  /**
226
270
  * Creates an S3File instance for the given path.
227
271
  *
@@ -335,6 +379,18 @@ declare class S3Client {
335
379
  * @remarks Uses [`HeadBucket`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadBucket.html).
336
380
  */
337
381
  bucketExists(name: string, options?: BucketExistsOptions): Promise<boolean>;
382
+ /**
383
+ * @remarks Uses [`PutBucketCors`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketCors.html).
384
+ */
385
+ putBucketCors(rules: BucketCorsRules, options?: PutBucketCorsOptions): Promise<void>;
386
+ /**
387
+ * @remarks Uses [`GetBucketCors`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketCors.html).
388
+ */
389
+ getBucketCors(options?: GetBucketCorsOptions): Promise<GetBucketCorsResult>;
390
+ /**
391
+ * @remarks Uses [`DeleteBucketCors`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteBucketCors.html).
392
+ */
393
+ deleteBucketCors(options?: DeleteBucketCorsOptions): Promise<void>;
338
394
  /**
339
395
  * Uses [`ListObjectsV2`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html) to iterate over all keys. Pagination and continuation is handled internally.
340
396
  */
@@ -348,9 +404,7 @@ declare class S3Client {
348
404
  /**
349
405
  * Uses [`DeleteObjects`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObjects.html) to delete multiple objects in a single request.
350
406
  */
351
- deleteObjects(objects: readonly S3BucketEntry[] | readonly string[], options?: DeleteObjectsOptions): Promise<{
352
- errors: any;
353
- } | null>;
407
+ deleteObjects(objects: readonly S3BucketEntry[] | readonly string[], options?: DeleteObjectsOptions): Promise<DeleteObjectsResult>;
354
408
  /**
355
409
  * Do not use this. This is an internal method.
356
410
  * TODO: Maybe move this into a separate free function?
@@ -379,9 +433,7 @@ declare class S3Stat {
379
433
 
380
434
  declare class S3File {
381
435
  #private;
382
- /**
383
- * @internal
384
- */
436
+ /** @internal */
385
437
  constructor(client: S3Client, path: ObjectKey, start: number | undefined, end: number | undefined, contentType: string | undefined);
386
438
  slice(start?: number | undefined, end?: number | undefined, contentType?: string | undefined): S3File;
387
439
  /**
@@ -391,13 +443,13 @@ declare class S3File {
391
443
  * @throws {S3Error} If the file does not exist or the server has some other issues.
392
444
  * @throws {Error} If the server returns an invalid response.
393
445
  */
394
- stat({ signal }?: Partial<S3StatOptions>): Promise<S3Stat>;
446
+ stat(options?: Partial<S3StatOptions>): Promise<S3Stat>;
395
447
  /**
396
448
  * Check if a file exists in the bucket. Uses `HEAD` request to check existence.
397
449
  *
398
450
  * @remarks Uses [`HeadObject`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html).
399
451
  */
400
- exists({ signal, }?: Partial<S3FileExistsOptions>): Promise<boolean>;
452
+ exists(options?: Partial<S3FileExistsOptions>): Promise<boolean>;
401
453
  /**
402
454
  * Delete a file from the bucket.
403
455
  *
@@ -409,18 +461,18 @@ declare class S3File {
409
461
  * @example
410
462
  * ```js
411
463
  * // Simple delete
412
- * await client.unlink("old-file.txt");
464
+ * await client.delete("old-file.txt");
413
465
  *
414
466
  * // With error handling
415
467
  * try {
416
- * await client.unlink("file.dat");
468
+ * await client.delete("file.dat");
417
469
  * console.log("File deleted");
418
470
  * } catch (err) {
419
471
  * console.error("Delete failed:", err);
420
472
  * }
421
473
  * ```
422
474
  */
423
- delete({ signal }?: Partial<S3FileDeleteOptions>): Promise<void>;
475
+ delete(options?: Partial<S3FileDeleteOptions>): Promise<void>;
424
476
  toString(): string;
425
477
  json(): Promise<unknown>;
426
478
  bytes(): Promise<Uint8Array>;
@@ -484,4 +536,4 @@ type BucketInfo = {
484
536
  type?: string;
485
537
  };
486
538
 
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 };
539
+ 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 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, S3Stat, type S3StatOptions, type StorageClass, type UndiciBodyInit, type UploadPartOptions, type UploadPartResult };
package/dist/index.js CHANGED
@@ -202,7 +202,7 @@ function buildRequestUrl(endpoint, bucket, region, path) {
202
202
  const result = new URL(endpointWithBucketAndRegion);
203
203
  const pathPrefix = result.pathname.endsWith("/") ? result.pathname : `${result.pathname}/`;
204
204
  const pathSuffix = replacedBucket ? normalizePath(path) : `${normalizedBucket}/${normalizePath(path)}`;
205
- result.pathname = pathPrefix + pathSuffix.replaceAll(":", "%3A").replaceAll("+", "%2B").replaceAll("(", "%28").replaceAll(")", "%29");
205
+ result.pathname = pathPrefix + pathSuffix.replaceAll(":", "%3A").replaceAll("+", "%2B").replaceAll("(", "%28").replaceAll(")", "%29").replaceAll(",", "%2C");
206
206
  return result;
207
207
  }
208
208
  function replaceDomainPlaceholders(endpoint, bucket, region) {
@@ -404,6 +404,14 @@ var S3Client = class {
404
404
  sessionToken
405
405
  };
406
406
  }
407
+ cors = {
408
+ get: () => {
409
+ },
410
+ set: () => {
411
+ },
412
+ delete: () => {
413
+ }
414
+ };
407
415
  /**
408
416
  * Creates an S3File instance for the given path.
409
417
  *
@@ -721,7 +729,7 @@ var S3Client = class {
721
729
  options.signal
722
730
  );
723
731
  if (response.statusCode === 200) {
724
- await response.body.dump();
732
+ response.body.dump();
725
733
  const etag = response.headers.etag;
726
734
  if (typeof etag !== "string" || etag.length === 0) {
727
735
  throw new S3Error("Unknown", "", {
@@ -848,7 +856,7 @@ var S3Client = class {
848
856
  if (400 <= response.statusCode && response.statusCode < 500) {
849
857
  throw await getResponseError(response, "");
850
858
  }
851
- await response.body.dump();
859
+ response.body.dump();
852
860
  if (response.statusCode === 200) {
853
861
  return;
854
862
  }
@@ -876,7 +884,7 @@ var S3Client = class {
876
884
  if (400 <= response.statusCode && response.statusCode < 500) {
877
885
  throw await getResponseError(response, "");
878
886
  }
879
- await response.body.dump();
887
+ response.body.dump();
880
888
  if (response.statusCode === 204) {
881
889
  return;
882
890
  }
@@ -903,7 +911,7 @@ var S3Client = class {
903
911
  if (response.statusCode !== 404 && 400 <= response.statusCode && response.statusCode < 500) {
904
912
  throw await getResponseError(response, "");
905
913
  }
906
- await response.body.dump();
914
+ response.body.dump();
907
915
  if (response.statusCode === 200) {
908
916
  return true;
909
917
  }
@@ -912,6 +920,90 @@ var S3Client = class {
912
920
  }
913
921
  throw new Error(`Response code not supported: ${response.statusCode}`);
914
922
  }
923
+ //#region bucket cors
924
+ /**
925
+ * @remarks Uses [`PutBucketCors`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketCors.html).
926
+ */
927
+ async putBucketCors(rules, options = {}) {
928
+ const body = xmlBuilder.build({
929
+ CORSConfiguration: {
930
+ CORSRule: rules.map((r) => ({
931
+ AllowedOrigin: r.allowedOrigins,
932
+ AllowedMethod: r.allowedMethods,
933
+ ExposeHeader: r.exposeHeaders,
934
+ ID: r.id ?? void 0,
935
+ MaxAgeSeconds: r.maxAgeSeconds ?? void 0
936
+ }))
937
+ }
938
+ });
939
+ const response = await this[signedRequest](
940
+ "PUT",
941
+ "",
942
+ "cors=",
943
+ // "=" is needed by minio for some reason
944
+ body,
945
+ {
946
+ "content-md5": md5Base64(body)
947
+ },
948
+ void 0,
949
+ void 0,
950
+ ensureValidBucketName(options.bucket ?? this.#options.bucket),
951
+ options.signal
952
+ );
953
+ if (response.statusCode === 200) {
954
+ response.body.dump();
955
+ return;
956
+ }
957
+ if (400 <= response.statusCode && response.statusCode < 500) {
958
+ throw await getResponseError(response, "");
959
+ }
960
+ throw new Error(
961
+ `Response code not implemented yet: ${response.statusCode}`
962
+ );
963
+ }
964
+ /**
965
+ * @remarks Uses [`GetBucketCors`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketCors.html).
966
+ */
967
+ async getBucketCors(options = {}) {
968
+ const response = await this[signedRequest](
969
+ "GET",
970
+ "",
971
+ "cors=",
972
+ // "=" is needed by minio for some reason
973
+ void 0,
974
+ void 0,
975
+ void 0,
976
+ void 0,
977
+ ensureValidBucketName(options.bucket ?? this.#options.bucket),
978
+ options.signal
979
+ );
980
+ if (response.statusCode !== 200) {
981
+ response.body.dump();
982
+ throw fromStatusCode(response.statusCode, "");
983
+ }
984
+ throw new Error("Not implemented");
985
+ }
986
+ /**
987
+ * @remarks Uses [`DeleteBucketCors`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteBucketCors.html).
988
+ */
989
+ async deleteBucketCors(options = {}) {
990
+ const response = await this[signedRequest](
991
+ "DELETE",
992
+ "",
993
+ "cors=",
994
+ // "=" is needed by minio for some reason
995
+ void 0,
996
+ void 0,
997
+ void 0,
998
+ void 0,
999
+ ensureValidBucketName(options.bucket ?? this.#options.bucket),
1000
+ options.signal
1001
+ );
1002
+ if (response.statusCode !== 204) {
1003
+ response.body.dump();
1004
+ throw fromStatusCode(response.statusCode, "");
1005
+ }
1006
+ }
915
1007
  //#endregion
916
1008
  //#region list objects
917
1009
  /**
@@ -956,6 +1048,12 @@ var S3Client = class {
956
1048
  }
957
1049
  query += `&max-keys=${options.maxKeys}`;
958
1050
  }
1051
+ if (typeof options.delimiter !== "undefined") {
1052
+ if (typeof options.delimiter !== "string") {
1053
+ throw new TypeError("`delimiter` must be a `string`.");
1054
+ }
1055
+ query += `&delimiter=${options.delimiter === "/" ? "/" : encodeURIComponent(options.delimiter)}`;
1056
+ }
959
1057
  if (options.prefix) {
960
1058
  if (typeof options.prefix !== "string") {
961
1059
  throw new TypeError("`prefix` must be a `string`.");
@@ -1051,7 +1149,7 @@ var S3Client = class {
1051
1149
  versionId: e.VersionId
1052
1150
  })) ?? []
1053
1151
  );
1054
- return errors.length > 0 ? { errors } : null;
1152
+ return { errors };
1055
1153
  }
1056
1154
  if (400 <= response.statusCode && response.statusCode < 500) {
1057
1155
  throw await getResponseError(response, "");
@@ -1352,9 +1450,7 @@ var S3File = class _S3File {
1352
1450
  #start;
1353
1451
  #end;
1354
1452
  #contentType;
1355
- /**
1356
- * @internal
1357
- */
1453
+ /** @internal */
1358
1454
  constructor(client, path, start, end, contentType) {
1359
1455
  if (typeof start === "number" && start < 0) {
1360
1456
  throw new Error("Invalid slice `start`.");
@@ -1385,7 +1481,7 @@ var S3File = class _S3File {
1385
1481
  * @throws {S3Error} If the file does not exist or the server has some other issues.
1386
1482
  * @throws {Error} If the server returns an invalid response.
1387
1483
  */
1388
- async stat({ signal } = {}) {
1484
+ async stat(options = {}) {
1389
1485
  const response = await this.#client[signedRequest](
1390
1486
  "HEAD",
1391
1487
  this.#path,
@@ -1395,9 +1491,9 @@ var S3File = class _S3File {
1395
1491
  void 0,
1396
1492
  void 0,
1397
1493
  void 0,
1398
- signal
1494
+ options.signal
1399
1495
  );
1400
- await response.body.dump();
1496
+ response.body.dump();
1401
1497
  if (200 <= response.statusCode && response.statusCode < 300) {
1402
1498
  const result = S3Stat.tryParseFromHeaders(response.headers);
1403
1499
  if (!result) {
@@ -1416,9 +1512,7 @@ var S3File = class _S3File {
1416
1512
  *
1417
1513
  * @remarks Uses [`HeadObject`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html).
1418
1514
  */
1419
- async exists({
1420
- signal
1421
- } = {}) {
1515
+ async exists(options = {}) {
1422
1516
  const response = await this.#client[signedRequest](
1423
1517
  "HEAD",
1424
1518
  this.#path,
@@ -1428,9 +1522,9 @@ var S3File = class _S3File {
1428
1522
  void 0,
1429
1523
  void 0,
1430
1524
  void 0,
1431
- signal
1525
+ options.signal
1432
1526
  );
1433
- await response.body.dump();
1527
+ response.body.dump();
1434
1528
  if (200 <= response.statusCode && response.statusCode < 300) {
1435
1529
  return true;
1436
1530
  }
@@ -1452,18 +1546,18 @@ var S3File = class _S3File {
1452
1546
  * @example
1453
1547
  * ```js
1454
1548
  * // Simple delete
1455
- * await client.unlink("old-file.txt");
1549
+ * await client.delete("old-file.txt");
1456
1550
  *
1457
1551
  * // With error handling
1458
1552
  * try {
1459
- * await client.unlink("file.dat");
1553
+ * await client.delete("file.dat");
1460
1554
  * console.log("File deleted");
1461
1555
  * } catch (err) {
1462
1556
  * console.error("Delete failed:", err);
1463
1557
  * }
1464
1558
  * ```
1465
1559
  */
1466
- async delete({ signal } = {}) {
1560
+ async delete(options = {}) {
1467
1561
  const response = await this.#client[signedRequest](
1468
1562
  "DELETE",
1469
1563
  this.#path,
@@ -1473,10 +1567,10 @@ var S3File = class _S3File {
1473
1567
  void 0,
1474
1568
  void 0,
1475
1569
  void 0,
1476
- signal
1570
+ options.signal
1477
1571
  );
1478
1572
  if (response.statusCode === 204) {
1479
- await response.body.dump();
1573
+ response.body.dump();
1480
1574
  return;
1481
1575
  }
1482
1576
  throw await getResponseError(response, this.#path);
@@ -1556,10 +1650,6 @@ var S3File = class _S3File {
1556
1650
  }
1557
1651
  /*
1558
1652
  // Future API?
1559
- writer(): WritableStream<ArrayBufferLike | ArrayBufferView> {
1560
- throw new Error("Not implemented");
1561
- }
1562
- // Future API?
1563
1653
  setTags(): Promise<void> {
1564
1654
  throw new Error("Not implemented");
1565
1655
  }
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.1",
5
+ "version": "0.6.3",
6
6
  "description": "A server-side S3 API for the regular user.",
7
7
  "keywords": [
8
8
  "s3",
@@ -44,20 +44,19 @@
44
44
  },
45
45
  "dependencies": {
46
46
  "fast-xml-parser": "^5.2.5",
47
- "i": "^0.3.7",
48
- "undici": "^7.10.0"
47
+ "undici": "^7.11.0"
49
48
  },
50
49
  "devDependencies": {
51
- "@biomejs/biome": "2.0.0",
50
+ "@biomejs/biome": "2.0.6",
52
51
  "@testcontainers/localstack": "^11.0.3",
53
52
  "@testcontainers/minio": "^11.0.3",
54
- "@types/node": "^24.0.3",
55
- "@typescript/native-preview": "^7.0.0-dev.20250620.1",
56
- "expect": "^30.0.2",
53
+ "@types/node": "^24.0.7",
54
+ "@typescript/native-preview": "^7.0.0-dev.20250629.1",
55
+ "expect": "^30.0.3",
57
56
  "lefthook": "^1.11.14",
58
57
  "tsup": "^8.5.0",
59
58
  "tsx": "^4.20.3",
60
- "typedoc": "^0.28.5"
59
+ "typedoc": "^0.28.6"
61
60
  },
62
61
  "engines": {
63
62
  "node": "^20.19.2 || ^22.16.0 || ^24.2.0"