lean-s3 0.8.12 → 0.9.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 +16 -0
- package/dist/{index.d.ts → index.d.mts} +34 -9
- package/dist/{index.js → index.mjs} +110 -9
- package/package.json +18 -17
- package/tsdown.config.ts +0 -6
package/README.md
CHANGED
|
@@ -189,6 +189,19 @@ const client = new S3Client({
|
|
|
189
189
|
});
|
|
190
190
|
```
|
|
191
191
|
|
|
192
|
+
### Backblaze B2
|
|
193
|
+
```js
|
|
194
|
+
// Docs: https://www.backblaze.com/apidocs/introduction-to-the-s3-compatible-api
|
|
195
|
+
const client = new S3Client({
|
|
196
|
+
// keep {region} placeholders (it is used internally).
|
|
197
|
+
endpoint: "https://s3.{region}.backblazeb2.com",
|
|
198
|
+
region: "<your-region>", // something like "eu-central-003"
|
|
199
|
+
bucket: "<your-bucket-name>",
|
|
200
|
+
accessKeyId: process.env.S3_ACCESS_KEY_ID, // <your-application-key-id>,
|
|
201
|
+
secretAccessKey: process.env.S3_SECRET_KEY, // <your-application-key>,
|
|
202
|
+
});
|
|
203
|
+
```
|
|
204
|
+
|
|
192
205
|
Popular S3 provider missing? Open an issue or file a PR!
|
|
193
206
|
|
|
194
207
|
## Tested On
|
|
@@ -197,6 +210,9 @@ To ensure compability across various providers and self-hosted services, all tes
|
|
|
197
210
|
- Hetzner Object Storage
|
|
198
211
|
- Cloudflare R2
|
|
199
212
|
- Garage
|
|
213
|
+
- Backblaze B2
|
|
200
214
|
- Minio
|
|
201
215
|
- LocalStack
|
|
216
|
+
- RustFS
|
|
202
217
|
- Ceph
|
|
218
|
+
- S3Mock
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as nodeUtil from "node:util";
|
|
1
2
|
import { Readable } from "node:stream";
|
|
2
3
|
import { Dispatcher } from "undici";
|
|
3
4
|
|
|
@@ -97,7 +98,10 @@ type DeleteObjectsError = {
|
|
|
97
98
|
};
|
|
98
99
|
interface S3FilePresignOptions extends OverridableS3ClientOptions {
|
|
99
100
|
contentHash?: Buffer;
|
|
100
|
-
/**
|
|
101
|
+
/**
|
|
102
|
+
* In seconds.
|
|
103
|
+
* @default 3600 (1 hour)
|
|
104
|
+
*/
|
|
101
105
|
expiresIn?: number;
|
|
102
106
|
method?: PresignableHttpMethod;
|
|
103
107
|
contentLength?: number;
|
|
@@ -127,6 +131,25 @@ interface S3FilePresignOptions extends OverridableS3ClientOptions {
|
|
|
127
131
|
contentDisposition?: ContentDisposition;
|
|
128
132
|
};
|
|
129
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Ref: https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html#sigv4-ConditionMatching
|
|
136
|
+
*/
|
|
137
|
+
type ConditionMatchType = "starts-with" | "eq" | "content-length-range";
|
|
138
|
+
type PostPolicyCondition = [string, string] | [ConditionMatchType, string | number, string | number];
|
|
139
|
+
interface PresignPostOptions extends OverridableS3ClientOptions {
|
|
140
|
+
key: string;
|
|
141
|
+
/**
|
|
142
|
+
* In seconds.
|
|
143
|
+
* @default 3600 (1 hour)
|
|
144
|
+
*/
|
|
145
|
+
expiresIn?: number;
|
|
146
|
+
fields?: Record<string, string>;
|
|
147
|
+
conditions?: PostPolicyCondition[];
|
|
148
|
+
}
|
|
149
|
+
type PresignPostResult = {
|
|
150
|
+
url: string;
|
|
151
|
+
fields: Record<string, string>;
|
|
152
|
+
};
|
|
130
153
|
type CopyObjectOptions = {
|
|
131
154
|
/** Set this to override the {@link S3ClientOptions#bucket} that was passed on creation of the {@link S3Client}. */
|
|
132
155
|
sourceBucket?: string;
|
|
@@ -386,7 +409,7 @@ declare class S3Client {
|
|
|
386
409
|
/**
|
|
387
410
|
* 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.
|
|
388
411
|
*
|
|
389
|
-
* @param
|
|
412
|
+
* @param options The default options to use for the S3 client.
|
|
390
413
|
*/
|
|
391
414
|
constructor(options: S3ClientOptions);
|
|
392
415
|
/** @internal */
|
|
@@ -451,6 +474,7 @@ declare class S3Client {
|
|
|
451
474
|
* ```
|
|
452
475
|
*/
|
|
453
476
|
presign(path: string, options?: S3FilePresignOptions): string;
|
|
477
|
+
presignPost(options: PresignPostOptions): PresignPostResult;
|
|
454
478
|
/**
|
|
455
479
|
* Copies an object from a source to a destination.
|
|
456
480
|
* @remarks Uses [`CopyObject`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_CopyObject.html).
|
|
@@ -557,6 +581,7 @@ declare class S3Client {
|
|
|
557
581
|
* @internal
|
|
558
582
|
*/
|
|
559
583
|
[kStream](path: ObjectKey, contentHash: Buffer | undefined, rageStart: number | undefined, rangeEndExclusive: number | undefined): ReadableStream<Uint8Array>;
|
|
584
|
+
[nodeUtil.inspect.custom](_depth?: number, options?: nodeUtil.InspectOptions): string;
|
|
560
585
|
}
|
|
561
586
|
//#endregion
|
|
562
587
|
//#region src/S3Stat.d.ts
|
|
@@ -643,6 +668,7 @@ declare class S3File {
|
|
|
643
668
|
* @param options Options for the copy operation.
|
|
644
669
|
*/
|
|
645
670
|
copyTo(destination: string, options?: CopyObjectOptions): Promise<void>;
|
|
671
|
+
[nodeUtil.inspect.custom](_depth?: number, options?: nodeUtil.InspectOptions): string;
|
|
646
672
|
}
|
|
647
673
|
interface S3FileDeleteOptions extends OverridableS3ClientOptions {
|
|
648
674
|
/** Signal to abort the request. */
|
|
@@ -666,22 +692,21 @@ type S3FileWriteOptions = {
|
|
|
666
692
|
//#region src/S3Error.d.ts
|
|
667
693
|
declare class S3Error extends Error {
|
|
668
694
|
readonly code: string;
|
|
695
|
+
/** Path/key of the affected object. */
|
|
669
696
|
readonly path: string;
|
|
670
697
|
readonly message: string;
|
|
671
|
-
|
|
672
|
-
readonly
|
|
698
|
+
/** The HTTP status code. */
|
|
699
|
+
readonly status: number | undefined;
|
|
673
700
|
constructor(code: string, path: string, {
|
|
674
701
|
message,
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
cause
|
|
702
|
+
cause,
|
|
703
|
+
status
|
|
678
704
|
}?: S3ErrorOptions);
|
|
679
705
|
}
|
|
680
706
|
type S3ErrorOptions = {
|
|
681
707
|
message?: string | undefined;
|
|
682
|
-
requestId?: string | undefined;
|
|
683
|
-
hostId?: string | undefined;
|
|
684
708
|
cause?: unknown;
|
|
709
|
+
status?: number;
|
|
685
710
|
};
|
|
686
711
|
//#endregion
|
|
687
712
|
//#region src/index.d.ts
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as nodeUtil from "node:util";
|
|
1
2
|
import { Readable } from "node:stream";
|
|
2
3
|
import { Agent, request } from "undici";
|
|
3
4
|
import { XMLBuilder, XMLParser } from "fast-xml-parser";
|
|
@@ -34,17 +35,17 @@ var S3Stat = class S3Stat {
|
|
|
34
35
|
//#region src/S3Error.ts
|
|
35
36
|
var S3Error = class extends Error {
|
|
36
37
|
code;
|
|
38
|
+
/** Path/key of the affected object. */
|
|
37
39
|
path;
|
|
38
40
|
message;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
constructor(code, path, { message = void 0,
|
|
41
|
+
/** The HTTP status code. */
|
|
42
|
+
status;
|
|
43
|
+
constructor(code, path, { message = void 0, cause = void 0, status = void 0 } = {}) {
|
|
42
44
|
super(message, { cause });
|
|
43
45
|
this.code = code;
|
|
44
46
|
this.path = path;
|
|
45
47
|
this.message = message ?? "Some unknown error occurred.";
|
|
46
|
-
this.
|
|
47
|
-
this.hostId = hostId;
|
|
48
|
+
this.status = status;
|
|
48
49
|
}
|
|
49
50
|
};
|
|
50
51
|
|
|
@@ -83,8 +84,11 @@ var S3BucketEntry = class S3BucketEntry {
|
|
|
83
84
|
function deriveSigningKey(date, region, secretAccessKey) {
|
|
84
85
|
return createHmac("sha256", createHmac("sha256", createHmac("sha256", createHmac("sha256", `AWS4${secretAccessKey}`).update(date).digest()).update(region).digest()).update("s3").digest()).update("aws4_request").digest();
|
|
85
86
|
}
|
|
86
|
-
function
|
|
87
|
-
return createHmac("sha256",
|
|
87
|
+
function signEncodedPolicy(signingKey, encodedPolicy) {
|
|
88
|
+
return createHmac("sha256", signingKey).update(encodedPolicy).digest("hex");
|
|
89
|
+
}
|
|
90
|
+
function signCanonicalDataHash(signingKey, canonicalDataHash, date, region) {
|
|
91
|
+
return createHmac("sha256", signingKey).update(`AWS4-HMAC-SHA256\n${date.dateTime}\n${date.date}/${region}/s3/aws4_request\n${canonicalDataHash}`).digest("hex");
|
|
88
92
|
}
|
|
89
93
|
const unsignedPayload = "UNSIGNED-PAYLOAD";
|
|
90
94
|
/**
|
|
@@ -345,7 +349,7 @@ var S3Client = class {
|
|
|
345
349
|
/**
|
|
346
350
|
* 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.
|
|
347
351
|
*
|
|
348
|
-
* @param
|
|
352
|
+
* @param options The default options to use for the S3 client.
|
|
349
353
|
*/
|
|
350
354
|
constructor(options) {
|
|
351
355
|
if (!options) throw new Error("`options` is required.");
|
|
@@ -456,6 +460,76 @@ var S3Client = class {
|
|
|
456
460
|
res.search = `${query}&X-Amz-Signature=${signCanonicalDataHash(signingKey, dataDigest, date, region)}`;
|
|
457
461
|
return res.toString();
|
|
458
462
|
}
|
|
463
|
+
presignPost(options) {
|
|
464
|
+
const now$1 = /* @__PURE__ */ new Date();
|
|
465
|
+
const date = getAmzDate(now$1);
|
|
466
|
+
const key = options.key;
|
|
467
|
+
const region = ensureValidRegion(options.region ?? this.#options.region);
|
|
468
|
+
const bucket = ensureValidBucketName(options.bucket ?? this.#options.bucket);
|
|
469
|
+
const endpoint = ensureValidEndpoint(options.endpoint ?? this.#options.endpoint);
|
|
470
|
+
const expiresIn = options.expiresIn ?? 3600;
|
|
471
|
+
const credential = `${this.#options.accessKeyId}/${date.date}/${region}/s3/aws4_request`;
|
|
472
|
+
const fields = {
|
|
473
|
+
...options.fields,
|
|
474
|
+
bucket,
|
|
475
|
+
"X-Amz-Algorithm": "AWS4-HMAC-SHA256",
|
|
476
|
+
"X-Amz-Credential": credential,
|
|
477
|
+
"X-Amz-Date": date.dateTime,
|
|
478
|
+
...this.#options.sessionToken ? { "X-Amz-Security-Token": this.#options.sessionToken } : void 0
|
|
479
|
+
};
|
|
480
|
+
const policy = {
|
|
481
|
+
expiration: new Date(now$1.getTime() + expiresIn * 1e3).toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
482
|
+
conditions: [
|
|
483
|
+
[
|
|
484
|
+
"eq",
|
|
485
|
+
"$bucket",
|
|
486
|
+
bucket
|
|
487
|
+
],
|
|
488
|
+
key.endsWith("{{filename}}") ? [
|
|
489
|
+
"starts-with",
|
|
490
|
+
"$key",
|
|
491
|
+
key.substring(0, key.lastIndexOf("{{filename}}"))
|
|
492
|
+
] : [
|
|
493
|
+
"eq",
|
|
494
|
+
"$key",
|
|
495
|
+
key
|
|
496
|
+
],
|
|
497
|
+
...options.conditions ? options.conditions : [],
|
|
498
|
+
[
|
|
499
|
+
"eq",
|
|
500
|
+
"$x-amz-algorithm",
|
|
501
|
+
"AWS4-HMAC-SHA256"
|
|
502
|
+
],
|
|
503
|
+
[
|
|
504
|
+
"eq",
|
|
505
|
+
"$x-amz-credential",
|
|
506
|
+
credential
|
|
507
|
+
],
|
|
508
|
+
[
|
|
509
|
+
"eq",
|
|
510
|
+
"$x-amz-date",
|
|
511
|
+
date.dateTime
|
|
512
|
+
]
|
|
513
|
+
]
|
|
514
|
+
};
|
|
515
|
+
if (this.#options.sessionToken) policy.conditions.push([
|
|
516
|
+
"eq",
|
|
517
|
+
"$x-amz-security-token",
|
|
518
|
+
this.#options.sessionToken
|
|
519
|
+
]);
|
|
520
|
+
const policyJson = JSON.stringify(policy);
|
|
521
|
+
const encodedPolicy = Buffer.from(policyJson).toString("base64");
|
|
522
|
+
const signingKey = this.#keyCache.computeIfAbsent(date, region, this.#options.accessKeyId, this.#options.secretAccessKey);
|
|
523
|
+
return {
|
|
524
|
+
url: buildRequestUrl(endpoint, bucket, region, "").toString(),
|
|
525
|
+
fields: {
|
|
526
|
+
...fields,
|
|
527
|
+
key,
|
|
528
|
+
Policy: encodedPolicy,
|
|
529
|
+
"X-Amz-Signature": signEncodedPolicy(signingKey, encodedPolicy)
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
}
|
|
459
533
|
/**
|
|
460
534
|
* Copies an object from a source to a destination.
|
|
461
535
|
* @remarks Uses [`CopyObject`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_CopyObject.html).
|
|
@@ -977,12 +1051,17 @@ var S3Client = class {
|
|
|
977
1051
|
} catch (cause) {
|
|
978
1052
|
return controller.error(new S3Error("Unknown", path, {
|
|
979
1053
|
message: "Could not parse XML error response.",
|
|
1054
|
+
status: response.statusCode,
|
|
980
1055
|
cause
|
|
981
1056
|
}));
|
|
982
1057
|
}
|
|
983
|
-
return controller.error(new S3Error(error.Error.Code || "Unknown", path, {
|
|
1058
|
+
return controller.error(new S3Error(error.Error.Code || "Unknown", path, {
|
|
1059
|
+
message: error.Error.Message || void 0,
|
|
1060
|
+
status: response.statusCode
|
|
1061
|
+
}));
|
|
984
1062
|
}, onNetworkError);
|
|
985
1063
|
return controller.error(new S3Error("Unknown", path, {
|
|
1064
|
+
status: response.statusCode,
|
|
986
1065
|
message: void 0,
|
|
987
1066
|
cause: responseText
|
|
988
1067
|
}));
|
|
@@ -995,6 +1074,17 @@ var S3Client = class {
|
|
|
995
1074
|
}
|
|
996
1075
|
});
|
|
997
1076
|
}
|
|
1077
|
+
[nodeUtil.inspect.custom](_depth, options = {}) {
|
|
1078
|
+
if (options.depth === null) options.depth = 2;
|
|
1079
|
+
options.colors ??= true;
|
|
1080
|
+
const properties = {
|
|
1081
|
+
endpoint: this.#options.endpoint,
|
|
1082
|
+
bucket: this.#options.bucket,
|
|
1083
|
+
region: this.#options.region,
|
|
1084
|
+
accessKeyId: this.#options.accessKeyId
|
|
1085
|
+
};
|
|
1086
|
+
return `S3Client ${nodeUtil.formatWithOptions(options, properties)}`;
|
|
1087
|
+
}
|
|
998
1088
|
};
|
|
999
1089
|
function buildSearchParams(amzCredential, date, expiresIn, headerList, contentHashStr, storageClass, sessionToken, acl, responseContentDisposition) {
|
|
1000
1090
|
let res = "";
|
|
@@ -1179,6 +1269,17 @@ var S3File = class S3File {
|
|
|
1179
1269
|
async copyTo(destination, options = {}) {
|
|
1180
1270
|
await this.#client.copyObject(this.#path, destination, options);
|
|
1181
1271
|
}
|
|
1272
|
+
[nodeUtil.inspect.custom](_depth, options = {}) {
|
|
1273
|
+
if (options.depth === null) options.depth = 2;
|
|
1274
|
+
options.colors ??= true;
|
|
1275
|
+
const properties = {
|
|
1276
|
+
path: this.#path,
|
|
1277
|
+
start: this.#start,
|
|
1278
|
+
end: this.#end,
|
|
1279
|
+
contentType: this.#contentType
|
|
1280
|
+
};
|
|
1281
|
+
return `S3File ${nodeUtil.formatWithOptions(options, properties)}`;
|
|
1282
|
+
}
|
|
1182
1283
|
};
|
|
1183
1284
|
|
|
1184
1285
|
//#endregion
|
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.
|
|
5
|
+
"version": "0.9.0",
|
|
6
6
|
"description": "A server-side S3 API for the regular user.",
|
|
7
7
|
"keywords": [
|
|
8
8
|
"s3",
|
|
@@ -26,12 +26,12 @@
|
|
|
26
26
|
"type": "git",
|
|
27
27
|
"url": "https://github.com/nikeee/lean-s3"
|
|
28
28
|
},
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
"default": "./dist/index.js"
|
|
32
|
-
},
|
|
33
|
-
"types": "./dist/index.d.ts",
|
|
29
|
+
"types": "./dist/index.d.mts",
|
|
30
|
+
"main": "./dist/index.mjs",
|
|
34
31
|
"type": "module",
|
|
32
|
+
"files": [
|
|
33
|
+
"dist"
|
|
34
|
+
],
|
|
35
35
|
"scripts": {
|
|
36
36
|
"build": "tsdown",
|
|
37
37
|
"test": "tsgo && tsx --test src/*.test.ts src/test/*.test.ts",
|
|
@@ -39,27 +39,28 @@
|
|
|
39
39
|
"ci": "biome ci ./src",
|
|
40
40
|
"docs": "typedoc",
|
|
41
41
|
"lint": "biome lint ./src",
|
|
42
|
-
"format": "biome
|
|
42
|
+
"format": "biome check --write ./src",
|
|
43
43
|
"prepublishOnly": "npm run build"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"fast-xml-parser": "^5.3.
|
|
46
|
+
"fast-xml-parser": "^5.3.2",
|
|
47
47
|
"undici": "^7.16.0"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@biomejs/biome": "2.3.
|
|
51
|
-
"@testcontainers/localstack": "^11.
|
|
52
|
-
"@testcontainers/minio": "^11.
|
|
53
|
-
"@
|
|
54
|
-
"@
|
|
50
|
+
"@biomejs/biome": "2.3.6",
|
|
51
|
+
"@testcontainers/localstack": "^11.8.1",
|
|
52
|
+
"@testcontainers/minio": "^11.8.1",
|
|
53
|
+
"@testcontainers/s3mock": "^11.8.1",
|
|
54
|
+
"@types/node": "^24.10.1",
|
|
55
|
+
"@typescript/native-preview": "^7.0.0-dev.20251118.1",
|
|
55
56
|
"expect": "^30.2.0",
|
|
56
|
-
"lefthook": "^2.0.
|
|
57
|
-
"testcontainers": "^11.
|
|
58
|
-
"tsdown": "^0.
|
|
57
|
+
"lefthook": "^2.0.4",
|
|
58
|
+
"testcontainers": "^11.8.1",
|
|
59
|
+
"tsdown": "^0.16.5",
|
|
59
60
|
"tsx": "^4.20.6",
|
|
60
61
|
"typedoc": "^0.28.14"
|
|
61
62
|
},
|
|
62
63
|
"engines": {
|
|
63
|
-
"node": "^20.19.
|
|
64
|
+
"node": "^20.19.5 || ^22.21.1 || ^24.11.0 || ^25.1.0"
|
|
64
65
|
}
|
|
65
66
|
}
|