@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.
Files changed (78) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/LICENSE +21 -0
  3. package/README.md +56 -0
  4. package/dist/base64.d.ts +8 -0
  5. package/dist/base64.js +25 -0
  6. package/dist/client/index.d.ts +50 -0
  7. package/dist/client/index.js +243 -0
  8. package/dist/decorators/inject-storage.decorator.d.ts +17 -0
  9. package/dist/decorators/inject-storage.decorator.js +21 -0
  10. package/dist/drivers/memory/index.d.ts +16 -0
  11. package/dist/drivers/memory/index.js +202 -0
  12. package/dist/drivers/r2/index.d.ts +11 -0
  13. package/dist/drivers/r2/index.js +166 -0
  14. package/dist/drivers/r2/r2.types.d.ts +67 -0
  15. package/dist/drivers/r2/r2.types.js +5 -0
  16. package/dist/drivers/r2-http/index.d.ts +33 -0
  17. package/dist/drivers/r2-http/index.js +52 -0
  18. package/dist/drivers/s3/index.d.ts +5 -0
  19. package/dist/drivers/s3/index.js +3 -0
  20. package/dist/drivers/s3/s3-client.d.ts +33 -0
  21. package/dist/drivers/s3/s3-client.js +154 -0
  22. package/dist/drivers/s3/s3.driver.d.ts +4 -0
  23. package/dist/drivers/s3/s3.driver.js +404 -0
  24. package/dist/drivers/s3/s3.types.d.ts +28 -0
  25. package/dist/drivers/s3/s3.types.js +1 -0
  26. package/dist/drivers/s3/xml.d.ts +6 -0
  27. package/dist/drivers/s3/xml.js +52 -0
  28. package/dist/http/authorizer.types.d.ts +59 -0
  29. package/dist/http/authorizer.types.js +1 -0
  30. package/dist/http/http-helpers.d.ts +22 -0
  31. package/dist/http/http-helpers.js +80 -0
  32. package/dist/http/protocol.types.d.ts +76 -0
  33. package/dist/http/protocol.types.js +1 -0
  34. package/dist/index.d.ts +17 -0
  35. package/dist/index.js +16 -0
  36. package/dist/internal/body.d.ts +18 -0
  37. package/dist/internal/body.js +90 -0
  38. package/dist/internal/retry.d.ts +41 -0
  39. package/dist/internal/retry.js +127 -0
  40. package/dist/internal/stored-file.d.ts +27 -0
  41. package/dist/internal/stored-file.js +114 -0
  42. package/dist/middleware/cache.d.ts +50 -0
  43. package/dist/middleware/cache.js +101 -0
  44. package/dist/middleware/compose.d.ts +12 -0
  45. package/dist/middleware/compose.js +11 -0
  46. package/dist/middleware/compression.d.ts +16 -0
  47. package/dist/middleware/compression.js +101 -0
  48. package/dist/middleware/encryption.d.ts +14 -0
  49. package/dist/middleware/encryption.js +134 -0
  50. package/dist/middleware/failover.d.ts +18 -0
  51. package/dist/middleware/failover.js +52 -0
  52. package/dist/middleware/index.d.ts +15 -0
  53. package/dist/middleware/index.js +8 -0
  54. package/dist/middleware/retry.d.ts +14 -0
  55. package/dist/middleware/retry.js +47 -0
  56. package/dist/middleware/versioning.d.ts +36 -0
  57. package/dist/middleware/versioning.js +89 -0
  58. package/dist/middleware/wrap.d.ts +11 -0
  59. package/dist/middleware/wrap.js +46 -0
  60. package/dist/object-key.d.ts +17 -0
  61. package/dist/object-key.js +42 -0
  62. package/dist/storage.controller.d.ts +21 -0
  63. package/dist/storage.controller.js +380 -0
  64. package/dist/storage.error.d.ts +28 -0
  65. package/dist/storage.error.js +46 -0
  66. package/dist/storage.facade.d.ts +47 -0
  67. package/dist/storage.facade.js +443 -0
  68. package/dist/storage.module.d.ts +46 -0
  69. package/dist/storage.module.js +113 -0
  70. package/dist/storage.service.d.ts +42 -0
  71. package/dist/storage.service.js +86 -0
  72. package/dist/storage.tokens.d.ts +13 -0
  73. package/dist/storage.tokens.js +26 -0
  74. package/dist/storage.types.d.ts +245 -0
  75. package/dist/storage.types.js +1 -0
  76. package/dist/storagesdk/index.d.ts +85 -0
  77. package/dist/storagesdk/index.js +136 -0
  78. 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,3 @@
1
+ export { s3Driver } from "./s3.driver.js";
2
+ export { S3Client } from "./s3-client.js";
3
+ export { escapeXml, unescapeXml, tagText, tagBlocks } from "./xml.js";
@@ -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
+ }
@@ -0,0 +1,4 @@
1
+ import type { StorageDriver } from '../../storage.types';
2
+ import type { S3DriverOptions } from './s3.types';
3
+ /** S3 / S3-compatible driver signed with aws4fetch (edge-safe). */
4
+ export declare function s3Driver(options: S3DriverOptions): StorageDriver;