@ubercode/multipart-stream 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +261 -0
- package/dist/index.cjs +930 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +785 -0
- package/dist/index.d.ts +785 -0
- package/dist/index.js +913 -0
- package/dist/index.js.map +1 -0
- package/package.json +94 -0
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,785 @@
|
|
|
1
|
+
import { Readable } from 'node:stream';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Public error classes (FR-019, NFR-DR-S-001, NFR-DR-S-004, NFR-DR-S-012,
|
|
5
|
+
* NFR-DR-D-007).
|
|
6
|
+
*
|
|
7
|
+
* Every class:
|
|
8
|
+
* - extends `Error`
|
|
9
|
+
* - sets `this.name` literally to its class-name string in the constructor
|
|
10
|
+
* (NFR-DR-D-007 — survives minification AND is the documented fallback
|
|
11
|
+
* when `instanceof` returns false across the ESM/CJS module-format
|
|
12
|
+
* boundary)
|
|
13
|
+
* - supports `cause` plumbing via `super(message, options)`
|
|
14
|
+
* - exposes typed structured property fields for caller-side branching
|
|
15
|
+
*
|
|
16
|
+
* The classes are runtime values (NFR-012); consumers may import them as
|
|
17
|
+
* values for `instanceof` checks AND as types in signatures.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Thrown when no source-stream bytes arrive for `idleTimeoutMs` consecutive
|
|
21
|
+
* milliseconds. The source has been destroyed and all listeners removed by
|
|
22
|
+
* the time this surfaces.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* try {
|
|
26
|
+
* await fetchAndHandleMultipart(url, { idleTimeoutMs: 5000, totalTimeoutMs: 60_000, parser });
|
|
27
|
+
* } catch (err) {
|
|
28
|
+
* if (err instanceof MultipartIdleTimeoutError) {
|
|
29
|
+
* metrics.increment('multipart.idle_timeout', { ms: err.idleTimeoutMs });
|
|
30
|
+
* }
|
|
31
|
+
* throw err;
|
|
32
|
+
* }
|
|
33
|
+
*/
|
|
34
|
+
declare class MultipartIdleTimeoutError extends Error {
|
|
35
|
+
/** Stable cross-format discriminator (NFR-DR-D-007). */
|
|
36
|
+
readonly name = "MultipartIdleTimeoutError";
|
|
37
|
+
/** The configured idle window (ms) that elapsed without source activity. */
|
|
38
|
+
readonly idleTimeoutMs: number;
|
|
39
|
+
/**
|
|
40
|
+
* @param idleTimeoutMs - The configured idle window in ms.
|
|
41
|
+
* @param options - Optional `{ cause }` for wrapping a lower-level error.
|
|
42
|
+
*/
|
|
43
|
+
constructor(idleTimeoutMs: number, options?: ErrorOptions);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Thrown when the total wallclock budget (`totalTimeoutMs`) elapses,
|
|
47
|
+
* regardless of source activity.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* if (err instanceof MultipartTotalTimeoutError) {
|
|
51
|
+
* metrics.increment('multipart.total_timeout');
|
|
52
|
+
* }
|
|
53
|
+
*/
|
|
54
|
+
declare class MultipartTotalTimeoutError extends Error {
|
|
55
|
+
readonly name = "MultipartTotalTimeoutError";
|
|
56
|
+
/** The configured total window (ms) that elapsed. */
|
|
57
|
+
readonly totalTimeoutMs: number;
|
|
58
|
+
/**
|
|
59
|
+
* @param totalTimeoutMs - The configured total budget in ms.
|
|
60
|
+
* @param options - Optional `{ cause }`.
|
|
61
|
+
*/
|
|
62
|
+
constructor(totalTimeoutMs: number, options?: ErrorOptions);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Thrown when the caller-provided `AbortSignal` fires (FR-009), or is already
|
|
66
|
+
* aborted at call time. `reason` carries the signal's `reason` verbatim, or
|
|
67
|
+
* is `undefined` if the signal had no reason. The library NEVER synthesizes
|
|
68
|
+
* a server-derived reason (F-S-006).
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* const ctrl = new AbortController();
|
|
72
|
+
* setTimeout(() => ctrl.abort(new Error('user cancelled')), 5000);
|
|
73
|
+
* try {
|
|
74
|
+
* await fetchAndHandleMultipart(url, { signal: ctrl.signal, idleTimeoutMs: 5000, totalTimeoutMs: 30000, parser });
|
|
75
|
+
* } catch (err) {
|
|
76
|
+
* if (err instanceof MultipartAbortError) console.warn('aborted because:', err.reason);
|
|
77
|
+
* }
|
|
78
|
+
*/
|
|
79
|
+
declare class MultipartAbortError extends Error {
|
|
80
|
+
readonly name = "MultipartAbortError";
|
|
81
|
+
/**
|
|
82
|
+
* The signal's `reason` if the caller supplied one, else `undefined`. Per
|
|
83
|
+
* F-S-006 the library never synthesizes a reason that embeds server bytes.
|
|
84
|
+
*/
|
|
85
|
+
readonly reason?: unknown;
|
|
86
|
+
/**
|
|
87
|
+
* @param reason - Optional caller-supplied abort reason.
|
|
88
|
+
* @param options - Optional `{ cause }`.
|
|
89
|
+
*/
|
|
90
|
+
constructor(reason?: unknown, options?: ErrorOptions);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Thrown when the source stream ends without dicer observing the closing
|
|
94
|
+
* multipart boundary (FR-022) — typically a mid-flight server hangup or
|
|
95
|
+
* transport-layer cut. Cleanup per FR-010 still runs.
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* if (err instanceof MultipartTruncatedError) {
|
|
99
|
+
* retryQueue.enqueue({ url, bytesReceived: err.bytesReceived });
|
|
100
|
+
* }
|
|
101
|
+
*/
|
|
102
|
+
declare class MultipartTruncatedError extends Error {
|
|
103
|
+
readonly name = "MultipartTruncatedError";
|
|
104
|
+
/** Total bytes pulled from the source before it ended prematurely. */
|
|
105
|
+
readonly bytesReceived: number;
|
|
106
|
+
/**
|
|
107
|
+
* @param bytesReceived - Cumulative bytes received before the source ended.
|
|
108
|
+
* @param options - Optional `{ cause }`.
|
|
109
|
+
*/
|
|
110
|
+
constructor(bytesReceived: number, options?: ErrorOptions);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Info-bag for {@link MultipartPartTooLargeError}.
|
|
114
|
+
*/
|
|
115
|
+
interface MultipartPartTooLargeInfo {
|
|
116
|
+
/** The configured `maxPartBytes` cap that was exceeded. */
|
|
117
|
+
readonly maxPartBytes: number;
|
|
118
|
+
/** Zero-based ordinal of the offending part. */
|
|
119
|
+
readonly partIndex: number;
|
|
120
|
+
/** Bytes observed at the moment the cap tripped. */
|
|
121
|
+
readonly bytesReceived: number;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Thrown when a part body's accumulated bytes exceed `maxPartBytes`
|
|
125
|
+
* (NFR-DR-S-001). The offending part body is destroyed and full FR-010
|
|
126
|
+
* cleanup runs before this surfaces.
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* if (err instanceof MultipartPartTooLargeError) {
|
|
130
|
+
* metrics.increment('multipart.part_too_large', { partIndex: err.partIndex });
|
|
131
|
+
* }
|
|
132
|
+
*/
|
|
133
|
+
declare class MultipartPartTooLargeError extends Error {
|
|
134
|
+
readonly name = "MultipartPartTooLargeError";
|
|
135
|
+
readonly maxPartBytes: number;
|
|
136
|
+
readonly partIndex: number;
|
|
137
|
+
readonly bytesReceived: number;
|
|
138
|
+
/**
|
|
139
|
+
* @param info - Structured trip info: `{ maxPartBytes, partIndex, bytesReceived }`.
|
|
140
|
+
* @param options - Optional `{ cause }`.
|
|
141
|
+
*/
|
|
142
|
+
constructor(info: MultipartPartTooLargeInfo, options?: ErrorOptions);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Info-bag for {@link MultipartHeadersTooLargeError}.
|
|
146
|
+
*/
|
|
147
|
+
interface MultipartHeadersTooLargeInfo {
|
|
148
|
+
/** Discriminator: which limit was hit. */
|
|
149
|
+
readonly limit: 'count' | 'bytes';
|
|
150
|
+
/** Zero-based ordinal of the offending part. */
|
|
151
|
+
readonly partIndex: number;
|
|
152
|
+
/** The configured cap. */
|
|
153
|
+
readonly cap: number;
|
|
154
|
+
/** Observed value at trip-time. */
|
|
155
|
+
readonly observed: number;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Thrown when a part has more headers than `maxHeadersPerPart` (default 100)
|
|
159
|
+
* OR its header block bytes exceed `maxHeaderBytesPerPart` (default 16 KiB)
|
|
160
|
+
* — NFR-DR-S-004.
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* if (err instanceof MultipartHeadersTooLargeError) {
|
|
164
|
+
* log.warn({ limit: err.limit, observed: err.observed }, 'oversized part headers');
|
|
165
|
+
* }
|
|
166
|
+
*/
|
|
167
|
+
declare class MultipartHeadersTooLargeError extends Error {
|
|
168
|
+
readonly name = "MultipartHeadersTooLargeError";
|
|
169
|
+
readonly limit: 'count' | 'bytes';
|
|
170
|
+
readonly partIndex: number;
|
|
171
|
+
readonly cap: number;
|
|
172
|
+
readonly observed: number;
|
|
173
|
+
/**
|
|
174
|
+
* @param info - Structured trip info: `{ limit, partIndex, cap, observed }`.
|
|
175
|
+
* @param options - Optional `{ cause }`.
|
|
176
|
+
*/
|
|
177
|
+
constructor(info: MultipartHeadersTooLargeInfo, options?: ErrorOptions);
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Info-bag for {@link MultipartTooManyPartsError}.
|
|
181
|
+
*/
|
|
182
|
+
interface MultipartTooManyPartsInfo {
|
|
183
|
+
/** The configured `maxParts` cap that was exceeded. */
|
|
184
|
+
readonly maxParts: number;
|
|
185
|
+
/** Observed part count when the cap tripped (== `maxParts + 1`). */
|
|
186
|
+
readonly observed: number;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Thrown when the multipart envelope contains more parts than `maxParts`
|
|
190
|
+
* (default `10_000`) — NFR-DR-S-012.
|
|
191
|
+
*
|
|
192
|
+
* @example
|
|
193
|
+
* if (err instanceof MultipartTooManyPartsError) {
|
|
194
|
+
* log.warn({ cap: err.maxParts }, 'too many parts');
|
|
195
|
+
* }
|
|
196
|
+
*/
|
|
197
|
+
declare class MultipartTooManyPartsError extends Error {
|
|
198
|
+
readonly name = "MultipartTooManyPartsError";
|
|
199
|
+
readonly maxParts: number;
|
|
200
|
+
readonly observed: number;
|
|
201
|
+
/**
|
|
202
|
+
* @param info - Structured trip info: `{ maxParts, observed }`.
|
|
203
|
+
* @param options - Optional `{ cause }`.
|
|
204
|
+
*/
|
|
205
|
+
constructor(info: MultipartTooManyPartsInfo, options?: ErrorOptions);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* `extractBoundary` — RFC 2046 boundary extractor (FR-016).
|
|
210
|
+
*
|
|
211
|
+
* Public utility. Pure function — no I/O, no allocations beyond the result
|
|
212
|
+
* and a single intermediate buffer for unescaping a quoted string.
|
|
213
|
+
*
|
|
214
|
+
* Implementation note (NFR-DR-S-011, T-080): this is a hand-written,
|
|
215
|
+
* non-backtracking tokenizer. A naive regex with an alternation between the
|
|
216
|
+
* quoted-string and bare-token forms is ReDoS-prone on adversarial input
|
|
217
|
+
* (megabytes of `\\\"` sequences); the deliberate single-pass tokenizer
|
|
218
|
+
* below runs in O(n) time on any input.
|
|
219
|
+
*/
|
|
220
|
+
/**
|
|
221
|
+
* Parse the `boundary=` parameter out of an HTTP `Content-Type` header value.
|
|
222
|
+
*
|
|
223
|
+
* Handles RFC 2046 quoted-string and bare-token forms. The first `boundary=`
|
|
224
|
+
* occurrence wins (parameters are scanned left-to-right). Boundary parameter
|
|
225
|
+
* names are matched case-insensitively. The returned token is unquoted and
|
|
226
|
+
* has its backslash escapes resolved.
|
|
227
|
+
*
|
|
228
|
+
* @param contentTypeHeader - The full `Content-Type` header value, e.g.
|
|
229
|
+
* `multipart/related; boundary="weird;boundary"`. May be `null` or
|
|
230
|
+
* `undefined`; both are treated as missing-input errors.
|
|
231
|
+
* @returns The unquoted boundary token.
|
|
232
|
+
* @throws {Error} `multipart: Content-Type header is required to extract
|
|
233
|
+
* boundary` when the input is null, undefined, or empty.
|
|
234
|
+
* @throws {Error} `multipart: boundary parameter missing from Content-Type:
|
|
235
|
+
* <header>` when the header has no `boundary=` parameter.
|
|
236
|
+
* @throws {Error} `multipart: boundary parameter is empty in Content-Type:
|
|
237
|
+
* <header>` when the parameter is present but the value is empty.
|
|
238
|
+
*
|
|
239
|
+
* In every error path that embeds the input header, the embedded value is
|
|
240
|
+
* sanitized via the full sanitizer (truncate to 120 chars, control-char
|
|
241
|
+
* redact, ANSI redact, JSON.stringify per NFR-DR-S-006).
|
|
242
|
+
*
|
|
243
|
+
* @example
|
|
244
|
+
* extractBoundary('multipart/related; boundary=foo'); // 'foo'
|
|
245
|
+
*
|
|
246
|
+
* @example
|
|
247
|
+
* // Quoted-string form with embedded special chars (RFC 2046):
|
|
248
|
+
* extractBoundary('multipart/related; boundary="weird;boundary"');
|
|
249
|
+
* // 'weird;boundary'
|
|
250
|
+
*
|
|
251
|
+
* @example
|
|
252
|
+
* // Multiple parameters in any order; case-insensitive parameter name:
|
|
253
|
+
* extractBoundary('multipart/related; type="application/dicom"; BOUNDARY=BAR; charset=utf-8');
|
|
254
|
+
* // 'BAR'
|
|
255
|
+
*/
|
|
256
|
+
declare function extractBoundary(contentTypeHeader: string | null | undefined): string;
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Public type aliases re-exported from `src/index.ts`.
|
|
260
|
+
*
|
|
261
|
+
* Per NFR-DR-A-013, every optional input field on the option-bag interfaces
|
|
262
|
+
* (`ParseMultipartOptions`, `MultipartHandlerOptions<T>`) uses the explicit
|
|
263
|
+
* `field?: T | undefined` form (NOT bare `field?: T`). This lets callers
|
|
264
|
+
* spread-merge dynamically-built option records under
|
|
265
|
+
* `exactOptionalPropertyTypes: true` without TypeScript complaining about an
|
|
266
|
+
* `undefined` slot the schema does not accept.
|
|
267
|
+
*
|
|
268
|
+
* Read-only output fields on `StreamingMultipartPart` and the result struct
|
|
269
|
+
* use the `field?: T | undefined` form too for symmetry.
|
|
270
|
+
*
|
|
271
|
+
* Internal-only types (`src/internal/`) are NOT re-exported and may use the
|
|
272
|
+
* simpler `field?: T` form since callers never construct them.
|
|
273
|
+
*/
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* One sub-part yielded by `parseMultipartRelated`. The body is a *streaming*
|
|
277
|
+
* Node `Readable` — callers MUST drain or destroy it; if they neither drain
|
|
278
|
+
* nor destroy, the library destroys it for them in the iterator's `finally`
|
|
279
|
+
* (FR-010).
|
|
280
|
+
*
|
|
281
|
+
* Headers are normalized to lowercase keys (consistent with Node `http`) and
|
|
282
|
+
* collapsed to single strings; convenience getters (`contentType`,
|
|
283
|
+
* `contentId`, `contentLength`) pre-compute the common ones.
|
|
284
|
+
*/
|
|
285
|
+
interface StreamingMultipartPart {
|
|
286
|
+
/**
|
|
287
|
+
* Zero-based ordinal of this part within the multipart envelope, in the
|
|
288
|
+
* order dicer emits them.
|
|
289
|
+
*/
|
|
290
|
+
readonly index: number;
|
|
291
|
+
/**
|
|
292
|
+
* The multipart boundary that delimits this envelope. Echoed onto every
|
|
293
|
+
* part for caller-side logging only — parsing has already consumed it.
|
|
294
|
+
*/
|
|
295
|
+
readonly boundary: string;
|
|
296
|
+
/**
|
|
297
|
+
* Lowercased part headers as flat strings. Names are normalized to
|
|
298
|
+
* lowercase; values are the result of internal flattening over dicer's
|
|
299
|
+
* `Buffer | Buffer[] | Buffer[][]` shapes.
|
|
300
|
+
*
|
|
301
|
+
* Reads are `string | undefined` due to `noUncheckedIndexedAccess`.
|
|
302
|
+
*/
|
|
303
|
+
readonly headers: Readonly<Record<string, string | undefined>>;
|
|
304
|
+
/**
|
|
305
|
+
* Raw header block bytes captured from dicer (concatenated).
|
|
306
|
+
*
|
|
307
|
+
* Surfaced for callers who need byte-exact framing for re-emission or
|
|
308
|
+
* signature verification.
|
|
309
|
+
*/
|
|
310
|
+
readonly rawHeaders: Buffer;
|
|
311
|
+
/**
|
|
312
|
+
* Pre-extracted `content-type` header value, or `''` if absent.
|
|
313
|
+
*/
|
|
314
|
+
readonly contentType: string;
|
|
315
|
+
/**
|
|
316
|
+
* Pre-extracted `content-id` header value (raw, with angle brackets if the
|
|
317
|
+
* sender included them), or `undefined` if absent.
|
|
318
|
+
*/
|
|
319
|
+
readonly contentId?: string | undefined;
|
|
320
|
+
/**
|
|
321
|
+
* Pre-extracted `content-length` parsed via `parseInt(_, 10)`, or
|
|
322
|
+
* `undefined` if the header is absent or non-numeric. Note: dicer streams
|
|
323
|
+
* regardless — this value is informational, not a contract.
|
|
324
|
+
*/
|
|
325
|
+
readonly contentLength?: number | undefined;
|
|
326
|
+
/**
|
|
327
|
+
* The streaming body of this part as a Node `Readable`. Backed directly by
|
|
328
|
+
* dicer's per-part stream — no intermediate buffering. Callers MUST consume
|
|
329
|
+
* (drain, pipe, or destroy) before requesting the next part; the library
|
|
330
|
+
* cannot pause dicer's state machine for them.
|
|
331
|
+
*/
|
|
332
|
+
readonly body: Readable;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Caller-supplied per-part decision function. Receives a part, returns
|
|
336
|
+
* either a value of type `T` (collected into `MultipartFetchResult.parts`)
|
|
337
|
+
* or `undefined` ("skip this part, contribute nothing").
|
|
338
|
+
*
|
|
339
|
+
* The parser is responsible for either:
|
|
340
|
+
* - draining `part.body` (e.g. via `streamToString` / `streamToBuffer` /
|
|
341
|
+
* a pipe), OR
|
|
342
|
+
* - returning `undefined` without touching `part.body` (the library will
|
|
343
|
+
* drain it).
|
|
344
|
+
*
|
|
345
|
+
* If the parser throws (or its returned promise rejects), the operation
|
|
346
|
+
* rejects with that error and the source stream is destroyed (FR-014).
|
|
347
|
+
*/
|
|
348
|
+
type PartParser<T> = (part: StreamingMultipartPart) => Promise<T | undefined>;
|
|
349
|
+
/**
|
|
350
|
+
* The resolved value of `fetchAndHandleMultipart` (FR-DR-A-029 — JC-1 shape:
|
|
351
|
+
* `{ parts, bytes, elapsedMs, status, headers }`; the previously-considered
|
|
352
|
+
* `response: Response` field is REMOVED because the body is consumed by the
|
|
353
|
+
* time the result resolves).
|
|
354
|
+
*
|
|
355
|
+
* Successful operations resolve with `parts` populated. Per-part parser
|
|
356
|
+
* failures reject the whole call (FR-014) — they are not bundled into this
|
|
357
|
+
* struct.
|
|
358
|
+
*/
|
|
359
|
+
interface MultipartFetchResult<T> {
|
|
360
|
+
/**
|
|
361
|
+
* Array of values returned by the caller's `PartParser<T>`, in part order,
|
|
362
|
+
* filtered to drop `undefined`s.
|
|
363
|
+
*/
|
|
364
|
+
readonly parts: readonly T[];
|
|
365
|
+
/**
|
|
366
|
+
* Total bytes pulled from the source stream (raw multipart envelope size,
|
|
367
|
+
* not sum of part body sizes).
|
|
368
|
+
*/
|
|
369
|
+
readonly bytes: number;
|
|
370
|
+
/** Wall-clock duration from `fetchAndHandleMultipart` entry to resolution. */
|
|
371
|
+
readonly elapsedMs: number;
|
|
372
|
+
/**
|
|
373
|
+
* HTTP status code from the underlying `fetch` response, captured before
|
|
374
|
+
* the body was consumed.
|
|
375
|
+
*/
|
|
376
|
+
readonly status: number;
|
|
377
|
+
/**
|
|
378
|
+
* The `Headers` object from the underlying `fetch` response, captured
|
|
379
|
+
* before the body was consumed.
|
|
380
|
+
*/
|
|
381
|
+
readonly headers: Headers;
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Snapshot passed to `onProgress`. All fields are computed at the moment of
|
|
385
|
+
* the call.
|
|
386
|
+
*/
|
|
387
|
+
interface ProgressSnapshot {
|
|
388
|
+
/** Cumulative bytes received from the source stream so far. */
|
|
389
|
+
readonly bytes: number;
|
|
390
|
+
/** Wall-clock ms since the operation started. */
|
|
391
|
+
readonly elapsedMs: number;
|
|
392
|
+
/** Bytes per second over `[start, now]`; `0` when `elapsedMs === 0`. */
|
|
393
|
+
readonly rateBps: number;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Pluggable structured-logging shim (FR-018, JC-2). Event-style: the library
|
|
397
|
+
* calls the function with a single object describing the event.
|
|
398
|
+
*
|
|
399
|
+
* The library currently only emits events with `level: 'warn'`. Adding more
|
|
400
|
+
* levels in a future minor version is non-breaking because `level` is a
|
|
401
|
+
* union, not a positional argument.
|
|
402
|
+
*
|
|
403
|
+
* Per NFR-DR-S-008, `meta` NEVER contains raw chunk bytes. When the library
|
|
404
|
+
* logs an `Error` whose source is dicer or the source stream, it passes only
|
|
405
|
+
* an `errSummary: { name, message }` object with `message` truncated to <=
|
|
406
|
+
* 120 chars, control characters redacted, and the value JSON-stringified
|
|
407
|
+
* per NFR-DR-S-006.
|
|
408
|
+
*
|
|
409
|
+
* @example
|
|
410
|
+
* // Pino adapter:
|
|
411
|
+
* const logger: Logger = (event) => log[event.level](event.meta, event.msg);
|
|
412
|
+
*
|
|
413
|
+
* @example
|
|
414
|
+
* // Default (when omitted): falls back to `console.warn(msg, meta)`.
|
|
415
|
+
*/
|
|
416
|
+
type Logger = (event: {
|
|
417
|
+
level: 'warn';
|
|
418
|
+
msg: string;
|
|
419
|
+
meta?: unknown;
|
|
420
|
+
}) => void;
|
|
421
|
+
/**
|
|
422
|
+
* Options accepted by `parseMultipartRelated`. `idleTimeoutMs` and
|
|
423
|
+
* `totalTimeoutMs` are REQUIRED on this entry point too (FR-006 / JC-3 —
|
|
424
|
+
* required-on-both kills the slow-loris vector when the function is used
|
|
425
|
+
* server-side on `req.body`).
|
|
426
|
+
*/
|
|
427
|
+
interface ParseMultipartOptions {
|
|
428
|
+
/**
|
|
429
|
+
* REQUIRED. Idle timeout (ms). Resets on every chunk received from the
|
|
430
|
+
* source `Readable` (per-chunk `'data'` listener; FR-DR-A-025).
|
|
431
|
+
* Validated as a positive finite integer in the inclusive range
|
|
432
|
+
* `[1, 2_147_483_647]` (NFR-DR-S-009 — Node clamps `setTimeout` delays
|
|
433
|
+
* above `2^31 - 1`).
|
|
434
|
+
*/
|
|
435
|
+
idleTimeoutMs: number;
|
|
436
|
+
/**
|
|
437
|
+
* REQUIRED. Total timeout (ms), measured from the call. Same validation
|
|
438
|
+
* rules as `idleTimeoutMs`.
|
|
439
|
+
*/
|
|
440
|
+
totalTimeoutMs: number;
|
|
441
|
+
/**
|
|
442
|
+
* Explicit boundary for raw `Readable` inputs. REQUIRED when `input` is a
|
|
443
|
+
* Node `Readable` (no `Content-Type` to parse); IGNORED when `input` is a
|
|
444
|
+
* `Response` (boundary is extracted from `Content-Type`).
|
|
445
|
+
*/
|
|
446
|
+
boundary?: string | undefined;
|
|
447
|
+
/**
|
|
448
|
+
* Caller's `AbortSignal`. Already-aborted at call time → synchronous
|
|
449
|
+
* `MultipartAbortError`. Aborted mid-stream → next yield rejects.
|
|
450
|
+
*/
|
|
451
|
+
signal?: AbortSignal | undefined;
|
|
452
|
+
/**
|
|
453
|
+
* Progress callback (FR-013). Fires at least once per yielded part and
|
|
454
|
+
* once at completion. Caller exceptions are caught and routed through
|
|
455
|
+
* `logger`; the library guarantees parsing is not derailed by a faulty
|
|
456
|
+
* sink. Per FR-DR-A-025, `onProgress` does NOT drive idle-timer reset.
|
|
457
|
+
*/
|
|
458
|
+
onProgress?: ((snap: ProgressSnapshot) => void) | undefined;
|
|
459
|
+
/**
|
|
460
|
+
* Pluggable structured-logging shim (FR-018, JC-2). Event-style. When
|
|
461
|
+
* omitted, internal warnings fall back to `console.warn(msg, meta)`.
|
|
462
|
+
*/
|
|
463
|
+
logger?: Logger | undefined;
|
|
464
|
+
/**
|
|
465
|
+
* Per-part body-size cap (NFR-DR-S-001). Omit (default) for no cap. Must
|
|
466
|
+
* be a positive finite integer when set.
|
|
467
|
+
*/
|
|
468
|
+
maxPartBytes?: number | undefined;
|
|
469
|
+
/**
|
|
470
|
+
* Maximum part count (NFR-DR-S-012). Defaults to `10_000` when omitted.
|
|
471
|
+
*/
|
|
472
|
+
maxParts?: number | undefined;
|
|
473
|
+
/**
|
|
474
|
+
* Maximum number of distinct headers per part (NFR-DR-S-004). Defaults to
|
|
475
|
+
* `100` when omitted.
|
|
476
|
+
*/
|
|
477
|
+
maxHeadersPerPart?: number | undefined;
|
|
478
|
+
/**
|
|
479
|
+
* Maximum total bytes across the header block of a single part
|
|
480
|
+
* (NFR-DR-S-004). Defaults to `16_384` (16 KiB).
|
|
481
|
+
*/
|
|
482
|
+
maxHeaderBytesPerPart?: number | undefined;
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Options accepted by `fetchAndHandleMultipart<T>`. Same shape as
|
|
486
|
+
* {@link ParseMultipartOptions} (forwarded down per FR-DR-A-026) plus the
|
|
487
|
+
* required `parser` callback and the optional `fetchInit` for the underlying
|
|
488
|
+
* `fetch` call.
|
|
489
|
+
*/
|
|
490
|
+
interface MultipartHandlerOptions<T> {
|
|
491
|
+
/** REQUIRED. Per-part decision function. See {@link PartParser}. */
|
|
492
|
+
parser: PartParser<T>;
|
|
493
|
+
/** REQUIRED. Idle timeout (ms). See {@link ParseMultipartOptions.idleTimeoutMs}. */
|
|
494
|
+
idleTimeoutMs: number;
|
|
495
|
+
/** REQUIRED. Total timeout (ms). See {@link ParseMultipartOptions.totalTimeoutMs}. */
|
|
496
|
+
totalTimeoutMs: number;
|
|
497
|
+
/** Caller's `AbortSignal`. Same semantics as in {@link ParseMultipartOptions}. */
|
|
498
|
+
signal?: AbortSignal | undefined;
|
|
499
|
+
/** Progress callback. Same semantics as in {@link ParseMultipartOptions}. */
|
|
500
|
+
onProgress?: ((snap: ProgressSnapshot) => void) | undefined;
|
|
501
|
+
/** Logger. Same semantics as in {@link ParseMultipartOptions}. */
|
|
502
|
+
logger?: Logger | undefined;
|
|
503
|
+
/** Per-part body-size cap. Forwarded. (NFR-DR-S-001) */
|
|
504
|
+
maxPartBytes?: number | undefined;
|
|
505
|
+
/** Maximum part count. Forwarded. (NFR-DR-S-012) */
|
|
506
|
+
maxParts?: number | undefined;
|
|
507
|
+
/** Maximum headers per part. Forwarded. (NFR-DR-S-004) */
|
|
508
|
+
maxHeadersPerPart?: number | undefined;
|
|
509
|
+
/** Maximum header-block bytes per part. Forwarded. (NFR-DR-S-004) */
|
|
510
|
+
maxHeaderBytesPerPart?: number | undefined;
|
|
511
|
+
/**
|
|
512
|
+
* Optional `RequestInit` for the underlying `fetch` call. The library
|
|
513
|
+
* unconditionally REJECTS `signal` here (FR-024) — pass it via
|
|
514
|
+
* `options.signal` instead. The static `Omit<RequestInit, 'signal'>`
|
|
515
|
+
* surfaces the rule at compile time; the runtime check fires for callers
|
|
516
|
+
* who narrow with `as`.
|
|
517
|
+
*/
|
|
518
|
+
fetchInit?: Omit<RequestInit, 'signal'> | undefined;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* `fetchAndHandleMultipart` — Layer B (fetch orchestration). FR-005.
|
|
523
|
+
*
|
|
524
|
+
* The wrapper:
|
|
525
|
+
* 1. Validates the option bag synchronously: `parser` presence,
|
|
526
|
+
* `validatePositiveTimeout` on both timeouts, FR-024 `fetchInit.signal`
|
|
527
|
+
* ban, and FR-009 already-aborted short-circuit. ALL of these run
|
|
528
|
+
* BEFORE `fetch` is invoked — T-013 spies on `globalThis.fetch` and
|
|
529
|
+
* asserts it was never called when the caller's signal is pre-aborted.
|
|
530
|
+
* 2. Calls `fetch(url, { ...fetchInit, signal: options.signal })` —
|
|
531
|
+
* forwards the caller's `AbortSignal` to fetch directly so a network-
|
|
532
|
+
* time abort cancels the request before any bytes flow.
|
|
533
|
+
* 3. Validates the response Content-Type (FR-021) case-insensitively
|
|
534
|
+
* against `multipart/related` BEFORE constructing dicer. The offending
|
|
535
|
+
* Content-Type is sanitized via `truncateForErrorEmbed` per
|
|
536
|
+
* NFR-DR-S-006. T-035 asserts the dicer-activity harness sees zero
|
|
537
|
+
* Dicer instances on this path.
|
|
538
|
+
* 4. Captures `status` + `headers` BEFORE consuming the body
|
|
539
|
+
* (FR-DR-A-029 — the Response is gone after iteration).
|
|
540
|
+
* 5. Forwards `idleTimeoutMs` / `totalTimeoutMs` / `signal` / `onProgress`
|
|
541
|
+
* / `logger` / cap fields DOWN to `parseMultipartRelated`. Per
|
|
542
|
+
* FR-DR-A-026 timer ownership lives in Layer A; Layer B does NOT
|
|
543
|
+
* construct a TimerState.
|
|
544
|
+
* 6. Drives the for-await loop, awaits `options.parser(part)` per part,
|
|
545
|
+
* collects non-undefined returns into `parts`.
|
|
546
|
+
* 7. Resolves with `{ parts, bytes, elapsedMs, status, headers }` per
|
|
547
|
+
* FR-DR-A-029. The previously-considered `response: Response` field
|
|
548
|
+
* is REMOVED at the type level.
|
|
549
|
+
*
|
|
550
|
+
* Bytes-tracking strategy: the wrapper supplies its own `onProgress`
|
|
551
|
+
* callback to `parseMultipartRelated` regardless of whether the caller
|
|
552
|
+
* supplied one. The internal callback captures `lastBytes` and (when the
|
|
553
|
+
* caller supplied one) forwards the snapshot to the caller — wrapping the
|
|
554
|
+
* caller's call in a try/catch that routes via the resolved logger
|
|
555
|
+
* (FR-017 silent-catch replacement). After the loop ends, the wrapper
|
|
556
|
+
* fires ONE final completion-tick `onProgress` call (T-014 full
|
|
557
|
+
* assertion) with the final `bytes` / `elapsedMs` / `rateBps`.
|
|
558
|
+
*/
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Wrap `fetch` end-to-end: call, validate Content-Type, parse multipart,
|
|
562
|
+
* route each part through the caller's `parser`, and resolve with
|
|
563
|
+
* {@link MultipartFetchResult} (FR-DR-A-029 — `{ parts, bytes, elapsedMs,
|
|
564
|
+
* status, headers }`; NO `response` field).
|
|
565
|
+
*
|
|
566
|
+
* @param url - Forwarded to `fetch`. `URL` is supported for parity with
|
|
567
|
+
* `fetch` itself.
|
|
568
|
+
* @param options - {@link MultipartHandlerOptions}. `parser`,
|
|
569
|
+
* `idleTimeoutMs`, and `totalTimeoutMs` are REQUIRED (FR-005 / FR-006).
|
|
570
|
+
* The static `Omit<RequestInit, 'signal'>` on `options.fetchInit` enforces
|
|
571
|
+
* the FR-024 signal-ban at compile time; the runtime check below catches
|
|
572
|
+
* dynamic spreads / `as` consumers.
|
|
573
|
+
* @returns A `Promise<MultipartFetchResult<T>>` that resolves once every
|
|
574
|
+
* part has been processed.
|
|
575
|
+
*
|
|
576
|
+
* @throws {TypeError} `multipart: options.parser is required` when
|
|
577
|
+
* `options.parser` is missing or not a function.
|
|
578
|
+
* @throws {TypeError} from `validatePositiveTimeout` when either timeout
|
|
579
|
+
* is missing/invalid (FR-006 / NFR-DR-S-009 — message mentions Node's
|
|
580
|
+
* `setTimeout` clamping).
|
|
581
|
+
* @throws {Error} `multipart: pass signal via options.signal — fetchInit.signal
|
|
582
|
+
* is reserved for internal use` when `options.fetchInit.signal` is set
|
|
583
|
+
* (FR-024).
|
|
584
|
+
* @throws {MultipartAbortError} synchronously when `options.signal?.aborted`
|
|
585
|
+
* is `true` at call time (FR-009). `fetch` is NEVER invoked on this path
|
|
586
|
+
* — T-013 spies on `globalThis.fetch` to verify.
|
|
587
|
+
* @throws {Error} `multipart: response Content-Type is not multipart/related;
|
|
588
|
+
* got <actual>` when the response Content-Type doesn't start with
|
|
589
|
+
* `multipart/related` (case-insensitive) (FR-021). Dicer is NEVER
|
|
590
|
+
* constructed on this path — T-035 asserts via the dicer-activity
|
|
591
|
+
* harness.
|
|
592
|
+
* @throws Any error from `parseMultipartRelated` (idle/total timeout,
|
|
593
|
+
* abort mid-stream, truncation, source error, dicer error, cap overflow).
|
|
594
|
+
* @throws Any error from `options.parser`.
|
|
595
|
+
*
|
|
596
|
+
* @example
|
|
597
|
+
* const result = await fetchAndHandleMultipart(url, {
|
|
598
|
+
* idleTimeoutMs: 10_000,
|
|
599
|
+
* totalTimeoutMs: 60_000,
|
|
600
|
+
* parser: async (part) => streamToString(part.body),
|
|
601
|
+
* });
|
|
602
|
+
* console.log(result.parts.length, 'parts in', result.elapsedMs, 'ms');
|
|
603
|
+
*/
|
|
604
|
+
declare function fetchAndHandleMultipart<T>(url: string | URL, options: MultipartHandlerOptions<T>): Promise<MultipartFetchResult<T>>;
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* `parseMultipartRelated` — Layer A (parser/dicer adapter). FR-001.
|
|
608
|
+
*
|
|
609
|
+
* Resource-cap enforcement (NFR-DR-S-001/004/012):
|
|
610
|
+
* - `maxPartBytes` (NFR-DR-S-001): each per-part body Readable gets a
|
|
611
|
+
* 'data' listener that increments a per-part byte counter; on overflow
|
|
612
|
+
* the listener pushes `MultipartPartTooLargeError` into the queue and
|
|
613
|
+
* destroys the offending part body. No default — when undefined, no
|
|
614
|
+
* cap is enforced.
|
|
615
|
+
* - `maxParts` (NFR-DR-S-012): the dicer 'part' counter is checked at
|
|
616
|
+
* each emit; on overflow, push `MultipartTooManyPartsError`. Default
|
|
617
|
+
* `10_000` when undefined.
|
|
618
|
+
* - `maxHeadersPerPart` + `maxHeaderBytesPerPart` (NFR-DR-S-004): on
|
|
619
|
+
* each per-part 'header' event, count headers and sum the byte length
|
|
620
|
+
* of `name + ': ' + value + '\r\n'` framing for every header line; on
|
|
621
|
+
* overflow, push `MultipartHeadersTooLargeError`. Defaults: count=100,
|
|
622
|
+
* bytes=16384 (16 KiB).
|
|
623
|
+
*
|
|
624
|
+
* Timer + abort machinery (FR-006/FR-007/FR-008/FR-009/FR-DR-A-025/
|
|
625
|
+
* FR-DR-A-026):
|
|
626
|
+
* - `validatePositiveTimeout` calls at the top of the generator enforce
|
|
627
|
+
* the FR-006 / JC-3 contract (both timeouts REQUIRED, both validated
|
|
628
|
+
* against `[1, 2^31-1]`).
|
|
629
|
+
* - `setupTimers(...)` constructs the composite abort signal aggregating
|
|
630
|
+
* idle, total, and caller-supplied AbortSignal — one source of truth
|
|
631
|
+
* for all three. The signal listens for any of those firing and pushes
|
|
632
|
+
* the right error class into the queue.
|
|
633
|
+
* - The per-chunk source `'data'` listener resets the idle timer
|
|
634
|
+
* (FR-DR-A-025 — onProgress is NOT used for this).
|
|
635
|
+
* - The cleanup function calls `timers.cleanup()` — same idempotent
|
|
636
|
+
* `cleaned` flag.
|
|
637
|
+
* - Already-aborted callers short-circuit on first `.next()` per FR-009.
|
|
638
|
+
*
|
|
639
|
+
* Cleanup contract: the `finally` cleanup drains unyielded part bodies
|
|
640
|
+
* (FR-010), removes 'data'/'error'/'end' listeners on source and 'part'/
|
|
641
|
+
* 'finish' on dicer, unpipes + destroys source, and KEEPS dicer's 'error'
|
|
642
|
+
* listener for late-emit observability (FR-011). Truncation detection
|
|
643
|
+
* (FR-022) fires when source 'end' arrives without dicer 'finish' having
|
|
644
|
+
* fired.
|
|
645
|
+
*/
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Parse a `multipart/related` envelope as a typed async-iterator of
|
|
649
|
+
* streaming parts (FR-001).
|
|
650
|
+
*
|
|
651
|
+
* @param input - A Web `Response` (boundary auto-extracted from
|
|
652
|
+
* `Content-Type`) or a Node `Readable` (caller supplies `boundary` via
|
|
653
|
+
* options).
|
|
654
|
+
* @param opts - {@link ParseMultipartOptions}. `idleTimeoutMs` and
|
|
655
|
+
* `totalTimeoutMs` are REQUIRED on this entry point too (FR-006 / JC-3) and
|
|
656
|
+
* are validated synchronously via `validatePositiveTimeout` — both must be
|
|
657
|
+
* positive finite integers in `[1, 2_147_483_647]` (NFR-DR-S-009).
|
|
658
|
+
* @returns An `AsyncGenerator<StreamingMultipartPart, void, void>` that
|
|
659
|
+
* yields parts in dicer's emit order.
|
|
660
|
+
*
|
|
661
|
+
* @throws {TypeError} `multipart: idleTimeoutMs must be a positive finite
|
|
662
|
+
* integer in [1, 2_147_483_647]; …` when `idleTimeoutMs` is missing,
|
|
663
|
+
* non-numeric, non-finite, non-integer, `< 1`, or `> 2^31 - 1`. Same for
|
|
664
|
+
* `totalTimeoutMs`.
|
|
665
|
+
* @throws {Error} `multipart: response body is null` when `input` is a
|
|
666
|
+
* `Response` whose `.body` is `null` (FR-004). The first `.next()` rejects.
|
|
667
|
+
* @throws {Error} `multipart: Content-Type header is required to extract
|
|
668
|
+
* boundary` when the Response has no `Content-Type`.
|
|
669
|
+
* @throws {Error} `multipart: boundary parameter missing from Content-Type`
|
|
670
|
+
* when `Content-Type` is present but lacks a `boundary=` parameter.
|
|
671
|
+
* @throws {Error} `multipart: boundary option is required when input is a
|
|
672
|
+
* Readable` when the input is a Node `Readable` and `opts.boundary` is
|
|
673
|
+
* missing or empty.
|
|
674
|
+
* @throws {MultipartIdleTimeoutError} when no source bytes arrive for
|
|
675
|
+
* `idleTimeoutMs` consecutive ms (FR-007). The idle timer resets on every
|
|
676
|
+
* chunk received from the source (FR-DR-A-025).
|
|
677
|
+
* @throws {MultipartTotalTimeoutError} when the total operation wallclock
|
|
678
|
+
* exceeds `totalTimeoutMs` (FR-008).
|
|
679
|
+
* @throws {MultipartAbortError} when `opts.signal` fires (or is already
|
|
680
|
+
* aborted at call time — first `.next()` rejects synchronously per FR-009).
|
|
681
|
+
* `error.reason` is the caller's `signal.reason` verbatim (F-S-006).
|
|
682
|
+
* @throws {MultipartTruncatedError} when the source emits `'end'` before
|
|
683
|
+
* dicer emits `'finish'` (FR-022).
|
|
684
|
+
*
|
|
685
|
+
* @example
|
|
686
|
+
* for await (const part of parseMultipartRelated(res, {
|
|
687
|
+
* idleTimeoutMs: 5000,
|
|
688
|
+
* totalTimeoutMs: 60_000,
|
|
689
|
+
* })) {
|
|
690
|
+
* console.log(part.contentType, part.contentId);
|
|
691
|
+
* }
|
|
692
|
+
*/
|
|
693
|
+
declare function parseMultipartRelated(input: Response, opts: ParseMultipartOptions): AsyncGenerator<StreamingMultipartPart, void, void>;
|
|
694
|
+
declare function parseMultipartRelated(input: Readable, opts: ParseMultipartOptions & {
|
|
695
|
+
boundary: string;
|
|
696
|
+
}): AsyncGenerator<StreamingMultipartPart, void, void>;
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Stream-collection helpers (FR-015 + NFR-DR-S-002).
|
|
700
|
+
*
|
|
701
|
+
* Both helpers drain a Node `Readable` to a single value. They are
|
|
702
|
+
* convenience for small parts (text / binary metadata, manifests) — for
|
|
703
|
+
* larger payloads callers SHOULD pipe directly to a sink instead of
|
|
704
|
+
* buffering.
|
|
705
|
+
*
|
|
706
|
+
* The optional `options.maxBytes` cap (NFR-DR-S-002): when set and the
|
|
707
|
+
* accumulated bytes exceed the cap, the source `Readable` is destroyed and
|
|
708
|
+
* the promise rejects with a clear `Error`. Existing callers that pass only
|
|
709
|
+
* `(readable)` or `(readable, encoding)` see no behavior change.
|
|
710
|
+
*
|
|
711
|
+
* Per kiln/spec/api.md §3 + §4, `streamToBuffer`'s `options` parameter is the
|
|
712
|
+
* second positional arg; `streamToString`'s `options` parameter is the
|
|
713
|
+
* THIRD positional arg (after the legacy `encoding?`). The cap-overflow
|
|
714
|
+
* error is a generic `Error` (NOT a custom class) because these are
|
|
715
|
+
* utilities — the parsing-domain error classes are reserved for
|
|
716
|
+
* `parseMultipartRelated`.
|
|
717
|
+
*/
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Options bag accepted by both {@link streamToString} and
|
|
721
|
+
* {@link streamToBuffer}. Currently exposes only `maxBytes` (NFR-DR-S-002);
|
|
722
|
+
* forward-compat-shaped as an interface so additional cap fields can land
|
|
723
|
+
* without breaking callers.
|
|
724
|
+
*/
|
|
725
|
+
interface StreamCollectOptions {
|
|
726
|
+
/**
|
|
727
|
+
* Soft cap on accumulated input bytes. When set and the source produces
|
|
728
|
+
* more than this many bytes total, the source `Readable` is destroyed
|
|
729
|
+
* and the promise rejects with a clear `Error`. Omit (default) for no
|
|
730
|
+
* cap. Must be a positive finite integer when set.
|
|
731
|
+
*/
|
|
732
|
+
readonly maxBytes?: number | undefined;
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Drain a Node `Readable` to a single string.
|
|
736
|
+
*
|
|
737
|
+
* Calls `Buffer.from(chunk).toString(encoding)` for non-Buffer chunks,
|
|
738
|
+
* defending against object-mode-ish streams that emit strings already.
|
|
739
|
+
*
|
|
740
|
+
* @param readable - Source stream. Must end (rejection on `'error'`).
|
|
741
|
+
* @param encoding - Optional `BufferEncoding` (defaults to `'utf8'`).
|
|
742
|
+
* @param options - Optional {@link StreamCollectOptions}. When
|
|
743
|
+
* `options.maxBytes` is set and accumulated bytes exceed the cap, the
|
|
744
|
+
* source is destroyed and the promise rejects (NFR-DR-S-002).
|
|
745
|
+
* @returns A `Promise<string>` resolving to the full decoded contents.
|
|
746
|
+
* @throws Any `'error'` event from `readable` rejects the promise with that
|
|
747
|
+
* error.
|
|
748
|
+
* @throws {Error} `streamToString: input exceeded maxBytes (<n>)` when
|
|
749
|
+
* `options.maxBytes` is set and exceeded. The source `readable` is
|
|
750
|
+
* destroyed before the promise rejects.
|
|
751
|
+
*
|
|
752
|
+
* @example
|
|
753
|
+
* const text = await streamToString(part.body, 'utf8');
|
|
754
|
+
* console.log(JSON.parse(text));
|
|
755
|
+
*
|
|
756
|
+
* @example
|
|
757
|
+
* // With a 1 MiB cap to defend against attacker-controlled part bodies:
|
|
758
|
+
* const text = await streamToString(part.body, 'utf8', { maxBytes: 1_048_576 });
|
|
759
|
+
*/
|
|
760
|
+
declare function streamToString(readable: Readable, encoding?: BufferEncoding, options?: StreamCollectOptions): Promise<string>;
|
|
761
|
+
/**
|
|
762
|
+
* Drain a Node `Readable` to a single `Buffer`.
|
|
763
|
+
*
|
|
764
|
+
* @param readable - Source stream.
|
|
765
|
+
* @param options - Optional {@link StreamCollectOptions}. When
|
|
766
|
+
* `options.maxBytes` is set and accumulated bytes exceed the cap, the
|
|
767
|
+
* source is destroyed and the promise rejects (NFR-DR-S-002).
|
|
768
|
+
* @returns A `Promise<Buffer>`. Zero-byte streams resolve to
|
|
769
|
+
* `Buffer.alloc(0)`.
|
|
770
|
+
* @throws Any `'error'` event from `readable` rejects the promise.
|
|
771
|
+
* @throws {Error} `streamToBuffer: input exceeded maxBytes (<n>)` when
|
|
772
|
+
* `options.maxBytes` is set and exceeded. The source `readable` is
|
|
773
|
+
* destroyed before the promise rejects.
|
|
774
|
+
*
|
|
775
|
+
* @example
|
|
776
|
+
* const buf = await streamToBuffer(part.body);
|
|
777
|
+
* await fs.writeFile(`/tmp/${part.contentId ?? 'part'}.bin`, buf);
|
|
778
|
+
*
|
|
779
|
+
* @example
|
|
780
|
+
* // With a 5 MiB cap to defend against attacker-controlled part bodies:
|
|
781
|
+
* const buf = await streamToBuffer(part.body, { maxBytes: 5_242_880 });
|
|
782
|
+
*/
|
|
783
|
+
declare function streamToBuffer(readable: Readable, options?: StreamCollectOptions): Promise<Buffer>;
|
|
784
|
+
|
|
785
|
+
export { type Logger, MultipartAbortError, type MultipartFetchResult, type MultipartHandlerOptions, MultipartHeadersTooLargeError, MultipartIdleTimeoutError, MultipartPartTooLargeError, MultipartTooManyPartsError, MultipartTotalTimeoutError, MultipartTruncatedError, type ParseMultipartOptions, type PartParser, type ProgressSnapshot, type StreamingMultipartPart, extractBoundary, fetchAndHandleMultipart, parseMultipartRelated, streamToBuffer, streamToString };
|