formdata-io 1.1.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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # 🚀 FormData IO
2
2
 
3
- > TypeScript-first library for seamless FormData handling in frontend and backend.
3
+ > TypeScript-first library for seamless FormData handling in frontend and backend — plus cloud storage for AWS S3 and Supabase.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/formdata-io.svg)](https://www.npmjs.com/package/formdata-io)
6
6
  [![Bundle size](https://img.shields.io/bundlephobia/minzip/formdata-io)](https://bundlephobia.com/package/formdata-io)
@@ -25,6 +25,10 @@ const formData = payload({ name: "João", avatar: file });
25
25
  app.post('/upload', parser(), (req, res) => {
26
26
  const { name, avatar } = req.payload; // ✨ Type-safe!
27
27
  });
28
+
29
+ // Storage: Upload to S3 or Supabase in one line
30
+ const result = await storage.upload(avatar);
31
+ console.log(result.url); // "https://bucket.s3.us-east-1.amazonaws.com/..."
28
32
  ```
29
33
 
30
34
  ## Installation
@@ -33,6 +37,15 @@ app.post('/upload', parser(), (req, res) => {
33
37
  npm install formdata-io
34
38
  ```
35
39
 
40
+ **Optional peer dependencies** (install only what you need):
41
+
42
+ ```bash
43
+ # For AWS S3 storage
44
+ npm install @aws-sdk/client-s3
45
+
46
+ # Supabase Storage uses native fetch — no extra dependencies needed
47
+ ```
48
+
36
49
  ## Quick Start
37
50
 
38
51
  ### Frontend (React, Vue, Vanilla JS)
@@ -79,6 +92,27 @@ app.post('/api/upload', parser(), (req, res) => {
79
92
  });
80
93
  ```
81
94
 
95
+ ### Storage (AWS S3 or Supabase)
96
+
97
+ ```typescript
98
+ import { parser } from 'formdata-io/server';
99
+ import { createStorage } from 'formdata-io/storage';
100
+
101
+ const storage = createStorage({
102
+ provider: 'aws',
103
+ bucket: process.env.AWS_BUCKET!,
104
+ region: process.env.AWS_REGION!,
105
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
106
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
107
+ });
108
+
109
+ app.post('/api/upload', parser(), async (req, res) => {
110
+ const { avatar } = req.payload;
111
+ const result = await storage.upload(avatar);
112
+ res.json({ url: result.url });
113
+ });
114
+ ```
115
+
82
116
  ## API Reference
83
117
 
84
118
  ### Client API
@@ -251,20 +285,22 @@ Express middleware for parsing multipart/form-data.
251
285
  **Returns:** Express middleware function
252
286
 
253
287
  **Options:**
254
- ```typescript
255
- {
256
- maxFileSize: number; // Max file size in bytes (default: 10MB)
257
- maxFiles: number; // Max number of files (default: 10)
258
- autoParseJSON: boolean; // Auto-parse JSON strings (default: true)
259
- autoParseNumbers: boolean; // Auto-convert numeric strings (default: true)
260
- autoParseBooleans: boolean; // Auto-convert "true"/"false" (default: true)
261
- }
262
- ```
288
+
289
+ | Option | Type | Default | Description |
290
+ |--------|------|---------|-------------|
291
+ | `maxFileSize` | `number` | `10485760` (10MB) | Max size per file in bytes |
292
+ | `maxFiles` | `number` | `10` | Max number of files per request |
293
+ | `maxFields` | `number` | `100` | Max number of text fields per request |
294
+ | `maxFieldSize` | `number` | `65536` (64KB) | Max size of each text field in bytes |
295
+ | `maxTotalFileSize` | `number` | `Infinity` | Combined size limit for all files in bytes |
296
+ | `autoParseJSON` | `boolean` | `true` | Auto-parse JSON strings to objects |
297
+ | `autoParseNumbers` | `boolean` | `true` | Auto-convert numeric strings to numbers |
298
+ | `autoParseBooleans` | `boolean` | `true` | Auto-convert "true"/"false" to booleans |
263
299
 
264
300
  **Examples:**
265
301
 
266
302
  ```typescript
267
- // Default options (10MB, 10 files)
303
+ // Default options (10MB per file, 10 files, 100 fields)
268
304
  app.post('/upload', parser(), (req, res) => {
269
305
  // req.payload contains all fields and files
270
306
  });
@@ -274,12 +310,34 @@ app.post('/photos', parser({ maxFileSize: 50 * 1024 * 1024 }), (req, res) => {
274
310
  // Allow up to 50MB files
275
311
  });
276
312
 
313
+ // Cap total upload size (e.g. gallery endpoint)
314
+ app.post('/gallery', parser({ maxTotalFileSize: 100 * 1024 * 1024 }), (req, res) => {
315
+ // All files combined must be under 100MB
316
+ });
317
+
318
+ // Limit text fields to prevent DoS
319
+ app.post('/form', parser({ maxFields: 20, maxFieldSize: 8 * 1024 }), (req, res) => {
320
+ // Max 20 fields, each up to 8KB
321
+ });
322
+
277
323
  // Disable auto-parsing
278
324
  app.post('/raw', parser({ autoParseJSON: false }), (req, res) => {
279
325
  // All fields remain as strings
280
326
  });
281
327
  ```
282
328
 
329
+ #### `parseMultipart(req, options?)`
330
+
331
+ Lower-level function that parses a multipart request and returns a promise — useful when you need direct control outside of Express middleware.
332
+
333
+ ```typescript
334
+ import { parseMultipart } from 'formdata-io/server';
335
+
336
+ // Inside a custom handler or framework adapter
337
+ const payload = await parseMultipart(req, { maxFiles: 5 });
338
+ console.log(payload.avatar); // ParsedFile
339
+ ```
340
+
283
341
  #### `ParsedFile` Interface
284
342
 
285
343
  ```typescript
@@ -293,20 +351,226 @@ interface ParsedFile {
293
351
  }
294
352
  ```
295
353
 
354
+ ### Storage API
355
+
356
+ The `formdata-io/storage` package provides a unified adapter for uploading and deleting files on AWS S3 and Supabase Storage.
357
+
358
+ #### `createStorage(config)`
359
+
360
+ Factory function that returns a `StorageAdapter` for the configured provider.
361
+
362
+ ```typescript
363
+ import { createStorage } from 'formdata-io/storage';
364
+
365
+ const storage = createStorage(config);
366
+ ```
367
+
368
+ **AWS S3 config:**
369
+
370
+ | Field | Type | Required | Description |
371
+ |-------|------|----------|-------------|
372
+ | `provider` | `'aws'` | ✅ | Provider identifier |
373
+ | `bucket` | `string` | ✅ | S3 bucket name |
374
+ | `region` | `string` | ✅ | AWS region (e.g. `'us-east-1'`) |
375
+ | `accessKeyId` | `string` | ✅ | AWS access key ID |
376
+ | `secretAccessKey` | `string` | ✅ | AWS secret access key |
377
+ | `sessionToken` | `string` | — | STS/IAM temporary session token |
378
+ | `endpoint` | `string` | — | Custom endpoint for S3-compatible providers |
379
+ | `keyPrefix` | `string` | — | Default path prefix for all keys |
380
+ | `acl` | `'public-read' \| 'private'` | — | Object ACL (see note below) |
381
+
382
+ > **ACL note:** Since April 2023, new S3 buckets have Object Ownership set to "Bucket owner enforced", which disables ACLs entirely. Setting `acl` on such buckets throws an `AccessControlListNotSupported` error. Either remove the `acl` option or change the bucket's Object Ownership setting in the S3 console.
383
+
384
+ ```typescript
385
+ // .env
386
+ // AWS_BUCKET=my-bucket
387
+ // AWS_REGION=us-east-1
388
+ // AWS_ACCESS_KEY_ID=AKIA...
389
+ // AWS_SECRET_ACCESS_KEY=...
390
+
391
+ const storage = createStorage({
392
+ provider: 'aws',
393
+ bucket: process.env.AWS_BUCKET!,
394
+ region: process.env.AWS_REGION!,
395
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
396
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
397
+ keyPrefix: 'uploads',
398
+ });
399
+ ```
400
+
401
+ **Supabase Storage config:**
402
+
403
+ | Field | Type | Required | Description |
404
+ |-------|------|----------|-------------|
405
+ | `provider` | `'supabase'` | ✅ | Provider identifier |
406
+ | `bucket` | `string` | ✅ | Supabase storage bucket name |
407
+ | `url` | `string` | ✅ | Supabase project URL |
408
+ | `serviceKey` | `string` | ✅ | Supabase service role key |
409
+ | `keyPrefix` | `string` | — | Default path prefix for all keys |
410
+ | `publicBucket` | `boolean` | — | Whether the bucket is public (default: `true`) |
411
+
412
+ > When `publicBucket` is `true`, `UploadResult.url` is the full public URL. When `false`, `url` contains only the storage key and you are responsible for generating a signed URL via the Supabase client before serving the file.
413
+
414
+ ```typescript
415
+ // .env
416
+ // SUPABASE_URL=https://xyz.supabase.co
417
+ // SUPABASE_SERVICE_KEY=eyJ...
418
+
419
+ const storage = createStorage({
420
+ provider: 'supabase',
421
+ bucket: 'avatars',
422
+ url: process.env.SUPABASE_URL!,
423
+ serviceKey: process.env.SUPABASE_SERVICE_KEY!,
424
+ keyPrefix: 'users',
425
+ });
426
+ ```
427
+
428
+ #### `storage.upload(input, options?)`
429
+
430
+ Uploads a single file and returns an `UploadResult`.
431
+
432
+ **`UploadInput`** — accepts three forms:
433
+ - `ParsedFile` — file parsed by `parser()` middleware (preferred)
434
+ - `Buffer` — raw buffer (requires `filename` in options)
435
+ - `string` — base64 data URI (requires `filename` in options)
436
+
437
+ **`UploadOptions`:**
438
+
439
+ | Field | Type | Description |
440
+ |-------|------|-------------|
441
+ | `filename` | `string` | Required for Buffer and base64 inputs |
442
+ | `mimetype` | `string` | Override detected MIME type |
443
+ | `keyPrefix` | `string` | Override key prefix for this upload only |
444
+
445
+ **`UploadResult`:**
446
+
447
+ ```typescript
448
+ interface UploadResult {
449
+ url: string; // Public URL (or key for private Supabase buckets)
450
+ key: string; // Storage key: "{prefix}/{uuid}-{sanitized-filename}"
451
+ size: number; // File size in bytes
452
+ mimetype: string; // MIME type
453
+ }
454
+ ```
455
+
456
+ **Examples:**
457
+
458
+ ```typescript
459
+ // Upload a ParsedFile from parser()
460
+ const result = await storage.upload(req.payload.avatar);
461
+ console.log(result.url); // "https://bucket.s3.us-east-1.amazonaws.com/uploads/abc-avatar.jpg"
462
+ console.log(result.key); // "uploads/abc123-avatar.jpg"
463
+ console.log(result.size); // 204800
464
+ console.log(result.mimetype); // "image/jpeg"
465
+
466
+ // Upload a Buffer
467
+ const result = await storage.upload(buffer, {
468
+ filename: 'report.pdf',
469
+ mimetype: 'application/pdf',
470
+ });
471
+
472
+ // Upload a base64 data URI
473
+ const result = await storage.upload(dataUri, { filename: 'photo.png' });
474
+ ```
475
+
476
+ #### `storage.uploadMany(inputs, options?)`
477
+
478
+ Uploads multiple files in parallel and returns an array of `UploadResult`.
479
+
480
+ ```typescript
481
+ const files = [req.payload.photo1, req.payload.photo2, req.payload.photo3];
482
+ const results = await storage.uploadMany(files);
483
+ // → [{ url, key, size, mimetype }, ...]
484
+ ```
485
+
486
+ #### `storage.delete(key)`
487
+
488
+ Deletes a file by its storage key.
489
+
490
+ ```typescript
491
+ await storage.delete('uploads/abc123-avatar.jpg');
492
+ ```
493
+
494
+ #### End-to-end Express example
495
+
496
+ ```typescript
497
+ import express from 'express';
498
+ import { parser } from 'formdata-io/server';
499
+ import { createStorage } from 'formdata-io/storage';
500
+
501
+ const app = express();
502
+
503
+ const storage = createStorage({
504
+ provider: 'aws',
505
+ bucket: process.env.AWS_BUCKET!,
506
+ region: process.env.AWS_REGION!,
507
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
508
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
509
+ keyPrefix: 'avatars',
510
+ });
511
+
512
+ app.post('/api/profile', parser({ maxFileSize: 5 * 1024 * 1024 }), async (req, res) => {
513
+ try {
514
+ const { name, avatar } = req.payload;
515
+
516
+ const uploaded = await storage.upload(avatar);
517
+
518
+ res.json({
519
+ name,
520
+ avatarUrl: uploaded.url,
521
+ avatarKey: uploaded.key,
522
+ });
523
+ } catch (err) {
524
+ res.status(500).json({ error: (err as Error).message });
525
+ }
526
+ });
527
+ ```
528
+
529
+ #### S3-compatible providers (MinIO, Cloudflare R2, etc.)
530
+
531
+ Pass a custom `endpoint` to use any S3-compatible storage service:
532
+
533
+ ```typescript
534
+ // MinIO
535
+ const storage = createStorage({
536
+ provider: 'aws',
537
+ bucket: 'my-bucket',
538
+ region: 'us-east-1',
539
+ accessKeyId: process.env.MINIO_ACCESS_KEY!,
540
+ secretAccessKey: process.env.MINIO_SECRET_KEY!,
541
+ endpoint: 'http://localhost:9000',
542
+ });
543
+
544
+ // Cloudflare R2
545
+ const storage = createStorage({
546
+ provider: 'aws',
547
+ bucket: 'my-bucket',
548
+ region: 'auto',
549
+ accessKeyId: process.env.R2_ACCESS_KEY_ID!,
550
+ secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
551
+ endpoint: `https://${process.env.CF_ACCOUNT_ID}.r2.cloudflarestorage.com`,
552
+ });
553
+ ```
554
+
296
555
  ## TypeScript Support
297
556
 
298
557
  Full TypeScript support with type inference:
299
558
 
300
559
  ```typescript
301
560
  import type { ParsedFile } from 'formdata-io/server';
561
+ import type { UploadResult } from 'formdata-io/storage';
302
562
 
303
- app.post('/upload', parser(), (req, res) => {
563
+ app.post('/upload', parser(), async (req, res) => {
304
564
  const avatar = req.payload?.avatar as ParsedFile;
305
565
 
306
566
  avatar.buffer; // Buffer
307
567
  avatar.originalname; // string
308
568
  avatar.mimetype; // string
309
569
  avatar.size; // number
570
+
571
+ const result: UploadResult = await storage.upload(avatar);
572
+ result.url; // string
573
+ result.key; // string
310
574
  });
311
575
  ```
312
576
 
@@ -333,6 +597,7 @@ open examples/basic/client.html
333
597
  | TypeScript-first | ✅ | ⚠️ | ⚠️ | ❌ |
334
598
  | Zero config | ✅ | ❌ | ❌ | ✅ |
335
599
  | Auto-parsing | ✅ | ❌ | ❌ | N/A |
600
+ | Cloud storage | ✅ | ❌ | ❌ | ❌ |
336
601
  | Bundle size | ~6KB | ~30KB | ~10KB | ~2KB |
337
602
 
338
603
  ## How It Works
@@ -358,23 +623,35 @@ The `payload()` function converts JavaScript objects to FormData by:
358
623
  The `parser()` middleware uses [busboy](https://github.com/mscdex/busboy) for stream-based parsing:
359
624
 
360
625
  1. **Stream processing**: Memory-efficient file handling
361
- 2. **Size limits**: Enforced per-file and total limits
626
+ 2. **Size limits**: Enforced per-file, per-field, and total file size limits
362
627
  3. **Auto-parsing**: Automatic type conversion (JSON, numbers, booleans)
363
628
  4. **Array normalization**: Multiple values with same key become arrays
364
629
 
630
+ ### Storage Side
631
+
632
+ The `createStorage()` factory returns a provider-agnostic `StorageAdapter`:
633
+
634
+ 1. **Unified interface**: Same `upload` / `uploadMany` / `delete` API across providers
635
+ 2. **Key generation**: Storage keys use `{prefix}/{uuid}-{sanitized-filename}` format, with NFD normalization to produce readable keys from accented filenames
636
+ 3. **Lazy loading**: The AWS SDK is loaded on first upload, keeping startup time unaffected if storage is unused
637
+ 4. **Input flexibility**: Accepts `ParsedFile`, raw `Buffer`, or base64 data URI as upload input
638
+
365
639
  ## Security
366
640
 
367
641
  **Built-in protections:**
368
642
  - ✅ File size limits (default: 10MB per file)
369
643
  - ✅ File count limits (default: 10 files max)
644
+ - ✅ Text field limits (default: 100 fields, 64KB each)
645
+ - ✅ Total file size limit (configurable via `maxTotalFileSize`)
370
646
  - ✅ Stream-based processing (no memory exhaustion)
371
647
  - ✅ Safe JSON parsing (fallback to string on error)
648
+ - ✅ ReDoS protection for base64 parsing (regex runs in isolated `vm` context with 50ms timeout, throws `RegExpTimeoutError` on timeout)
649
+ - ✅ Storage key sanitization (NFD normalization + accent stripping + alphanumeric enforcement prevents path traversal)
372
650
 
373
651
  **Your responsibility:**
374
652
  - ⚠️ File type validation (check `mimetype` and magic bytes)
375
- - ⚠️ Filename sanitization (prevent path traversal)
376
653
  - ⚠️ Virus scanning (if accepting user files)
377
- - ⚠️ Storage security (S3 permissions, disk quotas)
654
+ - ⚠️ Storage permissions (S3 bucket policies, Supabase RLS)
378
655
 
379
656
  ## License
380
657
 
@@ -58,110 +58,12 @@ interface PayloadOptions {
58
58
  booleansAsIntegers?: boolean;
59
59
  }
60
60
 
61
- /**
62
- * Converts a JavaScript object to FormData
63
- *
64
- * @param data - Object to be converted
65
- * @param options - Configuration options
66
- * @returns FormData instance ready for submission
67
- *
68
- * @example
69
- * ```typescript
70
- * const formData = payload({
71
- * name: "João Silva",
72
- * age: 25,
73
- * avatar: fileInput.files[0],
74
- * tags: ["admin", "user"],
75
- * metadata: { source: "web" }
76
- * });
77
- *
78
- * // Use with fetch
79
- * fetch('/upload', { method: 'POST', body: formData });
80
- *
81
- * // Use with axios
82
- * axios.post('/upload', formData);
83
- * ```
84
- *
85
- * @example
86
- * ```typescript
87
- * // With custom options
88
- * const formData = payload(data, {
89
- * indices: true, // tags[0]=admin&tags[1]=user
90
- * booleansAsIntegers: false // active=true instead of active=1
91
- * });
92
- * ```
93
- */
94
61
  declare function payload(data: FormDataPayload, options?: Partial<PayloadOptions>): FormData;
95
62
 
96
- /**
97
- * Converts a File or Blob to a base64-encoded data URI string.
98
- *
99
- * @param file - The File or Blob to convert
100
- * @returns Promise resolving to a data URI string (e.g., "data:image/png;base64,...")
101
- *
102
- * @example
103
- * ```typescript
104
- * const file = new File(['content'], 'example.txt', { type: 'text/plain' })
105
- * const base64 = await fileToBase64(file)
106
- * // "data:text/plain;base64,Y29udGVudA=="
107
- * ```
108
- */
109
63
  declare function fileToBase64(file: File | Blob): Promise<Base64String>;
110
- /**
111
- * Converts a base64 data URI string to a Blob.
112
- *
113
- * @param dataUri - Base64 data URI string (e.g., "data:image/png;base64,...")
114
- * @returns Blob object with the decoded data and MIME type
115
- *
116
- * @example
117
- * ```typescript
118
- * const blob = base64ToBlob("data:text/plain;base64,Y29udGVudA==")
119
- * // Blob { type: 'text/plain', size: 7 }
120
- * ```
121
- */
122
64
  declare function base64ToBlob(dataUri: Base64String): Blob;
123
- /**
124
- * Converts a base64 data URI string to a File object.
125
- *
126
- * @param dataUri - Base64 data URI string (e.g., "data:image/png;base64,...")
127
- * @param filename - Name for the created File object
128
- * @returns File object with the decoded data, MIME type, and filename
129
- *
130
- * @example
131
- * ```typescript
132
- * const file = base64ToFile("data:text/plain;base64,Y29udGVudA==", "example.txt")
133
- * // File { name: 'example.txt', type: 'text/plain', size: 7 }
134
- * ```
135
- */
136
65
  declare function base64ToFile(dataUri: Base64String, filename: string): File;
137
- /**
138
- * Converts a Blob to a File object with a specified filename.
139
- *
140
- * @param blob - Blob to convert
141
- * @param filename - Name for the created File object
142
- * @returns File object with the same content and MIME type as the Blob
143
- *
144
- * @example
145
- * ```typescript
146
- * const blob = new Blob(['content'], { type: 'text/plain' })
147
- * const file = blobToFile(blob, 'example.txt')
148
- * // File { name: 'example.txt', type: 'text/plain', size: 7 }
149
- * ```
150
- */
151
66
  declare function blobToFile(blob: Blob, filename: string): File;
152
- /**
153
- * Converts a File to a Blob (utility function for type conversion).
154
- *
155
- * @param file - File to convert
156
- * @returns Blob with the same content and MIME type as the File
157
- *
158
- * @example
159
- * ```typescript
160
- * const file = new File(['content'], 'example.txt', { type: 'text/plain' })
161
- * const blob = fileToBlob(file)
162
- * // Blob { type: 'text/plain', size: 7 }
163
- * ```
164
- */
165
67
  declare function fileToBlob(file: File): Blob;
166
68
 
167
69
  export { type Base64String, type FormDataPayload, type FormDataValue, type PayloadOptions, base64ToBlob, base64ToFile, blobToFile, fileToBase64, fileToBlob, payload };
@@ -58,110 +58,12 @@ interface PayloadOptions {
58
58
  booleansAsIntegers?: boolean;
59
59
  }
60
60
 
61
- /**
62
- * Converts a JavaScript object to FormData
63
- *
64
- * @param data - Object to be converted
65
- * @param options - Configuration options
66
- * @returns FormData instance ready for submission
67
- *
68
- * @example
69
- * ```typescript
70
- * const formData = payload({
71
- * name: "João Silva",
72
- * age: 25,
73
- * avatar: fileInput.files[0],
74
- * tags: ["admin", "user"],
75
- * metadata: { source: "web" }
76
- * });
77
- *
78
- * // Use with fetch
79
- * fetch('/upload', { method: 'POST', body: formData });
80
- *
81
- * // Use with axios
82
- * axios.post('/upload', formData);
83
- * ```
84
- *
85
- * @example
86
- * ```typescript
87
- * // With custom options
88
- * const formData = payload(data, {
89
- * indices: true, // tags[0]=admin&tags[1]=user
90
- * booleansAsIntegers: false // active=true instead of active=1
91
- * });
92
- * ```
93
- */
94
61
  declare function payload(data: FormDataPayload, options?: Partial<PayloadOptions>): FormData;
95
62
 
96
- /**
97
- * Converts a File or Blob to a base64-encoded data URI string.
98
- *
99
- * @param file - The File or Blob to convert
100
- * @returns Promise resolving to a data URI string (e.g., "data:image/png;base64,...")
101
- *
102
- * @example
103
- * ```typescript
104
- * const file = new File(['content'], 'example.txt', { type: 'text/plain' })
105
- * const base64 = await fileToBase64(file)
106
- * // "data:text/plain;base64,Y29udGVudA=="
107
- * ```
108
- */
109
63
  declare function fileToBase64(file: File | Blob): Promise<Base64String>;
110
- /**
111
- * Converts a base64 data URI string to a Blob.
112
- *
113
- * @param dataUri - Base64 data URI string (e.g., "data:image/png;base64,...")
114
- * @returns Blob object with the decoded data and MIME type
115
- *
116
- * @example
117
- * ```typescript
118
- * const blob = base64ToBlob("data:text/plain;base64,Y29udGVudA==")
119
- * // Blob { type: 'text/plain', size: 7 }
120
- * ```
121
- */
122
64
  declare function base64ToBlob(dataUri: Base64String): Blob;
123
- /**
124
- * Converts a base64 data URI string to a File object.
125
- *
126
- * @param dataUri - Base64 data URI string (e.g., "data:image/png;base64,...")
127
- * @param filename - Name for the created File object
128
- * @returns File object with the decoded data, MIME type, and filename
129
- *
130
- * @example
131
- * ```typescript
132
- * const file = base64ToFile("data:text/plain;base64,Y29udGVudA==", "example.txt")
133
- * // File { name: 'example.txt', type: 'text/plain', size: 7 }
134
- * ```
135
- */
136
65
  declare function base64ToFile(dataUri: Base64String, filename: string): File;
137
- /**
138
- * Converts a Blob to a File object with a specified filename.
139
- *
140
- * @param blob - Blob to convert
141
- * @param filename - Name for the created File object
142
- * @returns File object with the same content and MIME type as the Blob
143
- *
144
- * @example
145
- * ```typescript
146
- * const blob = new Blob(['content'], { type: 'text/plain' })
147
- * const file = blobToFile(blob, 'example.txt')
148
- * // File { name: 'example.txt', type: 'text/plain', size: 7 }
149
- * ```
150
- */
151
66
  declare function blobToFile(blob: Blob, filename: string): File;
152
- /**
153
- * Converts a File to a Blob (utility function for type conversion).
154
- *
155
- * @param file - File to convert
156
- * @returns Blob with the same content and MIME type as the File
157
- *
158
- * @example
159
- * ```typescript
160
- * const file = new File(['content'], 'example.txt', { type: 'text/plain' })
161
- * const blob = fileToBlob(file)
162
- * // Blob { type: 'text/plain', size: 7 }
163
- * ```
164
- */
165
67
  declare function fileToBlob(file: File): Blob;
166
68
 
167
69
  export { type Base64String, type FormDataPayload, type FormDataValue, type PayloadOptions, base64ToBlob, base64ToFile, blobToFile, fileToBase64, fileToBlob, payload };
@@ -82,6 +82,22 @@ function payload(data, options = {}) {
82
82
  }
83
83
 
84
84
  // src/client/converters.ts
85
+ var DATA_URI_MAX_LENGTH = 100 * 1024 * 1024;
86
+ var REGEX_TIMEOUT_MS = 100;
87
+ function matchWithTimeout(input, pattern, timeoutMs = REGEX_TIMEOUT_MS) {
88
+ if (input.length > DATA_URI_MAX_LENGTH) {
89
+ throw new Error(
90
+ `Input length (${input.length}) exceeds maximum allowed (${DATA_URI_MAX_LENGTH} bytes)`
91
+ );
92
+ }
93
+ const start = performance.now();
94
+ const result = input.match(pattern);
95
+ const elapsed = performance.now() - start;
96
+ if (elapsed > timeoutMs) {
97
+ throw new Error(`RegExp execution timed out after ${timeoutMs}ms`);
98
+ }
99
+ return result;
100
+ }
85
101
  async function fileToBase64(file) {
86
102
  return new Promise((resolve, reject) => {
87
103
  const reader = new FileReader();
@@ -100,12 +116,18 @@ function base64ToBlob(dataUri) {
100
116
  if (parts.length !== 2) {
101
117
  throw new Error("Invalid data URI format");
102
118
  }
103
- const mimeMatch = parts[0].match(/:(.*?);/);
119
+ const header = parts[0];
120
+ const base64Data = parts[1];
121
+ if (!header.includes(";base64")) {
122
+ throw new Error(
123
+ "Invalid data URI: only base64-encoded data URIs are supported. Expected format: data:{mimetype};base64,{data}"
124
+ );
125
+ }
126
+ const mimeMatch = matchWithTimeout(header, /:(.*?);/);
104
127
  if (!mimeMatch) {
105
128
  throw new Error("Invalid data URI: missing MIME type");
106
129
  }
107
130
  const mimeType = mimeMatch[1];
108
- const base64Data = parts[1];
109
131
  const binaryString = atob(base64Data);
110
132
  const bytes = new Uint8Array(binaryString.length);
111
133
  for (let i = 0; i < binaryString.length; i++) {