@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 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"}
@@ -0,0 +1,3 @@
1
+ export * from './types.js';
2
+ export * from './multipart.body.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -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"}
@@ -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
+ }