@velajs/storage 0.3.1 → 0.3.2
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/CHANGELOG.md +6 -1
- package/dist/body-BMUcBeQ0.js +93 -0
- package/dist/body-BMUcBeQ0.js.map +1 -0
- package/dist/client/index.d.ts +49 -46
- package/dist/client/index.js +207 -228
- package/dist/client/index.js.map +1 -0
- package/dist/drivers/memory/index.d.ts +17 -13
- package/dist/drivers/memory/index.js +190 -197
- package/dist/drivers/memory/index.js.map +1 -0
- package/dist/drivers/r2/index.d.ts +7 -4
- package/dist/drivers/r2/index.js +162 -160
- package/dist/drivers/r2/index.js.map +1 -0
- package/dist/drivers/r2-http/index.d.ts +23 -19
- package/dist/drivers/r2-http/index.js +52 -45
- package/dist/drivers/r2-http/index.js.map +1 -0
- package/dist/drivers/s3/index.d.ts +79 -5
- package/dist/drivers/s3/index.js +2 -3
- package/dist/index.d.ts +271 -17
- package/dist/index.js +1078 -16
- package/dist/index.js.map +1 -0
- package/dist/middleware/index.d.ts +177 -15
- package/dist/middleware/index.js +553 -8
- package/dist/middleware/index.js.map +1 -0
- package/dist/protocol.types-C5PPBD12.d.ts +80 -0
- package/dist/r2.types-Bw4Ft83t.d.ts +71 -0
- package/dist/retry-DXtO6R9D.js +119 -0
- package/dist/retry-DXtO6R9D.js.map +1 -0
- package/dist/s3-DrE9NrnD.js +557 -0
- package/dist/s3-DrE9NrnD.js.map +1 -0
- package/dist/storage.error-DVCOkJhc.js +49 -0
- package/dist/storage.error-DVCOkJhc.js.map +1 -0
- package/dist/storage.types-BJN7Yp1J.d.ts +288 -0
- package/dist/storagesdk/index.d.ts +73 -69
- package/dist/storagesdk/index.js +117 -130
- package/dist/storagesdk/index.js.map +1 -0
- package/dist/stored-file-2Yl07eRc.js +105 -0
- package/dist/stored-file-2Yl07eRc.js.map +1 -0
- package/dist/testing/index.d.ts +12 -8
- package/dist/testing/index.js +36 -53
- package/dist/testing/index.js.map +1 -0
- package/package.json +62 -48
- package/dist/base64.d.ts +0 -8
- package/dist/base64.js +0 -25
- package/dist/decorators/inject-storage.decorator.d.ts +0 -17
- package/dist/decorators/inject-storage.decorator.js +0 -21
- package/dist/drivers/r2/r2.types.d.ts +0 -67
- package/dist/drivers/r2/r2.types.js +0 -5
- package/dist/drivers/s3/s3-client.d.ts +0 -33
- package/dist/drivers/s3/s3-client.js +0 -154
- package/dist/drivers/s3/s3.driver.d.ts +0 -4
- package/dist/drivers/s3/s3.driver.js +0 -407
- package/dist/drivers/s3/s3.types.d.ts +0 -28
- package/dist/drivers/s3/s3.types.js +0 -1
- package/dist/drivers/s3/xml.d.ts +0 -6
- package/dist/drivers/s3/xml.js +0 -52
- package/dist/http/authorizer.types.d.ts +0 -59
- package/dist/http/authorizer.types.js +0 -1
- package/dist/http/http-helpers.d.ts +0 -22
- package/dist/http/http-helpers.js +0 -80
- package/dist/http/protocol.types.d.ts +0 -76
- package/dist/http/protocol.types.js +0 -1
- package/dist/internal/body.d.ts +0 -18
- package/dist/internal/body.js +0 -90
- package/dist/internal/retry.d.ts +0 -41
- package/dist/internal/retry.js +0 -127
- package/dist/internal/stored-file.d.ts +0 -27
- package/dist/internal/stored-file.js +0 -114
- package/dist/middleware/cache.d.ts +0 -50
- package/dist/middleware/cache.js +0 -101
- package/dist/middleware/compose.d.ts +0 -12
- package/dist/middleware/compose.js +0 -11
- package/dist/middleware/compression.d.ts +0 -16
- package/dist/middleware/compression.js +0 -101
- package/dist/middleware/encryption.d.ts +0 -14
- package/dist/middleware/encryption.js +0 -134
- package/dist/middleware/failover.d.ts +0 -18
- package/dist/middleware/failover.js +0 -52
- package/dist/middleware/retry.d.ts +0 -14
- package/dist/middleware/retry.js +0 -47
- package/dist/middleware/versioning.d.ts +0 -36
- package/dist/middleware/versioning.js +0 -89
- package/dist/middleware/wrap.d.ts +0 -11
- package/dist/middleware/wrap.js +0 -46
- package/dist/object-key.d.ts +0 -17
- package/dist/object-key.js +0 -42
- package/dist/storage.controller.d.ts +0 -21
- package/dist/storage.controller.js +0 -386
- package/dist/storage.error.d.ts +0 -38
- package/dist/storage.error.js +0 -52
- package/dist/storage.facade.d.ts +0 -47
- package/dist/storage.facade.js +0 -443
- package/dist/storage.module.d.ts +0 -46
- package/dist/storage.module.js +0 -113
- package/dist/storage.service.d.ts +0 -42
- package/dist/storage.service.js +0 -86
- package/dist/storage.tokens.d.ts +0 -13
- package/dist/storage.tokens.js +0 -26
- package/dist/storage.types.d.ts +0 -245
- package/dist/storage.types.js +0 -1
package/package.json
CHANGED
|
@@ -1,8 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velajs/storage",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Edge-first, driver-based object/file storage for the Vela framework",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cloudflare-workers",
|
|
7
|
+
"edge",
|
|
8
|
+
"framework",
|
|
9
|
+
"multipart",
|
|
10
|
+
"object-storage",
|
|
11
|
+
"presigned-url",
|
|
12
|
+
"r2",
|
|
13
|
+
"s3",
|
|
14
|
+
"storage",
|
|
15
|
+
"vela"
|
|
16
|
+
],
|
|
17
|
+
"homepage": "https://github.com/velajs/storage#readme",
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/velajs/storage/issues"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"author": "ksh",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/velajs/storage.git"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE",
|
|
31
|
+
"CHANGELOG.md"
|
|
32
|
+
],
|
|
5
33
|
"type": "module",
|
|
34
|
+
"sideEffects": false,
|
|
6
35
|
"main": "./dist/index.js",
|
|
7
36
|
"types": "./dist/index.d.ts",
|
|
8
37
|
"exports": {
|
|
@@ -43,68 +72,53 @@
|
|
|
43
72
|
"import": "./dist/testing/index.js"
|
|
44
73
|
}
|
|
45
74
|
},
|
|
46
|
-
"files": [
|
|
47
|
-
"dist",
|
|
48
|
-
"README.md",
|
|
49
|
-
"LICENSE",
|
|
50
|
-
"CHANGELOG.md"
|
|
51
|
-
],
|
|
52
|
-
"sideEffects": false,
|
|
53
|
-
"keywords": [
|
|
54
|
-
"vela",
|
|
55
|
-
"storage",
|
|
56
|
-
"object-storage",
|
|
57
|
-
"s3",
|
|
58
|
-
"r2",
|
|
59
|
-
"presigned-url",
|
|
60
|
-
"multipart",
|
|
61
|
-
"edge",
|
|
62
|
-
"cloudflare-workers",
|
|
63
|
-
"framework"
|
|
64
|
-
],
|
|
65
|
-
"author": "ksh",
|
|
66
|
-
"license": "MIT",
|
|
67
|
-
"repository": {
|
|
68
|
-
"type": "git",
|
|
69
|
-
"url": "git+https://github.com/velajs/storage.git"
|
|
70
|
-
},
|
|
71
|
-
"homepage": "https://github.com/velajs/storage#readme",
|
|
72
|
-
"bugs": {
|
|
73
|
-
"url": "https://github.com/velajs/storage/issues"
|
|
74
|
-
},
|
|
75
|
-
"engines": {
|
|
76
|
-
"node": ">=20"
|
|
77
|
-
},
|
|
78
75
|
"dependencies": {
|
|
79
76
|
"aws4fetch": "^1.0.20"
|
|
80
77
|
},
|
|
81
|
-
"peerDependencies": {
|
|
82
|
-
"@velajs/vela": ">=1.11.0",
|
|
83
|
-
"hono": ">=4",
|
|
84
|
-
"@cloudflare/workers-types": ">=4"
|
|
85
|
-
},
|
|
86
|
-
"peerDependenciesMeta": {
|
|
87
|
-
"@cloudflare/workers-types": {
|
|
88
|
-
"optional": true
|
|
89
|
-
}
|
|
90
|
-
},
|
|
91
78
|
"devDependencies": {
|
|
79
|
+
"@arethetypeswrong/cli": "^0.18.5",
|
|
80
|
+
"@changesets/cli": "^2.31.0",
|
|
92
81
|
"@cloudflare/vitest-pool-workers": "^0.16.18",
|
|
93
82
|
"@cloudflare/workers-types": "^4.20260624.1",
|
|
94
|
-
"@swc/cli": "^0.8.1",
|
|
95
83
|
"@swc/core": "^1.15.43",
|
|
96
84
|
"@velajs/testing": "^0.4.0",
|
|
97
85
|
"@velajs/vela": "^1.12.0",
|
|
98
86
|
"aws4fetch": "^1.0.20",
|
|
99
87
|
"hono": "^4.12.27",
|
|
100
|
-
"
|
|
88
|
+
"oxfmt": "^0.58.0",
|
|
89
|
+
"oxlint": "^1.73.0",
|
|
90
|
+
"publint": "^0.3.21",
|
|
91
|
+
"tsdown": "^0.22.4",
|
|
92
|
+
"typescript": "^7.0.2",
|
|
101
93
|
"unplugin-swc": "^1.5.9",
|
|
102
|
-
"vitest": "^4.1.
|
|
94
|
+
"vitest": "^4.1.10"
|
|
95
|
+
},
|
|
96
|
+
"peerDependencies": {
|
|
97
|
+
"@cloudflare/workers-types": ">=4",
|
|
98
|
+
"@velajs/vela": ">=1.11.0",
|
|
99
|
+
"hono": ">=4"
|
|
100
|
+
},
|
|
101
|
+
"peerDependenciesMeta": {
|
|
102
|
+
"@cloudflare/workers-types": {
|
|
103
|
+
"optional": true
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
"engines": {
|
|
107
|
+
"node": ">=24"
|
|
103
108
|
},
|
|
104
109
|
"scripts": {
|
|
105
|
-
"build": "
|
|
110
|
+
"build": "tsdown",
|
|
106
111
|
"test": "vitest run",
|
|
107
112
|
"test:workers": "vitest run --config vitest.config.workers.ts",
|
|
108
|
-
"typecheck": "tsc --noEmit"
|
|
113
|
+
"typecheck": "tsc --noEmit",
|
|
114
|
+
"lint": "oxlint .",
|
|
115
|
+
"format": "oxfmt .",
|
|
116
|
+
"format:check": "oxfmt --check .",
|
|
117
|
+
"publint": "publint",
|
|
118
|
+
"attw": "attw --pack . --profile esm-only",
|
|
119
|
+
"changeset": "changeset",
|
|
120
|
+
"version-packages": "changeset version",
|
|
121
|
+
"release": "pnpm build && changeset publish",
|
|
122
|
+
"verify": "pnpm lint && pnpm format:check && pnpm build && pnpm typecheck && pnpm test && pnpm test:workers && pnpm publint && pnpm attw"
|
|
109
123
|
}
|
|
110
124
|
}
|
package/dist/base64.d.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
/** Encode bytes to standard base64. */
|
|
2
|
-
export declare function bytesToBase64(bytes: Uint8Array): string;
|
|
3
|
-
/** Decode standard base64 to bytes. */
|
|
4
|
-
export declare function base64ToBytes(b64: string): Uint8Array;
|
|
5
|
-
/** Encode a UTF-8 string to base64 (unicode-safe, unlike raw `btoa`). */
|
|
6
|
-
export declare function utf8ToBase64(text: string): string;
|
|
7
|
-
/** URL-safe base64 (base64url) without padding — used by SigV4 / policies. */
|
|
8
|
-
export declare function bytesToBase64Url(bytes: Uint8Array): string;
|
package/dist/base64.js
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
// Edge-safe base64 — NO `Buffer` (banned by the edge-runtime audit).
|
|
2
|
-
// Uses `btoa`/`atob` over chunked `String.fromCharCode`, present on every
|
|
3
|
-
// target runtime (Workers, Deno, Bun, Node 20+, browsers).
|
|
4
|
-
// Chunk stays <= 0x8000 so the spread into String.fromCharCode never exceeds
|
|
5
|
-
// the engine argument limit.
|
|
6
|
-
const CHUNK = 0x8000;
|
|
7
|
-
/** Encode bytes to standard base64. */ export function bytesToBase64(bytes) {
|
|
8
|
-
let binary = '';
|
|
9
|
-
for(let i = 0; i < bytes.length; i += CHUNK){
|
|
10
|
-
binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
|
|
11
|
-
}
|
|
12
|
-
return btoa(binary);
|
|
13
|
-
}
|
|
14
|
-
/** Decode standard base64 to bytes. */ export function base64ToBytes(b64) {
|
|
15
|
-
const binary = atob(b64);
|
|
16
|
-
const out = new Uint8Array(binary.length);
|
|
17
|
-
for(let i = 0; i < binary.length; i++)out[i] = binary.charCodeAt(i);
|
|
18
|
-
return out;
|
|
19
|
-
}
|
|
20
|
-
/** Encode a UTF-8 string to base64 (unicode-safe, unlike raw `btoa`). */ export function utf8ToBase64(text) {
|
|
21
|
-
return bytesToBase64(new TextEncoder().encode(text));
|
|
22
|
-
}
|
|
23
|
-
/** URL-safe base64 (base64url) without padding — used by SigV4 / policies. */ export function bytesToBase64Url(bytes) {
|
|
24
|
-
return bytesToBase64(bytes).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
25
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Inject the {@link StorageService} for a named bucket.
|
|
3
|
-
*
|
|
4
|
-
* ```ts
|
|
5
|
-
* class UploadService {
|
|
6
|
-
* constructor(
|
|
7
|
-
* @InjectStorage() private readonly uploads: StorageService, // default bucket
|
|
8
|
-
* @InjectStorage('backups') private readonly backups: StorageService, // named bucket
|
|
9
|
-
* ) {}
|
|
10
|
-
* }
|
|
11
|
-
* ```
|
|
12
|
-
*
|
|
13
|
-
* The default bucket resolves the `StorageService` class token directly (so
|
|
14
|
-
* plain `@Inject(StorageService)` and `Test.overrideProvider(StorageService)`
|
|
15
|
-
* keep working); named buckets resolve their per-name token.
|
|
16
|
-
*/
|
|
17
|
-
export declare function InjectStorage(name?: string): ParameterDecorator;
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { Inject } from "@velajs/vela";
|
|
2
|
-
import { StorageService } from "../storage.service.js";
|
|
3
|
-
import { DEFAULT_STORAGE_NAME, storageToken } from "../storage.tokens.js";
|
|
4
|
-
/**
|
|
5
|
-
* Inject the {@link StorageService} for a named bucket.
|
|
6
|
-
*
|
|
7
|
-
* ```ts
|
|
8
|
-
* class UploadService {
|
|
9
|
-
* constructor(
|
|
10
|
-
* @InjectStorage() private readonly uploads: StorageService, // default bucket
|
|
11
|
-
* @InjectStorage('backups') private readonly backups: StorageService, // named bucket
|
|
12
|
-
* ) {}
|
|
13
|
-
* }
|
|
14
|
-
* ```
|
|
15
|
-
*
|
|
16
|
-
* The default bucket resolves the `StorageService` class token directly (so
|
|
17
|
-
* plain `@Inject(StorageService)` and `Test.overrideProvider(StorageService)`
|
|
18
|
-
* keep working); named buckets resolve their per-name token.
|
|
19
|
-
*/ export function InjectStorage(name = DEFAULT_STORAGE_NAME) {
|
|
20
|
-
return Inject(name === DEFAULT_STORAGE_NAME ? StorageService : storageToken(name));
|
|
21
|
-
}
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
export type R2PutValue = ReadableStream | ArrayBuffer | ArrayBufferView | string | Blob | null;
|
|
2
|
-
export interface R2HttpMetadataLike {
|
|
3
|
-
contentType?: string;
|
|
4
|
-
cacheControl?: string;
|
|
5
|
-
}
|
|
6
|
-
export interface R2PutOptionsLike {
|
|
7
|
-
httpMetadata?: R2HttpMetadataLike;
|
|
8
|
-
customMetadata?: Record<string, string>;
|
|
9
|
-
}
|
|
10
|
-
export interface R2GetOptionsLike {
|
|
11
|
-
range?: {
|
|
12
|
-
offset?: number;
|
|
13
|
-
length?: number;
|
|
14
|
-
suffix?: number;
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
export interface R2ListOptionsLike {
|
|
18
|
-
prefix?: string;
|
|
19
|
-
cursor?: string;
|
|
20
|
-
limit?: number;
|
|
21
|
-
delimiter?: string;
|
|
22
|
-
}
|
|
23
|
-
export interface R2ObjectLike {
|
|
24
|
-
key: string;
|
|
25
|
-
size: number;
|
|
26
|
-
etag: string;
|
|
27
|
-
uploaded: Date;
|
|
28
|
-
httpMetadata?: R2HttpMetadataLike;
|
|
29
|
-
customMetadata?: Record<string, string>;
|
|
30
|
-
}
|
|
31
|
-
export interface R2ObjectBodyLike extends R2ObjectLike {
|
|
32
|
-
body: ReadableStream;
|
|
33
|
-
arrayBuffer(): Promise<ArrayBuffer>;
|
|
34
|
-
}
|
|
35
|
-
export interface R2ListResultLike {
|
|
36
|
-
objects: R2ObjectLike[];
|
|
37
|
-
truncated: boolean;
|
|
38
|
-
cursor?: string;
|
|
39
|
-
delimitedPrefixes: string[];
|
|
40
|
-
}
|
|
41
|
-
export interface R2UploadedPartLike {
|
|
42
|
-
partNumber: number;
|
|
43
|
-
etag: string;
|
|
44
|
-
}
|
|
45
|
-
export interface R2MultipartUploadLike {
|
|
46
|
-
readonly uploadId: string;
|
|
47
|
-
uploadPart(partNumber: number, value: R2PutValue): Promise<R2UploadedPartLike>;
|
|
48
|
-
complete(parts: R2UploadedPartLike[]): Promise<R2ObjectLike>;
|
|
49
|
-
abort(): Promise<void>;
|
|
50
|
-
}
|
|
51
|
-
export interface R2BucketLike {
|
|
52
|
-
put(key: string, value: R2PutValue, options?: R2PutOptionsLike): Promise<R2ObjectLike | null>;
|
|
53
|
-
get(key: string, options?: R2GetOptionsLike): Promise<R2ObjectBodyLike | null>;
|
|
54
|
-
head(key: string): Promise<R2ObjectLike | null>;
|
|
55
|
-
delete(keys: string | string[]): Promise<void>;
|
|
56
|
-
list(options?: R2ListOptionsLike): Promise<R2ListResultLike>;
|
|
57
|
-
createMultipartUpload(key: string, options?: R2PutOptionsLike): Promise<R2MultipartUploadLike>;
|
|
58
|
-
resumeMultipartUpload?(key: string, uploadId: string): R2MultipartUploadLike;
|
|
59
|
-
}
|
|
60
|
-
export interface R2DriverOptions {
|
|
61
|
-
/** The Workers `R2Bucket` binding (e.g. `env.MY_BUCKET` or `R2Service.bucket`). */
|
|
62
|
-
bucket: R2BucketLike;
|
|
63
|
-
/** Public origin (r2.dev subdomain / custom domain) so `url()` can return a link. */
|
|
64
|
-
publicBaseUrl?: string;
|
|
65
|
-
/** Friendly driver name (defaults to `r2`). */
|
|
66
|
-
name?: string;
|
|
67
|
-
}
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
// Structural subset of the Cloudflare `R2Bucket` binding — only the members we
|
|
2
|
-
// call. Declaring it structurally (instead of importing @cloudflare/workers-types)
|
|
3
|
-
// keeps workers-types an optional peer; a real `R2Bucket` (and
|
|
4
|
-
// `@velajs/cloudflare`'s `R2Service.bucket`) is structurally assignable.
|
|
5
|
-
export { };
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import type { S3DriverOptions } from './s3.types';
|
|
2
|
-
export interface PostPolicyOptions {
|
|
3
|
-
expiresIn: number;
|
|
4
|
-
contentType?: string;
|
|
5
|
-
maxSize?: number;
|
|
6
|
-
minSize?: number;
|
|
7
|
-
}
|
|
8
|
-
/**
|
|
9
|
-
* Thin wrapper over aws4fetch: builds addressed URLs, sends SigV4-signed
|
|
10
|
-
* requests, and mints presigned GET/PUT URLs + presigned-POST policies.
|
|
11
|
-
* The single place S3 signing happens.
|
|
12
|
-
*/
|
|
13
|
-
export declare class S3Client {
|
|
14
|
-
#private;
|
|
15
|
-
constructor(cfg: S3DriverOptions);
|
|
16
|
-
get bucket(): string;
|
|
17
|
-
objectUrl(key: string, query?: Record<string, string>): URL;
|
|
18
|
-
/** The bucket-root URL used as the presigned-POST target. */
|
|
19
|
-
postUrl(): string;
|
|
20
|
-
/** Send a SigV4-signed request. */
|
|
21
|
-
send(url: URL, init: RequestInit): Promise<Response>;
|
|
22
|
-
/** Presign a GET or PUT URL via query signing (unsigned payload). */
|
|
23
|
-
presign(key: string, method: 'GET' | 'PUT', expiresIn: number, query?: Record<string, string>): Promise<string>;
|
|
24
|
-
/**
|
|
25
|
-
* Presigned POST policy — enforces `content-length-range` server-side.
|
|
26
|
-
* aws4fetch only signs requests, so the policy HMAC chain is hand-rolled
|
|
27
|
-
* on Web Crypto (edge-safe).
|
|
28
|
-
*/
|
|
29
|
-
signPostPolicy(key: string, opts: PostPolicyOptions): Promise<{
|
|
30
|
-
url: string;
|
|
31
|
-
fields: Record<string, string>;
|
|
32
|
-
}>;
|
|
33
|
-
}
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import { AwsClient } from "aws4fetch";
|
|
2
|
-
import { utf8ToBase64 } from "../../base64.js";
|
|
3
|
-
// --- Web-Crypto SigV4 helpers (used only for the POST-policy path; the
|
|
4
|
-
// request-signing path is aws4fetch). No node:crypto, no Buffer. ---
|
|
5
|
-
const encoder = new TextEncoder();
|
|
6
|
-
async function hmac(key, data) {
|
|
7
|
-
const cryptoKey = await crypto.subtle.importKey('raw', key, {
|
|
8
|
-
name: 'HMAC',
|
|
9
|
-
hash: 'SHA-256'
|
|
10
|
-
}, false, [
|
|
11
|
-
'sign'
|
|
12
|
-
]);
|
|
13
|
-
const sig = await crypto.subtle.sign('HMAC', cryptoKey, encoder.encode(data));
|
|
14
|
-
return new Uint8Array(sig);
|
|
15
|
-
}
|
|
16
|
-
function hex(bytes) {
|
|
17
|
-
let out = '';
|
|
18
|
-
for (const b of bytes)out += b.toString(16).padStart(2, '0');
|
|
19
|
-
return out;
|
|
20
|
-
}
|
|
21
|
-
async function sigV4SigningKey(secret, dateStamp, region, service = 's3') {
|
|
22
|
-
const kDate = await hmac(encoder.encode(`AWS4${secret}`), dateStamp);
|
|
23
|
-
const kRegion = await hmac(kDate, region);
|
|
24
|
-
const kService = await hmac(kRegion, service);
|
|
25
|
-
return hmac(kService, 'aws4_request');
|
|
26
|
-
}
|
|
27
|
-
function amzDates() {
|
|
28
|
-
const amzDate = new Date().toISOString().replace(/[:-]|\.\d{3}/g, '');
|
|
29
|
-
return {
|
|
30
|
-
amzDate,
|
|
31
|
-
dateStamp: amzDate.slice(0, 8)
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Thin wrapper over aws4fetch: builds addressed URLs, sends SigV4-signed
|
|
36
|
-
* requests, and mints presigned GET/PUT URLs + presigned-POST policies.
|
|
37
|
-
* The single place S3 signing happens.
|
|
38
|
-
*/ export class S3Client {
|
|
39
|
-
#aws;
|
|
40
|
-
#cfg;
|
|
41
|
-
#fetch;
|
|
42
|
-
constructor(cfg){
|
|
43
|
-
this.#cfg = cfg;
|
|
44
|
-
this.#fetch = cfg.fetch ?? fetch;
|
|
45
|
-
this.#aws = new AwsClient({
|
|
46
|
-
accessKeyId: cfg.credentials.accessKeyId,
|
|
47
|
-
secretAccessKey: cfg.credentials.secretAccessKey,
|
|
48
|
-
sessionToken: cfg.credentials.sessionToken,
|
|
49
|
-
region: cfg.region,
|
|
50
|
-
service: 's3'
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
get bucket() {
|
|
54
|
-
return this.#cfg.bucket;
|
|
55
|
-
}
|
|
56
|
-
/** Encode each key segment but keep `/` separators. */ #encodeKey(key) {
|
|
57
|
-
return key.split('/').map(encodeURIComponent).join('/');
|
|
58
|
-
}
|
|
59
|
-
objectUrl(key, query) {
|
|
60
|
-
const enc = this.#encodeKey(key);
|
|
61
|
-
const u = new URL(this.#cfg.endpoint);
|
|
62
|
-
if (this.#cfg.forcePathStyle) {
|
|
63
|
-
u.pathname = `/${this.#cfg.bucket}${enc ? `/${enc}` : ''}`;
|
|
64
|
-
} else {
|
|
65
|
-
u.hostname = `${this.#cfg.bucket}.${u.hostname}`;
|
|
66
|
-
u.pathname = `/${enc}`;
|
|
67
|
-
}
|
|
68
|
-
if (query) for (const [k, v] of Object.entries(query))u.searchParams.set(k, v);
|
|
69
|
-
return u;
|
|
70
|
-
}
|
|
71
|
-
/** The bucket-root URL used as the presigned-POST target. */ postUrl() {
|
|
72
|
-
const u = new URL(this.#cfg.endpoint);
|
|
73
|
-
if (this.#cfg.forcePathStyle) u.pathname = `/${this.#cfg.bucket}`;
|
|
74
|
-
else u.hostname = `${this.#cfg.bucket}.${u.hostname}`;
|
|
75
|
-
return u.toString();
|
|
76
|
-
}
|
|
77
|
-
/** Send a SigV4-signed request. */ async send(url, init) {
|
|
78
|
-
const signed = await this.#aws.sign(url.toString(), init);
|
|
79
|
-
return this.#fetch(signed);
|
|
80
|
-
}
|
|
81
|
-
/** Presign a GET or PUT URL via query signing (unsigned payload). */ async presign(key, method, expiresIn, query) {
|
|
82
|
-
const url = this.objectUrl(key, {
|
|
83
|
-
...query,
|
|
84
|
-
'X-Amz-Expires': String(expiresIn)
|
|
85
|
-
});
|
|
86
|
-
const signed = await this.#aws.sign(url.toString(), {
|
|
87
|
-
method,
|
|
88
|
-
aws: {
|
|
89
|
-
signQuery: true
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
return signed.url;
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* Presigned POST policy — enforces `content-length-range` server-side.
|
|
96
|
-
* aws4fetch only signs requests, so the policy HMAC chain is hand-rolled
|
|
97
|
-
* on Web Crypto (edge-safe).
|
|
98
|
-
*/ async signPostPolicy(key, opts) {
|
|
99
|
-
const { amzDate, dateStamp } = amzDates();
|
|
100
|
-
const { accessKeyId, secretAccessKey, sessionToken } = this.#cfg.credentials;
|
|
101
|
-
const credential = `${accessKeyId}/${dateStamp}/${this.#cfg.region}/s3/aws4_request`;
|
|
102
|
-
const expiration = new Date(Date.now() + opts.expiresIn * 1000).toISOString();
|
|
103
|
-
const conditions = [
|
|
104
|
-
{
|
|
105
|
-
bucket: this.#cfg.bucket
|
|
106
|
-
},
|
|
107
|
-
{
|
|
108
|
-
key
|
|
109
|
-
},
|
|
110
|
-
{
|
|
111
|
-
'x-amz-algorithm': 'AWS4-HMAC-SHA256'
|
|
112
|
-
},
|
|
113
|
-
{
|
|
114
|
-
'x-amz-credential': credential
|
|
115
|
-
},
|
|
116
|
-
{
|
|
117
|
-
'x-amz-date': amzDate
|
|
118
|
-
}
|
|
119
|
-
];
|
|
120
|
-
if (sessionToken) conditions.push({
|
|
121
|
-
'x-amz-security-token': sessionToken
|
|
122
|
-
});
|
|
123
|
-
if (opts.contentType) conditions.push({
|
|
124
|
-
'content-type': opts.contentType
|
|
125
|
-
});
|
|
126
|
-
if (opts.maxSize != null || opts.minSize != null) {
|
|
127
|
-
conditions.push([
|
|
128
|
-
'content-length-range',
|
|
129
|
-
opts.minSize ?? 0,
|
|
130
|
-
opts.maxSize ?? opts.minSize ?? 0
|
|
131
|
-
]);
|
|
132
|
-
}
|
|
133
|
-
const policyB64 = utf8ToBase64(JSON.stringify({
|
|
134
|
-
expiration,
|
|
135
|
-
conditions
|
|
136
|
-
}));
|
|
137
|
-
const signingKey = await sigV4SigningKey(secretAccessKey, dateStamp, this.#cfg.region);
|
|
138
|
-
const signature = hex(await hmac(signingKey, policyB64));
|
|
139
|
-
const fields = {
|
|
140
|
-
key,
|
|
141
|
-
'x-amz-algorithm': 'AWS4-HMAC-SHA256',
|
|
142
|
-
'x-amz-credential': credential,
|
|
143
|
-
'x-amz-date': amzDate,
|
|
144
|
-
policy: policyB64,
|
|
145
|
-
'x-amz-signature': signature
|
|
146
|
-
};
|
|
147
|
-
if (sessionToken) fields['x-amz-security-token'] = sessionToken;
|
|
148
|
-
if (opts.contentType) fields['content-type'] = opts.contentType;
|
|
149
|
-
return {
|
|
150
|
-
url: this.postUrl(),
|
|
151
|
-
fields
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
}
|