@voyant-travel/storage 0.104.1

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/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for describing the origin of the Work and
141
+ reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may accept and charge a
167
+ fee for, the acceptance of support, warranty, indemnity, or
168
+ other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2026 PixelMakers Studio SRL
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # @voyant-travel/storage
2
+
3
+ Storage provider abstraction for Voyant. `StorageProvider` interface plus providers for local (in-memory), Cloudflare R2, and S3-compatible (AWS SigV4 via Web Crypto — works in Cloudflare Workers).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pnpm add @voyant-travel/storage
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```typescript
14
+ import { createStorageService } from "@voyant-travel/storage"
15
+ import { s3Provider } from "@voyant-travel/storage/providers/s3"
16
+
17
+ const storage = createStorageService(
18
+ s3Provider({
19
+ region: "us-east-1",
20
+ bucket: "my-bucket",
21
+ accessKeyId: env.AWS_ACCESS_KEY_ID,
22
+ secretAccessKey: env.AWS_SECRET_ACCESS_KEY,
23
+ }),
24
+ )
25
+
26
+ await storage.upload({ key: "files/x.pdf", body: buffer })
27
+ const url = await storage.signedUrl({ key: "files/x.pdf", expiresIn: 300 })
28
+ ```
29
+
30
+ The S3 provider supports `forcePathStyle` and a custom `endpoint` for S3-compatible services (Wasabi, MinIO, etc.). SigV4 signing is verified against AWS canonical test vectors.
31
+
32
+ The R2 binding provider cannot mint signed URLs by itself. Configure either
33
+ `publicBaseUrl` or a custom `signer` before calling `signedUrl`; otherwise the
34
+ provider throws instead of returning a raw storage key.
35
+
36
+ ## Exports
37
+
38
+ | Entry | Description |
39
+ | --- | --- |
40
+ | `.` | Barrel re-exports |
41
+ | `./types` | `StorageProvider` interface |
42
+ | `./service` | `createStorageService` |
43
+ | `./providers/local` | In-memory provider |
44
+ | `./providers/r2` | Cloudflare R2 binding provider |
45
+ | `./providers/s3` | S3 provider with SigV4 |
46
+ | `./lib/sigv4` | `signRequest`, `presignUrl` primitives |
47
+
48
+ ## License
49
+
50
+ Apache-2.0
@@ -0,0 +1,12 @@
1
+ export type { PresignUrlInput, SignedRequestHeaders, SignRequestInput, SigV4Context, SigV4Credentials, } from "./lib/sigv4.js";
2
+ export { presignUrl, signRequest } from "./lib/sigv4.js";
3
+ export type { LocalStorageOptions } from "./providers/local.js";
4
+ export { createLocalStorageProvider } from "./providers/local.js";
5
+ export type { R2BucketLike, R2ObjectLike, R2ProviderOptions, R2PutOptionsLike, } from "./providers/r2.js";
6
+ export { createR2Provider } from "./providers/r2.js";
7
+ export type { S3Fetch, S3ProviderOptions } from "./providers/s3.js";
8
+ export { createS3Provider } from "./providers/s3.js";
9
+ export type { StorageService } from "./service.js";
10
+ export { createStorageService, StorageError } from "./service.js";
11
+ export type { StorageObject, StorageProvider, StorageUploadBody, UploadOptions, } from "./types.js";
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,eAAe,EACf,oBAAoB,EACpB,gBAAgB,EAChB,YAAY,EACZ,gBAAgB,GACjB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AACxD,YAAY,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAA;AAC/D,OAAO,EAAE,0BAA0B,EAAE,MAAM,sBAAsB,CAAA;AACjE,YAAY,EACV,YAAY,EACZ,YAAY,EACZ,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AACpD,YAAY,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AACnE,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AACpD,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAClD,OAAO,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AACjE,YAAY,EACV,aAAa,EACb,eAAe,EACf,iBAAiB,EACjB,aAAa,GACd,MAAM,YAAY,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { presignUrl, signRequest } from "./lib/sigv4.js";
2
+ export { createLocalStorageProvider } from "./providers/local.js";
3
+ export { createR2Provider } from "./providers/r2.js";
4
+ export { createS3Provider } from "./providers/s3.js";
5
+ export { createStorageService, StorageError } from "./service.js";
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Minimal AWS SigV4 signing implementation using Web Crypto. Works in
3
+ * Cloudflare Workers, modern Node, Deno, and browsers.
4
+ *
5
+ * Supports two use cases needed by the S3 storage provider:
6
+ * - `signRequest`: attach an `Authorization` header for a direct request
7
+ * - `presignUrl`: produce a time-limited URL via query-string signing
8
+ *
9
+ * Reference:
10
+ * https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv_create-signed-request.html
11
+ */
12
+ export interface SigV4Credentials {
13
+ accessKeyId: string;
14
+ secretAccessKey: string;
15
+ sessionToken?: string;
16
+ }
17
+ export interface SigV4Context {
18
+ credentials: SigV4Credentials;
19
+ region: string;
20
+ service: string;
21
+ }
22
+ export interface SignRequestInput extends SigV4Context {
23
+ method: string;
24
+ url: string;
25
+ headers?: Record<string, string>;
26
+ body?: Uint8Array;
27
+ /** Override "now" (milliseconds since epoch). Useful for tests. */
28
+ now?: number;
29
+ }
30
+ export interface SignedRequestHeaders {
31
+ headers: Record<string, string>;
32
+ }
33
+ export interface PresignUrlInput extends SigV4Context {
34
+ method: string;
35
+ url: string;
36
+ expiresIn: number;
37
+ /** Extra signed headers beyond the default `host`. */
38
+ headers?: Record<string, string>;
39
+ /** Override "now" (milliseconds since epoch). Useful for tests. */
40
+ now?: number;
41
+ }
42
+ /**
43
+ * Sign a request and return the `Authorization` (and related) headers.
44
+ */
45
+ export declare function signRequest(input: SignRequestInput): Promise<SignedRequestHeaders>;
46
+ /**
47
+ * Produce a presigned URL (query-string signing) for the given method.
48
+ */
49
+ export declare function presignUrl(input: PresignUrlInput): Promise<string>;
50
+ //# sourceMappingURL=sigv4.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sigv4.d.ts","sourceRoot":"","sources":["../../src/lib/sigv4.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,CAAA;IACnB,eAAe,EAAE,MAAM,CAAA;IACvB,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,gBAAgB,CAAA;IAC7B,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,gBAAiB,SAAQ,YAAY;IACpD,MAAM,EAAE,MAAM,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;IACX,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB,mEAAmE;IACnE,GAAG,CAAC,EAAE,MAAM,CAAA;CACb;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAChC;AAED,MAAM,WAAW,eAAgB,SAAQ,YAAY;IACnD,MAAM,EAAE,MAAM,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,EAAE,MAAM,CAAA;IACjB,sDAAsD;IACtD,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,mEAAmE;IACnE,GAAG,CAAC,EAAE,MAAM,CAAA;CACb;AAED;;GAEG;AACH,wBAAsB,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAqDxF;AAED;;GAEG;AACH,wBAAsB,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CAgDxE"}
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Minimal AWS SigV4 signing implementation using Web Crypto. Works in
3
+ * Cloudflare Workers, modern Node, Deno, and browsers.
4
+ *
5
+ * Supports two use cases needed by the S3 storage provider:
6
+ * - `signRequest`: attach an `Authorization` header for a direct request
7
+ * - `presignUrl`: produce a time-limited URL via query-string signing
8
+ *
9
+ * Reference:
10
+ * https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv_create-signed-request.html
11
+ */
12
+ const encoder = new TextEncoder();
13
+ /**
14
+ * Sign a request and return the `Authorization` (and related) headers.
15
+ */
16
+ export async function signRequest(input) {
17
+ const { amzDate, dateStamp } = datesFromNow(input.now);
18
+ const url = new URL(input.url);
19
+ const bodyBytes = input.body ?? new Uint8Array();
20
+ const payloadHash = await hexHash(bodyBytes);
21
+ const baseHeaders = {
22
+ ...(input.headers ?? {}),
23
+ host: url.host,
24
+ "x-amz-date": amzDate,
25
+ "x-amz-content-sha256": payloadHash,
26
+ };
27
+ if (input.credentials.sessionToken) {
28
+ baseHeaders["x-amz-security-token"] = input.credentials.sessionToken;
29
+ }
30
+ const canonicalQuery = canonicalQueryString(url);
31
+ const { canonicalHeaders, signedHeaders } = canonicalizeHeaders(baseHeaders);
32
+ const canonicalRequest = [
33
+ input.method.toUpperCase(),
34
+ canonicalUri(url.pathname),
35
+ canonicalQuery,
36
+ canonicalHeaders,
37
+ signedHeaders,
38
+ payloadHash,
39
+ ].join("\n");
40
+ const scope = `${dateStamp}/${input.region}/${input.service}/aws4_request`;
41
+ const stringToSign = [
42
+ "AWS4-HMAC-SHA256",
43
+ amzDate,
44
+ scope,
45
+ await hexHash(encoder.encode(canonicalRequest)),
46
+ ].join("\n");
47
+ const signingKey = await deriveSigningKey(input.credentials.secretAccessKey, dateStamp, input.region, input.service);
48
+ const signature = hex(await hmac(signingKey, stringToSign));
49
+ const authHeader = `AWS4-HMAC-SHA256 Credential=${input.credentials.accessKeyId}/${scope}` +
50
+ `, SignedHeaders=${signedHeaders}, Signature=${signature}`;
51
+ return {
52
+ headers: {
53
+ ...baseHeaders,
54
+ Authorization: authHeader,
55
+ },
56
+ };
57
+ }
58
+ /**
59
+ * Produce a presigned URL (query-string signing) for the given method.
60
+ */
61
+ export async function presignUrl(input) {
62
+ const { amzDate, dateStamp } = datesFromNow(input.now);
63
+ const url = new URL(input.url);
64
+ const scope = `${dateStamp}/${input.region}/${input.service}/aws4_request`;
65
+ const headers = {
66
+ ...(input.headers ?? {}),
67
+ host: url.host,
68
+ };
69
+ const { canonicalHeaders, signedHeaders } = canonicalizeHeaders(headers);
70
+ const params = new URLSearchParams(url.searchParams);
71
+ params.set("X-Amz-Algorithm", "AWS4-HMAC-SHA256");
72
+ params.set("X-Amz-Credential", `${input.credentials.accessKeyId}/${scope}`);
73
+ params.set("X-Amz-Date", amzDate);
74
+ params.set("X-Amz-Expires", String(input.expiresIn));
75
+ params.set("X-Amz-SignedHeaders", signedHeaders);
76
+ if (input.credentials.sessionToken) {
77
+ params.set("X-Amz-Security-Token", input.credentials.sessionToken);
78
+ }
79
+ url.search = params.toString();
80
+ const canonicalRequest = [
81
+ input.method.toUpperCase(),
82
+ canonicalUri(url.pathname),
83
+ canonicalQueryString(url),
84
+ canonicalHeaders,
85
+ signedHeaders,
86
+ "UNSIGNED-PAYLOAD",
87
+ ].join("\n");
88
+ const stringToSign = [
89
+ "AWS4-HMAC-SHA256",
90
+ amzDate,
91
+ scope,
92
+ await hexHash(encoder.encode(canonicalRequest)),
93
+ ].join("\n");
94
+ const signingKey = await deriveSigningKey(input.credentials.secretAccessKey, dateStamp, input.region, input.service);
95
+ const signature = hex(await hmac(signingKey, stringToSign));
96
+ params.set("X-Amz-Signature", signature);
97
+ url.search = params.toString();
98
+ return url.toString();
99
+ }
100
+ // --- helpers --- //
101
+ function datesFromNow(nowMs) {
102
+ const d = new Date(nowMs ?? Date.now());
103
+ const iso = d.toISOString().replace(/[:-]|\.\d{3}/g, "");
104
+ // iso is like "20251005T223045Z"
105
+ return { amzDate: iso, dateStamp: iso.slice(0, 8) };
106
+ }
107
+ function canonicalUri(path) {
108
+ // S3 keys should be path-encoded but preserve "/"
109
+ if (!path)
110
+ return "/";
111
+ return path
112
+ .split("/")
113
+ .map((segment) => encodeRfc3986(segment))
114
+ .join("/");
115
+ }
116
+ function canonicalQueryString(url) {
117
+ const pairs = [];
118
+ for (const [key, value] of url.searchParams.entries()) {
119
+ pairs.push([encodeRfc3986(key), encodeRfc3986(value)]);
120
+ }
121
+ pairs.sort((a, b) => (a[0] === b[0] ? (a[1] < b[1] ? -1 : 1) : a[0] < b[0] ? -1 : 1));
122
+ return pairs.map(([k, v]) => `${k}=${v}`).join("&");
123
+ }
124
+ function canonicalizeHeaders(headers) {
125
+ const entries = Object.entries(headers)
126
+ .map(([k, v]) => [k.toLowerCase(), v.trim().replace(/\s+/g, " ")])
127
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
128
+ const canonicalHeaders = `${entries.map(([k, v]) => `${k}:${v}`).join("\n")}\n`;
129
+ const signedHeaders = entries.map(([k]) => k).join(";");
130
+ return { canonicalHeaders, signedHeaders };
131
+ }
132
+ function encodeRfc3986(value) {
133
+ return encodeURIComponent(value).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
134
+ }
135
+ async function hexHash(data) {
136
+ const copy = new Uint8Array(data.byteLength);
137
+ copy.set(data);
138
+ const digest = await crypto.subtle.digest("SHA-256", copy);
139
+ return hex(digest);
140
+ }
141
+ async function hmac(key, data) {
142
+ const source = key instanceof Uint8Array ? key : new Uint8Array(key);
143
+ const keyBytes = new Uint8Array(source.byteLength);
144
+ keyBytes.set(source);
145
+ const cryptoKey = await crypto.subtle.importKey("raw", keyBytes, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
146
+ const payloadSource = encoder.encode(data);
147
+ const payload = new Uint8Array(payloadSource.byteLength);
148
+ payload.set(payloadSource);
149
+ return crypto.subtle.sign("HMAC", cryptoKey, payload);
150
+ }
151
+ async function deriveSigningKey(secret, dateStamp, region, service) {
152
+ const kDate = await hmac(encoder.encode(`AWS4${secret}`), dateStamp);
153
+ const kRegion = await hmac(kDate, region);
154
+ const kService = await hmac(kRegion, service);
155
+ const kSigning = await hmac(kService, "aws4_request");
156
+ return kSigning;
157
+ }
158
+ function hex(buffer) {
159
+ const bytes = new Uint8Array(buffer);
160
+ let out = "";
161
+ for (const b of bytes) {
162
+ out += b.toString(16).padStart(2, "0");
163
+ }
164
+ return out;
165
+ }
@@ -0,0 +1,26 @@
1
+ import type { StorageProvider } from "../types.js";
2
+ /**
3
+ * Options for {@link createLocalStorageProvider}.
4
+ */
5
+ export interface LocalStorageOptions {
6
+ /** Provider name (defaults to `"local"`). */
7
+ name?: string;
8
+ /**
9
+ * Base URL used to construct the string returned from `signedUrl` and
10
+ * `upload`. Defaults to `"local://"`. The final URL is `${baseUrl}${key}`.
11
+ */
12
+ baseUrl?: string;
13
+ /**
14
+ * Function used to mint random keys when `UploadOptions.key` is not
15
+ * provided. Defaults to `crypto.randomUUID()` via the global `crypto`.
16
+ */
17
+ generateKey?: () => string;
18
+ }
19
+ /**
20
+ * Create an in-memory storage provider. Useful for unit tests and for
21
+ * locally running workflows without touching remote storage. Data is
22
+ * kept in a `Map` held inside the closure and is lost when the process
23
+ * exits.
24
+ */
25
+ export declare function createLocalStorageProvider(options?: LocalStorageOptions): StorageProvider;
26
+ //# sourceMappingURL=local.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local.d.ts","sourceRoot":"","sources":["../../src/providers/local.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,eAAe,EAAoC,MAAM,aAAa,CAAA;AAEnG;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,6CAA6C;IAC7C,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,MAAM,CAAA;CAC3B;AAQD;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,GAAE,mBAAwB,GAAG,eAAe,CAwC7F"}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Create an in-memory storage provider. Useful for unit tests and for
3
+ * locally running workflows without touching remote storage. Data is
4
+ * kept in a `Map` held inside the closure and is lost when the process
5
+ * exits.
6
+ */
7
+ export function createLocalStorageProvider(options = {}) {
8
+ const name = options.name ?? "local";
9
+ const baseUrl = options.baseUrl ?? "local://";
10
+ const generateKey = options.generateKey ??
11
+ (() => {
12
+ const g = globalThis;
13
+ return g.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`;
14
+ });
15
+ const store = new Map();
16
+ async function upload(body, opts = {}) {
17
+ const key = opts.key ?? generateKey();
18
+ const bytes = await toBytes(body);
19
+ const record = { bytes };
20
+ if (opts.contentType !== undefined)
21
+ record.contentType = opts.contentType;
22
+ if (opts.metadata !== undefined)
23
+ record.metadata = opts.metadata;
24
+ store.set(key, record);
25
+ return { key, url: `${baseUrl}${key}` };
26
+ }
27
+ return {
28
+ name,
29
+ upload,
30
+ async delete(key) {
31
+ store.delete(key);
32
+ },
33
+ async signedUrl(key) {
34
+ return `${baseUrl}${key}`;
35
+ },
36
+ async get(key) {
37
+ const record = store.get(key);
38
+ if (!record)
39
+ return null;
40
+ // Copy into a fresh ArrayBuffer so downstream mutation can't corrupt the store.
41
+ const copy = new Uint8Array(record.bytes.byteLength);
42
+ copy.set(record.bytes);
43
+ return copy.buffer;
44
+ },
45
+ };
46
+ }
47
+ async function toBytes(body) {
48
+ if (body instanceof Uint8Array)
49
+ return body;
50
+ if (body instanceof ArrayBuffer)
51
+ return new Uint8Array(body);
52
+ const buffer = await body.arrayBuffer();
53
+ return new Uint8Array(buffer);
54
+ }
@@ -0,0 +1,71 @@
1
+ import type { StorageProvider } from "../types.js";
2
+ export declare const R2_SIGNED_URL_CONFIGURATION_ERROR_MESSAGE = "R2 provider: signedUrl requires a `signer` to be configured \u2014 a public base URL cannot produce a time-limited URL. Use publicUrl(key) for permanent public URLs, or configure a signer (e.g. R2's S3-compatible API with SigV4 credentials).";
3
+ /**
4
+ * Subset of the Cloudflare Workers `R2Bucket` binding we depend on. Kept
5
+ * as a minimal structural type so this package does not need a runtime
6
+ * dependency on `@cloudflare/workers-types`.
7
+ */
8
+ export interface R2BucketLike {
9
+ put(key: string, value: ArrayBuffer | ArrayBufferView | Blob | string | ReadableStream | null, options?: R2PutOptionsLike): Promise<unknown>;
10
+ delete(key: string | string[]): Promise<void>;
11
+ get(key: string): Promise<R2ObjectLike | null>;
12
+ }
13
+ export interface R2PutOptionsLike {
14
+ httpMetadata?: {
15
+ contentType?: string;
16
+ };
17
+ customMetadata?: Record<string, string>;
18
+ }
19
+ export interface R2ObjectLike {
20
+ arrayBuffer(): Promise<ArrayBuffer>;
21
+ }
22
+ /**
23
+ * Options for {@link createR2Provider}.
24
+ */
25
+ export interface R2ProviderOptions {
26
+ /** Cloudflare R2 bucket binding (from `env.BUCKET_NAME`). */
27
+ bucket: R2BucketLike;
28
+ /**
29
+ * Base URL used to construct public object URLs. Typical values:
30
+ * - a public R2 custom domain: `https://cdn.example.com/`
31
+ * - a Worker route that proxies to the binding: `https://api.example.com/assets/`
32
+ */
33
+ publicBaseUrl?: string;
34
+ /**
35
+ * Signer invoked by `signedUrl`. Cloudflare R2 bindings do not produce
36
+ * signed URLs directly; templates pass a custom signer that either:
37
+ * - returns a short-lived Worker route URL, or
38
+ * - calls R2's S3-compatible API with SigV4 credentials.
39
+ * Calling `signedUrl` without a signer is a configuration error: a
40
+ * `publicBaseUrl` alone can only produce **permanent, unauthenticated**
41
+ * URLs, which would silently ignore `expiresIn` and defeat the
42
+ * time-limited-access contract. Use `publicUrl(key)` when a permanent
43
+ * public URL is actually what you want.
44
+ */
45
+ signer?: (key: string, expiresIn: number) => Promise<string> | string;
46
+ /** Provider name (defaults to `"r2"`). */
47
+ name?: string;
48
+ /** Custom key generator; defaults to `crypto.randomUUID()`. */
49
+ generateKey?: () => string;
50
+ }
51
+ /**
52
+ * R2 storage provider. Extends the base {@link StorageProvider} contract
53
+ * with {@link R2StorageProvider.publicUrl} for the (R2-common) case where
54
+ * objects are served through a public custom domain or Worker route.
55
+ */
56
+ export interface R2StorageProvider extends StorageProvider {
57
+ /**
58
+ * Permanent, unauthenticated URL for an object: `${publicBaseUrl}${key}`.
59
+ * Only valid for objects that are meant to be public. Throws when
60
+ * `publicBaseUrl` is not configured. For time-limited access to private
61
+ * objects use `signedUrl` (requires a `signer`).
62
+ */
63
+ publicUrl(key: string): string;
64
+ }
65
+ /**
66
+ * Create a Cloudflare R2 storage provider bound to an R2 bucket binding.
67
+ * The R2 binding handles authentication transparently at the Worker
68
+ * runtime boundary, so no credentials are required at this layer.
69
+ */
70
+ export declare function createR2Provider(options: R2ProviderOptions): R2StorageProvider;
71
+ //# sourceMappingURL=r2.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"r2.d.ts","sourceRoot":"","sources":["../../src/providers/r2.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,eAAe,EAAoC,MAAM,aAAa,CAAA;AAEnG,eAAO,MAAM,yCAAyC,sPAC0L,CAAA;AAEhP;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,GAAG,CACD,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,WAAW,GAAG,eAAe,GAAG,IAAI,GAAG,MAAM,GAAG,cAAc,GAAG,IAAI,EAC5E,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC,OAAO,CAAC,CAAA;IACnB,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC7C,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAA;CAC/C;AAED,MAAM,WAAW,gBAAgB;IAC/B,YAAY,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IACvC,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACxC;AAED,MAAM,WAAW,YAAY;IAC3B,WAAW,IAAI,OAAO,CAAC,WAAW,CAAC,CAAA;CACpC;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,6DAA6D;IAC7D,MAAM,EAAE,YAAY,CAAA;IACpB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;;;;;;;;OAUG;IACH,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAA;IACrE,0CAA0C;IAC1C,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,+DAA+D;IAC/D,WAAW,CAAC,EAAE,MAAM,MAAM,CAAA;CAC3B;AAED;;;;GAIG;AACH,MAAM,WAAW,iBAAkB,SAAQ,eAAe;IACxD;;;;;OAKG;IACH,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;CAC/B;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,iBAAiB,GAAG,iBAAiB,CAoD9E"}
@@ -0,0 +1,62 @@
1
+ export const R2_SIGNED_URL_CONFIGURATION_ERROR_MESSAGE = "R2 provider: signedUrl requires a `signer` to be configured — a public base URL cannot produce a time-limited URL. Use publicUrl(key) for permanent public URLs, or configure a signer (e.g. R2's S3-compatible API with SigV4 credentials).";
2
+ /**
3
+ * Create a Cloudflare R2 storage provider bound to an R2 bucket binding.
4
+ * The R2 binding handles authentication transparently at the Worker
5
+ * runtime boundary, so no credentials are required at this layer.
6
+ */
7
+ export function createR2Provider(options) {
8
+ const name = options.name ?? "r2";
9
+ const publicBaseUrl = options.publicBaseUrl ?? "";
10
+ const generateKey = options.generateKey ??
11
+ (() => {
12
+ const g = globalThis;
13
+ return g.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`;
14
+ });
15
+ async function upload(body, opts = {}) {
16
+ const key = opts.key ?? generateKey();
17
+ const putOptions = {};
18
+ if (opts.contentType !== undefined) {
19
+ putOptions.httpMetadata = { contentType: opts.contentType };
20
+ }
21
+ if (opts.metadata !== undefined) {
22
+ putOptions.customMetadata = opts.metadata;
23
+ }
24
+ await options.bucket.put(key, await toPutBody(body), putOptions);
25
+ return { key, url: publicBaseUrl ? `${publicBaseUrl}${key}` : "" };
26
+ }
27
+ return {
28
+ name,
29
+ upload,
30
+ async delete(key) {
31
+ await options.bucket.delete(key);
32
+ },
33
+ async signedUrl(key, expiresIn) {
34
+ // Security (assessment L7): never fall back to `${publicBaseUrl}${key}`
35
+ // here — that would silently return a PERMANENT public URL while the
36
+ // caller believes it expires after `expiresIn` seconds.
37
+ if (!options.signer) {
38
+ throw new Error(R2_SIGNED_URL_CONFIGURATION_ERROR_MESSAGE);
39
+ }
40
+ return options.signer(key, expiresIn);
41
+ },
42
+ publicUrl(key) {
43
+ if (!publicBaseUrl) {
44
+ throw new Error("R2 provider: publicUrl requires `publicBaseUrl` to be configured (a public R2 custom domain or a Worker route that proxies the bucket)");
45
+ }
46
+ return `${publicBaseUrl}${key}`;
47
+ },
48
+ async get(key) {
49
+ const obj = await options.bucket.get(key);
50
+ if (!obj)
51
+ return null;
52
+ return obj.arrayBuffer();
53
+ },
54
+ };
55
+ }
56
+ async function toPutBody(body) {
57
+ if (body instanceof Uint8Array)
58
+ return body;
59
+ if (body instanceof ArrayBuffer)
60
+ return body;
61
+ return body;
62
+ }
@@ -0,0 +1,57 @@
1
+ import type { StorageProvider } from "../types.js";
2
+ /**
3
+ * Fetch shape used by the S3 provider. Matches the global `fetch` and
4
+ * Cloudflare Workers `fetch`. Tests can stub this.
5
+ */
6
+ export type S3Fetch = (input: string, init: {
7
+ method: string;
8
+ headers: Record<string, string>;
9
+ body?: Uint8Array;
10
+ }) => Promise<{
11
+ ok: boolean;
12
+ status: number;
13
+ arrayBuffer: () => Promise<ArrayBuffer>;
14
+ text: () => Promise<string>;
15
+ }>;
16
+ /**
17
+ * Options for {@link createS3Provider}.
18
+ */
19
+ export interface S3ProviderOptions {
20
+ /** AWS access key id. */
21
+ accessKeyId: string;
22
+ /** AWS secret access key. */
23
+ secretAccessKey: string;
24
+ /** Optional session token for temporary credentials. */
25
+ sessionToken?: string;
26
+ /** S3 region (e.g. `"us-east-1"`). */
27
+ region: string;
28
+ /** S3 bucket name. */
29
+ bucket: string;
30
+ /**
31
+ * Endpoint URL override. Defaults to the public AWS S3 endpoint for the
32
+ * region (`https://s3.<region>.amazonaws.com`). Set this for S3-compatible
33
+ * services (MinIO, Backblaze B2, DigitalOcean Spaces, Wasabi, R2 S3 API).
34
+ */
35
+ endpoint?: string;
36
+ /**
37
+ * When `true`, put the bucket in the URL path rather than the hostname
38
+ * subdomain. Defaults to `true` to stay compatible with the widest set
39
+ * of S3-compatible services. Set to `false` to use
40
+ * `https://<bucket>.s3.<region>.amazonaws.com` virtual-hosted style.
41
+ */
42
+ forcePathStyle?: boolean;
43
+ /** Base URL used for the public `url` field returned from `upload`. */
44
+ publicBaseUrl?: string;
45
+ /** Override `fetch` (e.g. in tests). Defaults to global `fetch`. */
46
+ fetch?: S3Fetch;
47
+ /** Provider name (defaults to `"s3"`). */
48
+ name?: string;
49
+ /** Custom key generator; defaults to `crypto.randomUUID()`. */
50
+ generateKey?: () => string;
51
+ }
52
+ /**
53
+ * Create an S3 / S3-compatible storage provider. Uses Web Crypto to sign
54
+ * requests with AWS SigV4, so no AWS SDK dependency is required.
55
+ */
56
+ export declare function createS3Provider(options: S3ProviderOptions): StorageProvider;
57
+ //# sourceMappingURL=s3.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"s3.d.ts","sourceRoot":"","sources":["../../src/providers/s3.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAiB,eAAe,EAAoC,MAAM,aAAa,CAAA;AAEnG;;;GAGG;AACH,MAAM,MAAM,OAAO,GAAG,CACpB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE;IACJ,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,IAAI,CAAC,EAAE,UAAU,CAAA;CAClB,KACE,OAAO,CAAC;IACX,EAAE,EAAE,OAAO,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,WAAW,EAAE,MAAM,OAAO,CAAC,WAAW,CAAC,CAAA;IACvC,IAAI,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAA;CAC5B,CAAC,CAAA;AAMF;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,yBAAyB;IACzB,WAAW,EAAE,MAAM,CAAA;IACnB,6BAA6B;IAC7B,eAAe,EAAE,MAAM,CAAA;IACvB,wDAAwD;IACxD,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,sCAAsC;IACtC,MAAM,EAAE,MAAM,CAAA;IACd,sBAAsB;IACtB,MAAM,EAAE,MAAM,CAAA;IACd;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,uEAAuE;IACvE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,oEAAoE;IACpE,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,0CAA0C;IAC1C,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,+DAA+D;IAC/D,WAAW,CAAC,EAAE,MAAM,MAAM,CAAA;CAC3B;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,iBAAiB,GAAG,eAAe,CAsH5E"}
@@ -0,0 +1,141 @@
1
+ import { presignUrl, signRequest } from "../lib/sigv4.js";
2
+ function defaultS3Fetch(input, init) {
3
+ return globalThis.fetch(input, { ...init, body: init.body });
4
+ }
5
+ /**
6
+ * Create an S3 / S3-compatible storage provider. Uses Web Crypto to sign
7
+ * requests with AWS SigV4, so no AWS SDK dependency is required.
8
+ */
9
+ export function createS3Provider(options) {
10
+ const name = options.name ?? "s3";
11
+ const forcePathStyle = options.forcePathStyle ?? true;
12
+ const endpoint = options.endpoint ??
13
+ (forcePathStyle
14
+ ? `https://s3.${options.region}.amazonaws.com`
15
+ : `https://${options.bucket}.s3.${options.region}.amazonaws.com`);
16
+ const publicBaseUrl = options.publicBaseUrl ?? "";
17
+ const fetchImpl = options.fetch ?? defaultS3Fetch;
18
+ const credentials = {
19
+ accessKeyId: options.accessKeyId,
20
+ secretAccessKey: options.secretAccessKey,
21
+ };
22
+ if (options.sessionToken !== undefined)
23
+ credentials.sessionToken = options.sessionToken;
24
+ const generateKey = options.generateKey ??
25
+ (() => {
26
+ const g = globalThis;
27
+ return g.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`;
28
+ });
29
+ function buildUrl(key) {
30
+ if (forcePathStyle) {
31
+ return `${endpoint}/${encodeURIComponent(options.bucket)}/${encodeKey(key)}`;
32
+ }
33
+ return `${endpoint}/${encodeKey(key)}`;
34
+ }
35
+ async function upload(body, opts = {}) {
36
+ if (!fetchImpl)
37
+ throw new Error("S3 provider requires a fetch implementation");
38
+ const key = opts.key ?? generateKey();
39
+ const bytes = await toBytes(body);
40
+ const url = buildUrl(key);
41
+ const headers = {};
42
+ if (opts.contentType)
43
+ headers["content-type"] = opts.contentType;
44
+ if (opts.metadata) {
45
+ for (const [k, v] of Object.entries(opts.metadata)) {
46
+ headers[`x-amz-meta-${k.toLowerCase()}`] = v;
47
+ }
48
+ }
49
+ const signed = await signRequest({
50
+ method: "PUT",
51
+ url,
52
+ headers,
53
+ body: bytes,
54
+ credentials,
55
+ region: options.region,
56
+ service: "s3",
57
+ });
58
+ const response = await fetchImpl(url, {
59
+ method: "PUT",
60
+ headers: signed.headers,
61
+ body: bytes,
62
+ });
63
+ if (!response.ok) {
64
+ const text = await response.text().catch(() => "");
65
+ throw new Error(`S3 upload failed (${response.status}): ${text}`);
66
+ }
67
+ return { key, url: publicBaseUrl ? `${publicBaseUrl}${key}` : "" };
68
+ }
69
+ return {
70
+ name,
71
+ upload,
72
+ async delete(key) {
73
+ if (!fetchImpl)
74
+ throw new Error("S3 provider requires a fetch implementation");
75
+ const url = buildUrl(key);
76
+ const signed = await signRequest({
77
+ method: "DELETE",
78
+ url,
79
+ credentials,
80
+ region: options.region,
81
+ service: "s3",
82
+ });
83
+ const response = await fetchImpl(url, {
84
+ method: "DELETE",
85
+ headers: signed.headers,
86
+ });
87
+ // S3 returns 204 on successful delete, 404 on missing — treat both as success.
88
+ if (!response.ok && response.status !== 404) {
89
+ const text = await response.text().catch(() => "");
90
+ throw new Error(`S3 delete failed (${response.status}): ${text}`);
91
+ }
92
+ },
93
+ async signedUrl(key, expiresIn) {
94
+ return presignUrl({
95
+ method: "GET",
96
+ url: buildUrl(key),
97
+ expiresIn,
98
+ credentials,
99
+ region: options.region,
100
+ service: "s3",
101
+ });
102
+ },
103
+ async get(key) {
104
+ if (!fetchImpl)
105
+ throw new Error("S3 provider requires a fetch implementation");
106
+ const url = buildUrl(key);
107
+ const signed = await signRequest({
108
+ method: "GET",
109
+ url,
110
+ credentials,
111
+ region: options.region,
112
+ service: "s3",
113
+ });
114
+ const response = await fetchImpl(url, {
115
+ method: "GET",
116
+ headers: signed.headers,
117
+ });
118
+ if (response.status === 404)
119
+ return null;
120
+ if (!response.ok) {
121
+ const text = await response.text().catch(() => "");
122
+ throw new Error(`S3 get failed (${response.status}): ${text}`);
123
+ }
124
+ return response.arrayBuffer();
125
+ },
126
+ };
127
+ }
128
+ function encodeKey(key) {
129
+ return key
130
+ .split("/")
131
+ .map((segment) => encodeURIComponent(segment).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`))
132
+ .join("/");
133
+ }
134
+ async function toBytes(body) {
135
+ if (body instanceof Uint8Array)
136
+ return body;
137
+ if (body instanceof ArrayBuffer)
138
+ return new Uint8Array(body);
139
+ const buffer = await body.arrayBuffer();
140
+ return new Uint8Array(buffer);
141
+ }
@@ -0,0 +1,21 @@
1
+ import type { StorageProvider } from "./types.js";
2
+ /**
3
+ * Thrown when a storage operation cannot find the requested provider.
4
+ */
5
+ export declare class StorageError extends Error {
6
+ constructor(message: string);
7
+ }
8
+ /**
9
+ * Convenience wrapper exposing a single provider. Most deployments use
10
+ * exactly one storage backend at a time; for those cases the service is
11
+ * just a named wrapper.
12
+ */
13
+ export interface StorageService extends StorageProvider {
14
+ /** The wrapped provider. */
15
+ readonly provider: StorageProvider;
16
+ }
17
+ /**
18
+ * Create a storage service that delegates all calls to the given provider.
19
+ */
20
+ export declare function createStorageService(provider: StorageProvider): StorageService;
21
+ //# sourceMappingURL=service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAEjD;;GAEG;AACH,qBAAa,YAAa,SAAQ,KAAK;gBACzB,OAAO,EAAE,MAAM;CAI5B;AAED;;;;GAIG;AACH,MAAM,WAAW,cAAe,SAAQ,eAAe;IACrD,4BAA4B;IAC5B,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAA;CACnC;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,eAAe,GAAG,cAAc,CAS9E"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Thrown when a storage operation cannot find the requested provider.
3
+ */
4
+ export class StorageError extends Error {
5
+ constructor(message) {
6
+ super(message);
7
+ this.name = "StorageError";
8
+ }
9
+ }
10
+ /**
11
+ * Create a storage service that delegates all calls to the given provider.
12
+ */
13
+ export function createStorageService(provider) {
14
+ return {
15
+ provider,
16
+ name: provider.name,
17
+ upload: provider.upload.bind(provider),
18
+ delete: provider.delete.bind(provider),
19
+ signedUrl: provider.signedUrl.bind(provider),
20
+ get: provider.get.bind(provider),
21
+ };
22
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Accepted body shapes for uploads. Providers normalize to `Uint8Array`
3
+ * or pass through to their native API.
4
+ */
5
+ export type StorageUploadBody = ArrayBuffer | Uint8Array | Blob;
6
+ /**
7
+ * Options controlling an upload.
8
+ */
9
+ export interface UploadOptions {
10
+ /**
11
+ * Override the object key. When omitted, providers generate a random
12
+ * key (typically `${randomUUID()}` — UUID v4 where available).
13
+ */
14
+ key?: string;
15
+ /** MIME content type (e.g. `"image/png"`). */
16
+ contentType?: string;
17
+ /** Custom metadata; persisted by providers that support it (R2, S3). */
18
+ metadata?: Record<string, string>;
19
+ }
20
+ /**
21
+ * Result of a successful upload.
22
+ */
23
+ export interface StorageObject {
24
+ /** Object key inside the bucket/store. */
25
+ key: string;
26
+ /**
27
+ * Public URL for the object when the provider exposes one. Empty string
28
+ * when the object is private and can only be accessed via `signedUrl`.
29
+ */
30
+ url: string;
31
+ }
32
+ /**
33
+ * Pluggable object storage provider.
34
+ *
35
+ * Built-in implementations:
36
+ * - `local` — in-memory, for dev and tests
37
+ * - `r2` — Cloudflare R2 via the workers binding
38
+ * - `s3` — Amazon S3 / S3-compatible via SigV4
39
+ */
40
+ export interface StorageProvider {
41
+ /** Unique provider name (e.g. `"r2"`, `"s3"`, `"local"`). */
42
+ readonly name: string;
43
+ /** Upload an object. */
44
+ upload(body: StorageUploadBody, options?: UploadOptions): Promise<StorageObject>;
45
+ /** Delete an object by key. No-op if the key does not exist. */
46
+ delete(key: string): Promise<void>;
47
+ /**
48
+ * Produce a time-limited URL that grants GET access to the object.
49
+ * `expiresIn` is in seconds.
50
+ */
51
+ signedUrl(key: string, expiresIn: number): Promise<string>;
52
+ /**
53
+ * Fetch an object's bytes. Returns `null` when the object is absent.
54
+ */
55
+ get(key: string): Promise<ArrayBuffer | null>;
56
+ }
57
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,MAAM,iBAAiB,GAAG,WAAW,GAAG,UAAU,GAAG,IAAI,CAAA;AAE/D;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,8CAA8C;IAC9C,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAClC;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,0CAA0C;IAC1C,GAAG,EAAE,MAAM,CAAA;IACX;;;OAGG;IACH,GAAG,EAAE,MAAM,CAAA;CACZ;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,eAAe;IAC9B,6DAA6D;IAC7D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,wBAAwB;IACxB,MAAM,CAAC,IAAI,EAAE,iBAAiB,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CAAA;IAChF,gEAAgE;IAChE,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAClC;;;OAGG;IACH,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAC1D;;OAEG;IACH,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAA;CAC9C"}
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@voyant-travel/storage",
3
+ "version": "0.104.1",
4
+ "license": "Apache-2.0",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "import": "./dist/index.js",
10
+ "default": "./dist/index.js"
11
+ },
12
+ "./types": {
13
+ "types": "./dist/types.d.ts",
14
+ "import": "./dist/types.js",
15
+ "default": "./dist/types.js"
16
+ },
17
+ "./service": {
18
+ "types": "./dist/service.d.ts",
19
+ "import": "./dist/service.js",
20
+ "default": "./dist/service.js"
21
+ },
22
+ "./providers/local": {
23
+ "types": "./dist/providers/local.d.ts",
24
+ "import": "./dist/providers/local.js",
25
+ "default": "./dist/providers/local.js"
26
+ },
27
+ "./providers/r2": {
28
+ "types": "./dist/providers/r2.d.ts",
29
+ "import": "./dist/providers/r2.js",
30
+ "default": "./dist/providers/r2.js"
31
+ },
32
+ "./providers/s3": {
33
+ "types": "./dist/providers/s3.d.ts",
34
+ "import": "./dist/providers/s3.js",
35
+ "default": "./dist/providers/s3.js"
36
+ },
37
+ "./lib/sigv4": {
38
+ "types": "./dist/lib/sigv4.d.ts",
39
+ "import": "./dist/lib/sigv4.js",
40
+ "default": "./dist/lib/sigv4.js"
41
+ }
42
+ },
43
+ "files": [
44
+ "dist"
45
+ ],
46
+ "publishConfig": {
47
+ "access": "public"
48
+ },
49
+ "dependencies": {},
50
+ "devDependencies": {
51
+ "typescript": "^6.0.2",
52
+ "vitest": "^4.1.2",
53
+ "@voyant-travel/voyant-typescript-config": "^0.1.0"
54
+ },
55
+ "repository": {
56
+ "type": "git",
57
+ "url": "https://github.com/voyant-travel/voyant.git",
58
+ "directory": "packages/storage"
59
+ },
60
+ "scripts": {
61
+ "typecheck": "tsc --noEmit",
62
+ "lint": "biome check src/",
63
+ "test": "vitest run",
64
+ "build": "tsc -p tsconfig.json",
65
+ "clean": "rm -rf dist tsconfig.tsbuildinfo"
66
+ },
67
+ "main": "./dist/index.js",
68
+ "types": "./dist/index.d.ts"
69
+ }