lean-s3 0.7.4 → 0.7.6
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 +10 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +54 -81
- package/package.json +7 -7
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
|
-
|
|
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
|
|
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(
|
|
323
|
-
if (typeof
|
|
324
|
-
throw new TypeError("`
|
|
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 (
|
|
327
|
-
throw new Error("`
|
|
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 (
|
|
330
|
-
throw new Error("`
|
|
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(
|
|
332
|
+
if (!/^[a-z0-9.-]+$/.test(bucket)) {
|
|
333
333
|
throw new Error(
|
|
334
|
-
"`
|
|
334
|
+
"`bucket` can only contain lowercase letters, numbers, periods (.), and hyphens (-)."
|
|
335
335
|
);
|
|
336
336
|
}
|
|
337
|
-
if (
|
|
338
|
-
throw new Error("`
|
|
337
|
+
if (bucket.includes("..")) {
|
|
338
|
+
throw new Error("`bucket` must not contain two adjacent periods (..)");
|
|
339
339
|
}
|
|
340
|
-
return
|
|
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
|
-
|
|
454
|
+
/** @internal */
|
|
455
|
+
[kGetEffectiveParams](options) {
|
|
456
456
|
return [
|
|
457
|
-
ensureValidRegion(region
|
|
458
|
-
ensureValidEndpoint(endpoint
|
|
459
|
-
ensureValidBucketName(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
|
|
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
|
|
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.
|
|
5
|
+
"version": "0.7.6",
|
|
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",
|
|
@@ -50,16 +50,16 @@
|
|
|
50
50
|
"@biomejs/biome": "2.1.1",
|
|
51
51
|
"@testcontainers/localstack": "^11.2.1",
|
|
52
52
|
"@testcontainers/minio": "^11.2.1",
|
|
53
|
-
"@types/node": "^24.0.
|
|
54
|
-
"@typescript/native-preview": "^7.0.0-dev.
|
|
53
|
+
"@types/node": "^24.0.13",
|
|
54
|
+
"@typescript/native-preview": "^7.0.0-dev.20250711.1",
|
|
55
55
|
"expect": "^30.0.4",
|
|
56
|
-
"lefthook": "^1.12.
|
|
56
|
+
"lefthook": "^1.12.2",
|
|
57
57
|
"testcontainers": "^11.2.1",
|
|
58
58
|
"tsup": "^8.5.0",
|
|
59
59
|
"tsx": "^4.20.3",
|
|
60
60
|
"typedoc": "^0.28.7"
|
|
61
61
|
},
|
|
62
62
|
"engines": {
|
|
63
|
-
"node": "^20.19.
|
|
63
|
+
"node": "^20.19.3 || ^22.17.0 || ^24.4.0"
|
|
64
64
|
}
|
|
65
65
|
}
|