@mr-aftab-ahmad-khan/upflow 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,706 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var src_exports = {};
32
+ __export(src_exports, {
33
+ DiskStorage: () => DiskStorage,
34
+ FileTooLargeError: () => FileTooLargeError,
35
+ InvalidMimeTypeError: () => InvalidMimeTypeError,
36
+ MemoryStorage: () => MemoryStorage,
37
+ R2Storage: () => R2Storage,
38
+ S3Storage: () => S3Storage,
39
+ StorageError: () => StorageError,
40
+ Upflow: () => Upflow,
41
+ UploadAbortedError: () => UploadAbortedError,
42
+ UploadError: () => UploadError,
43
+ appendUuidSuffix: () => appendUuidSuffix,
44
+ detectMimeFromBytes: () => detectMimeFromBytes,
45
+ mimeMatchesAllowed: () => mimeMatchesAllowed,
46
+ sanitizeFilename: () => sanitizeFilename,
47
+ upflow: () => upflow
48
+ });
49
+ module.exports = __toCommonJS(src_exports);
50
+
51
+ // src/upflow.ts
52
+ var import_node_events = require("events");
53
+ var import_node_stream3 = require("stream");
54
+
55
+ // src/multipart.ts
56
+ var import_node_stream = require("stream");
57
+ async function* parseMultipart(body, boundary) {
58
+ const buf = await collect(body);
59
+ const parts = splitParts(buf, boundary);
60
+ for (const p of parts) yield p;
61
+ }
62
+ async function collect(body) {
63
+ const chunks = [];
64
+ for await (const c of body) {
65
+ chunks.push(Buffer.isBuffer(c) ? c : Buffer.from(c));
66
+ }
67
+ return Buffer.concat(chunks);
68
+ }
69
+ function splitParts(buf, boundary) {
70
+ const out = [];
71
+ const delim = Buffer.from(`--${boundary}`);
72
+ const CRLF = Buffer.from("\r\n");
73
+ let cursor = buf.indexOf(delim);
74
+ if (cursor === -1) return out;
75
+ cursor += delim.length;
76
+ while (cursor < buf.length) {
77
+ if (buf.slice(cursor, cursor + 2).equals(Buffer.from("--"))) return out;
78
+ if (buf.slice(cursor, cursor + 2).equals(CRLF)) cursor += 2;
79
+ const headerEnd = buf.indexOf(Buffer.from("\r\n\r\n"), cursor);
80
+ if (headerEnd === -1) return out;
81
+ const headerStr = buf.slice(cursor, headerEnd).toString("utf8");
82
+ cursor = headerEnd + 4;
83
+ const headers = {};
84
+ for (const line of headerStr.split("\r\n")) {
85
+ const c = line.indexOf(":");
86
+ if (c === -1) continue;
87
+ headers[line.slice(0, c).toLowerCase().trim()] = line.slice(c + 1).trim();
88
+ }
89
+ const sep = Buffer.concat([CRLF, delim]);
90
+ const partEnd = buf.indexOf(sep, cursor);
91
+ if (partEnd === -1) return out;
92
+ const body = buf.slice(cursor, partEnd);
93
+ cursor = partEnd + sep.length;
94
+ const disposition = headers["content-disposition"] ?? "";
95
+ const nameMatch = /name="([^"]*)"/.exec(disposition);
96
+ const filenameMatch = /filename="([^"]*)"/.exec(disposition);
97
+ const fieldName = nameMatch ? nameMatch[1] ?? "" : "";
98
+ const filename = filenameMatch ? filenameMatch[1] ?? "" : "";
99
+ const mimeType = headers["content-type"] ?? "application/octet-stream";
100
+ if (filename) {
101
+ out.push({
102
+ type: "file",
103
+ fieldName,
104
+ filename,
105
+ mimeType,
106
+ stream: import_node_stream.Readable.from(body)
107
+ });
108
+ } else {
109
+ out.push({ type: "field", fieldName, value: body.toString("utf8") });
110
+ }
111
+ }
112
+ return out;
113
+ }
114
+ function getBoundary(contentType) {
115
+ if (!contentType) return void 0;
116
+ const m = /boundary="?([^";]+)"?/i.exec(contentType);
117
+ return m ? m[1] : void 0;
118
+ }
119
+
120
+ // src/errors.ts
121
+ var UploadError = class _UploadError extends Error {
122
+ status;
123
+ constructor(message, status = 400) {
124
+ super(message);
125
+ this.name = "UploadError";
126
+ this.status = status;
127
+ Object.setPrototypeOf(this, _UploadError.prototype);
128
+ }
129
+ };
130
+ var FileTooLargeError = class _FileTooLargeError extends UploadError {
131
+ constructor(limit) {
132
+ super(`File exceeds maximum size of ${limit} bytes`, 413);
133
+ this.limit = limit;
134
+ this.name = "FileTooLargeError";
135
+ Object.setPrototypeOf(this, _FileTooLargeError.prototype);
136
+ }
137
+ limit;
138
+ };
139
+ var InvalidMimeTypeError = class _InvalidMimeTypeError extends UploadError {
140
+ constructor(mime, allowed) {
141
+ super(`Mime type "${mime}" is not in allowed list: ${allowed.join(", ")}`, 415);
142
+ this.mime = mime;
143
+ this.allowed = allowed;
144
+ this.name = "InvalidMimeTypeError";
145
+ Object.setPrototypeOf(this, _InvalidMimeTypeError.prototype);
146
+ }
147
+ mime;
148
+ allowed;
149
+ };
150
+ var StorageError = class _StorageError extends UploadError {
151
+ constructor(message, cause) {
152
+ super(message, 500);
153
+ this.cause = cause;
154
+ this.name = "StorageError";
155
+ Object.setPrototypeOf(this, _StorageError.prototype);
156
+ }
157
+ cause;
158
+ };
159
+ var UploadAbortedError = class _UploadAbortedError extends UploadError {
160
+ constructor() {
161
+ super("Upload was aborted by the client", 499);
162
+ this.name = "UploadAbortedError";
163
+ Object.setPrototypeOf(this, _UploadAbortedError.prototype);
164
+ }
165
+ };
166
+
167
+ // src/util.ts
168
+ var import_node_stream2 = require("stream");
169
+ var import_node_crypto = require("crypto");
170
+ var RESERVED_WIN = /[<>:"/\\|?*\x00-\x1f]/g;
171
+ function sanitizeFilename(name) {
172
+ let safe = name.replace(/\\/g, "/").split("/").pop() ?? "file";
173
+ safe = safe.replace(RESERVED_WIN, "_");
174
+ safe = safe.replace(/\.\./g, "_");
175
+ safe = safe.replace(/\s+/g, "_");
176
+ safe = safe.replace(/^[._-]+/, "");
177
+ if (!safe || safe === ".") safe = "file";
178
+ return safe.slice(0, 200);
179
+ }
180
+ function appendUuidSuffix(name) {
181
+ const dot = name.lastIndexOf(".");
182
+ const stem = dot > 0 ? name.slice(0, dot) : name;
183
+ const ext = dot > 0 ? name.slice(dot) : "";
184
+ return `${stem}-${(0, import_node_crypto.randomUUID)().slice(0, 8)}${ext}`;
185
+ }
186
+ var MAGIC_TABLE = [
187
+ { bytes: [255, 216, 255], mime: "image/jpeg" },
188
+ { bytes: [137, 80, 78, 71], mime: "image/png" },
189
+ { bytes: [71, 73, 70, 56], mime: "image/gif" },
190
+ { bytes: [66, 77], mime: "image/bmp" },
191
+ { bytes: [82, 73, 70, 70], mime: "image/webp" },
192
+ // also AVI/WAV — narrowed below
193
+ { bytes: [102, 116, 121, 112], mime: "video/mp4", offset: 4 },
194
+ { bytes: [37, 80, 68, 70], mime: "application/pdf" },
195
+ { bytes: [80, 75, 3, 4], mime: "application/zip" },
196
+ { bytes: [31, 139], mime: "application/gzip" }
197
+ ];
198
+ function detectMimeFromBytes(buf) {
199
+ for (const sig of MAGIC_TABLE) {
200
+ const off = sig.offset ?? 0;
201
+ if (buf.length < off + sig.bytes.length) continue;
202
+ let ok = true;
203
+ for (let i = 0; i < sig.bytes.length; i++) {
204
+ if (buf[off + i] !== sig.bytes[i]) {
205
+ ok = false;
206
+ break;
207
+ }
208
+ }
209
+ if (ok) {
210
+ if (sig.mime === "image/webp" && buf.length >= 12) {
211
+ const tag = buf.slice(8, 12).toString("ascii");
212
+ if (tag === "WEBP") return "image/webp";
213
+ if (tag === "WAVE") return "audio/wav";
214
+ if (tag.startsWith("AVI")) return "video/x-msvideo";
215
+ return void 0;
216
+ }
217
+ return sig.mime;
218
+ }
219
+ }
220
+ return void 0;
221
+ }
222
+ function mimeMatchesAllowed(mime, allowed) {
223
+ if (!allowed || allowed.length === 0) return true;
224
+ for (const pattern of allowed) {
225
+ if (pattern === mime) return true;
226
+ if (pattern.endsWith("/*") && mime.startsWith(pattern.slice(0, -1))) return true;
227
+ if (pattern === "*/*") return true;
228
+ }
229
+ return false;
230
+ }
231
+ function webStreamToNode(stream) {
232
+ const reader = stream.getReader();
233
+ return new import_node_stream2.Readable({
234
+ async read() {
235
+ try {
236
+ const { done, value } = await reader.read();
237
+ if (done) this.push(null);
238
+ else this.push(Buffer.from(value));
239
+ } catch (err) {
240
+ this.destroy(err);
241
+ }
242
+ }
243
+ });
244
+ }
245
+ function applyTemplate(template, vars) {
246
+ return template.replace(/\{([a-zA-Z0-9_-]+)\}/g, (_m, key) => vars[key] ?? "");
247
+ }
248
+
249
+ // src/upflow.ts
250
+ var Upflow = class extends import_node_events.EventEmitter {
251
+ options;
252
+ constructor(options) {
253
+ super();
254
+ this.options = options;
255
+ }
256
+ presign(opts) {
257
+ if (!this.options.storage.presign) {
258
+ throw new UploadError("Storage adapter does not support presigned uploads", 501);
259
+ }
260
+ return this.options.storage.presign(opts);
261
+ }
262
+ // -------------------- Express middleware --------------------
263
+ single(fieldName) {
264
+ return async (req, res, next) => {
265
+ try {
266
+ const files = await this.handleNodeRequest(req, { single: fieldName });
267
+ req.file = files[0];
268
+ next();
269
+ } catch (err) {
270
+ next(err);
271
+ }
272
+ };
273
+ }
274
+ array(fieldName, maxCount) {
275
+ return async (req, _res, next) => {
276
+ try {
277
+ const files = await this.handleNodeRequest(req, { array: fieldName, maxCount });
278
+ req.files = files;
279
+ next();
280
+ } catch (err) {
281
+ next(err);
282
+ }
283
+ };
284
+ }
285
+ // -------------------- Generic fetch Request handler --------------------
286
+ handler() {
287
+ return async (request) => {
288
+ const contentType = request.headers.get("content-type") ?? "";
289
+ const boundary = getBoundary(contentType);
290
+ if (!boundary) throw new UploadError("Missing multipart boundary");
291
+ const body = request.body;
292
+ if (!body) throw new UploadError("Missing request body");
293
+ const nodeStream = webStreamToNode(body);
294
+ const files = await this.consumeMultipart(nodeStream, boundary, request);
295
+ return { files };
296
+ };
297
+ }
298
+ // -------------------- Hono middleware --------------------
299
+ hono() {
300
+ return async (c, next) => {
301
+ const result = await this.handler()(c.req.raw);
302
+ c.set("uploadedFiles", result.files);
303
+ await next();
304
+ };
305
+ }
306
+ // -------------------- Fastify plugin --------------------
307
+ fastify() {
308
+ return async (instance) => {
309
+ instance.addContentTypeParser(
310
+ "multipart/form-data",
311
+ { parseAs: "buffer" },
312
+ async (req, payload) => {
313
+ const boundary = getBoundary(req.headers["content-type"]);
314
+ if (!boundary) throw new UploadError("Missing multipart boundary");
315
+ const stream = import_node_stream3.Readable.from(payload);
316
+ return { files: await this.consumeMultipart(stream, boundary, req) };
317
+ }
318
+ );
319
+ };
320
+ }
321
+ // -------------------- Next.js App Router --------------------
322
+ nextjs() {
323
+ return async (request) => {
324
+ const result = await this.handler()(request);
325
+ return new Response(JSON.stringify({ files: result.files }), {
326
+ status: 200,
327
+ headers: { "content-type": "application/json" }
328
+ });
329
+ };
330
+ }
331
+ // -------------------- Core --------------------
332
+ async handleNodeRequest(req, opts) {
333
+ const contentType = req.headers["content-type"] ?? "";
334
+ const boundary = getBoundary(contentType);
335
+ if (!boundary) throw new UploadError("Missing multipart boundary");
336
+ const files = await this.consumeMultipart(req, boundary, req);
337
+ const allowed = opts.single ?? opts.array;
338
+ const matched = files.filter((f) => f.fieldName === allowed);
339
+ if (opts.single) {
340
+ if (matched.length === 0) throw new UploadError(`Expected file under field "${allowed}"`);
341
+ return [matched[0]];
342
+ }
343
+ if (opts.maxCount && matched.length > opts.maxCount) {
344
+ throw new UploadError(
345
+ `Too many files for field "${allowed}" (got ${matched.length}, max ${opts.maxCount})`
346
+ );
347
+ }
348
+ return matched;
349
+ }
350
+ async consumeMultipart(body, boundary, req) {
351
+ const limits = this.options.limits ?? {};
352
+ const files = [];
353
+ let fileCount = 0;
354
+ for await (const part of parseMultipart(body, boundary)) {
355
+ if (part.type !== "file") continue;
356
+ fileCount += 1;
357
+ if (limits.files && fileCount > limits.files) {
358
+ throw new UploadError(`Too many files (max ${limits.files})`, 413);
359
+ }
360
+ const safeOriginal = sanitizeFilename(part.filename);
361
+ const filename = appendUuidSuffix(safeOriginal);
362
+ await this.options.hooks?.onUploadStart?.({ filename, mimeType: part.mimeType }, req);
363
+ const validated = await this.validateAndPipe(part, filename, limits);
364
+ const stored = await this.options.storage.upload({
365
+ stream: validated.stream,
366
+ filename,
367
+ mimeType: validated.mimeType
368
+ });
369
+ const result = {
370
+ fieldName: part.fieldName,
371
+ filename,
372
+ originalName: part.filename,
373
+ mimeType: validated.mimeType,
374
+ size: stored.size,
375
+ storageKey: stored.key,
376
+ ...stored.url !== void 0 ? { url: stored.url } : {},
377
+ metadata: {}
378
+ };
379
+ await this.options.hooks?.onUploadComplete?.(result, req);
380
+ files.push(result);
381
+ }
382
+ return files;
383
+ }
384
+ async validateAndPipe(part, filename, limits) {
385
+ const out = new import_node_stream3.PassThrough();
386
+ const maxSize = limits.fileSize ?? Number.MAX_SAFE_INTEGER;
387
+ let received = 0;
388
+ let detected = part.mimeType;
389
+ let head = Buffer.alloc(0);
390
+ let validated = false;
391
+ part.stream.on("error", (err) => out.destroy(err));
392
+ part.stream.on("data", (chunk) => {
393
+ received += chunk.length;
394
+ if (received > maxSize) {
395
+ const err = new FileTooLargeError(maxSize);
396
+ out.destroy(err);
397
+ part.stream.destroy(err);
398
+ return;
399
+ }
400
+ if (!validated) {
401
+ head = Buffer.concat([head, chunk]);
402
+ if (head.length >= 16) {
403
+ const sniffed = detectMimeFromBytes(head);
404
+ if (sniffed) detected = sniffed;
405
+ if (!mimeMatchesAllowed(detected, limits.allowedMimeTypes)) {
406
+ const err = new InvalidMimeTypeError(detected, limits.allowedMimeTypes ?? []);
407
+ out.destroy(err);
408
+ part.stream.destroy(err);
409
+ return;
410
+ }
411
+ validated = true;
412
+ out.write(head);
413
+ head = Buffer.alloc(0);
414
+ return;
415
+ }
416
+ return;
417
+ }
418
+ out.write(chunk);
419
+ this.emit("progress", {
420
+ filename,
421
+ bytesReceived: received,
422
+ bytesTotal: null,
423
+ percent: null
424
+ });
425
+ });
426
+ part.stream.on("end", () => {
427
+ if (!validated && head.length > 0) {
428
+ const sniffed = detectMimeFromBytes(head) ?? detected;
429
+ if (!mimeMatchesAllowed(sniffed, limits.allowedMimeTypes)) {
430
+ out.destroy(new InvalidMimeTypeError(sniffed, limits.allowedMimeTypes ?? []));
431
+ return;
432
+ }
433
+ detected = sniffed;
434
+ out.write(head);
435
+ }
436
+ out.end();
437
+ });
438
+ part.stream.on("close", () => {
439
+ if (!part.stream.readableEnded) out.destroy(new UploadAbortedError());
440
+ });
441
+ return { stream: out, mimeType: detected };
442
+ }
443
+ };
444
+ function upflow(options) {
445
+ return new Upflow(options);
446
+ }
447
+
448
+ // src/storage/disk.ts
449
+ var import_node_fs = require("fs");
450
+ var import_node_path = require("path");
451
+ var import_node_crypto2 = require("crypto");
452
+ var import_promises = require("stream/promises");
453
+ var DiskStorage = class {
454
+ root;
455
+ pathTemplate;
456
+ publicUrlPrefix;
457
+ constructor(opts) {
458
+ this.root = opts.root;
459
+ this.pathTemplate = opts.pathTemplate ?? "{date}/{uuid}-{filename}";
460
+ if (opts.publicUrlPrefix !== void 0) this.publicUrlPrefix = opts.publicUrlPrefix;
461
+ }
462
+ async upload(input) {
463
+ const safe = sanitizeFilename(input.filename);
464
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
465
+ const dot = safe.lastIndexOf(".");
466
+ const ext = dot > 0 ? safe.slice(dot + 1) : "";
467
+ const key = applyTemplate(this.pathTemplate, {
468
+ date,
469
+ uuid: (0, import_node_crypto2.randomUUID)(),
470
+ filename: safe,
471
+ ext
472
+ });
473
+ const fullPath = (0, import_node_path.join)(this.root, key);
474
+ if (!(0, import_node_fs.existsSync)((0, import_node_path.dirname)(fullPath))) (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(fullPath), { recursive: true });
475
+ let size = 0;
476
+ input.stream.on("data", (c) => {
477
+ size += c.length;
478
+ });
479
+ await (0, import_promises.pipeline)(input.stream, (0, import_node_fs.createWriteStream)(fullPath));
480
+ const result = { key, size };
481
+ if (this.publicUrlPrefix) {
482
+ result.url = `${this.publicUrlPrefix.replace(/\/$/, "")}/${key}`;
483
+ }
484
+ return result;
485
+ }
486
+ async presign(_opts) {
487
+ throw new Error("DiskStorage does not support presigned uploads");
488
+ }
489
+ };
490
+
491
+ // src/storage/s3.ts
492
+ var import_node_crypto3 = require("crypto");
493
+ var DEFAULT_PART_SIZE = 8 * 1024 * 1024;
494
+ var DEFAULT_THRESHOLD = 5 * 1024 * 1024;
495
+ var S3Storage = class {
496
+ bucket;
497
+ region;
498
+ endpoint;
499
+ partSize;
500
+ multipartThreshold;
501
+ publicUrlPrefix;
502
+ accessKeyId;
503
+ secretAccessKey;
504
+ client;
505
+ constructor(opts) {
506
+ this.bucket = opts.bucket;
507
+ this.region = opts.region;
508
+ this.endpoint = opts.endpoint ?? `https://s3.${opts.region}.amazonaws.com`;
509
+ this.partSize = opts.partSize ?? DEFAULT_PART_SIZE;
510
+ this.multipartThreshold = opts.multipartThreshold ?? DEFAULT_THRESHOLD;
511
+ if (opts.publicUrlPrefix !== void 0) this.publicUrlPrefix = opts.publicUrlPrefix;
512
+ if (opts.accessKeyId !== void 0) this.accessKeyId = opts.accessKeyId;
513
+ if (opts.secretAccessKey !== void 0) this.secretAccessKey = opts.secretAccessKey;
514
+ if (opts.client !== void 0) this.client = opts.client;
515
+ }
516
+ buildKey(filename) {
517
+ const safe = sanitizeFilename(filename);
518
+ return `${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}/${(0, import_node_crypto3.randomUUID)()}-${safe}`;
519
+ }
520
+ buildUrl(key) {
521
+ if (this.publicUrlPrefix) return `${this.publicUrlPrefix.replace(/\/$/, "")}/${key}`;
522
+ return void 0;
523
+ }
524
+ async upload(input) {
525
+ const key = this.buildKey(input.filename);
526
+ if (this.client && hasSdk(this.client)) {
527
+ try {
528
+ await this.client.send(new (await loadCmd("PutObjectCommand"))({
529
+ Bucket: this.bucket,
530
+ Key: key,
531
+ Body: input.stream,
532
+ ContentType: input.mimeType
533
+ }));
534
+ } catch (err) {
535
+ throw new StorageError("S3 PutObject failed", err);
536
+ }
537
+ const url2 = this.buildUrl(key);
538
+ const result2 = { key, size: input.size ?? 0 };
539
+ if (url2) result2.url = url2;
540
+ return result2;
541
+ }
542
+ const chunks = [];
543
+ let size = 0;
544
+ for await (const c of input.stream) {
545
+ const b = Buffer.isBuffer(c) ? c : Buffer.from(c);
546
+ chunks.push(b);
547
+ size += b.length;
548
+ }
549
+ const body = Buffer.concat(chunks);
550
+ const url = `${this.endpoint}/${this.bucket}/${encodeURI(key)}`;
551
+ const headers = await this.signRequest("PUT", url, body, input.mimeType);
552
+ const res = await fetch(url, { method: "PUT", headers, body });
553
+ if (!res.ok) throw new StorageError(`S3 PUT failed with ${res.status}`);
554
+ const result = { key, size };
555
+ const publicUrl = this.buildUrl(key);
556
+ if (publicUrl) result.url = publicUrl;
557
+ return result;
558
+ }
559
+ async presign(opts) {
560
+ const key = this.buildKey(opts.filename);
561
+ const expires = opts.expiresInSeconds ?? 600;
562
+ const date = /* @__PURE__ */ new Date();
563
+ const dateStamp = date.toISOString().slice(0, 10).replace(/-/g, "");
564
+ const amzDate = date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}/, "");
565
+ const credential = `${this.accessKeyId ?? ""}/${dateStamp}/${this.region}/s3/aws4_request`;
566
+ const policy = Buffer.from(JSON.stringify({
567
+ expiration: new Date(Date.now() + expires * 1e3).toISOString(),
568
+ conditions: [
569
+ { bucket: this.bucket },
570
+ ["starts-with", "$key", key.split("/")[0] ?? ""],
571
+ { "content-type": opts.contentType },
572
+ ["content-length-range", 0, opts.maxSizeBytes ?? 100 * 1024 * 1024],
573
+ { "x-amz-credential": credential },
574
+ { "x-amz-algorithm": "AWS4-HMAC-SHA256" },
575
+ { "x-amz-date": amzDate }
576
+ ]
577
+ })).toString("base64");
578
+ const signingKey = await this.getSigningKey(dateStamp);
579
+ const signature = (0, import_node_crypto3.createHmac)("sha256", signingKey).update(policy).digest("hex");
580
+ return {
581
+ url: `${this.endpoint}/${this.bucket}`,
582
+ fields: {
583
+ key,
584
+ "Content-Type": opts.contentType,
585
+ "x-amz-credential": credential,
586
+ "x-amz-algorithm": "AWS4-HMAC-SHA256",
587
+ "x-amz-date": amzDate,
588
+ Policy: policy,
589
+ "x-amz-signature": signature
590
+ },
591
+ storageKey: key,
592
+ expiresAt: new Date(Date.now() + expires * 1e3).toISOString()
593
+ };
594
+ }
595
+ async signRequest(method, url, body, contentType) {
596
+ const u = new URL(url);
597
+ const date = /* @__PURE__ */ new Date();
598
+ const amzDate = date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}/, "");
599
+ const dateStamp = amzDate.slice(0, 8);
600
+ const payloadHash = (0, import_node_crypto3.createHash)("sha256").update(body).digest("hex");
601
+ const canonicalUri = u.pathname;
602
+ const canonicalQuery = "";
603
+ const canonicalHeaders = `content-type:${contentType}
604
+ host:${u.host}
605
+ x-amz-content-sha256:${payloadHash}
606
+ x-amz-date:${amzDate}
607
+ `;
608
+ const signedHeaders = "content-type;host;x-amz-content-sha256;x-amz-date";
609
+ const canonicalReq = `${method}
610
+ ${canonicalUri}
611
+ ${canonicalQuery}
612
+ ${canonicalHeaders}
613
+ ${signedHeaders}
614
+ ${payloadHash}`;
615
+ const credentialScope = `${dateStamp}/${this.region}/s3/aws4_request`;
616
+ const stringToSign = `AWS4-HMAC-SHA256
617
+ ${amzDate}
618
+ ${credentialScope}
619
+ ${(0, import_node_crypto3.createHash)("sha256").update(canonicalReq).digest("hex")}`;
620
+ const signingKey = await this.getSigningKey(dateStamp);
621
+ const signature = (0, import_node_crypto3.createHmac)("sha256", signingKey).update(stringToSign).digest("hex");
622
+ return {
623
+ Authorization: `AWS4-HMAC-SHA256 Credential=${this.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`,
624
+ "Content-Type": contentType,
625
+ "x-amz-date": amzDate,
626
+ "x-amz-content-sha256": payloadHash,
627
+ host: u.host
628
+ };
629
+ }
630
+ async getSigningKey(dateStamp) {
631
+ const kDate = (0, import_node_crypto3.createHmac)("sha256", `AWS4${this.secretAccessKey ?? ""}`).update(dateStamp).digest();
632
+ const kRegion = (0, import_node_crypto3.createHmac)("sha256", kDate).update(this.region).digest();
633
+ const kService = (0, import_node_crypto3.createHmac)("sha256", kRegion).update("s3").digest();
634
+ return (0, import_node_crypto3.createHmac)("sha256", kService).update("aws4_request").digest();
635
+ }
636
+ };
637
+ function hasSdk(client) {
638
+ return typeof client?.send === "function";
639
+ }
640
+ async function loadCmd(name) {
641
+ try {
642
+ const mod = await import(
643
+ /* @vite-ignore */
644
+ "@aws-sdk/client-s3"
645
+ );
646
+ return mod[name];
647
+ } catch (err) {
648
+ throw new StorageError(
649
+ "@aws-sdk/client-s3 is required when passing a `client` to S3Storage; install it as a peer dependency.",
650
+ err
651
+ );
652
+ }
653
+ }
654
+
655
+ // src/storage/r2.ts
656
+ var R2Storage = class extends S3Storage {
657
+ constructor(opts) {
658
+ super({
659
+ ...opts,
660
+ region: "auto",
661
+ endpoint: opts.endpoint ?? `https://${opts.accountId}.r2.cloudflarestorage.com`
662
+ });
663
+ }
664
+ };
665
+
666
+ // src/storage/memory.ts
667
+ var import_node_crypto4 = require("crypto");
668
+ var MemoryStorage = class {
669
+ store = /* @__PURE__ */ new Map();
670
+ async upload(input) {
671
+ const chunks = [];
672
+ for await (const c of input.stream) chunks.push(Buffer.isBuffer(c) ? c : Buffer.from(c));
673
+ const buffer = Buffer.concat(chunks);
674
+ const key = `${(0, import_node_crypto4.randomUUID)()}-${input.filename}`;
675
+ this.store.set(key, { buffer, mimeType: input.mimeType });
676
+ return { key, size: buffer.length };
677
+ }
678
+ read(key) {
679
+ return this.store.get(key)?.buffer;
680
+ }
681
+ list() {
682
+ return [...this.store.keys()];
683
+ }
684
+ clear() {
685
+ this.store.clear();
686
+ }
687
+ };
688
+ // Annotate the CommonJS export names for ESM import in node:
689
+ 0 && (module.exports = {
690
+ DiskStorage,
691
+ FileTooLargeError,
692
+ InvalidMimeTypeError,
693
+ MemoryStorage,
694
+ R2Storage,
695
+ S3Storage,
696
+ StorageError,
697
+ Upflow,
698
+ UploadAbortedError,
699
+ UploadError,
700
+ appendUuidSuffix,
701
+ detectMimeFromBytes,
702
+ mimeMatchesAllowed,
703
+ sanitizeFilename,
704
+ upflow
705
+ });
706
+ //# sourceMappingURL=index.cjs.map