formdata-io 1.0.0 → 1.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.
@@ -0,0 +1,271 @@
1
+ 'use strict';
2
+
3
+ var crypto = require('crypto');
4
+ var vm = require('vm');
5
+
6
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
7
+
8
+ var vm__default = /*#__PURE__*/_interopDefault(vm);
9
+
10
+ // src/storage/utils.ts
11
+ var DATA_URI_MAX_LENGTH = 100 * 1024 * 1024;
12
+ var REGEX_TIMEOUT_MS = 50;
13
+ var VM_REGEX_SCRIPT = new vm__default.default.Script("input.match(pattern)");
14
+ var RegExpTimeoutError = class extends Error {
15
+ constructor(timeoutMs) {
16
+ super(`RegExp execution timed out after ${timeoutMs}ms`);
17
+ this.name = "RegExpTimeoutError";
18
+ }
19
+ };
20
+ function execRegex(input, pattern, timeoutMs = REGEX_TIMEOUT_MS) {
21
+ if (input.length > DATA_URI_MAX_LENGTH) {
22
+ throw new Error(
23
+ `Input length (${input.length}) exceeds maximum allowed (${DATA_URI_MAX_LENGTH} bytes)`
24
+ );
25
+ }
26
+ try {
27
+ const context = vm__default.default.createContext({ input, pattern });
28
+ return VM_REGEX_SCRIPT.runInContext(context, { timeout: timeoutMs });
29
+ } catch (err) {
30
+ const msg = err instanceof Error ? err.message : "";
31
+ if (msg.toLowerCase().includes("timed out") || msg.toLowerCase().includes("timeout")) {
32
+ throw new RegExpTimeoutError(timeoutMs);
33
+ }
34
+ throw err;
35
+ }
36
+ }
37
+ function sanitizeFilename(filename) {
38
+ return filename.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9._-]/g, "");
39
+ }
40
+ function generateKey(filename, prefix) {
41
+ const uuid = crypto.randomUUID();
42
+ const sanitized = sanitizeFilename(filename);
43
+ const name = `${uuid}-${sanitized}`;
44
+ if (prefix) {
45
+ const trimmed = prefix.replace(/\/+$/, "");
46
+ return `${trimmed}/${name}`;
47
+ }
48
+ return name;
49
+ }
50
+ function base64ToBuffer(dataUri) {
51
+ const match = execRegex(dataUri, /^data:([^;]+);base64,(.+)$/);
52
+ if (!match) {
53
+ throw new Error("Invalid base64 data URI format. Expected: data:{mimetype};base64,{data}");
54
+ }
55
+ const [, mimetype, data] = match;
56
+ const buffer = Buffer.from(data, "base64");
57
+ return { buffer, mimetype };
58
+ }
59
+ function isParsedFile(input) {
60
+ return typeof input === "object" && !Buffer.isBuffer(input) && "buffer" in input && "originalname" in input && "mimetype" in input;
61
+ }
62
+ function resolveInput(input, options) {
63
+ if (isParsedFile(input)) {
64
+ return {
65
+ buffer: input.buffer,
66
+ filename: options?.filename ?? input.originalname,
67
+ mimetype: options?.mimetype ?? input.mimetype,
68
+ size: input.size
69
+ };
70
+ }
71
+ if (Buffer.isBuffer(input)) {
72
+ if (!options?.filename) {
73
+ throw new Error("filename is required in options when uploading a Buffer");
74
+ }
75
+ const buffer = input;
76
+ return {
77
+ buffer,
78
+ filename: options.filename,
79
+ mimetype: options?.mimetype ?? "application/octet-stream",
80
+ size: buffer.length
81
+ };
82
+ }
83
+ if (typeof input === "string") {
84
+ const { buffer, mimetype } = base64ToBuffer(input);
85
+ if (!options?.filename) {
86
+ throw new Error("filename is required in options when uploading a base64 string");
87
+ }
88
+ return {
89
+ buffer,
90
+ filename: options.filename,
91
+ mimetype: options?.mimetype ?? mimetype,
92
+ size: buffer.length
93
+ };
94
+ }
95
+ throw new Error("Invalid upload input: must be a ParsedFile, Buffer, or base64 data URI string");
96
+ }
97
+
98
+ // src/storage/providers/aws.ts
99
+ var s3ModulePromise = null;
100
+ async function loadS3() {
101
+ if (!s3ModulePromise) {
102
+ const pkg = "@aws-sdk/client-s3";
103
+ s3ModulePromise = import(pkg).catch((err) => {
104
+ s3ModulePromise = null;
105
+ throw new Error(
106
+ "AWS S3 SDK not found. Install it with: npm install @aws-sdk/client-s3"
107
+ );
108
+ });
109
+ }
110
+ return s3ModulePromise;
111
+ }
112
+ function buildS3Client(config, S3Client) {
113
+ const credentials = {
114
+ accessKeyId: config.accessKeyId,
115
+ secretAccessKey: config.secretAccessKey
116
+ };
117
+ if (config.sessionToken) {
118
+ credentials.sessionToken = config.sessionToken;
119
+ }
120
+ const baseConfig = {
121
+ region: config.region,
122
+ credentials
123
+ };
124
+ if (config.endpoint) {
125
+ return new S3Client({ ...baseConfig, endpoint: config.endpoint, forcePathStyle: true });
126
+ }
127
+ return new S3Client(baseConfig);
128
+ }
129
+ function createAwsProvider(config) {
130
+ let clientPromise = null;
131
+ function getClient() {
132
+ if (!clientPromise) {
133
+ clientPromise = loadS3().then((sdk) => ({ client: buildS3Client(config, sdk.S3Client), sdk })).catch((err) => {
134
+ clientPromise = null;
135
+ throw err;
136
+ });
137
+ }
138
+ return clientPromise;
139
+ }
140
+ async function upload(resolved, keyPrefix) {
141
+ const { client, sdk } = await getClient();
142
+ const prefix = keyPrefix ?? config.keyPrefix;
143
+ const key = generateKey(resolved.filename, prefix);
144
+ const putParams = {
145
+ Bucket: config.bucket,
146
+ Key: key,
147
+ Body: resolved.buffer,
148
+ ContentType: resolved.mimetype,
149
+ ContentLength: resolved.size
150
+ };
151
+ if (config.acl) {
152
+ putParams.ACL = config.acl;
153
+ }
154
+ try {
155
+ await client.send(new sdk.PutObjectCommand(putParams));
156
+ } catch (err) {
157
+ const msg = err instanceof Error ? err.message : String(err);
158
+ if (msg.includes("AccessControlListNotSupported")) {
159
+ throw new Error(
160
+ `S3 ACL error on bucket "${config.bucket}": ACLs are disabled because the bucket uses Object Ownership = "Bucket owner enforced" (the default since April 2023). Remove the 'acl' option from your AWS config or change the bucket's Object Ownership setting in the S3 console. Original error: ${msg}`
161
+ );
162
+ }
163
+ throw err;
164
+ }
165
+ const url = config.endpoint ? `${config.endpoint.replace(/\/$/, "")}/${config.bucket}/${key}` : `https://${config.bucket}.s3.${config.region}.amazonaws.com/${key}`;
166
+ return {
167
+ url,
168
+ key,
169
+ size: resolved.size,
170
+ mimetype: resolved.mimetype
171
+ };
172
+ }
173
+ async function del(key) {
174
+ const { client, sdk } = await getClient();
175
+ await client.send(
176
+ new sdk.DeleteObjectCommand({
177
+ Bucket: config.bucket,
178
+ Key: key
179
+ })
180
+ );
181
+ }
182
+ return { upload, delete: del };
183
+ }
184
+
185
+ // src/storage/providers/supabase.ts
186
+ async function supabaseUpload(config, resolved, keyPrefix) {
187
+ const baseUrl = config.url.replace(/\/$/, "");
188
+ const prefix = keyPrefix ?? config.keyPrefix;
189
+ const key = generateKey(resolved.filename, prefix);
190
+ const uploadUrl = `${baseUrl}/storage/v1/object/${config.bucket}/${key}`;
191
+ const response = await fetch(uploadUrl, {
192
+ method: "POST",
193
+ headers: {
194
+ Authorization: `Bearer ${config.serviceKey}`,
195
+ "Content-Type": resolved.mimetype,
196
+ "Content-Length": String(resolved.size)
197
+ },
198
+ body: resolved.buffer
199
+ });
200
+ if (!response.ok) {
201
+ const text = await response.text().catch(() => response.statusText);
202
+ throw new Error(`Supabase Storage upload failed (${response.status}): ${text}`);
203
+ }
204
+ const isPublic = config.publicBucket !== false;
205
+ const url = isPublic ? `${baseUrl}/storage/v1/object/public/${config.bucket}/${key}` : key;
206
+ return {
207
+ url,
208
+ key,
209
+ size: resolved.size,
210
+ mimetype: resolved.mimetype
211
+ };
212
+ }
213
+ async function supabaseDelete(config, key) {
214
+ const baseUrl = config.url.replace(/\/$/, "");
215
+ const deleteUrl = `${baseUrl}/storage/v1/object/${config.bucket}`;
216
+ const response = await fetch(deleteUrl, {
217
+ method: "DELETE",
218
+ headers: {
219
+ Authorization: `Bearer ${config.serviceKey}`,
220
+ "Content-Type": "application/json"
221
+ },
222
+ body: JSON.stringify({ prefixes: [key] })
223
+ });
224
+ if (!response.ok) {
225
+ const text = await response.text().catch(() => response.statusText);
226
+ throw new Error(`Supabase Storage delete failed (${response.status}): ${text}`);
227
+ }
228
+ }
229
+
230
+ // src/storage/storage.ts
231
+ function validateConfig(config) {
232
+ if (!config.provider) {
233
+ throw new Error('Storage config must include a provider ("aws" or "supabase")');
234
+ }
235
+ if (config.provider === "aws") {
236
+ if (!config.bucket) throw new Error("AWS config requires bucket");
237
+ if (!config.region) throw new Error("AWS config requires region");
238
+ if (!config.accessKeyId) throw new Error("AWS config requires accessKeyId");
239
+ if (!config.secretAccessKey) throw new Error("AWS config requires secretAccessKey");
240
+ } else if (config.provider === "supabase") {
241
+ if (!config.bucket) throw new Error("Supabase config requires bucket");
242
+ if (!config.url) throw new Error("Supabase config requires url");
243
+ if (!config.serviceKey) throw new Error("Supabase config requires serviceKey");
244
+ } else {
245
+ throw new Error(`Unknown storage provider: "${config.provider}"`);
246
+ }
247
+ }
248
+ function createStorage(config) {
249
+ validateConfig(config);
250
+ if (config.provider === "aws") {
251
+ const aws = createAwsProvider(config);
252
+ const upload2 = async (input, options) => {
253
+ const resolved = resolveInput(input, options);
254
+ return aws.upload(resolved, options?.keyPrefix);
255
+ };
256
+ const uploadMany2 = async (inputs, options) => Promise.all(inputs.map((i) => upload2(i, options)));
257
+ return { upload: upload2, uploadMany: uploadMany2, delete: aws.delete };
258
+ }
259
+ const upload = async (input, options) => {
260
+ const resolved = resolveInput(input, options);
261
+ return supabaseUpload(config, resolved, options?.keyPrefix);
262
+ };
263
+ const uploadMany = async (inputs, options) => Promise.all(inputs.map((i) => upload(i, options)));
264
+ const del = async (key) => supabaseDelete(config, key);
265
+ return { upload, uploadMany, delete: del };
266
+ }
267
+
268
+ exports.RegExpTimeoutError = RegExpTimeoutError;
269
+ exports.createStorage = createStorage;
270
+ //# sourceMappingURL=index.js.map
271
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/storage/utils.ts","../../src/storage/providers/aws.ts","../../src/storage/providers/supabase.ts","../../src/storage/storage.ts"],"names":["vm","randomUUID","upload","uploadMany"],"mappings":"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","file":"index.js","sourcesContent":["import { randomUUID } from 'crypto';\nimport vm from 'node:vm';\nimport type { ParsedFile } from '../server/types';\nimport type { ResolvedInput, UploadInput, UploadOptions } from './types';\n\nconst DATA_URI_MAX_LENGTH = 100 * 1024 * 1024; // 100 MB\nconst REGEX_TIMEOUT_MS = 50;\n\n// Compiled once at module load; vm.createContext() is called per-invocation for isolation.\nconst VM_REGEX_SCRIPT = new vm.Script('input.match(pattern)');\n\nexport class RegExpTimeoutError extends Error {\n constructor(timeoutMs: number) {\n super(`RegExp execution timed out after ${timeoutMs}ms`);\n this.name = 'RegExpTimeoutError';\n }\n}\n\n// Runs a regex with a real timeout via vm.runInNewContext, preventing ReDoS attacks.\nfunction execRegex(\n input: string,\n pattern: RegExp,\n timeoutMs = REGEX_TIMEOUT_MS\n): RegExpMatchArray | null {\n if (input.length > DATA_URI_MAX_LENGTH) {\n throw new Error(\n `Input length (${input.length}) exceeds maximum allowed (${DATA_URI_MAX_LENGTH} bytes)`\n );\n }\n try {\n const context = vm.createContext({ input, pattern });\n return VM_REGEX_SCRIPT.runInContext(context, { timeout: timeoutMs }) as RegExpMatchArray | null;\n } catch (err) {\n const msg = err instanceof Error ? err.message : '';\n if (msg.toLowerCase().includes('timed out') || msg.toLowerCase().includes('timeout')) {\n throw new RegExpTimeoutError(timeoutMs);\n }\n throw err;\n }\n}\n\n// Uses NFD decomposition to transliterate accented chars (é → e) before stripping non-ASCII,\n// so accented filenames produce readable keys instead of empty strings.\nfunction sanitizeFilename(filename: string): string {\n return filename\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toLowerCase()\n .replace(/\\s+/g, '-')\n .replace(/[^a-z0-9._-]/g, '');\n}\n\n/** Format: {prefix}/{uuid}-{sanitized-filename} */\nexport function generateKey(filename: string, prefix?: string): string {\n const uuid = randomUUID();\n const sanitized = sanitizeFilename(filename);\n const name = `${uuid}-${sanitized}`;\n if (prefix) {\n const trimmed = prefix.replace(/\\/+$/, '');\n return `${trimmed}/${name}`;\n }\n return name;\n}\n\n/** Parses a base64 data URI into a Buffer and MIME type. */\nexport function base64ToBuffer(dataUri: string): { buffer: Buffer; mimetype: string } {\n const match = execRegex(dataUri, /^data:([^;]+);base64,(.+)$/);\n if (!match) {\n throw new Error('Invalid base64 data URI format. Expected: data:{mimetype};base64,{data}');\n }\n const [, mimetype, data] = match;\n const buffer = Buffer.from(data, 'base64');\n return { buffer, mimetype };\n}\n\nfunction isParsedFile(input: UploadInput): input is ParsedFile {\n return (\n typeof input === 'object' &&\n !Buffer.isBuffer(input) &&\n 'buffer' in input &&\n 'originalname' in input &&\n 'mimetype' in input\n );\n}\n\n/** Normalizes any UploadInput into a ResolvedInput with buffer, filename, mimetype, and size. */\nexport function resolveInput(input: UploadInput, options?: UploadOptions): ResolvedInput {\n if (isParsedFile(input)) {\n return {\n buffer: input.buffer,\n filename: options?.filename ?? input.originalname,\n mimetype: options?.mimetype ?? input.mimetype,\n size: input.size,\n };\n }\n\n if (Buffer.isBuffer(input)) {\n if (!options?.filename) {\n throw new Error('filename is required in options when uploading a Buffer');\n }\n const buffer = input;\n return {\n buffer,\n filename: options.filename,\n mimetype: options?.mimetype ?? 'application/octet-stream',\n size: buffer.length,\n };\n }\n\n if (typeof input === 'string') {\n const { buffer, mimetype } = base64ToBuffer(input);\n if (!options?.filename) {\n throw new Error('filename is required in options when uploading a base64 string');\n }\n return {\n buffer,\n filename: options.filename,\n mimetype: options?.mimetype ?? mimetype,\n size: buffer.length,\n };\n }\n\n throw new Error('Invalid upload input: must be a ParsedFile, Buffer, or base64 data URI string');\n}\n","import type { AWSStorageConfig, ResolvedInput, UploadResult } from '../types';\nimport { generateKey } from '../utils';\n\n// Local type interfaces for @aws-sdk/client-s3 (optional peer dependency).\ninterface S3CredentialsConfig {\n accessKeyId: string;\n secretAccessKey: string;\n sessionToken?: string;\n}\n\ninterface S3ClientConfig {\n region: string;\n credentials: S3CredentialsConfig;\n endpoint?: string;\n forcePathStyle?: boolean;\n}\n\ninterface S3Command {\n readonly _tag: 'S3Command';\n}\n\ninterface S3ClientInstance {\n send(command: S3Command): Promise<void>;\n}\n\ninterface PutObjectCommandInput {\n Bucket: string;\n Key: string;\n Body: Buffer;\n ContentType: string;\n ContentLength: number;\n ACL?: string;\n}\n\ninterface DeleteObjectCommandInput {\n Bucket: string;\n Key: string;\n}\n\ninterface AwsS3Module {\n S3Client: new (config: S3ClientConfig) => S3ClientInstance;\n PutObjectCommand: new (input: PutObjectCommandInput) => S3Command;\n DeleteObjectCommand: new (input: DeleteObjectCommandInput) => S3Command;\n}\n\nlet s3ModulePromise: Promise<AwsS3Module> | null = null;\n\nasync function loadS3(): Promise<AwsS3Module> {\n if (!s3ModulePromise) {\n const pkg: string = '@aws-sdk/client-s3'; // variable prevents static resolution of optional peer dep\n s3ModulePromise = (import(pkg) as Promise<AwsS3Module>).catch((err) => {\n s3ModulePromise = null;\n void err;\n throw new Error(\n 'AWS S3 SDK not found. Install it with: npm install @aws-sdk/client-s3'\n );\n });\n }\n return s3ModulePromise;\n}\n\nfunction buildS3Client(\n config: AWSStorageConfig,\n S3Client: new (config: S3ClientConfig) => S3ClientInstance\n): S3ClientInstance {\n const credentials: S3CredentialsConfig = {\n accessKeyId: config.accessKeyId,\n secretAccessKey: config.secretAccessKey,\n };\n\n if (config.sessionToken) {\n credentials.sessionToken = config.sessionToken;\n }\n\n const baseConfig: S3ClientConfig = {\n region: config.region,\n credentials,\n };\n\n if (config.endpoint) {\n return new S3Client({ ...baseConfig, endpoint: config.endpoint, forcePathStyle: true });\n }\n\n return new S3Client(baseConfig);\n}\n\nexport interface AwsProvider {\n upload(resolved: ResolvedInput, keyPrefix?: string): Promise<UploadResult>;\n delete(key: string): Promise<void>;\n}\n\nexport function createAwsProvider(config: AWSStorageConfig): AwsProvider {\n let clientPromise: Promise<{ client: S3ClientInstance; sdk: AwsS3Module }> | null = null;\n\n function getClient(): Promise<{ client: S3ClientInstance; sdk: AwsS3Module }> {\n if (!clientPromise) {\n clientPromise = loadS3()\n .then((sdk) => ({ client: buildS3Client(config, sdk.S3Client), sdk }))\n .catch((err) => {\n clientPromise = null;\n throw err;\n });\n }\n return clientPromise;\n }\n\n async function upload(resolved: ResolvedInput, keyPrefix?: string): Promise<UploadResult> {\n const { client, sdk } = await getClient();\n\n const prefix = keyPrefix ?? config.keyPrefix;\n const key = generateKey(resolved.filename, prefix);\n\n const putParams: PutObjectCommandInput = {\n Bucket: config.bucket,\n Key: key,\n Body: resolved.buffer,\n ContentType: resolved.mimetype,\n ContentLength: resolved.size,\n };\n\n if (config.acl) {\n putParams.ACL = config.acl;\n }\n\n try {\n await client.send(new sdk.PutObjectCommand(putParams));\n } catch (err: unknown) {\n const msg = err instanceof Error ? err.message : String(err);\n if (msg.includes('AccessControlListNotSupported')) {\n throw new Error(\n `S3 ACL error on bucket \"${config.bucket}\": ACLs are disabled because the bucket uses ` +\n `Object Ownership = \"Bucket owner enforced\" (the default since April 2023). ` +\n `Remove the 'acl' option from your AWS config or change the bucket's Object Ownership ` +\n `setting in the S3 console. Original error: ${msg}`\n );\n }\n throw err;\n }\n\n const url = config.endpoint\n ? `${config.endpoint.replace(/\\/$/, '')}/${config.bucket}/${key}`\n : `https://${config.bucket}.s3.${config.region}.amazonaws.com/${key}`;\n\n return {\n url,\n key,\n size: resolved.size,\n mimetype: resolved.mimetype,\n };\n }\n\n async function del(key: string): Promise<void> {\n const { client, sdk } = await getClient();\n\n await client.send(\n new sdk.DeleteObjectCommand({\n Bucket: config.bucket,\n Key: key,\n })\n );\n }\n\n return { upload, delete: del };\n}\n","import type { ResolvedInput, SupabaseStorageConfig, UploadResult } from '../types';\nimport { generateKey } from '../utils';\n\nexport async function supabaseUpload(\n config: SupabaseStorageConfig,\n resolved: ResolvedInput,\n keyPrefix?: string\n): Promise<UploadResult> {\n const baseUrl = config.url.replace(/\\/$/, '');\n\n const prefix = keyPrefix ?? config.keyPrefix;\n const key = generateKey(resolved.filename, prefix);\n\n const uploadUrl = `${baseUrl}/storage/v1/object/${config.bucket}/${key}`;\n\n const response = await fetch(uploadUrl, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${config.serviceKey}`,\n 'Content-Type': resolved.mimetype,\n 'Content-Length': String(resolved.size),\n },\n body: resolved.buffer as unknown as BodyInit,\n });\n\n if (!response.ok) {\n const text = await response.text().catch(() => response.statusText);\n throw new Error(`Supabase Storage upload failed (${response.status}): ${text}`);\n }\n\n const isPublic = config.publicBucket !== false;\n const url = isPublic\n ? `${baseUrl}/storage/v1/object/public/${config.bucket}/${key}`\n : key;\n\n return {\n url,\n key,\n size: resolved.size,\n mimetype: resolved.mimetype,\n };\n}\n\nexport async function supabaseDelete(\n config: SupabaseStorageConfig,\n key: string\n): Promise<void> {\n const baseUrl = config.url.replace(/\\/$/, '');\n const deleteUrl = `${baseUrl}/storage/v1/object/${config.bucket}`;\n\n const response = await fetch(deleteUrl, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${config.serviceKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ prefixes: [key] }),\n });\n\n if (!response.ok) {\n const text = await response.text().catch(() => response.statusText);\n throw new Error(`Supabase Storage delete failed (${response.status}): ${text}`);\n }\n}\n","import type {\n StorageConfig,\n StorageAdapter,\n UploadInput,\n UploadOptions,\n UploadResult,\n} from './types';\nimport { resolveInput } from './utils';\nimport { createAwsProvider } from './providers/aws';\nimport { supabaseUpload, supabaseDelete } from './providers/supabase';\n\nfunction validateConfig(config: StorageConfig): void {\n if (!config.provider) {\n throw new Error('Storage config must include a provider (\"aws\" or \"supabase\")');\n }\n\n if (config.provider === 'aws') {\n if (!config.bucket) throw new Error('AWS config requires bucket');\n if (!config.region) throw new Error('AWS config requires region');\n if (!config.accessKeyId) throw new Error('AWS config requires accessKeyId');\n if (!config.secretAccessKey) throw new Error('AWS config requires secretAccessKey');\n } else if (config.provider === 'supabase') {\n if (!config.bucket) throw new Error('Supabase config requires bucket');\n if (!config.url) throw new Error('Supabase config requires url');\n if (!config.serviceKey) throw new Error('Supabase config requires serviceKey');\n } else {\n throw new Error(`Unknown storage provider: \"${(config as StorageConfig).provider}\"`);\n }\n}\n\nexport function createStorage(config: StorageConfig): StorageAdapter {\n validateConfig(config);\n\n if (config.provider === 'aws') {\n const aws = createAwsProvider(config);\n\n const upload = async (input: UploadInput, options?: UploadOptions): Promise<UploadResult> => {\n const resolved = resolveInput(input, options);\n return aws.upload(resolved, options?.keyPrefix);\n };\n\n const uploadMany = async (\n inputs: UploadInput[],\n options?: UploadOptions\n ): Promise<UploadResult[]> => Promise.all(inputs.map((i) => upload(i, options)));\n\n return { upload, uploadMany, delete: aws.delete };\n }\n\n const upload = async (input: UploadInput, options?: UploadOptions): Promise<UploadResult> => {\n const resolved = resolveInput(input, options);\n return supabaseUpload(config, resolved, options?.keyPrefix);\n };\n\n const uploadMany = async (\n inputs: UploadInput[],\n options?: UploadOptions\n ): Promise<UploadResult[]> => Promise.all(inputs.map((i) => upload(i, options)));\n\n const del = async (key: string): Promise<void> => supabaseDelete(config, key);\n\n return { upload, uploadMany, delete: del };\n}\n"]}
@@ -0,0 +1,264 @@
1
+ import { randomUUID } from 'crypto';
2
+ import vm from 'vm';
3
+
4
+ // src/storage/utils.ts
5
+ var DATA_URI_MAX_LENGTH = 100 * 1024 * 1024;
6
+ var REGEX_TIMEOUT_MS = 50;
7
+ var VM_REGEX_SCRIPT = new vm.Script("input.match(pattern)");
8
+ var RegExpTimeoutError = class extends Error {
9
+ constructor(timeoutMs) {
10
+ super(`RegExp execution timed out after ${timeoutMs}ms`);
11
+ this.name = "RegExpTimeoutError";
12
+ }
13
+ };
14
+ function execRegex(input, pattern, timeoutMs = REGEX_TIMEOUT_MS) {
15
+ if (input.length > DATA_URI_MAX_LENGTH) {
16
+ throw new Error(
17
+ `Input length (${input.length}) exceeds maximum allowed (${DATA_URI_MAX_LENGTH} bytes)`
18
+ );
19
+ }
20
+ try {
21
+ const context = vm.createContext({ input, pattern });
22
+ return VM_REGEX_SCRIPT.runInContext(context, { timeout: timeoutMs });
23
+ } catch (err) {
24
+ const msg = err instanceof Error ? err.message : "";
25
+ if (msg.toLowerCase().includes("timed out") || msg.toLowerCase().includes("timeout")) {
26
+ throw new RegExpTimeoutError(timeoutMs);
27
+ }
28
+ throw err;
29
+ }
30
+ }
31
+ function sanitizeFilename(filename) {
32
+ return filename.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9._-]/g, "");
33
+ }
34
+ function generateKey(filename, prefix) {
35
+ const uuid = randomUUID();
36
+ const sanitized = sanitizeFilename(filename);
37
+ const name = `${uuid}-${sanitized}`;
38
+ if (prefix) {
39
+ const trimmed = prefix.replace(/\/+$/, "");
40
+ return `${trimmed}/${name}`;
41
+ }
42
+ return name;
43
+ }
44
+ function base64ToBuffer(dataUri) {
45
+ const match = execRegex(dataUri, /^data:([^;]+);base64,(.+)$/);
46
+ if (!match) {
47
+ throw new Error("Invalid base64 data URI format. Expected: data:{mimetype};base64,{data}");
48
+ }
49
+ const [, mimetype, data] = match;
50
+ const buffer = Buffer.from(data, "base64");
51
+ return { buffer, mimetype };
52
+ }
53
+ function isParsedFile(input) {
54
+ return typeof input === "object" && !Buffer.isBuffer(input) && "buffer" in input && "originalname" in input && "mimetype" in input;
55
+ }
56
+ function resolveInput(input, options) {
57
+ if (isParsedFile(input)) {
58
+ return {
59
+ buffer: input.buffer,
60
+ filename: options?.filename ?? input.originalname,
61
+ mimetype: options?.mimetype ?? input.mimetype,
62
+ size: input.size
63
+ };
64
+ }
65
+ if (Buffer.isBuffer(input)) {
66
+ if (!options?.filename) {
67
+ throw new Error("filename is required in options when uploading a Buffer");
68
+ }
69
+ const buffer = input;
70
+ return {
71
+ buffer,
72
+ filename: options.filename,
73
+ mimetype: options?.mimetype ?? "application/octet-stream",
74
+ size: buffer.length
75
+ };
76
+ }
77
+ if (typeof input === "string") {
78
+ const { buffer, mimetype } = base64ToBuffer(input);
79
+ if (!options?.filename) {
80
+ throw new Error("filename is required in options when uploading a base64 string");
81
+ }
82
+ return {
83
+ buffer,
84
+ filename: options.filename,
85
+ mimetype: options?.mimetype ?? mimetype,
86
+ size: buffer.length
87
+ };
88
+ }
89
+ throw new Error("Invalid upload input: must be a ParsedFile, Buffer, or base64 data URI string");
90
+ }
91
+
92
+ // src/storage/providers/aws.ts
93
+ var s3ModulePromise = null;
94
+ async function loadS3() {
95
+ if (!s3ModulePromise) {
96
+ const pkg = "@aws-sdk/client-s3";
97
+ s3ModulePromise = import(pkg).catch((err) => {
98
+ s3ModulePromise = null;
99
+ throw new Error(
100
+ "AWS S3 SDK not found. Install it with: npm install @aws-sdk/client-s3"
101
+ );
102
+ });
103
+ }
104
+ return s3ModulePromise;
105
+ }
106
+ function buildS3Client(config, S3Client) {
107
+ const credentials = {
108
+ accessKeyId: config.accessKeyId,
109
+ secretAccessKey: config.secretAccessKey
110
+ };
111
+ if (config.sessionToken) {
112
+ credentials.sessionToken = config.sessionToken;
113
+ }
114
+ const baseConfig = {
115
+ region: config.region,
116
+ credentials
117
+ };
118
+ if (config.endpoint) {
119
+ return new S3Client({ ...baseConfig, endpoint: config.endpoint, forcePathStyle: true });
120
+ }
121
+ return new S3Client(baseConfig);
122
+ }
123
+ function createAwsProvider(config) {
124
+ let clientPromise = null;
125
+ function getClient() {
126
+ if (!clientPromise) {
127
+ clientPromise = loadS3().then((sdk) => ({ client: buildS3Client(config, sdk.S3Client), sdk })).catch((err) => {
128
+ clientPromise = null;
129
+ throw err;
130
+ });
131
+ }
132
+ return clientPromise;
133
+ }
134
+ async function upload(resolved, keyPrefix) {
135
+ const { client, sdk } = await getClient();
136
+ const prefix = keyPrefix ?? config.keyPrefix;
137
+ const key = generateKey(resolved.filename, prefix);
138
+ const putParams = {
139
+ Bucket: config.bucket,
140
+ Key: key,
141
+ Body: resolved.buffer,
142
+ ContentType: resolved.mimetype,
143
+ ContentLength: resolved.size
144
+ };
145
+ if (config.acl) {
146
+ putParams.ACL = config.acl;
147
+ }
148
+ try {
149
+ await client.send(new sdk.PutObjectCommand(putParams));
150
+ } catch (err) {
151
+ const msg = err instanceof Error ? err.message : String(err);
152
+ if (msg.includes("AccessControlListNotSupported")) {
153
+ throw new Error(
154
+ `S3 ACL error on bucket "${config.bucket}": ACLs are disabled because the bucket uses Object Ownership = "Bucket owner enforced" (the default since April 2023). Remove the 'acl' option from your AWS config or change the bucket's Object Ownership setting in the S3 console. Original error: ${msg}`
155
+ );
156
+ }
157
+ throw err;
158
+ }
159
+ const url = config.endpoint ? `${config.endpoint.replace(/\/$/, "")}/${config.bucket}/${key}` : `https://${config.bucket}.s3.${config.region}.amazonaws.com/${key}`;
160
+ return {
161
+ url,
162
+ key,
163
+ size: resolved.size,
164
+ mimetype: resolved.mimetype
165
+ };
166
+ }
167
+ async function del(key) {
168
+ const { client, sdk } = await getClient();
169
+ await client.send(
170
+ new sdk.DeleteObjectCommand({
171
+ Bucket: config.bucket,
172
+ Key: key
173
+ })
174
+ );
175
+ }
176
+ return { upload, delete: del };
177
+ }
178
+
179
+ // src/storage/providers/supabase.ts
180
+ async function supabaseUpload(config, resolved, keyPrefix) {
181
+ const baseUrl = config.url.replace(/\/$/, "");
182
+ const prefix = keyPrefix ?? config.keyPrefix;
183
+ const key = generateKey(resolved.filename, prefix);
184
+ const uploadUrl = `${baseUrl}/storage/v1/object/${config.bucket}/${key}`;
185
+ const response = await fetch(uploadUrl, {
186
+ method: "POST",
187
+ headers: {
188
+ Authorization: `Bearer ${config.serviceKey}`,
189
+ "Content-Type": resolved.mimetype,
190
+ "Content-Length": String(resolved.size)
191
+ },
192
+ body: resolved.buffer
193
+ });
194
+ if (!response.ok) {
195
+ const text = await response.text().catch(() => response.statusText);
196
+ throw new Error(`Supabase Storage upload failed (${response.status}): ${text}`);
197
+ }
198
+ const isPublic = config.publicBucket !== false;
199
+ const url = isPublic ? `${baseUrl}/storage/v1/object/public/${config.bucket}/${key}` : key;
200
+ return {
201
+ url,
202
+ key,
203
+ size: resolved.size,
204
+ mimetype: resolved.mimetype
205
+ };
206
+ }
207
+ async function supabaseDelete(config, key) {
208
+ const baseUrl = config.url.replace(/\/$/, "");
209
+ const deleteUrl = `${baseUrl}/storage/v1/object/${config.bucket}`;
210
+ const response = await fetch(deleteUrl, {
211
+ method: "DELETE",
212
+ headers: {
213
+ Authorization: `Bearer ${config.serviceKey}`,
214
+ "Content-Type": "application/json"
215
+ },
216
+ body: JSON.stringify({ prefixes: [key] })
217
+ });
218
+ if (!response.ok) {
219
+ const text = await response.text().catch(() => response.statusText);
220
+ throw new Error(`Supabase Storage delete failed (${response.status}): ${text}`);
221
+ }
222
+ }
223
+
224
+ // src/storage/storage.ts
225
+ function validateConfig(config) {
226
+ if (!config.provider) {
227
+ throw new Error('Storage config must include a provider ("aws" or "supabase")');
228
+ }
229
+ if (config.provider === "aws") {
230
+ if (!config.bucket) throw new Error("AWS config requires bucket");
231
+ if (!config.region) throw new Error("AWS config requires region");
232
+ if (!config.accessKeyId) throw new Error("AWS config requires accessKeyId");
233
+ if (!config.secretAccessKey) throw new Error("AWS config requires secretAccessKey");
234
+ } else if (config.provider === "supabase") {
235
+ if (!config.bucket) throw new Error("Supabase config requires bucket");
236
+ if (!config.url) throw new Error("Supabase config requires url");
237
+ if (!config.serviceKey) throw new Error("Supabase config requires serviceKey");
238
+ } else {
239
+ throw new Error(`Unknown storage provider: "${config.provider}"`);
240
+ }
241
+ }
242
+ function createStorage(config) {
243
+ validateConfig(config);
244
+ if (config.provider === "aws") {
245
+ const aws = createAwsProvider(config);
246
+ const upload2 = async (input, options) => {
247
+ const resolved = resolveInput(input, options);
248
+ return aws.upload(resolved, options?.keyPrefix);
249
+ };
250
+ const uploadMany2 = async (inputs, options) => Promise.all(inputs.map((i) => upload2(i, options)));
251
+ return { upload: upload2, uploadMany: uploadMany2, delete: aws.delete };
252
+ }
253
+ const upload = async (input, options) => {
254
+ const resolved = resolveInput(input, options);
255
+ return supabaseUpload(config, resolved, options?.keyPrefix);
256
+ };
257
+ const uploadMany = async (inputs, options) => Promise.all(inputs.map((i) => upload(i, options)));
258
+ const del = async (key) => supabaseDelete(config, key);
259
+ return { upload, uploadMany, delete: del };
260
+ }
261
+
262
+ export { RegExpTimeoutError, createStorage };
263
+ //# sourceMappingURL=index.mjs.map
264
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/storage/utils.ts","../../src/storage/providers/aws.ts","../../src/storage/providers/supabase.ts","../../src/storage/storage.ts"],"names":["upload","uploadMany"],"mappings":"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","file":"index.mjs","sourcesContent":["import { randomUUID } from 'crypto';\nimport vm from 'node:vm';\nimport type { ParsedFile } from '../server/types';\nimport type { ResolvedInput, UploadInput, UploadOptions } from './types';\n\nconst DATA_URI_MAX_LENGTH = 100 * 1024 * 1024; // 100 MB\nconst REGEX_TIMEOUT_MS = 50;\n\n// Compiled once at module load; vm.createContext() is called per-invocation for isolation.\nconst VM_REGEX_SCRIPT = new vm.Script('input.match(pattern)');\n\nexport class RegExpTimeoutError extends Error {\n constructor(timeoutMs: number) {\n super(`RegExp execution timed out after ${timeoutMs}ms`);\n this.name = 'RegExpTimeoutError';\n }\n}\n\n// Runs a regex with a real timeout via vm.runInNewContext, preventing ReDoS attacks.\nfunction execRegex(\n input: string,\n pattern: RegExp,\n timeoutMs = REGEX_TIMEOUT_MS\n): RegExpMatchArray | null {\n if (input.length > DATA_URI_MAX_LENGTH) {\n throw new Error(\n `Input length (${input.length}) exceeds maximum allowed (${DATA_URI_MAX_LENGTH} bytes)`\n );\n }\n try {\n const context = vm.createContext({ input, pattern });\n return VM_REGEX_SCRIPT.runInContext(context, { timeout: timeoutMs }) as RegExpMatchArray | null;\n } catch (err) {\n const msg = err instanceof Error ? err.message : '';\n if (msg.toLowerCase().includes('timed out') || msg.toLowerCase().includes('timeout')) {\n throw new RegExpTimeoutError(timeoutMs);\n }\n throw err;\n }\n}\n\n// Uses NFD decomposition to transliterate accented chars (é → e) before stripping non-ASCII,\n// so accented filenames produce readable keys instead of empty strings.\nfunction sanitizeFilename(filename: string): string {\n return filename\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toLowerCase()\n .replace(/\\s+/g, '-')\n .replace(/[^a-z0-9._-]/g, '');\n}\n\n/** Format: {prefix}/{uuid}-{sanitized-filename} */\nexport function generateKey(filename: string, prefix?: string): string {\n const uuid = randomUUID();\n const sanitized = sanitizeFilename(filename);\n const name = `${uuid}-${sanitized}`;\n if (prefix) {\n const trimmed = prefix.replace(/\\/+$/, '');\n return `${trimmed}/${name}`;\n }\n return name;\n}\n\n/** Parses a base64 data URI into a Buffer and MIME type. */\nexport function base64ToBuffer(dataUri: string): { buffer: Buffer; mimetype: string } {\n const match = execRegex(dataUri, /^data:([^;]+);base64,(.+)$/);\n if (!match) {\n throw new Error('Invalid base64 data URI format. Expected: data:{mimetype};base64,{data}');\n }\n const [, mimetype, data] = match;\n const buffer = Buffer.from(data, 'base64');\n return { buffer, mimetype };\n}\n\nfunction isParsedFile(input: UploadInput): input is ParsedFile {\n return (\n typeof input === 'object' &&\n !Buffer.isBuffer(input) &&\n 'buffer' in input &&\n 'originalname' in input &&\n 'mimetype' in input\n );\n}\n\n/** Normalizes any UploadInput into a ResolvedInput with buffer, filename, mimetype, and size. */\nexport function resolveInput(input: UploadInput, options?: UploadOptions): ResolvedInput {\n if (isParsedFile(input)) {\n return {\n buffer: input.buffer,\n filename: options?.filename ?? input.originalname,\n mimetype: options?.mimetype ?? input.mimetype,\n size: input.size,\n };\n }\n\n if (Buffer.isBuffer(input)) {\n if (!options?.filename) {\n throw new Error('filename is required in options when uploading a Buffer');\n }\n const buffer = input;\n return {\n buffer,\n filename: options.filename,\n mimetype: options?.mimetype ?? 'application/octet-stream',\n size: buffer.length,\n };\n }\n\n if (typeof input === 'string') {\n const { buffer, mimetype } = base64ToBuffer(input);\n if (!options?.filename) {\n throw new Error('filename is required in options when uploading a base64 string');\n }\n return {\n buffer,\n filename: options.filename,\n mimetype: options?.mimetype ?? mimetype,\n size: buffer.length,\n };\n }\n\n throw new Error('Invalid upload input: must be a ParsedFile, Buffer, or base64 data URI string');\n}\n","import type { AWSStorageConfig, ResolvedInput, UploadResult } from '../types';\nimport { generateKey } from '../utils';\n\n// Local type interfaces for @aws-sdk/client-s3 (optional peer dependency).\ninterface S3CredentialsConfig {\n accessKeyId: string;\n secretAccessKey: string;\n sessionToken?: string;\n}\n\ninterface S3ClientConfig {\n region: string;\n credentials: S3CredentialsConfig;\n endpoint?: string;\n forcePathStyle?: boolean;\n}\n\ninterface S3Command {\n readonly _tag: 'S3Command';\n}\n\ninterface S3ClientInstance {\n send(command: S3Command): Promise<void>;\n}\n\ninterface PutObjectCommandInput {\n Bucket: string;\n Key: string;\n Body: Buffer;\n ContentType: string;\n ContentLength: number;\n ACL?: string;\n}\n\ninterface DeleteObjectCommandInput {\n Bucket: string;\n Key: string;\n}\n\ninterface AwsS3Module {\n S3Client: new (config: S3ClientConfig) => S3ClientInstance;\n PutObjectCommand: new (input: PutObjectCommandInput) => S3Command;\n DeleteObjectCommand: new (input: DeleteObjectCommandInput) => S3Command;\n}\n\nlet s3ModulePromise: Promise<AwsS3Module> | null = null;\n\nasync function loadS3(): Promise<AwsS3Module> {\n if (!s3ModulePromise) {\n const pkg: string = '@aws-sdk/client-s3'; // variable prevents static resolution of optional peer dep\n s3ModulePromise = (import(pkg) as Promise<AwsS3Module>).catch((err) => {\n s3ModulePromise = null;\n void err;\n throw new Error(\n 'AWS S3 SDK not found. Install it with: npm install @aws-sdk/client-s3'\n );\n });\n }\n return s3ModulePromise;\n}\n\nfunction buildS3Client(\n config: AWSStorageConfig,\n S3Client: new (config: S3ClientConfig) => S3ClientInstance\n): S3ClientInstance {\n const credentials: S3CredentialsConfig = {\n accessKeyId: config.accessKeyId,\n secretAccessKey: config.secretAccessKey,\n };\n\n if (config.sessionToken) {\n credentials.sessionToken = config.sessionToken;\n }\n\n const baseConfig: S3ClientConfig = {\n region: config.region,\n credentials,\n };\n\n if (config.endpoint) {\n return new S3Client({ ...baseConfig, endpoint: config.endpoint, forcePathStyle: true });\n }\n\n return new S3Client(baseConfig);\n}\n\nexport interface AwsProvider {\n upload(resolved: ResolvedInput, keyPrefix?: string): Promise<UploadResult>;\n delete(key: string): Promise<void>;\n}\n\nexport function createAwsProvider(config: AWSStorageConfig): AwsProvider {\n let clientPromise: Promise<{ client: S3ClientInstance; sdk: AwsS3Module }> | null = null;\n\n function getClient(): Promise<{ client: S3ClientInstance; sdk: AwsS3Module }> {\n if (!clientPromise) {\n clientPromise = loadS3()\n .then((sdk) => ({ client: buildS3Client(config, sdk.S3Client), sdk }))\n .catch((err) => {\n clientPromise = null;\n throw err;\n });\n }\n return clientPromise;\n }\n\n async function upload(resolved: ResolvedInput, keyPrefix?: string): Promise<UploadResult> {\n const { client, sdk } = await getClient();\n\n const prefix = keyPrefix ?? config.keyPrefix;\n const key = generateKey(resolved.filename, prefix);\n\n const putParams: PutObjectCommandInput = {\n Bucket: config.bucket,\n Key: key,\n Body: resolved.buffer,\n ContentType: resolved.mimetype,\n ContentLength: resolved.size,\n };\n\n if (config.acl) {\n putParams.ACL = config.acl;\n }\n\n try {\n await client.send(new sdk.PutObjectCommand(putParams));\n } catch (err: unknown) {\n const msg = err instanceof Error ? err.message : String(err);\n if (msg.includes('AccessControlListNotSupported')) {\n throw new Error(\n `S3 ACL error on bucket \"${config.bucket}\": ACLs are disabled because the bucket uses ` +\n `Object Ownership = \"Bucket owner enforced\" (the default since April 2023). ` +\n `Remove the 'acl' option from your AWS config or change the bucket's Object Ownership ` +\n `setting in the S3 console. Original error: ${msg}`\n );\n }\n throw err;\n }\n\n const url = config.endpoint\n ? `${config.endpoint.replace(/\\/$/, '')}/${config.bucket}/${key}`\n : `https://${config.bucket}.s3.${config.region}.amazonaws.com/${key}`;\n\n return {\n url,\n key,\n size: resolved.size,\n mimetype: resolved.mimetype,\n };\n }\n\n async function del(key: string): Promise<void> {\n const { client, sdk } = await getClient();\n\n await client.send(\n new sdk.DeleteObjectCommand({\n Bucket: config.bucket,\n Key: key,\n })\n );\n }\n\n return { upload, delete: del };\n}\n","import type { ResolvedInput, SupabaseStorageConfig, UploadResult } from '../types';\nimport { generateKey } from '../utils';\n\nexport async function supabaseUpload(\n config: SupabaseStorageConfig,\n resolved: ResolvedInput,\n keyPrefix?: string\n): Promise<UploadResult> {\n const baseUrl = config.url.replace(/\\/$/, '');\n\n const prefix = keyPrefix ?? config.keyPrefix;\n const key = generateKey(resolved.filename, prefix);\n\n const uploadUrl = `${baseUrl}/storage/v1/object/${config.bucket}/${key}`;\n\n const response = await fetch(uploadUrl, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${config.serviceKey}`,\n 'Content-Type': resolved.mimetype,\n 'Content-Length': String(resolved.size),\n },\n body: resolved.buffer as unknown as BodyInit,\n });\n\n if (!response.ok) {\n const text = await response.text().catch(() => response.statusText);\n throw new Error(`Supabase Storage upload failed (${response.status}): ${text}`);\n }\n\n const isPublic = config.publicBucket !== false;\n const url = isPublic\n ? `${baseUrl}/storage/v1/object/public/${config.bucket}/${key}`\n : key;\n\n return {\n url,\n key,\n size: resolved.size,\n mimetype: resolved.mimetype,\n };\n}\n\nexport async function supabaseDelete(\n config: SupabaseStorageConfig,\n key: string\n): Promise<void> {\n const baseUrl = config.url.replace(/\\/$/, '');\n const deleteUrl = `${baseUrl}/storage/v1/object/${config.bucket}`;\n\n const response = await fetch(deleteUrl, {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${config.serviceKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ prefixes: [key] }),\n });\n\n if (!response.ok) {\n const text = await response.text().catch(() => response.statusText);\n throw new Error(`Supabase Storage delete failed (${response.status}): ${text}`);\n }\n}\n","import type {\n StorageConfig,\n StorageAdapter,\n UploadInput,\n UploadOptions,\n UploadResult,\n} from './types';\nimport { resolveInput } from './utils';\nimport { createAwsProvider } from './providers/aws';\nimport { supabaseUpload, supabaseDelete } from './providers/supabase';\n\nfunction validateConfig(config: StorageConfig): void {\n if (!config.provider) {\n throw new Error('Storage config must include a provider (\"aws\" or \"supabase\")');\n }\n\n if (config.provider === 'aws') {\n if (!config.bucket) throw new Error('AWS config requires bucket');\n if (!config.region) throw new Error('AWS config requires region');\n if (!config.accessKeyId) throw new Error('AWS config requires accessKeyId');\n if (!config.secretAccessKey) throw new Error('AWS config requires secretAccessKey');\n } else if (config.provider === 'supabase') {\n if (!config.bucket) throw new Error('Supabase config requires bucket');\n if (!config.url) throw new Error('Supabase config requires url');\n if (!config.serviceKey) throw new Error('Supabase config requires serviceKey');\n } else {\n throw new Error(`Unknown storage provider: \"${(config as StorageConfig).provider}\"`);\n }\n}\n\nexport function createStorage(config: StorageConfig): StorageAdapter {\n validateConfig(config);\n\n if (config.provider === 'aws') {\n const aws = createAwsProvider(config);\n\n const upload = async (input: UploadInput, options?: UploadOptions): Promise<UploadResult> => {\n const resolved = resolveInput(input, options);\n return aws.upload(resolved, options?.keyPrefix);\n };\n\n const uploadMany = async (\n inputs: UploadInput[],\n options?: UploadOptions\n ): Promise<UploadResult[]> => Promise.all(inputs.map((i) => upload(i, options)));\n\n return { upload, uploadMany, delete: aws.delete };\n }\n\n const upload = async (input: UploadInput, options?: UploadOptions): Promise<UploadResult> => {\n const resolved = resolveInput(input, options);\n return supabaseUpload(config, resolved, options?.keyPrefix);\n };\n\n const uploadMany = async (\n inputs: UploadInput[],\n options?: UploadOptions\n ): Promise<UploadResult[]> => Promise.all(inputs.map((i) => upload(i, options)));\n\n const del = async (key: string): Promise<void> => supabaseDelete(config, key);\n\n return { upload, uploadMany, delete: del };\n}\n"]}