@maroonedsoftware/multipart 1.0.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/LICENSE +21 -0
- package/README.md +247 -0
- package/dist/busboy.wrapper.d.ts +107 -0
- package/dist/busboy.wrapper.d.ts.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +243 -0
- package/dist/index.js.map +1 -0
- package/dist/multipart.body.d.ts +68 -0
- package/dist/multipart.body.d.ts.map +1 -0
- package/dist/multipart.related.parser.d.ts +120 -0
- package/dist/multipart.related.parser.d.ts.map +1 -0
- package/dist/types.d.ts +129 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Marooned Software
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# @maroonedsoftware/multipart
|
|
2
|
+
|
|
3
|
+
A robust multipart form-data and multipart/related parser for Node.js HTTP servers. Built on top of [@fastify/busboy](https://github.com/fastify/busboy) with a promise-based API and sensible defaults.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @maroonedsoftware/multipart
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **Promise-based API** – Clean async/await interface for parsing multipart requests
|
|
14
|
+
- **Configurable limits** – Protect against resource exhaustion with file size, count, and field limits
|
|
15
|
+
- **Stream-based file handling** – Process files efficiently without loading entire files into memory
|
|
16
|
+
- **Type-safe** – Full TypeScript support with comprehensive type definitions
|
|
17
|
+
- **Automatic cleanup** – Properly removes event listeners to prevent memory leaks
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { createServer, IncomingMessage, ServerResponse } from 'node:http';
|
|
23
|
+
import { createWriteStream } from 'node:fs';
|
|
24
|
+
import { pipeline } from 'node:stream/promises';
|
|
25
|
+
import { MultipartBody, isMultipartFieldData } from '@maroonedsoftware/multipart';
|
|
26
|
+
|
|
27
|
+
const server = createServer(async (req: IncomingMessage, res: ServerResponse) => {
|
|
28
|
+
if (req.method === 'POST' && req.headers['content-type']?.includes('multipart/form-data')) {
|
|
29
|
+
const multipart = new MultipartBody(req);
|
|
30
|
+
|
|
31
|
+
const fields = await multipart.parse(async (fieldname, stream, filename) => {
|
|
32
|
+
// Save uploaded file to disk
|
|
33
|
+
await pipeline(stream, createWriteStream(`./uploads/${filename}`));
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Access form fields
|
|
37
|
+
const description = fields.get('description');
|
|
38
|
+
if (description && isMultipartFieldData(description)) {
|
|
39
|
+
console.log('Description:', description.value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
43
|
+
res.end(JSON.stringify({ success: true }));
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
server.listen(3000);
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## API Reference
|
|
51
|
+
|
|
52
|
+
### `MultipartBody`
|
|
53
|
+
|
|
54
|
+
The main class for parsing multipart/form-data requests.
|
|
55
|
+
|
|
56
|
+
#### Constructor
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
new MultipartBody(req: IncomingMessage, limits?: MultipartLimits)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
| Parameter | Type | Description |
|
|
63
|
+
| --------- | ----------------- | ----------------------------------- |
|
|
64
|
+
| `req` | `IncomingMessage` | The incoming HTTP request |
|
|
65
|
+
| `limits` | `MultipartLimits` | Optional default limits for parsing |
|
|
66
|
+
|
|
67
|
+
**Default limits:**
|
|
68
|
+
|
|
69
|
+
- `files`: 1
|
|
70
|
+
- `fileSize`: 20 MB
|
|
71
|
+
|
|
72
|
+
#### `parse(fileHandler, limits?)`
|
|
73
|
+
|
|
74
|
+
Parses the multipart request body.
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
parse(
|
|
78
|
+
fileHandler: FileHandler,
|
|
79
|
+
limits?: MultipartLimits
|
|
80
|
+
): Promise<Map<string, MultipartData | MultipartData[]>>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
| Parameter | Type | Description |
|
|
84
|
+
| ------------- | ----------------- | ------------------------------------- |
|
|
85
|
+
| `fileHandler` | `FileHandler` | Callback invoked for each file upload |
|
|
86
|
+
| `limits` | `MultipartLimits` | Optional per-request limit overrides |
|
|
87
|
+
|
|
88
|
+
**Returns:** A `Map` where keys are field names and values are either a single `MultipartData` object or an array if multiple values were submitted.
|
|
89
|
+
|
|
90
|
+
### `FileHandler`
|
|
91
|
+
|
|
92
|
+
Callback type for handling file uploads:
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
type FileHandler = (fieldname: string, stream: Readable, filename: string, encoding: string, mimeType: string) => Promise<void>;
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### `MultipartLimits`
|
|
99
|
+
|
|
100
|
+
Configuration options for limiting request sizes:
|
|
101
|
+
|
|
102
|
+
| Option | Type | Default | Description |
|
|
103
|
+
| --------------- | -------- | -------- | -------------------------------- |
|
|
104
|
+
| `fieldNameSize` | `number` | 100 | Max field name size in bytes |
|
|
105
|
+
| `fieldSize` | `number` | 1 MB | Max field value size in bytes |
|
|
106
|
+
| `fields` | `number` | Infinity | Max number of non-file fields |
|
|
107
|
+
| `fileSize` | `number` | Infinity | Max file size in bytes |
|
|
108
|
+
| `files` | `number` | Infinity | Max number of file fields |
|
|
109
|
+
| `parts` | `number` | Infinity | Max total parts (fields + files) |
|
|
110
|
+
| `headerPairs` | `number` | 2000 | Max header key-value pairs |
|
|
111
|
+
| `headerSize` | `number` | 81920 | Max header part size in bytes |
|
|
112
|
+
|
|
113
|
+
### Type Guards
|
|
114
|
+
|
|
115
|
+
#### `isMultipartFieldData(data)`
|
|
116
|
+
|
|
117
|
+
Type guard to check if parsed data is a form field.
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
if (isMultipartFieldData(data)) {
|
|
121
|
+
console.log(data.value); // TypeScript knows this is FieldData
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
#### `isMultipartFileData(data)`
|
|
126
|
+
|
|
127
|
+
Type guard to check if parsed data is a file.
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
if (isMultipartFileData(data)) {
|
|
131
|
+
data.stream.pipe(destination); // TypeScript knows this is FileData
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Data Types
|
|
136
|
+
|
|
137
|
+
#### `FieldData`
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
type FieldData = {
|
|
141
|
+
value: string;
|
|
142
|
+
nameTruncated: boolean;
|
|
143
|
+
valueTruncated: boolean;
|
|
144
|
+
encoding: string;
|
|
145
|
+
mimeType: string;
|
|
146
|
+
};
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
#### `FileData`
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
type FileData = {
|
|
153
|
+
stream: Readable;
|
|
154
|
+
filename: string;
|
|
155
|
+
encoding: string;
|
|
156
|
+
mimeType: string;
|
|
157
|
+
};
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Advanced Usage
|
|
161
|
+
|
|
162
|
+
### Custom File Size Limits
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
// Set default limits in constructor
|
|
166
|
+
const multipart = new MultipartBody(req, {
|
|
167
|
+
files: 5,
|
|
168
|
+
fileSize: 50 * 1024 * 1024, // 50 MB
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Override per-request
|
|
172
|
+
const fields = await multipart.parse(fileHandler, {
|
|
173
|
+
fileSize: 100 * 1024 * 1024, // 100 MB for this request only
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Handling Multiple Files
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
const multipart = new MultipartBody(req, { files: 10 });
|
|
181
|
+
|
|
182
|
+
const uploadedFiles: string[] = [];
|
|
183
|
+
|
|
184
|
+
const fields = await multipart.parse(async (fieldname, stream, filename) => {
|
|
185
|
+
const path = `./uploads/${Date.now()}-${filename}`;
|
|
186
|
+
await pipeline(stream, createWriteStream(path));
|
|
187
|
+
uploadedFiles.push(path);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
console.log('Uploaded files:', uploadedFiles);
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Error Handling
|
|
194
|
+
|
|
195
|
+
The parser throws HTTP 413 errors when limits are exceeded:
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
import { MultipartBody } from '@maroonedsoftware/multipart';
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
const fields = await multipart.parse(fileHandler);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
if (error.statusCode === 413) {
|
|
204
|
+
// Handle limit exceeded
|
|
205
|
+
console.error('Upload too large:', error.internalDetails?.reason);
|
|
206
|
+
}
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Example
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
type ParsedMultipartBody = {
|
|
215
|
+
fields: Map<string, MultipartData>;
|
|
216
|
+
file: {
|
|
217
|
+
filename: string;
|
|
218
|
+
stream: Readable;
|
|
219
|
+
encoding: string;
|
|
220
|
+
mimeType: string;
|
|
221
|
+
};
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
export class Service {
|
|
225
|
+
private async parseMultipartBody(multipartReq: MultipartBody) {
|
|
226
|
+
let file: ParsedMultipartBody['file'] | undefined;
|
|
227
|
+
const fields = await multipartReq.parse(async (fieldname, stream, fileFieldName, encoding, mimeType) => {
|
|
228
|
+
if (fieldname === 'file') {
|
|
229
|
+
file = { stream, filename: fileFieldName, encoding, mimeType };
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
return { fields, file } as ParsedMultipartBody;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async handle(body: MultipartBody): Promise<void> {
|
|
236
|
+
const parsedBody = await this.parseMultipartBody(body);
|
|
237
|
+
filename = parsedBody.file?.filename ?? 'missing name';
|
|
238
|
+
content = parsedBody.file?.stream;
|
|
239
|
+
|
|
240
|
+
// use content
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## License
|
|
246
|
+
|
|
247
|
+
MIT
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { IncomingMessage } from 'node:http';
|
|
2
|
+
import { Busboy } from '@fastify/busboy';
|
|
3
|
+
import { FileHandler, MultipartData, MultipartLimits } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* A wrapper around the Busboy multipart parser that provides a promise-based API
|
|
6
|
+
* for parsing multipart/form-data requests.
|
|
7
|
+
*
|
|
8
|
+
* This class handles both file and field parsing, automatically managing the lifecycle
|
|
9
|
+
* of streams and cleanup of resources. It enforces configurable limits on file sizes,
|
|
10
|
+
* field counts, and other parameters to prevent resource exhaustion.
|
|
11
|
+
*
|
|
12
|
+
* @extends Busboy
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* import { IncomingMessage } from 'node:http';
|
|
17
|
+
* import { BusboyWrapper } from './busboy.wrapper.js';
|
|
18
|
+
*
|
|
19
|
+
* async function handleUpload(req: IncomingMessage) {
|
|
20
|
+
* const parser = new BusboyWrapper(req, { fileSize: 10 * 1024 * 1024 });
|
|
21
|
+
*
|
|
22
|
+
* const fields = await parser.parse(async (fieldname, stream, filename) => {
|
|
23
|
+
* // Handle file upload
|
|
24
|
+
* await pipeline(stream, fs.createWriteStream(`./uploads/${filename}`));
|
|
25
|
+
* });
|
|
26
|
+
*
|
|
27
|
+
* // Access parsed fields
|
|
28
|
+
* const name = fields.get('name');
|
|
29
|
+
* }
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export declare class BusboyWrapper extends Busboy {
|
|
33
|
+
/** The incoming HTTP request being parsed */
|
|
34
|
+
private readonly req;
|
|
35
|
+
/** Map storing parsed fields and files keyed by field name */
|
|
36
|
+
private readonly fields;
|
|
37
|
+
/** Optional handler for processing file uploads */
|
|
38
|
+
private fileHandler?;
|
|
39
|
+
/** A null stream used to drain file streams when errors occur */
|
|
40
|
+
private readonly nullStream;
|
|
41
|
+
/**
|
|
42
|
+
* Creates a new BusboyWrapper instance.
|
|
43
|
+
*
|
|
44
|
+
* @param req - The incoming HTTP request containing multipart data
|
|
45
|
+
* @param limits - Optional limits configuration for parsing
|
|
46
|
+
*/
|
|
47
|
+
constructor(req: IncomingMessage, limits?: MultipartLimits);
|
|
48
|
+
/**
|
|
49
|
+
* Parses the multipart request body.
|
|
50
|
+
*
|
|
51
|
+
* @param fileHandler - A callback function to handle file uploads as they are received
|
|
52
|
+
* @returns A promise that resolves to a Map of field names to their parsed data.
|
|
53
|
+
* If multiple values exist for a field name, they are stored as an array.
|
|
54
|
+
*
|
|
55
|
+
* @throws {HttpError} 413 error if parts, files, or fields limits are exceeded
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```typescript
|
|
59
|
+
* const fields = await parser.parse(async (fieldname, stream, filename) => {
|
|
60
|
+
* const chunks: Buffer[] = [];
|
|
61
|
+
* for await (const chunk of stream) {
|
|
62
|
+
* chunks.push(chunk);
|
|
63
|
+
* }
|
|
64
|
+
* await fs.writeFile(`./uploads/${filename}`, Buffer.concat(chunks));
|
|
65
|
+
* });
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
parse(fileHandler: FileHandler): Promise<Map<string, MultipartData | MultipartData[]>>;
|
|
69
|
+
/**
|
|
70
|
+
* Stores parsed data in the fields map, handling multiple values for the same field.
|
|
71
|
+
*
|
|
72
|
+
* @param name - The field name
|
|
73
|
+
* @param data - The parsed field or file data
|
|
74
|
+
*/
|
|
75
|
+
private setData;
|
|
76
|
+
/**
|
|
77
|
+
* Handler for parsed form fields.
|
|
78
|
+
*/
|
|
79
|
+
private onField;
|
|
80
|
+
/**
|
|
81
|
+
* Handler for parsed file uploads.
|
|
82
|
+
*/
|
|
83
|
+
private onFile;
|
|
84
|
+
private resolve;
|
|
85
|
+
private reject;
|
|
86
|
+
/**
|
|
87
|
+
* Handler called when parsing completes or an error occurs.
|
|
88
|
+
*/
|
|
89
|
+
private onEnd;
|
|
90
|
+
/**
|
|
91
|
+
* Handler for when the parts limit is exceeded.
|
|
92
|
+
*/
|
|
93
|
+
private onPartsLimit;
|
|
94
|
+
/**
|
|
95
|
+
* Handler for when the files limit is exceeded.
|
|
96
|
+
*/
|
|
97
|
+
private onFilesLimit;
|
|
98
|
+
/**
|
|
99
|
+
* Handler for when the fields limit is exceeded.
|
|
100
|
+
*/
|
|
101
|
+
private onFieldsLimit;
|
|
102
|
+
/**
|
|
103
|
+
* Cleans up event listeners to prevent memory leaks.
|
|
104
|
+
*/
|
|
105
|
+
private cleanup;
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=busboy.wrapper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"busboy.wrapper.d.ts","sourceRoot":"","sources":["../src/busboy.wrapper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAmC,MAAM,iBAAiB,CAAC;AAG1E,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAEzE;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,qBAAa,aAAc,SAAQ,MAAM;IACvC,6CAA6C;IAC7C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAkB;IAEtC,8DAA8D;IAC9D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAsD;IAE7E,mDAAmD;IACnD,OAAO,CAAC,WAAW,CAAC,CAAc;IAElC,iEAAiE;IACjE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAIxB;IAEH;;;;;OAKG;gBACS,GAAG,EAAE,eAAe,EAAE,MAAM,CAAC,EAAE,eAAe;IAe1D;;;;;;;;;;;;;;;;;;;OAmBG;IACH,KAAK,CAAC,WAAW,EAAE,WAAW;IAS9B;;;;;OAKG;IACH,OAAO,CAAC,OAAO;IAWf;;OAEG;IACH,OAAO,CAAC,OAAO;IAUf;;OAEG;IACH,OAAO,CAAC,MAAM;IAcd,OAAO,CAAC,OAAO;IACf,OAAO,CAAC,MAAM;IAEd;;OAEG;IACH,OAAO,CAAC,KAAK;IASb;;OAEG;IACH,OAAO,CAAC,YAAY;IAOpB;;OAEG;IACH,OAAO,CAAC,YAAY;IAOpB;;OAEG;IACH,OAAO,CAAC,aAAa;IAOrB;;OAEG;IACH,OAAO,CAAC,OAAO;CAUhB"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,qBAAqB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
|
+
|
|
4
|
+
// src/types.ts
|
|
5
|
+
var isMultipartFieldData = /* @__PURE__ */ __name((data) => {
|
|
6
|
+
return "value" in data;
|
|
7
|
+
}, "isMultipartFieldData");
|
|
8
|
+
var isMultipartFileData = /* @__PURE__ */ __name((data) => {
|
|
9
|
+
return "stream" in data;
|
|
10
|
+
}, "isMultipartFileData");
|
|
11
|
+
|
|
12
|
+
// src/busboy.wrapper.ts
|
|
13
|
+
import { Busboy } from "@fastify/busboy";
|
|
14
|
+
import { Writable } from "stream";
|
|
15
|
+
import { httpError } from "@maroonedsoftware/errors";
|
|
16
|
+
var BusboyWrapper = class extends Busboy {
|
|
17
|
+
static {
|
|
18
|
+
__name(this, "BusboyWrapper");
|
|
19
|
+
}
|
|
20
|
+
/** The incoming HTTP request being parsed */
|
|
21
|
+
req;
|
|
22
|
+
/** Map storing parsed fields and files keyed by field name */
|
|
23
|
+
fields = /* @__PURE__ */ new Map();
|
|
24
|
+
/** Optional handler for processing file uploads */
|
|
25
|
+
fileHandler;
|
|
26
|
+
/** A null stream used to drain file streams when errors occur */
|
|
27
|
+
nullStream = new Writable({
|
|
28
|
+
write(_chunk, _encding, callback) {
|
|
29
|
+
setImmediate(callback);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
/**
|
|
33
|
+
* Creates a new BusboyWrapper instance.
|
|
34
|
+
*
|
|
35
|
+
* @param req - The incoming HTTP request containing multipart data
|
|
36
|
+
* @param limits - Optional limits configuration for parsing
|
|
37
|
+
*/
|
|
38
|
+
constructor(req, limits) {
|
|
39
|
+
super({
|
|
40
|
+
headers: req.headers,
|
|
41
|
+
limits
|
|
42
|
+
});
|
|
43
|
+
this.req = req;
|
|
44
|
+
this.req.on("close", () => this.cleanup);
|
|
45
|
+
this.on("field", this.onField).on("file", this.onFile).on("finish", this.onEnd).on("error", this.onEnd).on("partsLimit", this.onPartsLimit).on("filesLimit", this.onFilesLimit).on("fieldsLimit", this.onFieldsLimit);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Parses the multipart request body.
|
|
49
|
+
*
|
|
50
|
+
* @param fileHandler - A callback function to handle file uploads as they are received
|
|
51
|
+
* @returns A promise that resolves to a Map of field names to their parsed data.
|
|
52
|
+
* If multiple values exist for a field name, they are stored as an array.
|
|
53
|
+
*
|
|
54
|
+
* @throws {HttpError} 413 error if parts, files, or fields limits are exceeded
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```typescript
|
|
58
|
+
* const fields = await parser.parse(async (fieldname, stream, filename) => {
|
|
59
|
+
* const chunks: Buffer[] = [];
|
|
60
|
+
* for await (const chunk of stream) {
|
|
61
|
+
* chunks.push(chunk);
|
|
62
|
+
* }
|
|
63
|
+
* await fs.writeFile(`./uploads/${filename}`, Buffer.concat(chunks));
|
|
64
|
+
* });
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
parse(fileHandler) {
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
this.fileHandler = fileHandler;
|
|
70
|
+
this.resolve = resolve;
|
|
71
|
+
this.reject = reject;
|
|
72
|
+
this.req.pipe(this);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Stores parsed data in the fields map, handling multiple values for the same field.
|
|
77
|
+
*
|
|
78
|
+
* @param name - The field name
|
|
79
|
+
* @param data - The parsed field or file data
|
|
80
|
+
*/
|
|
81
|
+
setData(name, data) {
|
|
82
|
+
const prev = this.fields.get(name);
|
|
83
|
+
if (prev == null) {
|
|
84
|
+
this.fields.set(name, data);
|
|
85
|
+
} else if (Array.isArray(prev)) {
|
|
86
|
+
prev.push(data);
|
|
87
|
+
} else {
|
|
88
|
+
this.fields.set(name, [
|
|
89
|
+
prev,
|
|
90
|
+
data
|
|
91
|
+
]);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Handler for parsed form fields.
|
|
96
|
+
*/
|
|
97
|
+
onField(name, value, nameTruncated, valueTruncated, encoding, mimeType) {
|
|
98
|
+
this.setData(name, {
|
|
99
|
+
value,
|
|
100
|
+
nameTruncated,
|
|
101
|
+
valueTruncated,
|
|
102
|
+
encoding,
|
|
103
|
+
mimeType
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Handler for parsed file uploads.
|
|
108
|
+
*/
|
|
109
|
+
onFile(fieldname, stream, filename, encoding, mimeType) {
|
|
110
|
+
this.setData(fieldname, {
|
|
111
|
+
stream,
|
|
112
|
+
filename,
|
|
113
|
+
encoding,
|
|
114
|
+
mimeType
|
|
115
|
+
});
|
|
116
|
+
if (this.fileHandler) {
|
|
117
|
+
this.fileHandler(fieldname, stream, filename, encoding, mimeType).then(() => {
|
|
118
|
+
this.onEnd();
|
|
119
|
+
}).catch((reason) => {
|
|
120
|
+
stream.pipe(this.nullStream);
|
|
121
|
+
this.onEnd(reason);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
resolve(_) {
|
|
126
|
+
}
|
|
127
|
+
reject(_) {
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Handler called when parsing completes or an error occurs.
|
|
131
|
+
*/
|
|
132
|
+
onEnd(err) {
|
|
133
|
+
this.cleanup();
|
|
134
|
+
if (err) {
|
|
135
|
+
this.reject(err);
|
|
136
|
+
} else {
|
|
137
|
+
this.resolve(this.fields);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Handler for when the parts limit is exceeded.
|
|
142
|
+
*/
|
|
143
|
+
onPartsLimit() {
|
|
144
|
+
const err = httpError(413).withInternalDetails({
|
|
145
|
+
reason: "Reached parts limit"
|
|
146
|
+
});
|
|
147
|
+
this.onEnd(err);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Handler for when the files limit is exceeded.
|
|
151
|
+
*/
|
|
152
|
+
onFilesLimit() {
|
|
153
|
+
const err = httpError(413).withInternalDetails({
|
|
154
|
+
reason: "Reached files limit"
|
|
155
|
+
});
|
|
156
|
+
this.onEnd(err);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Handler for when the fields limit is exceeded.
|
|
160
|
+
*/
|
|
161
|
+
onFieldsLimit() {
|
|
162
|
+
const err = httpError(413).withInternalDetails({
|
|
163
|
+
reason: "Reached fields limit"
|
|
164
|
+
});
|
|
165
|
+
this.onEnd(err);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Cleans up event listeners to prevent memory leaks.
|
|
169
|
+
*/
|
|
170
|
+
cleanup() {
|
|
171
|
+
this.req.removeListener("close", this.cleanup);
|
|
172
|
+
this.removeListener("field", this.onField);
|
|
173
|
+
this.removeListener("file", this.onFile);
|
|
174
|
+
this.removeListener("error", this.onEnd);
|
|
175
|
+
this.removeListener("partsLimit", this.onPartsLimit);
|
|
176
|
+
this.removeListener("filesLimit", this.onFilesLimit);
|
|
177
|
+
this.removeListener("fieldsLimit", this.onFieldsLimit);
|
|
178
|
+
this.removeListener("finish", this.onEnd);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// src/multipart.body.ts
|
|
183
|
+
var MAX_FILE_SIZE = 20 * 1024 * 1024;
|
|
184
|
+
var MultipartBody = class {
|
|
185
|
+
static {
|
|
186
|
+
__name(this, "MultipartBody");
|
|
187
|
+
}
|
|
188
|
+
req;
|
|
189
|
+
_limits;
|
|
190
|
+
/**
|
|
191
|
+
* Creates a new MultipartBody instance.
|
|
192
|
+
*
|
|
193
|
+
* @param req - The incoming HTTP request containing multipart data
|
|
194
|
+
* @param _limits - Default limits applied to all parse operations.
|
|
195
|
+
* Defaults to 1 file maximum and 20MB file size limit.
|
|
196
|
+
*/
|
|
197
|
+
constructor(req, _limits = {
|
|
198
|
+
files: 1,
|
|
199
|
+
fileSize: MAX_FILE_SIZE
|
|
200
|
+
}) {
|
|
201
|
+
this.req = req;
|
|
202
|
+
this._limits = _limits;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Parses the multipart request body and processes any file uploads.
|
|
206
|
+
*
|
|
207
|
+
* @param fileHandler - A callback function invoked for each file in the request.
|
|
208
|
+
* The callback receives the field name, file stream, filename,
|
|
209
|
+
* encoding, and MIME type. It should return a promise that
|
|
210
|
+
* resolves when the file has been fully processed.
|
|
211
|
+
* @param limits - Optional per-request limits that override the instance defaults.
|
|
212
|
+
* These are merged with the default limits (per-request takes precedence).
|
|
213
|
+
* @returns A promise that resolves to a Map containing all parsed fields and files.
|
|
214
|
+
* Field names are keys, and values are either a single MultipartData object
|
|
215
|
+
* or an array if multiple values were submitted for the same field name.
|
|
216
|
+
*
|
|
217
|
+
* @throws {HttpError} 413 error if configured limits are exceeded
|
|
218
|
+
*
|
|
219
|
+
* @example
|
|
220
|
+
* ```typescript
|
|
221
|
+
* // Parse with custom file size limit for this request
|
|
222
|
+
* const fields = await multipart.parse(
|
|
223
|
+
* async (fieldname, stream, filename) => {
|
|
224
|
+
* await saveFile(stream, filename);
|
|
225
|
+
* },
|
|
226
|
+
* { fileSize: 50 * 1024 * 1024 } // 50MB for this request
|
|
227
|
+
* );
|
|
228
|
+
* ```
|
|
229
|
+
*/
|
|
230
|
+
parse(fileHandler, limits) {
|
|
231
|
+
const busboy = new BusboyWrapper(this.req, {
|
|
232
|
+
...this._limits,
|
|
233
|
+
...limits
|
|
234
|
+
});
|
|
235
|
+
return busboy.parse(fileHandler);
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
export {
|
|
239
|
+
MultipartBody,
|
|
240
|
+
isMultipartFieldData,
|
|
241
|
+
isMultipartFileData
|
|
242
|
+
};
|
|
243
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/types.ts","../src/busboy.wrapper.ts","../src/multipart.body.ts"],"sourcesContent":["import { Readable } from 'node:stream';\n\n/**\n * Callback function for handling file uploads during multipart parsing.\n *\n * @param fieldname - The name of the form field\n * @param stream - A readable stream containing the file data\n * @param filename - The original filename from the upload\n * @param encoding - The encoding of the file (e.g., '7bit', 'binary')\n * @param mimeType - The MIME type of the file (e.g., 'image/png')\n * @returns A promise that resolves when the file has been fully processed\n *\n * @example\n * ```typescript\n * const handler: FileHandler = async (fieldname, stream, filename, encoding, mimeType) => {\n * const writeStream = fs.createWriteStream(`./uploads/${filename}`);\n * await pipeline(stream, writeStream);\n * };\n * ```\n */\nexport type FileHandler = (fieldname: string, stream: Readable, filename: string, encoding: string, mimeType: string) => Promise<void>;\n\n/**\n * Represents parsed form field data from a multipart request.\n */\nexport type FieldData = {\n /** The string value of the field */\n value: string;\n /** Whether the field name was truncated due to limits */\n nameTruncated: boolean;\n /** Whether the field value was truncated due to limits */\n valueTruncated: boolean;\n /** The encoding of the field value */\n encoding: string;\n /** The MIME type of the field */\n mimeType: string;\n};\n\n/**\n * Represents parsed file data from a multipart request.\n */\nexport type FileData = {\n /** A readable stream containing the file data */\n stream: Readable;\n /** The original filename from the upload */\n filename: string;\n /** The encoding of the file */\n encoding: string;\n /** The MIME type of the file */\n mimeType: string;\n};\n\n/**\n * Union type representing either field data or file data from a multipart request.\n */\nexport type MultipartData = FieldData | FileData;\n\n/**\n * Type guard to check if the multipart data is field data.\n *\n * @param data - The multipart data to check\n * @returns True if the data is field data, false otherwise\n *\n * @example\n * ```typescript\n * if (isMultipartFieldData(data)) {\n * console.log(data.value); // TypeScript knows this is FieldData\n * }\n * ```\n */\nexport const isMultipartFieldData = (data: MultipartData): data is FieldData => {\n return 'value' in data;\n};\n\n/**\n * Type guard to check if the multipart data is file data.\n *\n * @param data - The multipart data to check\n * @returns True if the data is file data, false otherwise\n *\n * @example\n * ```typescript\n * if (isMultipartFileData(data)) {\n * data.stream.pipe(destination); // TypeScript knows this is FileData\n * }\n * ```\n */\nexport const isMultipartFileData = (data: MultipartData): data is FileData => {\n return 'stream' in data;\n};\n\n/**\n * Configuration options for limiting multipart request sizes.\n * Used to prevent resource exhaustion from malicious or oversized uploads.\n *\n * @see https://github.com/fastify/busboy/blob/main/lib/main.d.ts#L104\n */\nexport interface MultipartLimits {\n /**\n * Maximum field name size in bytes.\n * @default 100\n */\n fieldNameSize?: number | undefined;\n /**\n * Maximum field value size in bytes.\n * @default 1048576 (1MB)\n */\n fieldSize?: number | undefined;\n /**\n * Maximum number of non-file fields.\n * @default Infinity\n */\n fields?: number | undefined;\n /**\n * Maximum file size in bytes for multipart forms.\n * @default Infinity\n */\n fileSize?: number | undefined;\n /**\n * Maximum number of file fields for multipart forms.\n * @default Infinity\n */\n files?: number | undefined;\n /**\n * Maximum number of parts (fields + files) for multipart forms.\n * @default Infinity\n */\n parts?: number | undefined;\n /**\n * Maximum number of header key-value pairs to parse for multipart forms.\n * @default 2000\n */\n headerPairs?: number | undefined;\n /**\n * Maximum size of a header part in bytes for multipart forms.\n * @default 81920\n */\n headerSize?: number | undefined;\n}\n","import { IncomingMessage } from 'node:http';\nimport { Busboy, BusboyFileStream, BusboyHeaders } from '@fastify/busboy';\nimport { Writable } from 'node:stream';\nimport { httpError } from '@maroonedsoftware/errors';\nimport { FileHandler, MultipartData, MultipartLimits } from './types.js';\n\n/**\n * A wrapper around the Busboy multipart parser that provides a promise-based API\n * for parsing multipart/form-data requests.\n *\n * This class handles both file and field parsing, automatically managing the lifecycle\n * of streams and cleanup of resources. It enforces configurable limits on file sizes,\n * field counts, and other parameters to prevent resource exhaustion.\n *\n * @extends Busboy\n *\n * @example\n * ```typescript\n * import { IncomingMessage } from 'node:http';\n * import { BusboyWrapper } from './busboy.wrapper.js';\n *\n * async function handleUpload(req: IncomingMessage) {\n * const parser = new BusboyWrapper(req, { fileSize: 10 * 1024 * 1024 });\n *\n * const fields = await parser.parse(async (fieldname, stream, filename) => {\n * // Handle file upload\n * await pipeline(stream, fs.createWriteStream(`./uploads/${filename}`));\n * });\n *\n * // Access parsed fields\n * const name = fields.get('name');\n * }\n * ```\n */\nexport class BusboyWrapper extends Busboy {\n /** The incoming HTTP request being parsed */\n private readonly req: IncomingMessage;\n\n /** Map storing parsed fields and files keyed by field name */\n private readonly fields = new Map<string, MultipartData | MultipartData[]>();\n\n /** Optional handler for processing file uploads */\n private fileHandler?: FileHandler;\n\n /** A null stream used to drain file streams when errors occur */\n private readonly nullStream = new Writable({\n write(_chunk, _encding, callback) {\n setImmediate(callback);\n },\n });\n\n /**\n * Creates a new BusboyWrapper instance.\n *\n * @param req - The incoming HTTP request containing multipart data\n * @param limits - Optional limits configuration for parsing\n */\n constructor(req: IncomingMessage, limits?: MultipartLimits) {\n super({ headers: req.headers as BusboyHeaders, limits });\n\n this.req = req;\n this.req.on('close', () => this.cleanup);\n\n this.on('field', this.onField)\n .on('file', this.onFile)\n .on('finish', this.onEnd)\n .on('error', this.onEnd)\n .on('partsLimit', this.onPartsLimit)\n .on('filesLimit', this.onFilesLimit)\n .on('fieldsLimit', this.onFieldsLimit);\n }\n\n /**\n * Parses the multipart request body.\n *\n * @param fileHandler - A callback function to handle file uploads as they are received\n * @returns A promise that resolves to a Map of field names to their parsed data.\n * If multiple values exist for a field name, they are stored as an array.\n *\n * @throws {HttpError} 413 error if parts, files, or fields limits are exceeded\n *\n * @example\n * ```typescript\n * const fields = await parser.parse(async (fieldname, stream, filename) => {\n * const chunks: Buffer[] = [];\n * for await (const chunk of stream) {\n * chunks.push(chunk);\n * }\n * await fs.writeFile(`./uploads/${filename}`, Buffer.concat(chunks));\n * });\n * ```\n */\n parse(fileHandler: FileHandler) {\n return new Promise<Map<string, MultipartData | MultipartData[]>>((resolve, reject) => {\n this.fileHandler = fileHandler;\n this.resolve = resolve;\n this.reject = reject;\n this.req.pipe(this);\n });\n }\n\n /**\n * Stores parsed data in the fields map, handling multiple values for the same field.\n *\n * @param name - The field name\n * @param data - The parsed field or file data\n */\n private setData(name: string, data: MultipartData) {\n const prev = this.fields.get(name);\n if (prev == null) {\n this.fields.set(name, data);\n } else if (Array.isArray(prev)) {\n prev.push(data);\n } else {\n this.fields.set(name, [prev, data]);\n }\n }\n\n /**\n * Handler for parsed form fields.\n */\n private onField(name: string, value: string, nameTruncated: boolean, valueTruncated: boolean, encoding: string, mimeType: string) {\n this.setData(name, {\n value,\n nameTruncated,\n valueTruncated,\n encoding,\n mimeType,\n });\n }\n\n /**\n * Handler for parsed file uploads.\n */\n private onFile(fieldname: string, stream: BusboyFileStream, filename: string, encoding: string, mimeType: string) {\n this.setData(fieldname, { stream, filename, encoding, mimeType });\n if (this.fileHandler) {\n this.fileHandler(fieldname, stream, filename, encoding, mimeType)\n .then(() => {\n this.onEnd();\n })\n .catch(reason => {\n stream.pipe(this.nullStream);\n this.onEnd(reason);\n });\n }\n }\n\n private resolve(_: Map<string, MultipartData | MultipartData[]>) {}\n private reject(_?: Error) {}\n\n /**\n * Handler called when parsing completes or an error occurs.\n */\n private onEnd(err?: Error) {\n this.cleanup();\n if (err) {\n this.reject(err);\n } else {\n this.resolve(this.fields);\n }\n }\n\n /**\n * Handler for when the parts limit is exceeded.\n */\n private onPartsLimit() {\n const err = httpError(413).withInternalDetails({\n reason: 'Reached parts limit',\n });\n this.onEnd(err);\n }\n\n /**\n * Handler for when the files limit is exceeded.\n */\n private onFilesLimit() {\n const err = httpError(413).withInternalDetails({\n reason: 'Reached files limit',\n });\n this.onEnd(err);\n }\n\n /**\n * Handler for when the fields limit is exceeded.\n */\n private onFieldsLimit() {\n const err = httpError(413).withInternalDetails({\n reason: 'Reached fields limit',\n });\n this.onEnd(err);\n }\n\n /**\n * Cleans up event listeners to prevent memory leaks.\n */\n private cleanup() {\n this.req.removeListener('close', this.cleanup);\n this.removeListener('field', this.onField);\n this.removeListener('file', this.onFile);\n this.removeListener('error', this.onEnd);\n this.removeListener('partsLimit', this.onPartsLimit);\n this.removeListener('filesLimit', this.onFilesLimit);\n this.removeListener('fieldsLimit', this.onFieldsLimit);\n this.removeListener('finish', this.onEnd);\n }\n}\n","import { IncomingMessage } from 'node:http';\nimport { BusboyWrapper } from './busboy.wrapper.js';\nimport { FileHandler, MultipartData } from './types.js';\nimport { MultipartLimits } from './types.js';\n\n/** Default maximum file size: 20 MB */\nconst MAX_FILE_SIZE = 20 * 1024 * 1024;\n\n/**\n * High-level API for parsing multipart/form-data request bodies.\n *\n * This class provides a simple interface for handling file uploads and form fields\n * from HTTP requests. It wraps the lower-level BusboyWrapper with sensible defaults\n * and allows per-request limit overrides.\n *\n * @example\n * ```typescript\n * import { IncomingMessage } from 'node:http';\n * import { MultipartBody } from '@maroonedsoftware/multipart';\n *\n * async function handleRequest(req: IncomingMessage) {\n * const multipart = new MultipartBody(req);\n *\n * const fields = await multipart.parse(async (fieldname, stream, filename) => {\n * // Save the file\n * await pipeline(stream, fs.createWriteStream(`./uploads/${filename}`));\n * });\n *\n * // Access form fields\n * const description = fields.get('description');\n * }\n * ```\n */\nexport class MultipartBody {\n /**\n * Creates a new MultipartBody instance.\n *\n * @param req - The incoming HTTP request containing multipart data\n * @param _limits - Default limits applied to all parse operations.\n * Defaults to 1 file maximum and 20MB file size limit.\n */\n constructor(\n private readonly req: IncomingMessage,\n private readonly _limits: MultipartLimits = {\n files: 1,\n fileSize: MAX_FILE_SIZE,\n },\n ) {}\n\n /**\n * Parses the multipart request body and processes any file uploads.\n *\n * @param fileHandler - A callback function invoked for each file in the request.\n * The callback receives the field name, file stream, filename,\n * encoding, and MIME type. It should return a promise that\n * resolves when the file has been fully processed.\n * @param limits - Optional per-request limits that override the instance defaults.\n * These are merged with the default limits (per-request takes precedence).\n * @returns A promise that resolves to a Map containing all parsed fields and files.\n * Field names are keys, and values are either a single MultipartData object\n * or an array if multiple values were submitted for the same field name.\n *\n * @throws {HttpError} 413 error if configured limits are exceeded\n *\n * @example\n * ```typescript\n * // Parse with custom file size limit for this request\n * const fields = await multipart.parse(\n * async (fieldname, stream, filename) => {\n * await saveFile(stream, filename);\n * },\n * { fileSize: 50 * 1024 * 1024 } // 50MB for this request\n * );\n * ```\n */\n parse(fileHandler: FileHandler, limits?: MultipartLimits): Promise<Map<string, MultipartData | MultipartData[]>> {\n const busboy = new BusboyWrapper(this.req, { ...this._limits, ...limits });\n\n return busboy.parse(fileHandler);\n }\n}\n"],"mappings":"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","names":["isMultipartFieldData","data","isMultipartFileData","Busboy","Writable","httpError","BusboyWrapper","Busboy","req","fields","Map","fileHandler","nullStream","Writable","write","_chunk","_encding","callback","setImmediate","limits","headers","on","cleanup","onField","onFile","onEnd","onPartsLimit","onFilesLimit","onFieldsLimit","parse","Promise","resolve","reject","pipe","setData","name","data","prev","get","set","Array","isArray","push","value","nameTruncated","valueTruncated","encoding","mimeType","fieldname","stream","filename","then","catch","reason","_","err","httpError","withInternalDetails","removeListener","MAX_FILE_SIZE","MultipartBody","req","_limits","files","fileSize","parse","fileHandler","limits","busboy","BusboyWrapper"]}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { IncomingMessage } from 'node:http';
|
|
2
|
+
import { FileHandler, MultipartData } from './types.js';
|
|
3
|
+
import { MultipartLimits } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* High-level API for parsing multipart/form-data request bodies.
|
|
6
|
+
*
|
|
7
|
+
* This class provides a simple interface for handling file uploads and form fields
|
|
8
|
+
* from HTTP requests. It wraps the lower-level BusboyWrapper with sensible defaults
|
|
9
|
+
* and allows per-request limit overrides.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* import { IncomingMessage } from 'node:http';
|
|
14
|
+
* import { MultipartBody } from '@maroonedsoftware/multipart';
|
|
15
|
+
*
|
|
16
|
+
* async function handleRequest(req: IncomingMessage) {
|
|
17
|
+
* const multipart = new MultipartBody(req);
|
|
18
|
+
*
|
|
19
|
+
* const fields = await multipart.parse(async (fieldname, stream, filename) => {
|
|
20
|
+
* // Save the file
|
|
21
|
+
* await pipeline(stream, fs.createWriteStream(`./uploads/${filename}`));
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* // Access form fields
|
|
25
|
+
* const description = fields.get('description');
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export declare class MultipartBody {
|
|
30
|
+
private readonly req;
|
|
31
|
+
private readonly _limits;
|
|
32
|
+
/**
|
|
33
|
+
* Creates a new MultipartBody instance.
|
|
34
|
+
*
|
|
35
|
+
* @param req - The incoming HTTP request containing multipart data
|
|
36
|
+
* @param _limits - Default limits applied to all parse operations.
|
|
37
|
+
* Defaults to 1 file maximum and 20MB file size limit.
|
|
38
|
+
*/
|
|
39
|
+
constructor(req: IncomingMessage, _limits?: MultipartLimits);
|
|
40
|
+
/**
|
|
41
|
+
* Parses the multipart request body and processes any file uploads.
|
|
42
|
+
*
|
|
43
|
+
* @param fileHandler - A callback function invoked for each file in the request.
|
|
44
|
+
* The callback receives the field name, file stream, filename,
|
|
45
|
+
* encoding, and MIME type. It should return a promise that
|
|
46
|
+
* resolves when the file has been fully processed.
|
|
47
|
+
* @param limits - Optional per-request limits that override the instance defaults.
|
|
48
|
+
* These are merged with the default limits (per-request takes precedence).
|
|
49
|
+
* @returns A promise that resolves to a Map containing all parsed fields and files.
|
|
50
|
+
* Field names are keys, and values are either a single MultipartData object
|
|
51
|
+
* or an array if multiple values were submitted for the same field name.
|
|
52
|
+
*
|
|
53
|
+
* @throws {HttpError} 413 error if configured limits are exceeded
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```typescript
|
|
57
|
+
* // Parse with custom file size limit for this request
|
|
58
|
+
* const fields = await multipart.parse(
|
|
59
|
+
* async (fieldname, stream, filename) => {
|
|
60
|
+
* await saveFile(stream, filename);
|
|
61
|
+
* },
|
|
62
|
+
* { fileSize: 50 * 1024 * 1024 } // 50MB for this request
|
|
63
|
+
* );
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
parse(fileHandler: FileHandler, limits?: MultipartLimits): Promise<Map<string, MultipartData | MultipartData[]>>;
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=multipart.body.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"multipart.body.d.ts","sourceRoot":"","sources":["../src/multipart.body.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAE5C,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AACxD,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAK7C;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,qBAAa,aAAa;IAStB,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAT1B;;;;;;OAMG;gBAEgB,GAAG,EAAE,eAAe,EACpB,OAAO,GAAE,eAGzB;IAGH;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACH,KAAK,CAAC,WAAW,EAAE,WAAW,EAAE,MAAM,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,aAAa,GAAG,aAAa,EAAE,CAAC,CAAC;CAKjH"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { IncomingMessage } from 'node:http';
|
|
2
|
+
import { FileHandler, MultipartData } from './types.js';
|
|
3
|
+
import { Readable, Writable } from 'node:stream';
|
|
4
|
+
/**
|
|
5
|
+
* Extended Readable stream interface for multipart/related file parts.
|
|
6
|
+
*
|
|
7
|
+
* Provides additional metadata about the stream state including whether
|
|
8
|
+
* the stream was truncated due to size limits and the total bytes read.
|
|
9
|
+
*/
|
|
10
|
+
export interface MultipartRelatedFileStream extends Readable {
|
|
11
|
+
/** Whether the file stream was truncated due to size limits */
|
|
12
|
+
truncated: boolean;
|
|
13
|
+
/** The number of bytes that have been read from the stream so far */
|
|
14
|
+
bytesRead: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Parser for multipart/related content type requests.
|
|
18
|
+
*
|
|
19
|
+
* The multipart/related content type is used when multiple related parts
|
|
20
|
+
* need to be processed together, such as email messages with inline attachments
|
|
21
|
+
* or SOAP messages with MIME attachments.
|
|
22
|
+
*
|
|
23
|
+
* This parser extends Writable to act as a stream destination for the incoming
|
|
24
|
+
* request body, parsing parts as they arrive and invoking handlers for files.
|
|
25
|
+
*
|
|
26
|
+
* @extends Writable
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* import { IncomingMessage } from 'node:http';
|
|
31
|
+
* import { MultipartRelatedParser } from './multipart.related.parser.js';
|
|
32
|
+
*
|
|
33
|
+
* async function handleRelatedContent(req: IncomingMessage) {
|
|
34
|
+
* const parser = new MultipartRelatedParser(req);
|
|
35
|
+
*
|
|
36
|
+
* const parts = await parser.parse(async (fieldname, stream, filename) => {
|
|
37
|
+
* // Process each related part
|
|
38
|
+
* const content = await streamToBuffer(stream);
|
|
39
|
+
* console.log(`Received ${filename}: ${content.length} bytes`);
|
|
40
|
+
* });
|
|
41
|
+
* }
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export declare class MultipartRelatedParser extends Writable {
|
|
45
|
+
private readonly req;
|
|
46
|
+
/** Map storing parsed fields and files keyed by field name */
|
|
47
|
+
private readonly fields;
|
|
48
|
+
/** Optional handler for processing file uploads */
|
|
49
|
+
private fileHandler?;
|
|
50
|
+
/** A null stream used to drain file streams when errors occur */
|
|
51
|
+
private readonly nullStream;
|
|
52
|
+
/**
|
|
53
|
+
* Creates a new MultipartRelatedParser instance.
|
|
54
|
+
*
|
|
55
|
+
* @param req - The incoming HTTP request containing multipart/related data
|
|
56
|
+
*/
|
|
57
|
+
constructor(req: IncomingMessage);
|
|
58
|
+
/**
|
|
59
|
+
* Parses the multipart/related request body.
|
|
60
|
+
*
|
|
61
|
+
* @param fileHandler - A callback function to handle file parts as they are received.
|
|
62
|
+
* Each file part triggers the handler with the field name, stream,
|
|
63
|
+
* filename, encoding, and MIME type.
|
|
64
|
+
* @returns A promise that resolves to a Map of field names to their parsed data.
|
|
65
|
+
* If multiple values exist for a field name, they are stored as an array.
|
|
66
|
+
*
|
|
67
|
+
* @throws {HttpError} 413 error if parts, files, or fields limits are exceeded
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```typescript
|
|
71
|
+
* const parts = await parser.parse(async (fieldname, stream, filename) => {
|
|
72
|
+
* const chunks: Buffer[] = [];
|
|
73
|
+
* for await (const chunk of stream) {
|
|
74
|
+
* chunks.push(chunk);
|
|
75
|
+
* }
|
|
76
|
+
* // Process the complete content
|
|
77
|
+
* processContent(Buffer.concat(chunks));
|
|
78
|
+
* });
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
parse(fileHandler: FileHandler): Promise<Map<string, MultipartData | MultipartData[]>>;
|
|
82
|
+
/**
|
|
83
|
+
* Stores parsed data in the fields map, handling multiple values for the same field.
|
|
84
|
+
*
|
|
85
|
+
* @param name - The field name
|
|
86
|
+
* @param data - The parsed field or file data
|
|
87
|
+
*/
|
|
88
|
+
private setData;
|
|
89
|
+
/**
|
|
90
|
+
* Handler for parsed form fields.
|
|
91
|
+
*/
|
|
92
|
+
private onField;
|
|
93
|
+
/**
|
|
94
|
+
* Handler for parsed file uploads.
|
|
95
|
+
*/
|
|
96
|
+
private onFile;
|
|
97
|
+
private resolve;
|
|
98
|
+
private reject;
|
|
99
|
+
/**
|
|
100
|
+
* Handler called when parsing completes or an error occurs.
|
|
101
|
+
*/
|
|
102
|
+
private onEnd;
|
|
103
|
+
/**
|
|
104
|
+
* Handler for when the parts limit is exceeded.
|
|
105
|
+
*/
|
|
106
|
+
private onPartsLimit;
|
|
107
|
+
/**
|
|
108
|
+
* Handler for when the files limit is exceeded.
|
|
109
|
+
*/
|
|
110
|
+
private onFilesLimit;
|
|
111
|
+
/**
|
|
112
|
+
* Handler for when the fields limit is exceeded.
|
|
113
|
+
*/
|
|
114
|
+
private onFieldsLimit;
|
|
115
|
+
/**
|
|
116
|
+
* Cleans up event listeners to prevent memory leaks.
|
|
117
|
+
*/
|
|
118
|
+
private cleanup;
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=multipart.related.parser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"multipart.related.parser.d.ts","sourceRoot":"","sources":["../src/multipart.related.parser.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AACxD,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAGjD;;;;;GAKG;AACH,MAAM,WAAW,0BAA2B,SAAQ,QAAQ;IAC1D,+DAA+D;IAC/D,SAAS,EAAE,OAAO,CAAC;IACnB,qEAAqE;IACrE,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,qBAAa,sBAAuB,SAAQ,QAAQ;IAmBtC,OAAO,CAAC,QAAQ,CAAC,GAAG;IAlBhC,8DAA8D;IAC9D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAsD;IAE7E,mDAAmD;IACnD,OAAO,CAAC,WAAW,CAAC,CAAc;IAElC,iEAAiE;IACjE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAIxB;IAEH;;;;OAIG;gBAC0B,GAAG,EAAE,eAAe;IAIjD;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,KAAK,CAAC,WAAW,EAAE,WAAW;IAS9B;;;;;OAKG;IACH,OAAO,CAAC,OAAO;IAWf;;OAEG;IACH,OAAO,CAAC,OAAO;IAUf;;OAEG;IACH,OAAO,CAAC,MAAM;IAcd,OAAO,CAAC,OAAO;IACf,OAAO,CAAC,MAAM;IAEd;;OAEG;IACH,OAAO,CAAC,KAAK;IASb;;OAEG;IACH,OAAO,CAAC,YAAY;IAOpB;;OAEG;IACH,OAAO,CAAC,YAAY;IAOpB;;OAEG;IACH,OAAO,CAAC,aAAa;IAOrB;;OAEG;IACH,OAAO,CAAC,OAAO;CAUhB"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { Readable } from 'node:stream';
|
|
2
|
+
/**
|
|
3
|
+
* Callback function for handling file uploads during multipart parsing.
|
|
4
|
+
*
|
|
5
|
+
* @param fieldname - The name of the form field
|
|
6
|
+
* @param stream - A readable stream containing the file data
|
|
7
|
+
* @param filename - The original filename from the upload
|
|
8
|
+
* @param encoding - The encoding of the file (e.g., '7bit', 'binary')
|
|
9
|
+
* @param mimeType - The MIME type of the file (e.g., 'image/png')
|
|
10
|
+
* @returns A promise that resolves when the file has been fully processed
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* const handler: FileHandler = async (fieldname, stream, filename, encoding, mimeType) => {
|
|
15
|
+
* const writeStream = fs.createWriteStream(`./uploads/${filename}`);
|
|
16
|
+
* await pipeline(stream, writeStream);
|
|
17
|
+
* };
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export type FileHandler = (fieldname: string, stream: Readable, filename: string, encoding: string, mimeType: string) => Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* Represents parsed form field data from a multipart request.
|
|
23
|
+
*/
|
|
24
|
+
export type FieldData = {
|
|
25
|
+
/** The string value of the field */
|
|
26
|
+
value: string;
|
|
27
|
+
/** Whether the field name was truncated due to limits */
|
|
28
|
+
nameTruncated: boolean;
|
|
29
|
+
/** Whether the field value was truncated due to limits */
|
|
30
|
+
valueTruncated: boolean;
|
|
31
|
+
/** The encoding of the field value */
|
|
32
|
+
encoding: string;
|
|
33
|
+
/** The MIME type of the field */
|
|
34
|
+
mimeType: string;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Represents parsed file data from a multipart request.
|
|
38
|
+
*/
|
|
39
|
+
export type FileData = {
|
|
40
|
+
/** A readable stream containing the file data */
|
|
41
|
+
stream: Readable;
|
|
42
|
+
/** The original filename from the upload */
|
|
43
|
+
filename: string;
|
|
44
|
+
/** The encoding of the file */
|
|
45
|
+
encoding: string;
|
|
46
|
+
/** The MIME type of the file */
|
|
47
|
+
mimeType: string;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Union type representing either field data or file data from a multipart request.
|
|
51
|
+
*/
|
|
52
|
+
export type MultipartData = FieldData | FileData;
|
|
53
|
+
/**
|
|
54
|
+
* Type guard to check if the multipart data is field data.
|
|
55
|
+
*
|
|
56
|
+
* @param data - The multipart data to check
|
|
57
|
+
* @returns True if the data is field data, false otherwise
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```typescript
|
|
61
|
+
* if (isMultipartFieldData(data)) {
|
|
62
|
+
* console.log(data.value); // TypeScript knows this is FieldData
|
|
63
|
+
* }
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export declare const isMultipartFieldData: (data: MultipartData) => data is FieldData;
|
|
67
|
+
/**
|
|
68
|
+
* Type guard to check if the multipart data is file data.
|
|
69
|
+
*
|
|
70
|
+
* @param data - The multipart data to check
|
|
71
|
+
* @returns True if the data is file data, false otherwise
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```typescript
|
|
75
|
+
* if (isMultipartFileData(data)) {
|
|
76
|
+
* data.stream.pipe(destination); // TypeScript knows this is FileData
|
|
77
|
+
* }
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
export declare const isMultipartFileData: (data: MultipartData) => data is FileData;
|
|
81
|
+
/**
|
|
82
|
+
* Configuration options for limiting multipart request sizes.
|
|
83
|
+
* Used to prevent resource exhaustion from malicious or oversized uploads.
|
|
84
|
+
*
|
|
85
|
+
* @see https://github.com/fastify/busboy/blob/main/lib/main.d.ts#L104
|
|
86
|
+
*/
|
|
87
|
+
export interface MultipartLimits {
|
|
88
|
+
/**
|
|
89
|
+
* Maximum field name size in bytes.
|
|
90
|
+
* @default 100
|
|
91
|
+
*/
|
|
92
|
+
fieldNameSize?: number | undefined;
|
|
93
|
+
/**
|
|
94
|
+
* Maximum field value size in bytes.
|
|
95
|
+
* @default 1048576 (1MB)
|
|
96
|
+
*/
|
|
97
|
+
fieldSize?: number | undefined;
|
|
98
|
+
/**
|
|
99
|
+
* Maximum number of non-file fields.
|
|
100
|
+
* @default Infinity
|
|
101
|
+
*/
|
|
102
|
+
fields?: number | undefined;
|
|
103
|
+
/**
|
|
104
|
+
* Maximum file size in bytes for multipart forms.
|
|
105
|
+
* @default Infinity
|
|
106
|
+
*/
|
|
107
|
+
fileSize?: number | undefined;
|
|
108
|
+
/**
|
|
109
|
+
* Maximum number of file fields for multipart forms.
|
|
110
|
+
* @default Infinity
|
|
111
|
+
*/
|
|
112
|
+
files?: number | undefined;
|
|
113
|
+
/**
|
|
114
|
+
* Maximum number of parts (fields + files) for multipart forms.
|
|
115
|
+
* @default Infinity
|
|
116
|
+
*/
|
|
117
|
+
parts?: number | undefined;
|
|
118
|
+
/**
|
|
119
|
+
* Maximum number of header key-value pairs to parse for multipart forms.
|
|
120
|
+
* @default 2000
|
|
121
|
+
*/
|
|
122
|
+
headerPairs?: number | undefined;
|
|
123
|
+
/**
|
|
124
|
+
* Maximum size of a header part in bytes for multipart forms.
|
|
125
|
+
* @default 81920
|
|
126
|
+
*/
|
|
127
|
+
headerSize?: number | undefined;
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvC;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAEvI;;GAEG;AACH,MAAM,MAAM,SAAS,GAAG;IACtB,oCAAoC;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,yDAAyD;IACzD,aAAa,EAAE,OAAO,CAAC;IACvB,0DAA0D;IAC1D,cAAc,EAAE,OAAO,CAAC;IACxB,sCAAsC;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,iCAAiC;IACjC,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG;IACrB,iDAAiD;IACjD,MAAM,EAAE,QAAQ,CAAC;IACjB,4CAA4C;IAC5C,QAAQ,EAAE,MAAM,CAAC;IACjB,+BAA+B;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,gCAAgC;IAChC,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,QAAQ,CAAC;AAEjD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,oBAAoB,GAAI,MAAM,aAAa,KAAG,IAAI,IAAI,SAElE,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,mBAAmB,GAAI,MAAM,aAAa,KAAG,IAAI,IAAI,QAEjE,CAAC;AAEF;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACjC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@maroonedsoftware/multipart",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A robust multipart form-data and multipart/related parser for Node.js HTTP servers.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Marooned Software",
|
|
7
|
+
"url": "https://github.com/MaroonedSoftware/serverkit"
|
|
8
|
+
},
|
|
9
|
+
"bugs": {
|
|
10
|
+
"url": "https://github.com/MaroonedSoftware/serverkit/issues"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/MaroonedSoftware/serverkit/packages/multipart#readme",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"backend",
|
|
15
|
+
"busboy",
|
|
16
|
+
"multipart",
|
|
17
|
+
"multipart/form-data",
|
|
18
|
+
"multipart/related",
|
|
19
|
+
"serverkit",
|
|
20
|
+
"typescript"
|
|
21
|
+
],
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/MaroonedSoftware/serverkit.git"
|
|
25
|
+
},
|
|
26
|
+
"private": false,
|
|
27
|
+
"type": "module",
|
|
28
|
+
"main": "./dist/index.js",
|
|
29
|
+
"module": "./dist/index.js",
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"files": [
|
|
33
|
+
"dist/**"
|
|
34
|
+
],
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@fastify/busboy": "^3.2.0",
|
|
37
|
+
"@maroonedsoftware/errors": "1.0.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@repo/config-typescript": "0.0.0",
|
|
41
|
+
"@repo/config-eslint": "0.0.0"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsup src/index.ts --format esm --sourcemap --dts && tsc --emitDeclarationOnly --declaration",
|
|
45
|
+
"build:ci": "eslint --max-warnings=0 && pnpm run build",
|
|
46
|
+
"lint": "eslint --fix",
|
|
47
|
+
"format": "prettier --write .",
|
|
48
|
+
"test": "vitest run",
|
|
49
|
+
"test:ci": "vitest run --coverage"
|
|
50
|
+
}
|
|
51
|
+
}
|