lean-s3 0.2.2 → 0.3.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/dist/index.d.ts +330 -24
- package/dist/index.js +1257 -5
- package/package.json +5 -7
- package/dist/AmzDate.d.ts +0 -7
- package/dist/AmzDate.js +0 -29
- package/dist/KeyCache.d.ts +0 -5
- package/dist/KeyCache.js +0 -21
- package/dist/S3BucketEntry.d.ts +0 -19
- package/dist/S3BucketEntry.js +0 -29
- package/dist/S3Client.d.ts +0 -210
- package/dist/S3Client.js +0 -632
- package/dist/S3Error.d.ts +0 -14
- package/dist/S3Error.js +0 -15
- package/dist/S3File.d.ts +0 -87
- package/dist/S3File.js +0 -192
- package/dist/S3Stat.d.ts +0 -8
- package/dist/S3Stat.js +0 -35
- package/dist/error.d.ts +0 -4
- package/dist/error.js +0 -57
- package/dist/index.test.d.ts +0 -1
- package/dist/sign.d.ts +0 -18
- package/dist/sign.js +0 -77
- package/dist/sign.test.d.ts +0 -1
- package/dist/test-common.d.ts +0 -1
- package/dist/test.integration.d.ts +0 -1
- package/dist/test.integration.js +0 -44
- package/dist/url.d.ts +0 -9
- package/dist/url.js +0 -52
- package/dist/url.test.d.ts +0 -1
package/dist/S3File.d.ts
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import type S3Client from "./S3Client.ts";
|
|
2
|
-
import type { ByteSource } from "./index.ts";
|
|
3
|
-
import S3Stat from "./S3Stat.ts";
|
|
4
|
-
import { type OverridableS3ClientOptions } from "./S3Client.ts";
|
|
5
|
-
// TODO: If we want to hack around, we can use this to access the private implementation of the "get stream" algorithm used by Node.js's blob internally
|
|
6
|
-
// We probably have to do this some day if the fetch implementation is moved to internals.
|
|
7
|
-
// If this happens, fetch will probably use `[kHandle].getReader()` instead of .stream() to read the Blob
|
|
8
|
-
// This would break our use-case of passing an S3File as a body
|
|
9
|
-
// Using this hack would also make `.text()`, `.bytes()` etc. "just work" in every case, since these use `[kHandle]` internally as well.
|
|
10
|
-
// We now resort back into overriding text/bytes/etc. But as soon as another internal Node.js API uses this functionality, this would probably also use `[kHandle]` and bypass our data.
|
|
11
|
-
// const kHandle = Object.getOwnPropertySymbols(new Blob).find(s => s.toString() === 'Symbol(kHandle)');
|
|
12
|
-
export default class S3File {
|
|
13
|
-
#private;
|
|
14
|
-
/**
|
|
15
|
-
* @internal
|
|
16
|
-
*/
|
|
17
|
-
constructor(client: S3Client, path: string, start: number | undefined, end: number | undefined, contentType: string | undefined);
|
|
18
|
-
// TODO: slice overloads
|
|
19
|
-
slice(start?: number | undefined, end?: number | undefined, contentType?: string | undefined): S3File;
|
|
20
|
-
/**
|
|
21
|
-
* Get the stat of a file in the bucket. Uses `HEAD` request to check existence.
|
|
22
|
-
*
|
|
23
|
-
* @remarks Uses [`HeadObject`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html).
|
|
24
|
-
* @throws {S3Error} If the file does not exist or the server has some other issues.
|
|
25
|
-
* @throws {Error} If the server returns an invalid response.
|
|
26
|
-
*/
|
|
27
|
-
stat({ signal }?: Partial<S3StatOptions>): Promise<S3Stat>;
|
|
28
|
-
/**
|
|
29
|
-
* Check if a file exists in the bucket. Uses `HEAD` request to check existence.
|
|
30
|
-
*
|
|
31
|
-
* @remarks Uses [`HeadObject`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html).
|
|
32
|
-
*/
|
|
33
|
-
exists({ signal }?: Partial<S3FileExistsOptions>): Promise<boolean>;
|
|
34
|
-
/**
|
|
35
|
-
* Delete a file from the bucket.
|
|
36
|
-
*
|
|
37
|
-
* @remarks Uses [`DeleteObject`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObject.html).
|
|
38
|
-
* @remarks `versionId` not supported.
|
|
39
|
-
*
|
|
40
|
-
* @param {Partial<S3FileDeleteOptions>} [options]
|
|
41
|
-
*
|
|
42
|
-
* @example
|
|
43
|
-
* ```js
|
|
44
|
-
* // Simple delete
|
|
45
|
-
* await client.unlink("old-file.txt");
|
|
46
|
-
*
|
|
47
|
-
* // With error handling
|
|
48
|
-
* try {
|
|
49
|
-
* await client.unlink("file.dat");
|
|
50
|
-
* console.log("File deleted");
|
|
51
|
-
* } catch (err) {
|
|
52
|
-
* console.error("Delete failed:", err);
|
|
53
|
-
* }
|
|
54
|
-
* ```
|
|
55
|
-
*/
|
|
56
|
-
delete({ signal }?: Partial<S3FileDeleteOptions>): Promise<void>;
|
|
57
|
-
toString(): string;
|
|
58
|
-
/** @returns {Promise<unknown>} */
|
|
59
|
-
json(): Promise<unknown>;
|
|
60
|
-
// TODO
|
|
61
|
-
// /** @returns {Promise<Uint8Array>} */
|
|
62
|
-
// bytes() {
|
|
63
|
-
// return new Response(this.stream()).bytes(); // TODO: Does this exist?
|
|
64
|
-
// }
|
|
65
|
-
/** @returns {Promise<ArrayBuffer>} */
|
|
66
|
-
arrayBuffer(): Promise<ArrayBuffer>;
|
|
67
|
-
/** @returns {Promise<string>} */
|
|
68
|
-
text(): Promise<string>;
|
|
69
|
-
/** @returns {Promise<Blob>} */
|
|
70
|
-
blob(): Promise<Blob>;
|
|
71
|
-
/** @returns {ReadableStream<Uint8Array>} */
|
|
72
|
-
stream(): ReadableStream<Uint8Array>;
|
|
73
|
-
/**
|
|
74
|
-
* @param {ByteSource} data
|
|
75
|
-
* @returns {Promise<void>}
|
|
76
|
-
*/
|
|
77
|
-
write(data: ByteSource): Promise<void>;
|
|
78
|
-
}
|
|
79
|
-
export interface S3FileDeleteOptions extends OverridableS3ClientOptions {
|
|
80
|
-
signal: AbortSignal;
|
|
81
|
-
}
|
|
82
|
-
export interface S3StatOptions extends OverridableS3ClientOptions {
|
|
83
|
-
signal: AbortSignal;
|
|
84
|
-
}
|
|
85
|
-
export interface S3FileExistsOptions extends OverridableS3ClientOptions {
|
|
86
|
-
signal: AbortSignal;
|
|
87
|
-
}
|
package/dist/S3File.js
DELETED
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
import { Readable } from "node:stream";
|
|
2
|
-
import S3Error from "./S3Error.js";
|
|
3
|
-
import S3Stat from "./S3Stat.js";
|
|
4
|
-
import { write, stream } from "./S3Client.js";
|
|
5
|
-
import { sha256 } from "./sign.js";
|
|
6
|
-
import { fromStatusCode, getResponseError } from "./error.js";
|
|
7
|
-
// TODO: If we want to hack around, we can use this to access the private implementation of the "get stream" algorithm used by Node.js's blob internally
|
|
8
|
-
// We probably have to do this some day if the fetch implementation is moved to internals.
|
|
9
|
-
// If this happens, fetch will probably use `[kHandle].getReader()` instead of .stream() to read the Blob
|
|
10
|
-
// This would break our use-case of passing an S3File as a body
|
|
11
|
-
// Using this hack would also make `.text()`, `.bytes()` etc. "just work" in every case, since these use `[kHandle]` internally as well.
|
|
12
|
-
// We now resort back into overriding text/bytes/etc. But as soon as another internal Node.js API uses this functionality, this would probably also use `[kHandle]` and bypass our data.
|
|
13
|
-
// const kHandle = Object.getOwnPropertySymbols(new Blob).find(s => s.toString() === 'Symbol(kHandle)');
|
|
14
|
-
export default class S3File {
|
|
15
|
-
#client;
|
|
16
|
-
#path;
|
|
17
|
-
#start;
|
|
18
|
-
#end;
|
|
19
|
-
#contentType;
|
|
20
|
-
/**
|
|
21
|
-
* @internal
|
|
22
|
-
*/
|
|
23
|
-
constructor(client, path, start, end, contentType) {
|
|
24
|
-
if (typeof start === "number" && start < 0) {
|
|
25
|
-
throw new Error("Invalid slice `start`.");
|
|
26
|
-
}
|
|
27
|
-
if (typeof end === "number" &&
|
|
28
|
-
(end < 0 || (typeof start === "number" && end < start))) {
|
|
29
|
-
throw new Error("Invalid slice `end`.");
|
|
30
|
-
}
|
|
31
|
-
this.#client = client;
|
|
32
|
-
this.#path = path;
|
|
33
|
-
this.#start = start;
|
|
34
|
-
this.#end = end;
|
|
35
|
-
this.#contentType = contentType ?? "application/octet-stream";
|
|
36
|
-
}
|
|
37
|
-
// TODO: slice overloads
|
|
38
|
-
slice(start, end, contentType) {
|
|
39
|
-
return new S3File(this.#client, this.#path, start ?? undefined, end ?? undefined, contentType ?? this.#contentType);
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Get the stat of a file in the bucket. Uses `HEAD` request to check existence.
|
|
43
|
-
*
|
|
44
|
-
* @remarks Uses [`HeadObject`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html).
|
|
45
|
-
* @throws {S3Error} If the file does not exist or the server has some other issues.
|
|
46
|
-
* @throws {Error} If the server returns an invalid response.
|
|
47
|
-
*/
|
|
48
|
-
async stat({ signal } = {}) {
|
|
49
|
-
// TODO: Support all options
|
|
50
|
-
const response = await this.#client._signedRequest("HEAD", this.#path, undefined, undefined, undefined, undefined, undefined, undefined, signal);
|
|
51
|
-
// Heads don't have a body, but we still need to consume it to avoid leaks
|
|
52
|
-
await response.body.dump();
|
|
53
|
-
if (200 <= response.statusCode && response.statusCode < 300) {
|
|
54
|
-
const result = S3Stat.tryParseFromHeaders(response.headers);
|
|
55
|
-
if (!result) {
|
|
56
|
-
throw new Error("S3 server returned an invalid response for `HeadObject`");
|
|
57
|
-
}
|
|
58
|
-
return result;
|
|
59
|
-
}
|
|
60
|
-
throw (fromStatusCode(response.statusCode, this.#path) ??
|
|
61
|
-
new Error(`S3 server returned an unsupported status code for \`HeadObject\`: ${response.statusCode}`));
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Check if a file exists in the bucket. Uses `HEAD` request to check existence.
|
|
65
|
-
*
|
|
66
|
-
* @remarks Uses [`HeadObject`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html).
|
|
67
|
-
*/
|
|
68
|
-
async exists({ signal, } = {}) {
|
|
69
|
-
// TODO: Support all options
|
|
70
|
-
const response = await this.#client._signedRequest("HEAD", this.#path, undefined, undefined, undefined, undefined, undefined, undefined, signal);
|
|
71
|
-
// Heads don't have a body, but we still need to consume it to avoid leaks
|
|
72
|
-
await response.body.dump();
|
|
73
|
-
if (200 <= response.statusCode && response.statusCode < 300) {
|
|
74
|
-
return true;
|
|
75
|
-
}
|
|
76
|
-
if (response.statusCode === 404) {
|
|
77
|
-
return false;
|
|
78
|
-
}
|
|
79
|
-
throw (fromStatusCode(response.statusCode, this.#path) ??
|
|
80
|
-
new Error(`S3 server returned an unsupported status code for \`HeadObject\`: ${response.statusCode}`));
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Delete a file from the bucket.
|
|
84
|
-
*
|
|
85
|
-
* @remarks Uses [`DeleteObject`](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObject.html).
|
|
86
|
-
* @remarks `versionId` not supported.
|
|
87
|
-
*
|
|
88
|
-
* @param {Partial<S3FileDeleteOptions>} [options]
|
|
89
|
-
*
|
|
90
|
-
* @example
|
|
91
|
-
* ```js
|
|
92
|
-
* // Simple delete
|
|
93
|
-
* await client.unlink("old-file.txt");
|
|
94
|
-
*
|
|
95
|
-
* // With error handling
|
|
96
|
-
* try {
|
|
97
|
-
* await client.unlink("file.dat");
|
|
98
|
-
* console.log("File deleted");
|
|
99
|
-
* } catch (err) {
|
|
100
|
-
* console.error("Delete failed:", err);
|
|
101
|
-
* }
|
|
102
|
-
* ```
|
|
103
|
-
*/
|
|
104
|
-
async delete({ signal } = {}) {
|
|
105
|
-
// TODO: Support all options
|
|
106
|
-
const response = await this.#client._signedRequest("DELETE", this.#path, undefined, undefined, undefined, undefined, undefined, undefined, signal);
|
|
107
|
-
if (response.statusCode === 204) {
|
|
108
|
-
await response.body.dump(); // Consume the body to avoid leaks
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
throw await getResponseError(response, this.#path);
|
|
112
|
-
}
|
|
113
|
-
toString() {
|
|
114
|
-
return `S3File { path: "${this.#path}" }`;
|
|
115
|
-
}
|
|
116
|
-
/** @returns {Promise<unknown>} */
|
|
117
|
-
json() {
|
|
118
|
-
// Not using JSON.parse(await this.text()), so the env can parse json while loading
|
|
119
|
-
// Also, see TODO note above this class
|
|
120
|
-
return new Response(this.stream()).json();
|
|
121
|
-
}
|
|
122
|
-
// TODO
|
|
123
|
-
// /** @returns {Promise<Uint8Array>} */
|
|
124
|
-
// bytes() {
|
|
125
|
-
// return new Response(this.stream()).bytes(); // TODO: Does this exist?
|
|
126
|
-
// }
|
|
127
|
-
/** @returns {Promise<ArrayBuffer>} */
|
|
128
|
-
arrayBuffer() {
|
|
129
|
-
return new Response(this.stream()).arrayBuffer();
|
|
130
|
-
}
|
|
131
|
-
/** @returns {Promise<string>} */
|
|
132
|
-
text() {
|
|
133
|
-
return new Response(this.stream()).text();
|
|
134
|
-
}
|
|
135
|
-
/** @returns {Promise<Blob>} */
|
|
136
|
-
blob() {
|
|
137
|
-
return new Response(this.stream()).blob();
|
|
138
|
-
}
|
|
139
|
-
/** @returns {ReadableStream<Uint8Array>} */
|
|
140
|
-
stream() {
|
|
141
|
-
// This function is called for every operation on the blob
|
|
142
|
-
return this.#client[stream](this.#path, undefined, this.#start, this.#end);
|
|
143
|
-
}
|
|
144
|
-
async #transformData(data) {
|
|
145
|
-
if (typeof data === "string") {
|
|
146
|
-
const binary = new TextEncoder();
|
|
147
|
-
const bytes = binary.encode(data);
|
|
148
|
-
return [
|
|
149
|
-
bytes,
|
|
150
|
-
bytes.byteLength,
|
|
151
|
-
sha256(bytes), // TODO: Maybe use some streaming to compute hash while encoding?
|
|
152
|
-
];
|
|
153
|
-
}
|
|
154
|
-
if (data instanceof Blob) {
|
|
155
|
-
const bytes = await data.bytes();
|
|
156
|
-
return [
|
|
157
|
-
bytes,
|
|
158
|
-
bytes.byteLength,
|
|
159
|
-
sha256(bytes), // TODO: Maybe use some streaming to compute hash while encoding?
|
|
160
|
-
];
|
|
161
|
-
}
|
|
162
|
-
if (data instanceof Readable) {
|
|
163
|
-
return [data, undefined, undefined];
|
|
164
|
-
}
|
|
165
|
-
if (data instanceof ArrayBuffer ||
|
|
166
|
-
data instanceof SharedArrayBuffer ||
|
|
167
|
-
ArrayBuffer.isView(data)) {
|
|
168
|
-
// TODO: Support hashing
|
|
169
|
-
return [
|
|
170
|
-
data,
|
|
171
|
-
data.byteLength,
|
|
172
|
-
undefined, // TODO: Compute hash some day
|
|
173
|
-
];
|
|
174
|
-
}
|
|
175
|
-
assertNever(data);
|
|
176
|
-
}
|
|
177
|
-
/**
|
|
178
|
-
* @param {ByteSource} data
|
|
179
|
-
* @returns {Promise<void>}
|
|
180
|
-
*/
|
|
181
|
-
async write(data) {
|
|
182
|
-
/** @type {AbortSignal | undefined} */
|
|
183
|
-
const signal = undefined; // TODO: Take this as param
|
|
184
|
-
// TODO: Support S3File as input and maybe use CopyObject
|
|
185
|
-
// TODO: Support Request and Response as input?
|
|
186
|
-
const [bytes, length, hash] = await this.#transformData(data);
|
|
187
|
-
return await this.#client[write](this.#path, bytes, this.#contentType, length, hash, this.#start, this.#end, signal);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
function assertNever(v) {
|
|
191
|
-
throw new TypeError(`Expected value not to have type ${typeof v}`);
|
|
192
|
-
}
|
package/dist/S3Stat.d.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
export default class S3Stat {
|
|
2
|
-
readonly etag: string;
|
|
3
|
-
readonly lastModified: Date;
|
|
4
|
-
readonly size: number;
|
|
5
|
-
readonly type: string;
|
|
6
|
-
constructor(etag: string, lastModified: Date, size: number, type: string);
|
|
7
|
-
static tryParseFromHeaders(headers: Record<string, string | string[] | undefined>): S3Stat | undefined;
|
|
8
|
-
}
|
package/dist/S3Stat.js
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
export default class S3Stat {
|
|
2
|
-
etag;
|
|
3
|
-
lastModified;
|
|
4
|
-
size;
|
|
5
|
-
type;
|
|
6
|
-
constructor(etag, lastModified, size, type) {
|
|
7
|
-
this.etag = etag;
|
|
8
|
-
this.lastModified = lastModified;
|
|
9
|
-
this.size = size;
|
|
10
|
-
this.type = type;
|
|
11
|
-
}
|
|
12
|
-
static tryParseFromHeaders(headers) {
|
|
13
|
-
const lm = headers["last-modified"];
|
|
14
|
-
if (lm === null || typeof lm !== "string") {
|
|
15
|
-
return undefined;
|
|
16
|
-
}
|
|
17
|
-
const etag = headers.etag;
|
|
18
|
-
if (etag === null || typeof etag !== "string") {
|
|
19
|
-
return undefined;
|
|
20
|
-
}
|
|
21
|
-
const cl = headers["content-length"];
|
|
22
|
-
if (cl === null) {
|
|
23
|
-
return undefined;
|
|
24
|
-
}
|
|
25
|
-
const size = Number(cl);
|
|
26
|
-
if (!Number.isSafeInteger(size)) {
|
|
27
|
-
return undefined;
|
|
28
|
-
}
|
|
29
|
-
const ct = headers["content-type"];
|
|
30
|
-
if (ct === null || typeof ct !== "string") {
|
|
31
|
-
return undefined;
|
|
32
|
-
}
|
|
33
|
-
return new S3Stat(etag, new Date(lm), size, ct);
|
|
34
|
-
}
|
|
35
|
-
}
|
package/dist/error.d.ts
DELETED
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import type { Dispatcher } from "undici";
|
|
2
|
-
import S3Error from "./S3Error.ts";
|
|
3
|
-
export declare function getResponseError(response: Dispatcher.ResponseData<unknown>, path: string): Promise<S3Error>;
|
|
4
|
-
export declare function fromStatusCode(code: number, path: string): S3Error | undefined;
|
package/dist/error.js
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { XMLParser } from "fast-xml-parser";
|
|
2
|
-
import S3Error from "./S3Error.js";
|
|
3
|
-
const xmlParser = new XMLParser();
|
|
4
|
-
export async function getResponseError(response, path) {
|
|
5
|
-
let body = undefined;
|
|
6
|
-
try {
|
|
7
|
-
body = await response.body.text();
|
|
8
|
-
}
|
|
9
|
-
catch (cause) {
|
|
10
|
-
return new S3Error("Unknown", path, {
|
|
11
|
-
message: "Could not read response body.",
|
|
12
|
-
cause,
|
|
13
|
-
});
|
|
14
|
-
}
|
|
15
|
-
if (response.headers["content-type"] === "application/xml") {
|
|
16
|
-
return parseAndGetXmlError(body, path);
|
|
17
|
-
}
|
|
18
|
-
return new S3Error("Unknown", path, {
|
|
19
|
-
message: "Unknown error during S3 request.",
|
|
20
|
-
});
|
|
21
|
-
}
|
|
22
|
-
export function fromStatusCode(code, path) {
|
|
23
|
-
switch (code) {
|
|
24
|
-
case 404:
|
|
25
|
-
return new S3Error("NoSuchKey", path, {
|
|
26
|
-
message: "The specified key does not exist.",
|
|
27
|
-
});
|
|
28
|
-
case 403:
|
|
29
|
-
return new S3Error("AccessDenied", path, {
|
|
30
|
-
message: "Access denied to the key.",
|
|
31
|
-
});
|
|
32
|
-
// TODO: Add more status codes as needed
|
|
33
|
-
default:
|
|
34
|
-
return undefined;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
function parseAndGetXmlError(body, path) {
|
|
38
|
-
let error = undefined;
|
|
39
|
-
try {
|
|
40
|
-
error = xmlParser.parse(body);
|
|
41
|
-
}
|
|
42
|
-
catch (cause) {
|
|
43
|
-
return new S3Error("Unknown", path, {
|
|
44
|
-
message: "Could not parse XML error response.",
|
|
45
|
-
cause,
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
if (error.Error) {
|
|
49
|
-
const e = error.Error;
|
|
50
|
-
return new S3Error(e.Code || "Unknown", path, {
|
|
51
|
-
message: e.Message || undefined, // Message might be "",
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
return new S3Error(error.Code || "Unknown", path, {
|
|
55
|
-
message: error.Message || undefined, // Message might be "",
|
|
56
|
-
});
|
|
57
|
-
}
|
package/dist/index.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/sign.d.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { type BinaryLike } from "node:crypto";
|
|
2
|
-
import type { AmzDate } from "./AmzDate.ts";
|
|
3
|
-
import type { HttpMethod, PresignableHttpMethod } from "./index.ts";
|
|
4
|
-
// Spec:
|
|
5
|
-
// https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
|
|
6
|
-
export declare function deriveSigningKey(date: string, region: string, secretAccessKey: string): Buffer;
|
|
7
|
-
export declare function signCanonicalDataHash(signinKey: Buffer, canonicalDataHash: string, date: AmzDate, region: string): string;
|
|
8
|
-
export declare const unsignedPayload = "UNSIGNED-PAYLOAD";
|
|
9
|
-
/**
|
|
10
|
-
* Same as {@see createCanonicalDataDigest}, but only sets the `host` header and the content hash to `UNSIGNED-PAYLOAD`.
|
|
11
|
-
*
|
|
12
|
-
* Used for pre-signing only. Pre-signed URLs [cannot contain content hashes](https://github.com/aws/aws-sdk-js/blob/966fa6c316dbb11ca9277564ff7120e6b16467f4/lib/signers/v4.js#L182-L183)
|
|
13
|
-
* and the only header that is signed is `host`. So we can use an optimized version for that.
|
|
14
|
-
*/
|
|
15
|
-
export declare function createCanonicalDataDigestHostOnly(method: PresignableHttpMethod, path: string, query: string, host: string): string;
|
|
16
|
-
export declare function createCanonicalDataDigest(method: HttpMethod, path: string, query: string, sortedHeaders: Record<string, string>, contentHashStr: string): string;
|
|
17
|
-
export declare function sha256(data: BinaryLike): Buffer;
|
|
18
|
-
export declare function md5Base64(data: BinaryLike): string;
|
package/dist/sign.js
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import { createHmac, createHash } from "node:crypto";
|
|
2
|
-
// Spec:
|
|
3
|
-
// https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
|
|
4
|
-
export function deriveSigningKey(date, region, secretAccessKey) {
|
|
5
|
-
const key = `AWS4${secretAccessKey}`;
|
|
6
|
-
const signedDate = createHmac("sha256", key).update(date).digest();
|
|
7
|
-
const signedDateRegion = createHmac("sha256", signedDate)
|
|
8
|
-
.update(region)
|
|
9
|
-
.digest();
|
|
10
|
-
const signedDateRegionService = createHmac("sha256", signedDateRegion)
|
|
11
|
-
.update("s3")
|
|
12
|
-
.digest();
|
|
13
|
-
return createHmac("sha256", signedDateRegionService)
|
|
14
|
-
.update("aws4_request")
|
|
15
|
-
.digest();
|
|
16
|
-
}
|
|
17
|
-
export function signCanonicalDataHash(signinKey, canonicalDataHash, date, region) {
|
|
18
|
-
// it is actually faster to pass a single large string instead of doing multiple .update() chains with the parameters
|
|
19
|
-
// see `benchmark-operations.js`
|
|
20
|
-
return createHmac("sha256", signinKey)
|
|
21
|
-
.update(`AWS4-HMAC-SHA256\n${date.dateTime}\n${date.date}/${region}/s3/aws4_request\n${canonicalDataHash}`)
|
|
22
|
-
.digest("hex");
|
|
23
|
-
}
|
|
24
|
-
export const unsignedPayload = "UNSIGNED-PAYLOAD";
|
|
25
|
-
/**
|
|
26
|
-
* Same as {@see createCanonicalDataDigest}, but only sets the `host` header and the content hash to `UNSIGNED-PAYLOAD`.
|
|
27
|
-
*
|
|
28
|
-
* Used for pre-signing only. Pre-signed URLs [cannot contain content hashes](https://github.com/aws/aws-sdk-js/blob/966fa6c316dbb11ca9277564ff7120e6b16467f4/lib/signers/v4.js#L182-L183)
|
|
29
|
-
* and the only header that is signed is `host`. So we can use an optimized version for that.
|
|
30
|
-
*/
|
|
31
|
-
export function createCanonicalDataDigestHostOnly(method, path, query, host) {
|
|
32
|
-
// it is actually faster to pass a single large string instead of doing multiple .update() chains with the parameters
|
|
33
|
-
// see `benchmark-operations.js`
|
|
34
|
-
return createHash("sha256")
|
|
35
|
-
.update(`${method}\n${path}\n${query}\nhost:${host}\n\nhost\nUNSIGNED-PAYLOAD`)
|
|
36
|
-
.digest("hex");
|
|
37
|
-
}
|
|
38
|
-
export function createCanonicalDataDigest(method, path, query, sortedHeaders, contentHashStr) {
|
|
39
|
-
// Use this for debugging
|
|
40
|
-
/*
|
|
41
|
-
const xHash = {
|
|
42
|
-
h: createHash("sha256"),
|
|
43
|
-
m: "",
|
|
44
|
-
update(v) {
|
|
45
|
-
this.m += v;
|
|
46
|
-
this.h.update(v);
|
|
47
|
-
return this;
|
|
48
|
-
},
|
|
49
|
-
digest(v) {
|
|
50
|
-
if (this.m.includes("continuation-token")) console.log(this.m);
|
|
51
|
-
return this.h.digest(v);
|
|
52
|
-
},
|
|
53
|
-
};
|
|
54
|
-
*/
|
|
55
|
-
const sortedHeaderNames = Object.keys(sortedHeaders);
|
|
56
|
-
// it is actually faster to pass a single large string instead of doing multiple .update() chains with the parameters
|
|
57
|
-
// see `benchmark-operations.js`
|
|
58
|
-
let canonData = `${method}\n${path}\n${query}\n`;
|
|
59
|
-
for (const header of sortedHeaderNames) {
|
|
60
|
-
canonData += `${header}:${sortedHeaders[header]}\n`;
|
|
61
|
-
}
|
|
62
|
-
canonData += "\n";
|
|
63
|
-
// emit the first header without ";", so we can avoid branching inside the loop for the other headers
|
|
64
|
-
// this is just a version of `sortedHeaderList.join(";")` that seems about 2x faster (see `benchmark-operations.js`)
|
|
65
|
-
canonData += sortedHeaderNames.length > 0 ? sortedHeaderNames[0] : "";
|
|
66
|
-
for (let i = 1; i < sortedHeaderNames.length; ++i) {
|
|
67
|
-
canonData += `;${sortedHeaderNames[i]}`;
|
|
68
|
-
}
|
|
69
|
-
canonData += `\n${contentHashStr}`;
|
|
70
|
-
return createHash("sha256").update(canonData).digest("hex");
|
|
71
|
-
}
|
|
72
|
-
export function sha256(data) {
|
|
73
|
-
return createHash("sha256").update(data).digest();
|
|
74
|
-
}
|
|
75
|
-
export function md5Base64(data) {
|
|
76
|
-
return createHash("md5").update(data).digest("base64");
|
|
77
|
-
}
|
package/dist/sign.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/test-common.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function runTests(runId: number, endpoint: string, accessKeyId: string, secretAccessKey: string, region: string, bucket: string): void;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/test.integration.js
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
import { describe, before, after } from "node:test";
|
|
3
|
-
import { expect } from "expect";
|
|
4
|
-
import { runTests } from "./test-common.js";
|
|
5
|
-
import { S3Client } from "./index.js";
|
|
6
|
-
const env = process.env;
|
|
7
|
-
const runId = Date.now();
|
|
8
|
-
for (const provider of ["hetzner", "aws", "cloudflare"]) {
|
|
9
|
-
describe(`integration with ${provider}@runId:${runId}`, () => {
|
|
10
|
-
const p = provider.toUpperCase();
|
|
11
|
-
const endpoint = env[`${p}_S3_ENDPOINT`];
|
|
12
|
-
const region = env[`${p}_S3_REGION`];
|
|
13
|
-
const bucket = env[`${p}_S3_BUCKET`];
|
|
14
|
-
const accessKeyId = env[`${p}_S3_ACCESS_KEY_ID`];
|
|
15
|
-
const secretAccessKey = env[`${p}_S3_SECRET_KEY`];
|
|
16
|
-
if (!endpoint || !region || !bucket || !accessKeyId || !secretAccessKey) {
|
|
17
|
-
throw new Error("Invalid config");
|
|
18
|
-
}
|
|
19
|
-
{
|
|
20
|
-
const client = new S3Client({
|
|
21
|
-
endpoint,
|
|
22
|
-
accessKeyId,
|
|
23
|
-
secretAccessKey,
|
|
24
|
-
region,
|
|
25
|
-
bucket,
|
|
26
|
-
});
|
|
27
|
-
before(async () => {
|
|
28
|
-
expect(await client.bucketExists(bucket)).toBe(true);
|
|
29
|
-
const objects = (await client.list({ prefix: `${runId}/` })).contents;
|
|
30
|
-
expect(objects.length).toBe(0);
|
|
31
|
-
});
|
|
32
|
-
after(async () => {
|
|
33
|
-
expect(await client.bucketExists(bucket)).toBe(true);
|
|
34
|
-
const objects = (await client.list({ prefix: `${runId}/`, maxKeys: 1000 })).contents;
|
|
35
|
-
// clean up after all tests, but we want to fail because there are still objects
|
|
36
|
-
if (objects.length > 0) {
|
|
37
|
-
await client.deleteObjects(objects);
|
|
38
|
-
}
|
|
39
|
-
expect(objects.length).toBe(0);
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
runTests(runId, endpoint, accessKeyId, secretAccessKey, region, bucket);
|
|
43
|
-
});
|
|
44
|
-
}
|
package/dist/url.d.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
export declare function buildRequestUrl(endpoint: string, bucket: string, region: string, path: string): URL;
|
|
2
|
-
/**
|
|
3
|
-
* Sorts headers alphabetically. Removes headers that are undefined/null.
|
|
4
|
-
*
|
|
5
|
-
* `http.request` doesn't allow passing `undefined` as header values (despite the types allowing it),
|
|
6
|
-
* so we have to filter afterwards.
|
|
7
|
-
*/
|
|
8
|
-
export declare function prepareHeadersForSigning(unfilteredHeadersUnsorted: Record<string, string | undefined>): Record<string, string>;
|
|
9
|
-
export declare function getRangeHeader(start: number | undefined, endExclusive: number | undefined): string | undefined;
|
package/dist/url.js
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
export function buildRequestUrl(endpoint, bucket, region, path) {
|
|
2
|
-
const normalizedBucket = normalizePath(bucket);
|
|
3
|
-
const [endpointWithBucketAndRegion, replacedBucket] = replaceDomainPlaceholders(endpoint, normalizedBucket, region);
|
|
4
|
-
const result = new URL(endpointWithBucketAndRegion);
|
|
5
|
-
const pathPrefix = result.pathname.endsWith("/")
|
|
6
|
-
? result.pathname
|
|
7
|
-
: `${result.pathname}/`;
|
|
8
|
-
const pathSuffix = replacedBucket
|
|
9
|
-
? normalizePath(path)
|
|
10
|
-
: `${normalizedBucket}/${normalizePath(path)}`;
|
|
11
|
-
result.pathname = pathPrefix + pathSuffix;
|
|
12
|
-
return result;
|
|
13
|
-
}
|
|
14
|
-
function replaceDomainPlaceholders(endpoint, bucket, region) {
|
|
15
|
-
const replacedBucket = endpoint.includes("{bucket}");
|
|
16
|
-
return [
|
|
17
|
-
endpoint.replaceAll("{bucket}", bucket).replaceAll("{region}", region),
|
|
18
|
-
replacedBucket,
|
|
19
|
-
];
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* Removes trailing and leading slash.
|
|
23
|
-
*/
|
|
24
|
-
function normalizePath(path) {
|
|
25
|
-
const start = path[0] === "/" ? 1 : 0;
|
|
26
|
-
const end = path[path.length - 1] === "/" ? path.length - 1 : path.length;
|
|
27
|
-
return path.substring(start, end);
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Sorts headers alphabetically. Removes headers that are undefined/null.
|
|
31
|
-
*
|
|
32
|
-
* `http.request` doesn't allow passing `undefined` as header values (despite the types allowing it),
|
|
33
|
-
* so we have to filter afterwards.
|
|
34
|
-
*/
|
|
35
|
-
export function prepareHeadersForSigning(unfilteredHeadersUnsorted) {
|
|
36
|
-
const result = {};
|
|
37
|
-
// TODO: `Object.keys(headersUnsorted).sort()` is constant in our case,
|
|
38
|
-
// maybe we want to move this somewhere else to avoid sorting every time
|
|
39
|
-
for (const header of Object.keys(unfilteredHeadersUnsorted).sort()) {
|
|
40
|
-
const v = unfilteredHeadersUnsorted[header];
|
|
41
|
-
if (v !== undefined && v !== null) {
|
|
42
|
-
result[header] = v;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return result;
|
|
46
|
-
}
|
|
47
|
-
export function getRangeHeader(start, endExclusive) {
|
|
48
|
-
return typeof start === "number" || typeof endExclusive === "number"
|
|
49
|
-
? // Http-ranges are end-inclusive, we are exclusiv ein our slice
|
|
50
|
-
`bytes=${start ?? 0}-${typeof endExclusive === "number" ? endExclusive - 1 : ""}`
|
|
51
|
-
: undefined;
|
|
52
|
-
}
|
package/dist/url.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|