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 +292 -15
- package/dist/client/index.d.mts +0 -98
- package/dist/client/index.d.ts +0 -98
- package/dist/client/index.js +24 -2
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +24 -2
- package/dist/client/index.mjs.map +1 -1
- package/dist/server/index.d.mts +27 -21
- package/dist/server/index.d.ts +27 -21
- package/dist/server/index.js +27 -7
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +27 -7
- package/dist/server/index.mjs.map +1 -1
- package/dist/storage/index.d.mts +108 -0
- package/dist/storage/index.d.ts +108 -0
- package/dist/storage/index.js +271 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/index.mjs +264 -0
- package/dist/storage/index.mjs.map +1 -0
- package/package.json +11 -2
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
|
[](https://www.npmjs.com/package/formdata-io)
|
|
6
6
|
[](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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
|
654
|
+
- ⚠️ Storage permissions (S3 bucket policies, Supabase RLS)
|
|
378
655
|
|
|
379
656
|
## License
|
|
380
657
|
|
package/dist/client/index.d.mts
CHANGED
|
@@ -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 };
|
package/dist/client/index.d.ts
CHANGED
|
@@ -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 };
|
package/dist/client/index.js
CHANGED
|
@@ -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
|
|
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++) {
|