@velajs/storage 0.2.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/CHANGELOG.md +14 -0
- package/LICENSE +21 -0
- package/README.md +56 -0
- package/dist/base64.d.ts +8 -0
- package/dist/base64.js +25 -0
- package/dist/client/index.d.ts +50 -0
- package/dist/client/index.js +243 -0
- package/dist/decorators/inject-storage.decorator.d.ts +17 -0
- package/dist/decorators/inject-storage.decorator.js +21 -0
- package/dist/drivers/memory/index.d.ts +16 -0
- package/dist/drivers/memory/index.js +202 -0
- package/dist/drivers/r2/index.d.ts +11 -0
- package/dist/drivers/r2/index.js +166 -0
- package/dist/drivers/r2/r2.types.d.ts +67 -0
- package/dist/drivers/r2/r2.types.js +5 -0
- package/dist/drivers/r2-http/index.d.ts +33 -0
- package/dist/drivers/r2-http/index.js +52 -0
- package/dist/drivers/s3/index.d.ts +5 -0
- package/dist/drivers/s3/index.js +3 -0
- package/dist/drivers/s3/s3-client.d.ts +33 -0
- package/dist/drivers/s3/s3-client.js +154 -0
- package/dist/drivers/s3/s3.driver.d.ts +4 -0
- package/dist/drivers/s3/s3.driver.js +404 -0
- package/dist/drivers/s3/s3.types.d.ts +28 -0
- package/dist/drivers/s3/s3.types.js +1 -0
- package/dist/drivers/s3/xml.d.ts +6 -0
- package/dist/drivers/s3/xml.js +52 -0
- package/dist/http/authorizer.types.d.ts +59 -0
- package/dist/http/authorizer.types.js +1 -0
- package/dist/http/http-helpers.d.ts +22 -0
- package/dist/http/http-helpers.js +80 -0
- package/dist/http/protocol.types.d.ts +76 -0
- package/dist/http/protocol.types.js +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +16 -0
- package/dist/internal/body.d.ts +18 -0
- package/dist/internal/body.js +90 -0
- package/dist/internal/retry.d.ts +41 -0
- package/dist/internal/retry.js +127 -0
- package/dist/internal/stored-file.d.ts +27 -0
- package/dist/internal/stored-file.js +114 -0
- package/dist/middleware/cache.d.ts +50 -0
- package/dist/middleware/cache.js +101 -0
- package/dist/middleware/compose.d.ts +12 -0
- package/dist/middleware/compose.js +11 -0
- package/dist/middleware/compression.d.ts +16 -0
- package/dist/middleware/compression.js +101 -0
- package/dist/middleware/encryption.d.ts +14 -0
- package/dist/middleware/encryption.js +134 -0
- package/dist/middleware/failover.d.ts +18 -0
- package/dist/middleware/failover.js +52 -0
- package/dist/middleware/index.d.ts +15 -0
- package/dist/middleware/index.js +8 -0
- package/dist/middleware/retry.d.ts +14 -0
- package/dist/middleware/retry.js +47 -0
- package/dist/middleware/versioning.d.ts +36 -0
- package/dist/middleware/versioning.js +89 -0
- package/dist/middleware/wrap.d.ts +11 -0
- package/dist/middleware/wrap.js +46 -0
- package/dist/object-key.d.ts +17 -0
- package/dist/object-key.js +42 -0
- package/dist/storage.controller.d.ts +21 -0
- package/dist/storage.controller.js +380 -0
- package/dist/storage.error.d.ts +28 -0
- package/dist/storage.error.js +46 -0
- package/dist/storage.facade.d.ts +47 -0
- package/dist/storage.facade.js +443 -0
- package/dist/storage.module.d.ts +46 -0
- package/dist/storage.module.js +113 -0
- package/dist/storage.service.d.ts +42 -0
- package/dist/storage.service.js +86 -0
- package/dist/storage.tokens.d.ts +13 -0
- package/dist/storage.tokens.js +26 -0
- package/dist/storage.types.d.ts +245 -0
- package/dist/storage.types.js +1 -0
- package/dist/storagesdk/index.d.ts +85 -0
- package/dist/storagesdk/index.js +136 -0
- package/package.json +116 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { StorageDriver } from '../../storage.types';
|
|
2
|
+
import type { R2BucketLike, R2DriverOptions } from './r2.types';
|
|
3
|
+
export type { R2BucketLike, R2DriverOptions, R2ObjectLike, R2ObjectBodyLike, R2MultipartUploadLike, } from './r2.types';
|
|
4
|
+
/**
|
|
5
|
+
* Cloudflare R2 driver — native binding mode only. Zero third-party deps
|
|
6
|
+
* (no aws4fetch), fully edge-native I/O with no egress fees.
|
|
7
|
+
*
|
|
8
|
+
* It cannot presign (`signedUploadUrl` throws; `url()` needs `publicBaseUrl`).
|
|
9
|
+
* For presigned URLs use `@velajs/storage/drivers/r2-http` (HTTP or hybrid mode).
|
|
10
|
+
*/
|
|
11
|
+
export declare function r2Driver(options: R2DriverOptions): StorageDriver<R2BucketLike>;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { createStoredFile } from "../../internal/stored-file.js";
|
|
2
|
+
import { StorageError } from "../../storage.error.js";
|
|
3
|
+
function joinPublic(base, key) {
|
|
4
|
+
const enc = key.split('/').map(encodeURIComponent).join('/');
|
|
5
|
+
return `${base.replace(/\/$/, '')}/${enc}`;
|
|
6
|
+
}
|
|
7
|
+
function metaOf(obj) {
|
|
8
|
+
return {
|
|
9
|
+
key: obj.key,
|
|
10
|
+
size: obj.size,
|
|
11
|
+
type: obj.httpMetadata?.contentType ?? 'application/octet-stream',
|
|
12
|
+
etag: obj.etag,
|
|
13
|
+
lastModified: obj.uploaded instanceof Date ? obj.uploaded.getTime() : undefined,
|
|
14
|
+
metadata: obj.customMetadata
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Cloudflare R2 driver — native binding mode only. Zero third-party deps
|
|
19
|
+
* (no aws4fetch), fully edge-native I/O with no egress fees.
|
|
20
|
+
*
|
|
21
|
+
* It cannot presign (`signedUploadUrl` throws; `url()` needs `publicBaseUrl`).
|
|
22
|
+
* For presigned URLs use `@velajs/storage/drivers/r2-http` (HTTP or hybrid mode).
|
|
23
|
+
*/ export function r2Driver(options) {
|
|
24
|
+
const bucket = options.bucket;
|
|
25
|
+
const mustGet = async (key)=>{
|
|
26
|
+
const obj = await bucket.get(key);
|
|
27
|
+
if (!obj) throw new StorageError('NotFound', `not found: ${key}`);
|
|
28
|
+
return obj;
|
|
29
|
+
};
|
|
30
|
+
return {
|
|
31
|
+
name: options.name ?? 'r2',
|
|
32
|
+
raw: bucket,
|
|
33
|
+
supportsRange: true,
|
|
34
|
+
supportsDelimiter: true,
|
|
35
|
+
supportsMetadata: true,
|
|
36
|
+
supportsCacheControl: true,
|
|
37
|
+
supportsServerSideCopy: false,
|
|
38
|
+
reportsUploadProgress: false,
|
|
39
|
+
signedUrl: {
|
|
40
|
+
supported: !!options.publicBaseUrl,
|
|
41
|
+
upload: false
|
|
42
|
+
},
|
|
43
|
+
async upload (key, body, opts) {
|
|
44
|
+
const obj = await bucket.put(key, body, {
|
|
45
|
+
httpMetadata: {
|
|
46
|
+
contentType: opts?.contentType,
|
|
47
|
+
cacheControl: opts?.cacheControl
|
|
48
|
+
},
|
|
49
|
+
customMetadata: opts?.metadata
|
|
50
|
+
});
|
|
51
|
+
if (!obj) throw new StorageError('Provider', `put failed: ${key}`);
|
|
52
|
+
return {
|
|
53
|
+
key,
|
|
54
|
+
size: obj.size,
|
|
55
|
+
contentType: opts?.contentType ?? 'application/octet-stream',
|
|
56
|
+
etag: obj.etag,
|
|
57
|
+
lastModified: obj.uploaded instanceof Date ? obj.uploaded.getTime() : undefined
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
async download (key, opts) {
|
|
61
|
+
const range = opts?.range ? {
|
|
62
|
+
offset: opts.range.start,
|
|
63
|
+
length: opts.range.end != null ? opts.range.end - opts.range.start + 1 : undefined
|
|
64
|
+
} : undefined;
|
|
65
|
+
const obj = await bucket.get(key, range ? {
|
|
66
|
+
range
|
|
67
|
+
} : undefined);
|
|
68
|
+
if (!obj) throw new StorageError('NotFound', `not found: ${key}`);
|
|
69
|
+
const meta = metaOf(obj);
|
|
70
|
+
const size = range ? range.length ?? obj.size - range.offset : obj.size;
|
|
71
|
+
return createStoredFile({
|
|
72
|
+
...meta,
|
|
73
|
+
size
|
|
74
|
+
}, {
|
|
75
|
+
kind: 'stream',
|
|
76
|
+
stream: obj.body
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
async head (key) {
|
|
80
|
+
const obj = await bucket.head(key);
|
|
81
|
+
if (!obj) throw new StorageError('NotFound', `not found: ${key}`);
|
|
82
|
+
return createStoredFile(metaOf(obj), {
|
|
83
|
+
kind: 'lazy',
|
|
84
|
+
fetch: async ()=>new Response((await mustGet(key)).body)
|
|
85
|
+
});
|
|
86
|
+
},
|
|
87
|
+
async exists (key) {
|
|
88
|
+
return await bucket.head(key) != null;
|
|
89
|
+
},
|
|
90
|
+
async delete (key) {
|
|
91
|
+
await bucket.delete(key);
|
|
92
|
+
},
|
|
93
|
+
async deleteMany (keys) {
|
|
94
|
+
await bucket.delete(keys);
|
|
95
|
+
return {
|
|
96
|
+
deleted: keys
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
async copy (from, to) {
|
|
100
|
+
const src = await bucket.get(from);
|
|
101
|
+
if (!src) throw new StorageError('NotFound', `not found: ${from}`);
|
|
102
|
+
await bucket.put(to, src.body, {
|
|
103
|
+
httpMetadata: src.httpMetadata,
|
|
104
|
+
customMetadata: src.customMetadata
|
|
105
|
+
});
|
|
106
|
+
},
|
|
107
|
+
async list (opts) {
|
|
108
|
+
const res = await bucket.list({
|
|
109
|
+
prefix: opts?.prefix,
|
|
110
|
+
cursor: opts?.cursor,
|
|
111
|
+
limit: opts?.limit,
|
|
112
|
+
delimiter: opts?.delimiter
|
|
113
|
+
});
|
|
114
|
+
return {
|
|
115
|
+
items: res.objects.map((obj)=>createStoredFile(metaOf(obj), {
|
|
116
|
+
kind: 'lazy',
|
|
117
|
+
fetch: async ()=>new Response((await mustGet(obj.key)).body)
|
|
118
|
+
})),
|
|
119
|
+
prefixes: res.delimitedPrefixes.length ? res.delimitedPrefixes : undefined,
|
|
120
|
+
cursor: res.truncated ? res.cursor : undefined
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
async url (key, _opts) {
|
|
124
|
+
if (options.publicBaseUrl) return joinPublic(options.publicBaseUrl, key);
|
|
125
|
+
throw new StorageError('Unsupported', 'r2 binding: url() needs publicBaseUrl (or use the r2-http/hybrid driver to presign)');
|
|
126
|
+
},
|
|
127
|
+
async signedUploadUrl () {
|
|
128
|
+
throw new StorageError('Unsupported', 'r2 binding cannot presign uploads; use the r2-http or r2-hybrid driver');
|
|
129
|
+
},
|
|
130
|
+
async createMultipartUpload (key, opts) {
|
|
131
|
+
const mpu = await bucket.createMultipartUpload(key, {
|
|
132
|
+
httpMetadata: {
|
|
133
|
+
contentType: opts?.contentType,
|
|
134
|
+
cacheControl: opts?.cacheControl
|
|
135
|
+
},
|
|
136
|
+
customMetadata: opts?.metadata
|
|
137
|
+
});
|
|
138
|
+
return {
|
|
139
|
+
key,
|
|
140
|
+
uploadId: mpu.uploadId,
|
|
141
|
+
async uploadPart (partNumber, part, _o) {
|
|
142
|
+
const up = await mpu.uploadPart(partNumber, part);
|
|
143
|
+
return {
|
|
144
|
+
partNumber: up.partNumber,
|
|
145
|
+
etag: up.etag
|
|
146
|
+
};
|
|
147
|
+
},
|
|
148
|
+
async complete (parts) {
|
|
149
|
+
const obj = await mpu.complete(parts.map((p)=>({
|
|
150
|
+
partNumber: p.partNumber,
|
|
151
|
+
etag: p.etag
|
|
152
|
+
})));
|
|
153
|
+
return {
|
|
154
|
+
key,
|
|
155
|
+
size: obj.size,
|
|
156
|
+
contentType: opts?.contentType ?? 'application/octet-stream',
|
|
157
|
+
etag: obj.etag
|
|
158
|
+
};
|
|
159
|
+
},
|
|
160
|
+
async abort () {
|
|
161
|
+
await mpu.abort();
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
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 { };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { StorageDriver } from '../../storage.types';
|
|
2
|
+
import type { R2BucketLike } from '../r2/r2.types';
|
|
3
|
+
export interface R2HttpOptions {
|
|
4
|
+
/** Cloudflare account ID — the default endpoint is derived from it. */
|
|
5
|
+
accountId: string;
|
|
6
|
+
accessKeyId: string;
|
|
7
|
+
secretAccessKey: string;
|
|
8
|
+
bucket: string;
|
|
9
|
+
/** Override the endpoint (jurisdiction-specific R2 URLs). */
|
|
10
|
+
endpoint?: string;
|
|
11
|
+
region?: string;
|
|
12
|
+
publicBaseUrl?: string;
|
|
13
|
+
defaultUrlExpiresIn?: number;
|
|
14
|
+
fetch?: typeof fetch;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* R2 over the S3-compatible HTTP API (aws4fetch). All I/O goes over HTTP and
|
|
18
|
+
* presigning works. Prefer the native binding driver (`../r2`) for I/O when
|
|
19
|
+
* running inside a Worker; use this from Node/other runtimes, or use
|
|
20
|
+
* {@link r2HybridDriver} to combine binding I/O with HTTP presigning.
|
|
21
|
+
*/
|
|
22
|
+
export declare function r2HttpDriver(o: R2HttpOptions): StorageDriver;
|
|
23
|
+
export interface R2HybridOptions extends R2HttpOptions {
|
|
24
|
+
/** The Workers `R2Bucket` binding used for reads/writes (no egress). */
|
|
25
|
+
binding: R2BucketLike;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Hybrid R2 driver: reads/writes go through the native binding (intra-Worker,
|
|
29
|
+
* no egress), while `url()` / `signedUploadUrl()` / `signedMultipart` /
|
|
30
|
+
* server-side `copy()` use the S3 HTTP signer. The best of both for Workers
|
|
31
|
+
* that need browser-facing presigned URLs.
|
|
32
|
+
*/
|
|
33
|
+
export declare function r2HybridDriver(o: R2HybridOptions): StorageDriver;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { r2Driver } from "../r2/index.js";
|
|
2
|
+
import { s3Driver } from "../s3/index.js";
|
|
3
|
+
function endpointFor(o) {
|
|
4
|
+
return o.endpoint ?? `https://${o.accountId}.r2.cloudflarestorage.com`;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* R2 over the S3-compatible HTTP API (aws4fetch). All I/O goes over HTTP and
|
|
8
|
+
* presigning works. Prefer the native binding driver (`../r2`) for I/O when
|
|
9
|
+
* running inside a Worker; use this from Node/other runtimes, or use
|
|
10
|
+
* {@link r2HybridDriver} to combine binding I/O with HTTP presigning.
|
|
11
|
+
*/ export function r2HttpDriver(o) {
|
|
12
|
+
return s3Driver({
|
|
13
|
+
endpoint: endpointFor(o),
|
|
14
|
+
region: o.region ?? 'auto',
|
|
15
|
+
bucket: o.bucket,
|
|
16
|
+
forcePathStyle: true,
|
|
17
|
+
credentials: {
|
|
18
|
+
accessKeyId: o.accessKeyId,
|
|
19
|
+
secretAccessKey: o.secretAccessKey
|
|
20
|
+
},
|
|
21
|
+
publicBaseUrl: o.publicBaseUrl,
|
|
22
|
+
defaultUrlExpiresIn: o.defaultUrlExpiresIn,
|
|
23
|
+
fetch: o.fetch,
|
|
24
|
+
name: 'r2-http'
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Hybrid R2 driver: reads/writes go through the native binding (intra-Worker,
|
|
29
|
+
* no egress), while `url()` / `signedUploadUrl()` / `signedMultipart` /
|
|
30
|
+
* server-side `copy()` use the S3 HTTP signer. The best of both for Workers
|
|
31
|
+
* that need browser-facing presigned URLs.
|
|
32
|
+
*/ export function r2HybridDriver(o) {
|
|
33
|
+
const binding = r2Driver({
|
|
34
|
+
bucket: o.binding,
|
|
35
|
+
publicBaseUrl: o.publicBaseUrl,
|
|
36
|
+
name: 'r2-hybrid'
|
|
37
|
+
});
|
|
38
|
+
const http = r2HttpDriver(o);
|
|
39
|
+
return {
|
|
40
|
+
...binding,
|
|
41
|
+
name: 'r2-hybrid',
|
|
42
|
+
supportsServerSideCopy: true,
|
|
43
|
+
signedUrl: {
|
|
44
|
+
supported: true,
|
|
45
|
+
upload: true
|
|
46
|
+
},
|
|
47
|
+
url: (key, opts)=>http.url(key, opts),
|
|
48
|
+
signedUploadUrl: (key, opts)=>http.signedUploadUrl(key, opts),
|
|
49
|
+
signedMultipart: http.signedMultipart,
|
|
50
|
+
copy: (from, to, opts)=>http.copy(from, to, opts)
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { s3Driver } from './s3.driver';
|
|
2
|
+
export { S3Client } from './s3-client';
|
|
3
|
+
export type { PostPolicyOptions } from './s3-client';
|
|
4
|
+
export type { S3DriverOptions, S3Credentials } from './s3.types';
|
|
5
|
+
export { escapeXml, unescapeXml, tagText, tagBlocks } from './xml';
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
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
|
+
}
|