@znight/http 0.0.0-stage → 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 +2333 -2
- package/dist/adapter-CGSceVEN.d.mts +876 -0
- package/dist/compiler.d.mts +156 -0
- package/dist/compiler.mjs +2180 -0
- package/dist/compiler.mjs.map +1 -0
- package/dist/express.d.mts +39 -0
- package/dist/express.mjs +135 -0
- package/dist/express.mjs.map +1 -0
- package/dist/fastify.d.mts +33 -0
- package/dist/fastify.mjs +191 -0
- package/dist/fastify.mjs.map +1 -0
- package/dist/hono.d.mts +49 -0
- package/dist/hono.mjs +319 -0
- package/dist/hono.mjs.map +1 -0
- package/dist/index.d.mts +1254 -0
- package/dist/index.mjs +5618 -0
- package/dist/index.mjs.map +1 -0
- package/dist/node-BUnkdSsa.mjs +183 -0
- package/dist/node-BUnkdSsa.mjs.map +1 -0
- package/dist/node-io-BY1ZDLRQ.mjs +1566 -0
- package/dist/node-io-BY1ZDLRQ.mjs.map +1 -0
- package/dist/node.d.mts +30 -0
- package/dist/node.mjs +1 -0
- package/dist/path-JI87C4k8.d.mts +109 -0
- package/dist/path.d.mts +2 -0
- package/dist/path.mjs +245 -0
- package/dist/path.mjs.map +1 -0
- package/dist/size-D8rsQcV0.mjs +184 -0
- package/dist/size-D8rsQcV0.mjs.map +1 -0
- package/dist/utils-DCbu3a4w.mjs +27 -0
- package/dist/utils-DCbu3a4w.mjs.map +1 -0
- package/package.json +142 -4
- package/src/adapter.ts +240 -0
- package/src/adapters/express.ts +201 -0
- package/src/adapters/fastify.ts +280 -0
- package/src/adapters/hono.ts +488 -0
- package/src/adapters/node-io.ts +368 -0
- package/src/adapters/node.ts +135 -0
- package/src/adapters/shared.ts +508 -0
- package/src/body.ts +135 -0
- package/src/coerce.ts +195 -0
- package/src/compiler/analyze.ts +1513 -0
- package/src/compiler/ast.ts +90 -0
- package/src/compiler/coercion.ts +267 -0
- package/src/compiler/generate.ts +374 -0
- package/src/compiler/index.ts +76 -0
- package/src/compiler/model.ts +260 -0
- package/src/compiler/route-decorators.ts +405 -0
- package/src/compiler/validate.ts +288 -0
- package/src/context.ts +533 -0
- package/src/cookie-settings.ts +68 -0
- package/src/cookie-signer.ts +106 -0
- package/src/cookies.ts +726 -0
- package/src/cors.ts +349 -0
- package/src/decisions.ts +331 -0
- package/src/decorators.ts +486 -0
- package/src/event-channel.ts +197 -0
- package/src/exceptions.ts +257 -0
- package/src/files.ts +429 -0
- package/src/headers.ts +39 -0
- package/src/index.ts +185 -0
- package/src/inject-sse.ts +309 -0
- package/src/interfaces.ts +101 -0
- package/src/metadata.ts +162 -0
- package/src/mime.ts +157 -0
- package/src/multipart.ts +597 -0
- package/src/option-error.ts +31 -0
- package/src/path.ts +288 -0
- package/src/pipeline.ts +918 -0
- package/src/plugin-routes.ts +188 -0
- package/src/plugin.ts +1432 -0
- package/src/proxy.ts +236 -0
- package/src/query.ts +30 -0
- package/src/response.ts +240 -0
- package/src/router.ts +119 -0
- package/src/security-headers.ts +161 -0
- package/src/serialize.ts +264 -0
- package/src/size.ts +66 -0
- package/src/sse-parse.ts +194 -0
- package/src/sse.ts +650 -0
- package/src/standard-schema.ts +81 -0
- package/src/status.ts +78 -0
- package/src/stream.ts +200 -0
- package/src/tokens.ts +21 -0
- package/src/uploads.ts +176 -0
- package/src/utils.ts +31 -0
- package/src/validate.ts +65 -0
|
@@ -0,0 +1,1566 @@
|
|
|
1
|
+
import { Readable } from "node:stream";
|
|
2
|
+
import { pipeline } from "node:stream/promises";
|
|
3
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
4
|
+
// src/status.ts
|
|
5
|
+
/** Standard reason phrases (RFC 9110 and common extensions), used in error bodies. */
|
|
6
|
+
const REASONS = {
|
|
7
|
+
100: "Continue",
|
|
8
|
+
101: "Switching Protocols",
|
|
9
|
+
102: "Processing",
|
|
10
|
+
103: "Early Hints",
|
|
11
|
+
200: "OK",
|
|
12
|
+
201: "Created",
|
|
13
|
+
202: "Accepted",
|
|
14
|
+
203: "Non-Authoritative Information",
|
|
15
|
+
204: "No Content",
|
|
16
|
+
205: "Reset Content",
|
|
17
|
+
206: "Partial Content",
|
|
18
|
+
207: "Multi-Status",
|
|
19
|
+
300: "Multiple Choices",
|
|
20
|
+
301: "Moved Permanently",
|
|
21
|
+
302: "Found",
|
|
22
|
+
303: "See Other",
|
|
23
|
+
304: "Not Modified",
|
|
24
|
+
307: "Temporary Redirect",
|
|
25
|
+
308: "Permanent Redirect",
|
|
26
|
+
400: "Bad Request",
|
|
27
|
+
401: "Unauthorized",
|
|
28
|
+
402: "Payment Required",
|
|
29
|
+
403: "Forbidden",
|
|
30
|
+
404: "Not Found",
|
|
31
|
+
405: "Method Not Allowed",
|
|
32
|
+
406: "Not Acceptable",
|
|
33
|
+
407: "Proxy Authentication Required",
|
|
34
|
+
408: "Request Timeout",
|
|
35
|
+
409: "Conflict",
|
|
36
|
+
410: "Gone",
|
|
37
|
+
411: "Length Required",
|
|
38
|
+
412: "Precondition Failed",
|
|
39
|
+
413: "Payload Too Large",
|
|
40
|
+
414: "URI Too Long",
|
|
41
|
+
415: "Unsupported Media Type",
|
|
42
|
+
416: "Range Not Satisfiable",
|
|
43
|
+
417: "Expectation Failed",
|
|
44
|
+
418: "I'm a Teapot",
|
|
45
|
+
421: "Misdirected Request",
|
|
46
|
+
422: "Unprocessable Entity",
|
|
47
|
+
423: "Locked",
|
|
48
|
+
424: "Failed Dependency",
|
|
49
|
+
425: "Too Early",
|
|
50
|
+
426: "Upgrade Required",
|
|
51
|
+
428: "Precondition Required",
|
|
52
|
+
429: "Too Many Requests",
|
|
53
|
+
431: "Request Header Fields Too Large",
|
|
54
|
+
451: "Unavailable For Legal Reasons",
|
|
55
|
+
500: "Internal Server Error",
|
|
56
|
+
501: "Not Implemented",
|
|
57
|
+
502: "Bad Gateway",
|
|
58
|
+
503: "Service Unavailable",
|
|
59
|
+
504: "Gateway Timeout",
|
|
60
|
+
505: "HTTP Version Not Supported",
|
|
61
|
+
507: "Insufficient Storage",
|
|
62
|
+
508: "Loop Detected",
|
|
63
|
+
511: "Network Authentication Required"
|
|
64
|
+
};
|
|
65
|
+
/** Reason phrase of `status` (`404` → `Not Found`), or a generic one for unknown codes. */
|
|
66
|
+
function statusText(status) {
|
|
67
|
+
const reason = REASONS[status];
|
|
68
|
+
if (reason !== void 0) return reason;
|
|
69
|
+
if (status >= 500) return "Server Error";
|
|
70
|
+
return status >= 400 ? "Client Error" : "Unknown Status";
|
|
71
|
+
}
|
|
72
|
+
/** Whether `status` is an integer in the 100–599 range. */
|
|
73
|
+
function isValidStatus(status) {
|
|
74
|
+
return typeof status === "number" && Number.isInteger(status) && status >= 100 && status <= 599;
|
|
75
|
+
}
|
|
76
|
+
/** Statuses whose responses never have a body. */
|
|
77
|
+
function isBodylessStatus(status) {
|
|
78
|
+
return status === 204 || status === 304 || status >= 100 && status < 200;
|
|
79
|
+
}
|
|
80
|
+
// src/exceptions.ts
|
|
81
|
+
const NO_HEADERS = Object.freeze({});
|
|
82
|
+
/**
|
|
83
|
+
* An error carrying an HTTP status. Thrown from handlers, guards, interceptors or services, it is
|
|
84
|
+
* rendered as `{ statusCode, error, message, details? }` with its status and headers.
|
|
85
|
+
*
|
|
86
|
+
* ```ts
|
|
87
|
+
* throw new HttpException(409, 'Email already used', { field: 'email' })
|
|
88
|
+
* throw new NotFoundException(`User ${id} not found`)
|
|
89
|
+
* throw new UnauthorizedException('Token expired', undefined, { headers: { 'www-authenticate': 'Bearer' } })
|
|
90
|
+
* ```
|
|
91
|
+
*
|
|
92
|
+
* Invalid statuses (not an integer in 100–599) become `500`.
|
|
93
|
+
*/
|
|
94
|
+
var HttpException = class extends Error {
|
|
95
|
+
/** HTTP status of the response. */
|
|
96
|
+
status;
|
|
97
|
+
/** Structured details rendered in the body (omitted when `undefined`). */
|
|
98
|
+
details;
|
|
99
|
+
/** Headers added to the response. */
|
|
100
|
+
headers;
|
|
101
|
+
constructor(status, message, details, options = {}) {
|
|
102
|
+
const code = isValidStatus(status) ? status : 500;
|
|
103
|
+
super(message ?? statusText(code), options.cause === void 0 ? void 0 : { cause: options.cause });
|
|
104
|
+
this.name = "HttpException";
|
|
105
|
+
this.status = code;
|
|
106
|
+
this.details = details;
|
|
107
|
+
this.headers = options.headers ?? NO_HEADERS;
|
|
108
|
+
}
|
|
109
|
+
/** The `statusCode` field of the body (alias of {@link status}). */
|
|
110
|
+
get statusCode() {
|
|
111
|
+
return this.status;
|
|
112
|
+
}
|
|
113
|
+
/** Body of the error response. Override it in a subclass to change the shape. */
|
|
114
|
+
toBody() {
|
|
115
|
+
const body = {
|
|
116
|
+
statusCode: this.status,
|
|
117
|
+
error: statusText(this.status),
|
|
118
|
+
message: this.message
|
|
119
|
+
};
|
|
120
|
+
if (this.details !== void 0) body.details = this.details;
|
|
121
|
+
return body;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
/** Whether `value` is an {@link HttpException} (also across duplicated package copies). */
|
|
125
|
+
function isHttpException(value) {
|
|
126
|
+
return value instanceof HttpException || value instanceof Error && typeof value.status === "number" && typeof value.toBody === "function";
|
|
127
|
+
}
|
|
128
|
+
/** 400 Bad Request. */
|
|
129
|
+
var BadRequestException = class extends HttpException {
|
|
130
|
+
constructor(message, details, options) {
|
|
131
|
+
super(400, message, details, options);
|
|
132
|
+
this.name = "BadRequestException";
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* 400 Bad Request raised when an input fails its schema (`@Body(schema)`, `@Query(schema)`...).
|
|
137
|
+
* `details` lists the issues as `{ path, message }`; the raw Standard Schema issues are kept in
|
|
138
|
+
* {@link issues} for exception mappers.
|
|
139
|
+
*/
|
|
140
|
+
var ValidationException = class extends BadRequestException {
|
|
141
|
+
/** What was validated: `body`, `query`, `query "page"`, `param "id"`... */
|
|
142
|
+
location;
|
|
143
|
+
issues;
|
|
144
|
+
constructor(location, issues, options) {
|
|
145
|
+
super(`Invalid ${location}`, issues.map(toIssueDetail), options);
|
|
146
|
+
this.name = "ValidationException";
|
|
147
|
+
this.location = location;
|
|
148
|
+
this.issues = issues;
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
/**
|
|
152
|
+
* The `details` of a 400 raised by the framework for one invalid input (a multipart field...): the
|
|
153
|
+
* shape of the issues of {@link ValidationException}, documented by `@znight/openapi`.
|
|
154
|
+
*/
|
|
155
|
+
function issue(path, message) {
|
|
156
|
+
return [{
|
|
157
|
+
path,
|
|
158
|
+
message
|
|
159
|
+
}];
|
|
160
|
+
}
|
|
161
|
+
/** Converts a Standard Schema issue to `{ path, message }` (`path` dotted, `''` for the root). */
|
|
162
|
+
function toIssueDetail(issue) {
|
|
163
|
+
return {
|
|
164
|
+
path: formatIssuePath(issue.path),
|
|
165
|
+
message: issue.message
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function formatIssuePath(path) {
|
|
169
|
+
if (path === void 0 || path.length === 0) return "";
|
|
170
|
+
let result = "";
|
|
171
|
+
for (const segment of path) {
|
|
172
|
+
const key = typeof segment === "object" && segment !== null ? segment.key : segment;
|
|
173
|
+
const text = typeof key === "symbol" ? key.description ?? "symbol" : String(key);
|
|
174
|
+
result = result === "" ? text : `${result}.${text}`;
|
|
175
|
+
}
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
178
|
+
/** 401 Unauthorized. */
|
|
179
|
+
var UnauthorizedException = class extends HttpException {
|
|
180
|
+
constructor(message, details, options) {
|
|
181
|
+
super(401, message, details, options);
|
|
182
|
+
this.name = "UnauthorizedException";
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
/** 403 Forbidden (also the response of a guard returning `false`). */
|
|
186
|
+
var ForbiddenException = class extends HttpException {
|
|
187
|
+
constructor(message, details, options) {
|
|
188
|
+
super(403, message, details, options);
|
|
189
|
+
this.name = "ForbiddenException";
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
/** 404 Not Found. */
|
|
193
|
+
var NotFoundException = class extends HttpException {
|
|
194
|
+
constructor(message, details, options) {
|
|
195
|
+
super(404, message, details, options);
|
|
196
|
+
this.name = "NotFoundException";
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
/** 405 Method Not Allowed (pass the `allow` header in `options.headers`). */
|
|
200
|
+
var MethodNotAllowedException = class extends HttpException {
|
|
201
|
+
constructor(message, details, options) {
|
|
202
|
+
super(405, message, details, options);
|
|
203
|
+
this.name = "MethodNotAllowedException";
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
/** 409 Conflict. */
|
|
207
|
+
var ConflictException = class extends HttpException {
|
|
208
|
+
constructor(message, details, options) {
|
|
209
|
+
super(409, message, details, options);
|
|
210
|
+
this.name = "ConflictException";
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
/** 410 Gone. */
|
|
214
|
+
var GoneException = class extends HttpException {
|
|
215
|
+
constructor(message, details, options) {
|
|
216
|
+
super(410, message, details, options);
|
|
217
|
+
this.name = "GoneException";
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
/** 413 Payload Too Large (request body above `bodyLimit`). */
|
|
221
|
+
var PayloadTooLargeException = class extends HttpException {
|
|
222
|
+
constructor(message, details, options) {
|
|
223
|
+
super(413, message, details, options);
|
|
224
|
+
this.name = "PayloadTooLargeException";
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
/** 415 Unsupported Media Type. */
|
|
228
|
+
var UnsupportedMediaTypeException = class extends HttpException {
|
|
229
|
+
constructor(message, details, options) {
|
|
230
|
+
super(415, message, details, options);
|
|
231
|
+
this.name = "UnsupportedMediaTypeException";
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
/** 422 Unprocessable Entity. */
|
|
235
|
+
var UnprocessableEntityException = class extends HttpException {
|
|
236
|
+
constructor(message, details, options) {
|
|
237
|
+
super(422, message, details, options);
|
|
238
|
+
this.name = "UnprocessableEntityException";
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
/** 429 Too Many Requests (pass `retry-after` in `options.headers`). */
|
|
242
|
+
var TooManyRequestsException = class extends HttpException {
|
|
243
|
+
constructor(message, details, options) {
|
|
244
|
+
super(429, message, details, options);
|
|
245
|
+
this.name = "TooManyRequestsException";
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
/** 500 Internal Server Error (its message is always sent, unlike unexpected errors). */
|
|
249
|
+
var InternalServerErrorException = class extends HttpException {
|
|
250
|
+
constructor(message, details, options) {
|
|
251
|
+
super(500, message, details, options);
|
|
252
|
+
this.name = "InternalServerErrorException";
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
/** 501 Not Implemented. */
|
|
256
|
+
var NotImplementedException = class extends HttpException {
|
|
257
|
+
constructor(message, details, options) {
|
|
258
|
+
super(501, message, details, options);
|
|
259
|
+
this.name = "NotImplementedException";
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
/** 502 Bad Gateway. */
|
|
263
|
+
var BadGatewayException = class extends HttpException {
|
|
264
|
+
constructor(message, details, options) {
|
|
265
|
+
super(502, message, details, options);
|
|
266
|
+
this.name = "BadGatewayException";
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
/** 503 Service Unavailable. */
|
|
270
|
+
var ServiceUnavailableException = class extends HttpException {
|
|
271
|
+
constructor(message, details, options) {
|
|
272
|
+
super(503, message, details, options);
|
|
273
|
+
this.name = "ServiceUnavailableException";
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
/** 504 Gateway Timeout. */
|
|
277
|
+
var GatewayTimeoutException = class extends HttpException {
|
|
278
|
+
constructor(message, details, options) {
|
|
279
|
+
super(504, message, details, options);
|
|
280
|
+
this.name = "GatewayTimeoutException";
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
// src/stream.ts
|
|
284
|
+
/**
|
|
285
|
+
* Web stream helpers shared by the pipeline and the adapters (internal): conversion of handler
|
|
286
|
+
* stream sources (`ReadableStream`, Node.js `Readable`, async iterables) to byte streams, tracking of
|
|
287
|
+
* response streams (request scope lifetime) and small constructors.
|
|
288
|
+
*/
|
|
289
|
+
const ENCODER = new TextEncoder();
|
|
290
|
+
/** The current async context, to run stream reads in it later (the request scope of a response). */
|
|
291
|
+
function captureContext() {
|
|
292
|
+
const snapshot = AsyncLocalStorage.snapshot;
|
|
293
|
+
return typeof snapshot === "function" ? snapshot() : (run) => run();
|
|
294
|
+
}
|
|
295
|
+
/** Whether `value` is a stream source (Web stream, Node.js readable or async iterable). */
|
|
296
|
+
function isStreamSource(value) {
|
|
297
|
+
return value instanceof ReadableStream || value !== null && typeof value === "object" && typeof value[Symbol.asyncIterator] === "function";
|
|
298
|
+
}
|
|
299
|
+
/** Byte streams created by {@link toByteStream} (already normalised: not wrapped again). */
|
|
300
|
+
const BYTE_STREAMS = /* @__PURE__ */ new WeakSet();
|
|
301
|
+
/** Whether `stream` was created by {@link toByteStream}. */
|
|
302
|
+
function isByteStream(stream) {
|
|
303
|
+
return BYTE_STREAMS.has(stream);
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Converts a stream source to a pull-based byte stream: the source is read only when the consumer
|
|
307
|
+
* reads (backpressure), strings are encoded as UTF-8, `ArrayBuffer`s and views are converted, empty
|
|
308
|
+
* chunks are skipped and cancelling the stream cancels the source (`iterator.return()`, which
|
|
309
|
+
* destroys a Node.js `Readable`; a `Readable` that was never read is destroyed directly).
|
|
310
|
+
*
|
|
311
|
+
* @throws TypeError (as a stream error) for chunks that are neither bytes nor strings.
|
|
312
|
+
*/
|
|
313
|
+
function toByteStream(source) {
|
|
314
|
+
let iterator;
|
|
315
|
+
let reader;
|
|
316
|
+
const next = () => {
|
|
317
|
+
if (source instanceof ReadableStream) {
|
|
318
|
+
const current = reader ?? source.getReader();
|
|
319
|
+
reader = current;
|
|
320
|
+
return current.read();
|
|
321
|
+
}
|
|
322
|
+
iterator ??= source[Symbol.asyncIterator]();
|
|
323
|
+
return iterator.next();
|
|
324
|
+
};
|
|
325
|
+
const stream = new ReadableStream({
|
|
326
|
+
async pull(controller) {
|
|
327
|
+
for (;;) {
|
|
328
|
+
const result = await next();
|
|
329
|
+
if (result.done === true) {
|
|
330
|
+
controller.close();
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
const chunk = toChunk(result.value);
|
|
334
|
+
if (chunk.byteLength > 0) {
|
|
335
|
+
controller.enqueue(chunk);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
async cancel(reason) {
|
|
341
|
+
if (reader !== void 0) await reader.cancel(reason);
|
|
342
|
+
else if (source instanceof ReadableStream) await source.cancel(reason);
|
|
343
|
+
else if (iterator === void 0) {
|
|
344
|
+
const destroyable = source;
|
|
345
|
+
if (typeof destroyable.destroy === "function") destroyable.destroy();
|
|
346
|
+
} else await iterator.return?.(reason);
|
|
347
|
+
}
|
|
348
|
+
}, { highWaterMark: 0 });
|
|
349
|
+
BYTE_STREAMS.add(stream);
|
|
350
|
+
return stream;
|
|
351
|
+
}
|
|
352
|
+
function toChunk(value) {
|
|
353
|
+
if (value instanceof Uint8Array) return value;
|
|
354
|
+
if (typeof value === "string") return ENCODER.encode(value);
|
|
355
|
+
if (value instanceof ArrayBuffer) return new Uint8Array(value);
|
|
356
|
+
if (ArrayBuffer.isView(value)) return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
357
|
+
throw new TypeError(`Invalid stream chunk: expected a Uint8Array or a string, received ${value === null ? "null" : typeof value}`);
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Wraps a response body so that the pipeline knows when it has been fully sent (or abandoned) and
|
|
361
|
+
* reads the source in `run` (the async context of the request scope, so that stream producers can
|
|
362
|
+
* use request-scoped providers). The wrapper is pull-based (no read-ahead): backpressure is the
|
|
363
|
+
* consumer's. A `Blob` is read through `blob.stream()`, opened on the first read only. `onCancel`
|
|
364
|
+
* runs when the consumer cancels the stream (the client left), before the source is cancelled.
|
|
365
|
+
*/
|
|
366
|
+
function trackStream(body, run, onCancel) {
|
|
367
|
+
let reader = body instanceof Blob ? void 0 : body.getReader();
|
|
368
|
+
let settle = () => void 0;
|
|
369
|
+
const done = new Promise((resolve) => {
|
|
370
|
+
settle = resolve;
|
|
371
|
+
});
|
|
372
|
+
let finished = false;
|
|
373
|
+
const finish = (error) => {
|
|
374
|
+
if (finished) return;
|
|
375
|
+
finished = true;
|
|
376
|
+
settle(error);
|
|
377
|
+
};
|
|
378
|
+
return {
|
|
379
|
+
stream: new ReadableStream({
|
|
380
|
+
async pull(controller) {
|
|
381
|
+
let result;
|
|
382
|
+
try {
|
|
383
|
+
result = await run(() => {
|
|
384
|
+
reader ??= body.stream().getReader();
|
|
385
|
+
return reader.read();
|
|
386
|
+
});
|
|
387
|
+
} catch (error) {
|
|
388
|
+
finish(error);
|
|
389
|
+
throw error;
|
|
390
|
+
}
|
|
391
|
+
if (result.done === true) {
|
|
392
|
+
controller.close();
|
|
393
|
+
finish();
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
controller.enqueue(result.value);
|
|
397
|
+
},
|
|
398
|
+
async cancel(reason) {
|
|
399
|
+
onCancel?.();
|
|
400
|
+
try {
|
|
401
|
+
const current = reader;
|
|
402
|
+
if (current !== void 0) await run(() => current.cancel(reason));
|
|
403
|
+
} catch {} finally {
|
|
404
|
+
finish();
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}, { highWaterMark: 0 }),
|
|
408
|
+
done
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
/** A stream failing with `error` on first read. */
|
|
412
|
+
function failedStream(error) {
|
|
413
|
+
return new ReadableStream({ start(controller) {
|
|
414
|
+
controller.error(error);
|
|
415
|
+
} });
|
|
416
|
+
}
|
|
417
|
+
/** Cancels a stream, ignoring failures (unconsumed response bodies). */
|
|
418
|
+
function discardStream(stream) {
|
|
419
|
+
if (stream instanceof ReadableStream && !stream.locked) stream.cancel().catch(() => void 0);
|
|
420
|
+
}
|
|
421
|
+
// src/cookies.ts
|
|
422
|
+
/** The secure defaults: `Path=/; Secure; HttpOnly; SameSite=Lax`, host-only. */
|
|
423
|
+
const DEFAULT_COOKIE_OPTIONS = Object.freeze({
|
|
424
|
+
path: "/",
|
|
425
|
+
secure: true,
|
|
426
|
+
httpOnly: true,
|
|
427
|
+
sameSite: "lax"
|
|
428
|
+
});
|
|
429
|
+
/** Largest `name=value` (bytes on the wire, after encoding and signing). */
|
|
430
|
+
const MAX_COOKIE_SIZE = 4096;
|
|
431
|
+
/** Largest `Domain` / `Path` attribute value (bytes). */
|
|
432
|
+
const MAX_COOKIE_ATTRIBUTE_SIZE = 1024;
|
|
433
|
+
const MAX_DOMAIN_LENGTH = 253;
|
|
434
|
+
const EPOCH = "Thu, 01 Jan 1970 00:00:00 GMT";
|
|
435
|
+
/** Years a cookie-date can carry (RFC 6265 §5.1.1; four digits in an IMF-fixdate). */
|
|
436
|
+
const MIN_EXPIRES_YEAR = 1601;
|
|
437
|
+
const MAX_EXPIRES_YEAR = 9999;
|
|
438
|
+
const OPTION_KEYS = /* @__PURE__ */ new Set([
|
|
439
|
+
"maxAge",
|
|
440
|
+
"expires",
|
|
441
|
+
"domain",
|
|
442
|
+
"path",
|
|
443
|
+
"secure",
|
|
444
|
+
"httpOnly",
|
|
445
|
+
"sameSite",
|
|
446
|
+
"partitioned",
|
|
447
|
+
"priority",
|
|
448
|
+
"signed"
|
|
449
|
+
]);
|
|
450
|
+
const SAME_SITE = {
|
|
451
|
+
strict: "Strict",
|
|
452
|
+
lax: "Lax",
|
|
453
|
+
none: "None"
|
|
454
|
+
};
|
|
455
|
+
const PRIORITY = {
|
|
456
|
+
low: "Low",
|
|
457
|
+
medium: "Medium",
|
|
458
|
+
high: "High"
|
|
459
|
+
};
|
|
460
|
+
/**
|
|
461
|
+
* Parses a `cookie` request header into a prototype-less, frozen record (`__proto__` is just a
|
|
462
|
+
* name). Pairs are split on `;` only (never `,`) and trimmed; a pair without `=` or with an empty
|
|
463
|
+
* name is ignored; names are kept as sent (case-sensitive); a value wrapped in double quotes is
|
|
464
|
+
* unquoted, and percent-decoded when it contains `%` (a malformed sequence keeps the raw value).
|
|
465
|
+
* When a name repeats, the first occurrence wins (browsers send the most specific path first).
|
|
466
|
+
* Linear time, never throws.
|
|
467
|
+
*/
|
|
468
|
+
function parseCookieHeader(header) {
|
|
469
|
+
const result = Object.create(null);
|
|
470
|
+
if (typeof header === "string") {
|
|
471
|
+
let start = 0;
|
|
472
|
+
const length = header.length;
|
|
473
|
+
let equals = -1;
|
|
474
|
+
while (start < length) {
|
|
475
|
+
let end = header.indexOf(";", start);
|
|
476
|
+
if (end === -1) end = length;
|
|
477
|
+
if (equals < start) {
|
|
478
|
+
equals = header.indexOf("=", start);
|
|
479
|
+
if (equals === -1) equals = length;
|
|
480
|
+
}
|
|
481
|
+
if (equals < end) {
|
|
482
|
+
const name = trim(header, start, equals);
|
|
483
|
+
if (name !== "" && !Object.hasOwn(result, name)) result[name] = decodeCookieValue(unquote(trim(header, equals + 1, end)));
|
|
484
|
+
}
|
|
485
|
+
start = end + 1;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return Object.freeze(result);
|
|
489
|
+
}
|
|
490
|
+
/** `text.slice(start, end)` without leading and trailing SP / HTAB. */
|
|
491
|
+
function trim(text, start, end) {
|
|
492
|
+
let from = start;
|
|
493
|
+
let to = end;
|
|
494
|
+
while (from < to && isBlank(text.charCodeAt(from))) from++;
|
|
495
|
+
while (to > from && isBlank(text.charCodeAt(to - 1))) to--;
|
|
496
|
+
return text.slice(from, to);
|
|
497
|
+
}
|
|
498
|
+
function isBlank(code) {
|
|
499
|
+
return code === 32 || code === 9;
|
|
500
|
+
}
|
|
501
|
+
function unquote(value) {
|
|
502
|
+
return value.length >= 2 && value.charCodeAt(0) === 34 && value.charCodeAt(value.length - 1) === 34 ? value.slice(1, -1) : value;
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Decodes a cookie value produced by {@link encodeCookieValue} (`decodeURIComponent` when it
|
|
506
|
+
* contains `%`; a malformed sequence keeps the raw value). Never throws.
|
|
507
|
+
*/
|
|
508
|
+
function decodeCookieValue(value) {
|
|
509
|
+
if (!value.includes("%")) return value;
|
|
510
|
+
try {
|
|
511
|
+
return decodeURIComponent(value);
|
|
512
|
+
} catch {
|
|
513
|
+
return value;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Encodes a cookie value canonically: the UTF-8 bytes outside RFC 6265 `cookie-octet` (space, `"`,
|
|
518
|
+
* `,`, `;`, `\`, controls, non-ASCII) and `%` are percent-encoded (upper-case hex), so that
|
|
519
|
+
* {@link decodeCookieValue} restores the value exactly (`a b;c` → `a%20b%3Bc`), and
|
|
520
|
+
* `encode(decode(w)) === w` for every encoded `w`. Quotes are never emitted.
|
|
521
|
+
*
|
|
522
|
+
* @throws TypeError for a non-string or a string with lone surrogates (they cannot round-trip).
|
|
523
|
+
*/
|
|
524
|
+
function encodeCookieValue(value) {
|
|
525
|
+
if (typeof value !== "string") throw new TypeError(`A cookie value must be a string (received ${describe(value)})`);
|
|
526
|
+
if (!value.isWellFormed()) throw new TypeError("A cookie value must be a well-formed string (it contains a lone surrogate)");
|
|
527
|
+
let encoded = "";
|
|
528
|
+
let plain = 0;
|
|
529
|
+
for (let index = 0; index < value.length; index++) {
|
|
530
|
+
const code = value.charCodeAt(index);
|
|
531
|
+
if (isCookieOctet(code)) continue;
|
|
532
|
+
encoded += value.slice(plain, index);
|
|
533
|
+
if (code < 128) {
|
|
534
|
+
encoded += `%${HEX[code]}`;
|
|
535
|
+
plain = index + 1;
|
|
536
|
+
} else {
|
|
537
|
+
const width = value.codePointAt(index) > 65535 ? 2 : 1;
|
|
538
|
+
encoded += encodeURIComponent(value.slice(index, index + width));
|
|
539
|
+
index += width - 1;
|
|
540
|
+
plain = index + 1;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return plain === 0 ? value : encoded + value.slice(plain);
|
|
544
|
+
}
|
|
545
|
+
const HEX = Array.from({ length: 128 }, (_, code) => code.toString(16).toUpperCase().padStart(2, "0"));
|
|
546
|
+
/** RFC 6265 `cookie-octet`, `%` excluded (the escape character). */
|
|
547
|
+
function isCookieOctet(code) {
|
|
548
|
+
return code === 33 || code >= 35 && code <= 43 && code !== 37 || code >= 45 && code <= 58 || code >= 60 && code <= 91 || code >= 93 && code <= 126;
|
|
549
|
+
}
|
|
550
|
+
/** Whether `name` is an RFC 9110 token (a cookie name Znight can set). */
|
|
551
|
+
function isCookieName(name) {
|
|
552
|
+
if (typeof name !== "string" || name === "") return false;
|
|
553
|
+
for (let index = 0; index < name.length; index++) if (!isTokenChar(name.charCodeAt(index))) return false;
|
|
554
|
+
return true;
|
|
555
|
+
}
|
|
556
|
+
function isTokenChar(code) {
|
|
557
|
+
return code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >= 97 && code <= 122 || code === 33 || code >= 35 && code <= 39 || code === 42 || code === 43 || code === 45 || code === 46 || code === 94 || code === 95 || code === 96 || code === 124 || code === 126;
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Checks what a cookie says about itself, without plugin defaults: the name, the value type, the
|
|
561
|
+
* options (types, `maxAge`, `expires`, `domain`, `path`) and the size before signing. Run by
|
|
562
|
+
* `HttpResponse.withCookie()` / `withoutCookie()` at call time; the rules depending on the defaults
|
|
563
|
+
* (prefixes, `SameSite=None` and `partitioned` with `secure`), signing and the final size are checked
|
|
564
|
+
* when the response is serialised.
|
|
565
|
+
*
|
|
566
|
+
* @throws TypeError / RangeError naming the cookie.
|
|
567
|
+
*/
|
|
568
|
+
function checkCookie(name, value, options) {
|
|
569
|
+
checkName(name);
|
|
570
|
+
const checked = checkOptions(name, options);
|
|
571
|
+
if (value !== void 0) checkSize(name, encodeValue(name, value));
|
|
572
|
+
if (typeof checked.domain === "string") normalizeDomain(name, checked.domain);
|
|
573
|
+
if (checked.path !== void 0) checkPath(name, checked.path);
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Serialises a response cookie with `defaults` (merged under the options): every rule of RFC 0006
|
|
577
|
+
* §4.3 is checked. `sign` turns the encoded value into its signed wire form (`signed: true`).
|
|
578
|
+
*
|
|
579
|
+
* @throws TypeError / RangeError naming the cookie.
|
|
580
|
+
*/
|
|
581
|
+
function serializeResponseCookie(cookie, defaults, sign) {
|
|
582
|
+
const { name, value } = cookie;
|
|
583
|
+
checkName(name);
|
|
584
|
+
const options = checkOptions(name, cookie.options);
|
|
585
|
+
checkOptions(name, defaults, true);
|
|
586
|
+
const attributes = resolveAttributes(name, {
|
|
587
|
+
...applicableDefaults(name, defaults),
|
|
588
|
+
...definedOnly(options)
|
|
589
|
+
});
|
|
590
|
+
let wire;
|
|
591
|
+
let maxAge;
|
|
592
|
+
let expires;
|
|
593
|
+
if (value === void 0) {
|
|
594
|
+
wire = "";
|
|
595
|
+
maxAge = 0;
|
|
596
|
+
expires = EPOCH;
|
|
597
|
+
} else {
|
|
598
|
+
wire = encodeValue(name, value);
|
|
599
|
+
if (options.signed === true) {
|
|
600
|
+
if (sign === void 0) throw new TypeError(`Cannot sign the cookie "${name}": no signing key`);
|
|
601
|
+
wire = sign(name, wire);
|
|
602
|
+
}
|
|
603
|
+
if (options.maxAge !== void 0) maxAge = Math.max(options.maxAge, 0);
|
|
604
|
+
if (options.expires !== void 0) expires = options.expires.toUTCString();
|
|
605
|
+
}
|
|
606
|
+
checkSize(name, wire);
|
|
607
|
+
let line = `${name}=${wire}`;
|
|
608
|
+
if (maxAge !== void 0) line += `; Max-Age=${maxAge}`;
|
|
609
|
+
if (expires !== void 0) line += `; Expires=${expires}`;
|
|
610
|
+
if (attributes.domain !== void 0) line += `; Domain=${attributes.domain}`;
|
|
611
|
+
line += `; Path=${attributes.path}`;
|
|
612
|
+
if (attributes.secure) line += "; Secure";
|
|
613
|
+
if (attributes.httpOnly) line += "; HttpOnly";
|
|
614
|
+
if (attributes.sameSite !== void 0) line += `; SameSite=${SAME_SITE[attributes.sameSite]}`;
|
|
615
|
+
if (attributes.partitioned) line += "; Partitioned";
|
|
616
|
+
if (attributes.priority !== void 0 && value !== void 0) line += `; Priority=${PRIORITY[attributes.priority]}`;
|
|
617
|
+
return {
|
|
618
|
+
key: cookieKey(name, attributes.domain, attributes.path),
|
|
619
|
+
line
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* The `Set-Cookie` line of a cookie with the secure defaults
|
|
624
|
+
* (`Path=/; Secure; HttpOnly; SameSite=Lax`, overridable per cookie), in the order
|
|
625
|
+
* `name=value; Max-Age; Expires; Domain; Path; Secure; HttpOnly; SameSite; Partitioned; Priority`.
|
|
626
|
+
* The value is percent-encoded (see {@link encodeCookieValue}). Signing (`signed: true`) needs the
|
|
627
|
+
* secrets of the plugin: use `ctx.setCookie()` or `HttpPlugin.signCookie()`.
|
|
628
|
+
*
|
|
629
|
+
* ```ts
|
|
630
|
+
* serializeCookie('theme', 'dark', { maxAge: 86_400, httpOnly: false })
|
|
631
|
+
* // theme=dark; Max-Age=86400; Path=/; Secure; SameSite=Lax
|
|
632
|
+
* ```
|
|
633
|
+
*
|
|
634
|
+
* @throws TypeError for an invalid name, value or attribute, a `SameSite=None` or partitioned cookie
|
|
635
|
+
* without `Secure`, a broken `__Secure-` / `__Host-` / `__Http-` / `__Host-Http-` prefix rule.
|
|
636
|
+
* @throws RangeError when `name=value` exceeds 4096 bytes, or `Domain` / `Path` 1024 bytes.
|
|
637
|
+
*/
|
|
638
|
+
function serializeCookie(name, value, options = {}) {
|
|
639
|
+
return serializeResponseCookie({
|
|
640
|
+
name,
|
|
641
|
+
value,
|
|
642
|
+
options
|
|
643
|
+
}, DEFAULT_COOKIE_OPTIONS).line;
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* The `Set-Cookie` line clearing a cookie: `name=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT`
|
|
647
|
+
* plus `Domain`, `Path`, `Secure`, `HttpOnly`, `SameSite` and `Partitioned` (secure defaults merged
|
|
648
|
+
* with `options`), so that a prefixed cookie is cleared with valid attributes.
|
|
649
|
+
*/
|
|
650
|
+
function serializeClearCookie(name, options = {}) {
|
|
651
|
+
return serializeResponseCookie({
|
|
652
|
+
name,
|
|
653
|
+
value: void 0,
|
|
654
|
+
options
|
|
655
|
+
}, DEFAULT_COOKIE_OPTIONS).line;
|
|
656
|
+
}
|
|
657
|
+
/** The jar key of a cookie (RFC 6265: a cookie is identified by its name, domain and path). */
|
|
658
|
+
function cookieKey(name, domain, path) {
|
|
659
|
+
return `${name}\u0000${domain ?? ""}\u0000${path}`;
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* The defaults a cookie takes: all of them, but a `__Host-` cookie (host-only, `Path=/` by
|
|
663
|
+
* definition) never takes a default domain or path. Its own options are still checked.
|
|
664
|
+
*/
|
|
665
|
+
function applicableDefaults(name, defaults) {
|
|
666
|
+
if (!name.toLowerCase().startsWith("__host-")) return defaults;
|
|
667
|
+
const { domain: _domain, path: _path, ...rest } = defaults;
|
|
668
|
+
return rest;
|
|
669
|
+
}
|
|
670
|
+
/** Attributes after the defaults, checked against the cross-attribute and prefix rules. */
|
|
671
|
+
function resolveAttributes(name, options) {
|
|
672
|
+
const domain = typeof options.domain === "string" ? normalizeDomain(name, options.domain) : void 0;
|
|
673
|
+
const path = options.path ?? "/";
|
|
674
|
+
checkPath(name, path);
|
|
675
|
+
const attributes = {
|
|
676
|
+
domain,
|
|
677
|
+
path,
|
|
678
|
+
secure: options.secure === true,
|
|
679
|
+
httpOnly: options.httpOnly === true,
|
|
680
|
+
sameSite: options.sameSite,
|
|
681
|
+
partitioned: options.partitioned === true,
|
|
682
|
+
priority: options.priority
|
|
683
|
+
};
|
|
684
|
+
if (attributes.sameSite === "none" && !attributes.secure) throw invalid(name, "SameSite=None requires secure: true (browsers reject it otherwise)");
|
|
685
|
+
if (attributes.partitioned && !attributes.secure) throw invalid(name, "partitioned cookies require secure: true");
|
|
686
|
+
checkPrefix(name, attributes);
|
|
687
|
+
return attributes;
|
|
688
|
+
}
|
|
689
|
+
/** `__Secure-`, `__Host-`, `__Http-`, `__Host-Http-` (case-insensitive, like browsers). */
|
|
690
|
+
function checkPrefix(name, attributes) {
|
|
691
|
+
const lower = name.toLowerCase();
|
|
692
|
+
const host = lower.startsWith("__host-");
|
|
693
|
+
const http = lower.startsWith("__http-") || lower.startsWith("__host-http-");
|
|
694
|
+
if (!host && !http && !lower.startsWith("__secure-")) return;
|
|
695
|
+
const prefix = prefixName(host, http);
|
|
696
|
+
if (!attributes.secure) throw invalid(name, `the ${prefix} prefix requires secure: true`);
|
|
697
|
+
if (http && !attributes.httpOnly) throw invalid(name, `the ${prefix} prefix requires httpOnly: true`);
|
|
698
|
+
if (host) {
|
|
699
|
+
if (attributes.domain !== void 0) throw invalid(name, `the ${prefix} prefix forbids a domain`);
|
|
700
|
+
if (attributes.path !== "/") throw invalid(name, `the ${prefix} prefix requires path: '/'`);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
function checkName(name) {
|
|
704
|
+
if (!isCookieName(name)) throw new TypeError(`Invalid cookie name ${describe(name)}: expected a token (letters, digits and ! # $ % & ' * + - . ^ _ \` | ~)`);
|
|
705
|
+
}
|
|
706
|
+
function encodeValue(name, value) {
|
|
707
|
+
if (typeof value !== "string") throw invalid(name, `the value must be a string (received ${describe(value)})`);
|
|
708
|
+
if (!value.isWellFormed()) throw invalid(name, "the value contains a lone surrogate");
|
|
709
|
+
return encodeCookieValue(value);
|
|
710
|
+
}
|
|
711
|
+
function checkSize(name, wire) {
|
|
712
|
+
const size = byteLength(name) + 1 + wire.length;
|
|
713
|
+
if (size > 4096) throw new RangeError(`Invalid cookie "${name}": name=value is ${size} bytes on the wire, above the limit of ${MAX_COOKIE_SIZE} (browsers drop it)`);
|
|
714
|
+
}
|
|
715
|
+
/** Checks the type of every option (and rejects unknown keys); returns the options. */
|
|
716
|
+
function checkOptions(name, options, defaults = false) {
|
|
717
|
+
if (options === void 0) return {};
|
|
718
|
+
const what = defaults ? "cookies.defaults" : "options";
|
|
719
|
+
if (options === null || typeof options !== "object" || Array.isArray(options)) throw invalid(name, `${what} must be an object (received ${describe(options)})`);
|
|
720
|
+
for (const key of Object.keys(options)) if (!OPTION_KEYS.has(key)) throw invalid(name, `unknown option "${key}"`);
|
|
721
|
+
const { maxAge, expires, domain, path, secure, httpOnly, sameSite, partitioned, priority, signed } = options;
|
|
722
|
+
if (maxAge !== void 0 && !(typeof maxAge === "number" && Number.isSafeInteger(maxAge))) throw invalid(name, `maxAge must be an integer number of seconds (received ${describe(maxAge)})`);
|
|
723
|
+
if (expires !== void 0 && !(expires instanceof Date && !Number.isNaN(expires.getTime()))) throw invalid(name, `expires must be a valid Date (received ${describe(expires)})`);
|
|
724
|
+
if (expires !== void 0) {
|
|
725
|
+
const year = expires.getUTCFullYear();
|
|
726
|
+
if (year < MIN_EXPIRES_YEAR || year > MAX_EXPIRES_YEAR) throw invalid(name, `expires must be in the years ${MIN_EXPIRES_YEAR} to ${MAX_EXPIRES_YEAR} (received ${expires.toISOString()}): browsers ignore other dates; use maxAge for a long lifetime (browsers cap it at 400 days)`);
|
|
727
|
+
}
|
|
728
|
+
if (domain !== void 0 && domain !== null && typeof domain !== "string") throw invalid(name, `domain must be a string or null (received ${describe(domain)})`);
|
|
729
|
+
if (path !== void 0 && typeof path !== "string") throw invalid(name, `path must be a string (received ${describe(path)})`);
|
|
730
|
+
for (const [key, flag] of [
|
|
731
|
+
["secure", secure],
|
|
732
|
+
["httpOnly", httpOnly],
|
|
733
|
+
["partitioned", partitioned],
|
|
734
|
+
["signed", signed]
|
|
735
|
+
]) if (flag !== void 0 && typeof flag !== "boolean") throw invalid(name, `${key} must be a boolean (received ${describe(flag)})`);
|
|
736
|
+
if (sameSite !== void 0 && !Object.hasOwn(SAME_SITE, sameSite)) throw invalid(name, `sameSite must be 'strict', 'lax' or 'none' (received ${describe(sameSite)})`);
|
|
737
|
+
if (priority !== void 0 && !Object.hasOwn(PRIORITY, priority)) throw invalid(name, `priority must be 'low', 'medium' or 'high' (received ${describe(priority)})`);
|
|
738
|
+
return options;
|
|
739
|
+
}
|
|
740
|
+
/** Lower-cased, one leading `.` removed; an ASCII host name of at most 253 characters. */
|
|
741
|
+
function normalizeDomain(name, domain) {
|
|
742
|
+
const normalized = (domain.startsWith(".") ? domain.slice(1) : domain).toLowerCase();
|
|
743
|
+
if (normalized.length > 1024) throw new RangeError(`Invalid cookie "${name}": the domain exceeds ${MAX_COOKIE_ATTRIBUTE_SIZE} bytes`);
|
|
744
|
+
if (!isHostName(normalized)) throw invalid(name, `invalid domain ${describe(domain)}: expected an ASCII host name (letters, digits, -, dot-separated labels; IDNs in punycode)`);
|
|
745
|
+
return normalized;
|
|
746
|
+
}
|
|
747
|
+
function isHostName(domain) {
|
|
748
|
+
if (domain === "" || domain.length > MAX_DOMAIN_LENGTH) return false;
|
|
749
|
+
let label = 0;
|
|
750
|
+
for (let index = 0; index < domain.length; index++) {
|
|
751
|
+
const code = domain.charCodeAt(index);
|
|
752
|
+
if (code === 46) {
|
|
753
|
+
if (label === 0) return false;
|
|
754
|
+
label = 0;
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
if (!(code >= 48 && code <= 57 || code >= 97 && code <= 122 || code === 45)) return false;
|
|
758
|
+
label++;
|
|
759
|
+
}
|
|
760
|
+
return label > 0;
|
|
761
|
+
}
|
|
762
|
+
/** Starts with `/`, `%x20-7E` except `;`, at most 1024 bytes. */
|
|
763
|
+
function checkPath(name, path) {
|
|
764
|
+
if (path.length > 1024) throw new RangeError(`Invalid cookie "${name}": the path exceeds ${MAX_COOKIE_ATTRIBUTE_SIZE} bytes`);
|
|
765
|
+
let valid = path.startsWith("/");
|
|
766
|
+
for (let index = 0; valid && index < path.length; index++) {
|
|
767
|
+
const code = path.charCodeAt(index);
|
|
768
|
+
valid = code >= 32 && code <= 126 && code !== 59;
|
|
769
|
+
}
|
|
770
|
+
if (!valid) throw invalid(name, `invalid path ${describe(path)}: expected "/..." made of visible ASCII characters without ";"`);
|
|
771
|
+
}
|
|
772
|
+
/** The options without `undefined` values (they would erase defaults when spread). */
|
|
773
|
+
function definedOnly(options) {
|
|
774
|
+
const result = {};
|
|
775
|
+
for (const key of Object.keys(options)) {
|
|
776
|
+
const value = options[key];
|
|
777
|
+
if (value !== void 0) result[key] = value;
|
|
778
|
+
}
|
|
779
|
+
return result;
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Parses a `Set-Cookie` line (attribute names are case-insensitive; the value is decoded like a
|
|
783
|
+
* request cookie). Used by `inject()` to fill `response.cookies`.
|
|
784
|
+
*
|
|
785
|
+
* @throws TypeError when the line has no `name=value` pair.
|
|
786
|
+
*/
|
|
787
|
+
function parseSetCookie(line) {
|
|
788
|
+
const parts = String(line).split(";");
|
|
789
|
+
const pair = parts[0];
|
|
790
|
+
const equals = pair.indexOf("=");
|
|
791
|
+
const name = equals === -1 ? "" : trim(pair, 0, equals);
|
|
792
|
+
if (name === "") throw new TypeError(`Invalid Set-Cookie line ${describe(line)}: missing name=value`);
|
|
793
|
+
const info = {
|
|
794
|
+
name,
|
|
795
|
+
value: decodeCookieValue(unquote(trim(pair, equals + 1, pair.length))),
|
|
796
|
+
raw: line,
|
|
797
|
+
secure: false,
|
|
798
|
+
httpOnly: false,
|
|
799
|
+
partitioned: false
|
|
800
|
+
};
|
|
801
|
+
for (const part of parts.slice(1)) {
|
|
802
|
+
const separator = part.indexOf("=");
|
|
803
|
+
const key = trim(part, 0, separator === -1 ? part.length : separator).toLowerCase();
|
|
804
|
+
const value = separator === -1 ? "" : trim(part, separator + 1, part.length);
|
|
805
|
+
switch (key) {
|
|
806
|
+
case "max-age": {
|
|
807
|
+
const seconds = /^-?\d+$/.test(value) ? Number(value) : NaN;
|
|
808
|
+
if (Number.isSafeInteger(seconds)) info.maxAge = seconds;
|
|
809
|
+
break;
|
|
810
|
+
}
|
|
811
|
+
case "expires": {
|
|
812
|
+
const date = new Date(value);
|
|
813
|
+
if (!Number.isNaN(date.getTime())) info.expires = date;
|
|
814
|
+
break;
|
|
815
|
+
}
|
|
816
|
+
case "domain":
|
|
817
|
+
if (value !== "") info.domain = (value.startsWith(".") ? value.slice(1) : value).toLowerCase();
|
|
818
|
+
break;
|
|
819
|
+
case "path":
|
|
820
|
+
if (value.startsWith("/")) info.path = value;
|
|
821
|
+
break;
|
|
822
|
+
case "secure":
|
|
823
|
+
info.secure = true;
|
|
824
|
+
break;
|
|
825
|
+
case "httponly":
|
|
826
|
+
info.httpOnly = true;
|
|
827
|
+
break;
|
|
828
|
+
case "partitioned":
|
|
829
|
+
info.partitioned = true;
|
|
830
|
+
break;
|
|
831
|
+
case "samesite": {
|
|
832
|
+
const sameSite = value.toLowerCase();
|
|
833
|
+
if (sameSite === "strict" || sameSite === "lax" || sameSite === "none") info.sameSite = sameSite;
|
|
834
|
+
break;
|
|
835
|
+
}
|
|
836
|
+
case "priority": {
|
|
837
|
+
const priority = value.toLowerCase();
|
|
838
|
+
if (priority === "low" || priority === "medium" || priority === "high") info.priority = priority;
|
|
839
|
+
break;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
return Object.freeze(info);
|
|
844
|
+
}
|
|
845
|
+
/** The `set-cookie` lines of a response parsed by name (the last one wins), prototype-less and frozen. */
|
|
846
|
+
function parseSetCookies(lines) {
|
|
847
|
+
const result = Object.create(null);
|
|
848
|
+
const list = typeof lines === "string" ? [lines] : lines ?? [];
|
|
849
|
+
for (const line of list) try {
|
|
850
|
+
const info = parseSetCookie(line);
|
|
851
|
+
result[info.name] = info;
|
|
852
|
+
} catch {}
|
|
853
|
+
return Object.freeze(result);
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* The jar key of a cookie as given (`HttpResponse.withCookie()`, before the defaults are known):
|
|
857
|
+
* an absent domain or path is keyed as "default", a `null` domain as host-only.
|
|
858
|
+
*/
|
|
859
|
+
function givenCookieKey(name, options) {
|
|
860
|
+
const given = options.domain;
|
|
861
|
+
let domain;
|
|
862
|
+
if (given === void 0) domain = "";
|
|
863
|
+
else if (given === null) domain = "";
|
|
864
|
+
else domain = (given.startsWith(".") ? given.slice(1) : given).toLowerCase();
|
|
865
|
+
return `${name}\u0000${domain}\u0000${options.path ?? ""}`;
|
|
866
|
+
}
|
|
867
|
+
function prefixName(host, http) {
|
|
868
|
+
if (host) return http ? "__Host-Http-" : "__Host-";
|
|
869
|
+
return http ? "__Http-" : "__Secure-";
|
|
870
|
+
}
|
|
871
|
+
function invalid(name, reason) {
|
|
872
|
+
return /* @__PURE__ */ new TypeError(`Invalid cookie "${name}": ${reason}`);
|
|
873
|
+
}
|
|
874
|
+
function byteLength(text) {
|
|
875
|
+
let size = 0;
|
|
876
|
+
for (let index = 0; index < text.length; index++) {
|
|
877
|
+
const code = text.charCodeAt(index);
|
|
878
|
+
if (code < 128) size += 1;
|
|
879
|
+
else if (code < 2048 || code >= 55296 && code <= 57343) size += 2;
|
|
880
|
+
else size += 3;
|
|
881
|
+
}
|
|
882
|
+
return size;
|
|
883
|
+
}
|
|
884
|
+
function describe(value) {
|
|
885
|
+
if (typeof value === "string") return JSON.stringify(value.length > 64 ? `${value.slice(0, 64)}…` : value);
|
|
886
|
+
if (value instanceof Date) return "an invalid Date";
|
|
887
|
+
if (value === null) return "null";
|
|
888
|
+
if (typeof value === "object") return "an object";
|
|
889
|
+
return typeof value === "bigint" ? `${value}n` : String(value);
|
|
890
|
+
}
|
|
891
|
+
// src/adapters/shared.ts
|
|
892
|
+
/**
|
|
893
|
+
* Applies the defaults of {@link InjectRequest}: query merged in the URL, cookies appended to the
|
|
894
|
+
* `cookie` header (encoded values joined with `; `), body encoding and content type (`FormData` →
|
|
895
|
+
* `multipart/form-data`, `Blob` → its type).
|
|
896
|
+
*/
|
|
897
|
+
async function normalizeInject(request) {
|
|
898
|
+
if (request === null || typeof request !== "object") throw new TypeError("inject() expects a request object");
|
|
899
|
+
if (typeof request.url !== "string" || !request.url.startsWith("/")) throw new TypeError(`inject() expects a url starting with "/" (received ${JSON.stringify(request.url)})`);
|
|
900
|
+
const method = (request.method ?? "GET").toUpperCase();
|
|
901
|
+
let url = request.url;
|
|
902
|
+
if (request.query !== void 0) {
|
|
903
|
+
const search = new URLSearchParams();
|
|
904
|
+
for (const [key, value] of Object.entries(request.query)) if (typeof value === "string") search.append(key, value);
|
|
905
|
+
else for (const item of value) search.append(key, item);
|
|
906
|
+
const query = search.toString();
|
|
907
|
+
if (query !== "") url += `${url.includes("?") ? "&" : "?"}${query}`;
|
|
908
|
+
}
|
|
909
|
+
const headers = {};
|
|
910
|
+
for (const [name, value] of Object.entries(request.headers ?? {})) headers[name.toLowerCase()] = value;
|
|
911
|
+
if (request.signedCookies !== void 0) throw new TypeError("inject(): signedCookies are signed by HttpPlugin.inject() (adapters only accept cookies)");
|
|
912
|
+
const cookie = cookieHeader(request.cookies);
|
|
913
|
+
if (cookie !== "") headers["cookie"] = headers["cookie"] === void 0 ? cookie : `${headers["cookie"]}; ${cookie}`;
|
|
914
|
+
const body = request.body;
|
|
915
|
+
let payload;
|
|
916
|
+
if (body === void 0) payload = void 0;
|
|
917
|
+
else if (typeof body === "string") {
|
|
918
|
+
payload = body;
|
|
919
|
+
headers["content-type"] ??= "text/plain; charset=utf-8";
|
|
920
|
+
} else if (body instanceof Uint8Array || body instanceof ArrayBuffer) {
|
|
921
|
+
payload = body instanceof Uint8Array ? body : new Uint8Array(body);
|
|
922
|
+
headers["content-type"] ??= "application/octet-stream";
|
|
923
|
+
} else if (body instanceof FormData) {
|
|
924
|
+
const encoded = new Response(body);
|
|
925
|
+
const type = encoded.headers.get("content-type");
|
|
926
|
+
payload = new Uint8Array(await encoded.arrayBuffer());
|
|
927
|
+
headers["content-type"] = type ?? encoded.headers.get("content-type") ?? "multipart/form-data";
|
|
928
|
+
} else if (body instanceof Blob) {
|
|
929
|
+
payload = new Uint8Array(await body.arrayBuffer());
|
|
930
|
+
headers["content-type"] ??= body.type === "" ? "application/octet-stream" : body.type;
|
|
931
|
+
} else {
|
|
932
|
+
payload = JSON.stringify(body);
|
|
933
|
+
headers["content-type"] ??= "application/json";
|
|
934
|
+
}
|
|
935
|
+
return {
|
|
936
|
+
method,
|
|
937
|
+
url,
|
|
938
|
+
headers,
|
|
939
|
+
payload
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
/** The `cookie` header of inject cookies: `name=encoded` pairs joined with `; `. */
|
|
943
|
+
function cookieHeader(cookies) {
|
|
944
|
+
if (cookies === void 0) return "";
|
|
945
|
+
if (cookies === null || typeof cookies !== "object") throw new TypeError("inject(): cookies must be an object");
|
|
946
|
+
const pairs = [];
|
|
947
|
+
for (const name of Object.keys(cookies)) {
|
|
948
|
+
if (!isRequestCookieName(name)) throw new TypeError(`inject(): invalid cookie name ${JSON.stringify(name)} (empty, or with ";", "=", spaces or controls)`);
|
|
949
|
+
const value = cookies[name];
|
|
950
|
+
if (typeof value !== "string") throw new TypeError(`inject(): the value of the cookie "${name}" must be a string`);
|
|
951
|
+
pairs.push(`${name}=${encodeCookieValue(value)}`);
|
|
952
|
+
}
|
|
953
|
+
return pairs.join("; ");
|
|
954
|
+
}
|
|
955
|
+
/** A name a `cookie` header can carry: no `;`, `=`, whitespace or controls, Latin-1. */
|
|
956
|
+
function isRequestCookieName(name) {
|
|
957
|
+
if (name === "") return false;
|
|
958
|
+
for (let index = 0; index < name.length; index++) {
|
|
959
|
+
const code = name.charCodeAt(index);
|
|
960
|
+
if (code <= 32 || code === 59 || code === 61 || code === 127 || code > 255) return false;
|
|
961
|
+
}
|
|
962
|
+
return true;
|
|
963
|
+
}
|
|
964
|
+
const DECODER = new TextDecoder();
|
|
965
|
+
/**
|
|
966
|
+
* Normalises response headers: lower-case names, repeated values joined with `, `, except
|
|
967
|
+
* `set-cookie` (one entry per cookie: an `Expires` attribute contains `, `).
|
|
968
|
+
*/
|
|
969
|
+
function injectHeaders(headers) {
|
|
970
|
+
const normalized = {};
|
|
971
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
972
|
+
if (value === void 0) continue;
|
|
973
|
+
const key = name.toLowerCase();
|
|
974
|
+
if (key === "set-cookie") normalized[key] = typeof value === "object" ? [...value] : [String(value)];
|
|
975
|
+
else normalized[key] = typeof value === "object" ? value.join(", ") : String(value);
|
|
976
|
+
}
|
|
977
|
+
return normalized;
|
|
978
|
+
}
|
|
979
|
+
/** Whether a `content-type` is `text/event-stream`. */
|
|
980
|
+
function isEventStreamType(contentType) {
|
|
981
|
+
if (typeof contentType !== "string") return false;
|
|
982
|
+
const semicolon = contentType.indexOf(";");
|
|
983
|
+
return (semicolon === -1 ? contentType : contentType.slice(0, semicolon)).trim().toLowerCase() === "text/event-stream";
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* `inject()` on top of `injectStream()`: reads the body to its end, except for an event stream
|
|
987
|
+
* (which would never end): its body is cancelled (the server sees a disconnect) and the call
|
|
988
|
+
* rejects with a hint.
|
|
989
|
+
*/
|
|
990
|
+
async function bufferInjectStream(injectStream, request) {
|
|
991
|
+
const response = await injectStream(request);
|
|
992
|
+
const method = (request.method ?? "GET").toUpperCase();
|
|
993
|
+
if (method !== "HEAD" && isEventStreamType(response.headers["content-type"])) {
|
|
994
|
+
await response.body.cancel().catch(() => void 0);
|
|
995
|
+
throw new TypeError(`inject(): ${method} ${request.url} answered an event stream (text/event-stream): use injectSse() or injectStream()`);
|
|
996
|
+
}
|
|
997
|
+
const reader = response.body.getReader();
|
|
998
|
+
const chunks = [];
|
|
999
|
+
let size = 0;
|
|
1000
|
+
for (;;) {
|
|
1001
|
+
const { done, value } = await reader.read();
|
|
1002
|
+
if (done) break;
|
|
1003
|
+
size += value.byteLength;
|
|
1004
|
+
chunks.push(value);
|
|
1005
|
+
}
|
|
1006
|
+
const bytes = concat(chunks, size);
|
|
1007
|
+
const body = DECODER.decode(bytes);
|
|
1008
|
+
return {
|
|
1009
|
+
status: response.status,
|
|
1010
|
+
headers: response.headers,
|
|
1011
|
+
body,
|
|
1012
|
+
bytes,
|
|
1013
|
+
cookies: response.cookies,
|
|
1014
|
+
json: () => JSON.parse(body)
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
/** Builds an {@link InjectStreamResponse}. */
|
|
1018
|
+
function injectStreamResponse(status, headers, body) {
|
|
1019
|
+
const normalized = injectHeaders(headers);
|
|
1020
|
+
return {
|
|
1021
|
+
status,
|
|
1022
|
+
headers: normalized,
|
|
1023
|
+
cookies: parseSetCookies(normalized["set-cookie"]),
|
|
1024
|
+
body
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
/** Builds an {@link HttpServerAddress} from a bound address. */
|
|
1028
|
+
function serverAddress(address, port, family) {
|
|
1029
|
+
const ipv6 = family === "IPv6" || family === 6 || address.includes(":");
|
|
1030
|
+
let client = address;
|
|
1031
|
+
if (address === "0.0.0.0") client = "127.0.0.1";
|
|
1032
|
+
else if (address === "::") client = "::1";
|
|
1033
|
+
const url = `http://${client.includes(":") ? `[${client}]` : client}:${port}`;
|
|
1034
|
+
return Object.freeze({
|
|
1035
|
+
host: address,
|
|
1036
|
+
port,
|
|
1037
|
+
family: ipv6 ? "IPv6" : "IPv4",
|
|
1038
|
+
url,
|
|
1039
|
+
toString: () => url
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
/** The 413 error of a body above `limit` bytes. */
|
|
1043
|
+
function payloadTooLarge(limit) {
|
|
1044
|
+
return new PayloadTooLargeException(`Request body exceeds the limit of ${limit} bytes`, void 0, { headers: { connection: "close" } });
|
|
1045
|
+
}
|
|
1046
|
+
/** Rejects early when `content-length` announces more than `limit` bytes. */
|
|
1047
|
+
function exceedsDeclaredLength(contentLength, limit) {
|
|
1048
|
+
if (contentLength === void 0) return false;
|
|
1049
|
+
const length = Number(contentLength);
|
|
1050
|
+
return Number.isFinite(length) && length > limit;
|
|
1051
|
+
}
|
|
1052
|
+
/** Bytes read (and discarded) from an abandoned Web request body before it is cancelled. */
|
|
1053
|
+
const MAX_DISCARD_BYTES = 67108864;
|
|
1054
|
+
/**
|
|
1055
|
+
* The body of a Web request as a pull-based stream enforcing `limit` (see
|
|
1056
|
+
* `AdapterRequest.streamBody`). `body` is only read when needed (a Node.js-backed request body
|
|
1057
|
+
* starts buffering once accessed). On overflow or cancellation the rest of the body is read and
|
|
1058
|
+
* discarded in the background (up to 64 MiB, then cancelled), so the server can write the response
|
|
1059
|
+
* and keep the connection usable instead of resetting it.
|
|
1060
|
+
*/
|
|
1061
|
+
function webBodyStream(body, limit, contentLength) {
|
|
1062
|
+
if (exceedsDeclaredLength(contentLength, limit)) return failedStream(payloadTooLarge(limit));
|
|
1063
|
+
const source = body();
|
|
1064
|
+
if (source === null) return new ReadableStream({ start: (controller) => controller.close() });
|
|
1065
|
+
const reader = source.getReader();
|
|
1066
|
+
let size = 0;
|
|
1067
|
+
let abandoned = false;
|
|
1068
|
+
const abandon = () => {
|
|
1069
|
+
if (abandoned) return;
|
|
1070
|
+
abandoned = true;
|
|
1071
|
+
discardReader(reader);
|
|
1072
|
+
};
|
|
1073
|
+
return new ReadableStream({
|
|
1074
|
+
async pull(controller) {
|
|
1075
|
+
let result;
|
|
1076
|
+
try {
|
|
1077
|
+
result = await reader.read();
|
|
1078
|
+
} catch (error) {
|
|
1079
|
+
abandoned = true;
|
|
1080
|
+
throw new BadRequestException("Failed to read the request body", void 0, { cause: error });
|
|
1081
|
+
}
|
|
1082
|
+
if (result.done) {
|
|
1083
|
+
abandoned = true;
|
|
1084
|
+
controller.close();
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
size += result.value.byteLength;
|
|
1088
|
+
if (size > limit) {
|
|
1089
|
+
abandon();
|
|
1090
|
+
throw payloadTooLarge(limit);
|
|
1091
|
+
}
|
|
1092
|
+
controller.enqueue(result.value);
|
|
1093
|
+
},
|
|
1094
|
+
cancel: abandon
|
|
1095
|
+
}, { highWaterMark: 0 });
|
|
1096
|
+
}
|
|
1097
|
+
/** Reads and drops the rest of a request body in the background, cancelling it past 64 MiB. */
|
|
1098
|
+
function discardReader(reader) {
|
|
1099
|
+
let discarded = 0;
|
|
1100
|
+
const next = () => {
|
|
1101
|
+
reader.read().then((result) => {
|
|
1102
|
+
if (result.done) return;
|
|
1103
|
+
discarded += result.value?.byteLength ?? 0;
|
|
1104
|
+
if (discarded > MAX_DISCARD_BYTES) reader.cancel().catch(() => void 0);
|
|
1105
|
+
else next();
|
|
1106
|
+
}, () => void 0);
|
|
1107
|
+
};
|
|
1108
|
+
next();
|
|
1109
|
+
}
|
|
1110
|
+
/** Reads a Web request body fully, enforcing `limit` (see {@link webBodyStream}). */
|
|
1111
|
+
async function readWebStream(body, limit, contentLength) {
|
|
1112
|
+
const reader = webBodyStream(body, limit, contentLength).getReader();
|
|
1113
|
+
const chunks = [];
|
|
1114
|
+
let size = 0;
|
|
1115
|
+
for (;;) {
|
|
1116
|
+
const { done, value } = await reader.read();
|
|
1117
|
+
if (done) break;
|
|
1118
|
+
size += value.byteLength;
|
|
1119
|
+
chunks.push(value);
|
|
1120
|
+
}
|
|
1121
|
+
return concat(chunks, size);
|
|
1122
|
+
}
|
|
1123
|
+
/** Concatenates chunks (no copy for a single chunk). */
|
|
1124
|
+
function concat(chunks, size) {
|
|
1125
|
+
if (chunks.length === 0) return /* @__PURE__ */ new Uint8Array(0);
|
|
1126
|
+
if (chunks.length === 1) return chunks[0];
|
|
1127
|
+
const result = new Uint8Array(size);
|
|
1128
|
+
let offset = 0;
|
|
1129
|
+
for (const chunk of chunks) {
|
|
1130
|
+
result.set(chunk, offset);
|
|
1131
|
+
offset += chunk.byteLength;
|
|
1132
|
+
}
|
|
1133
|
+
return result;
|
|
1134
|
+
}
|
|
1135
|
+
let lightMyRequest;
|
|
1136
|
+
/** Loads `light-my-request`, with an actionable error when it is not installed. */
|
|
1137
|
+
function loadLightMyRequest(adapter) {
|
|
1138
|
+
lightMyRequest ??= import("light-my-request").then((module) => {
|
|
1139
|
+
const loaded = module;
|
|
1140
|
+
return loaded.default ?? loaded;
|
|
1141
|
+
}, (error) => {
|
|
1142
|
+
lightMyRequest = void 0;
|
|
1143
|
+
throw new Error(`inject() with the ${adapter} adapter requires the optional peer dependency "light-my-request" (bun add -d light-my-request / npm i -D light-my-request)`, { cause: error });
|
|
1144
|
+
});
|
|
1145
|
+
return lightMyRequest;
|
|
1146
|
+
}
|
|
1147
|
+
/**
|
|
1148
|
+
* The light-my-request options of a normalised inject request (streaming mode). The signal is
|
|
1149
|
+
* handled by {@link lightStreamResponse} (light-my-request would destroy its fake request with an
|
|
1150
|
+
* unhandled error); an already aborted signal rejects at once, like `fetch()`.
|
|
1151
|
+
*/
|
|
1152
|
+
async function lightInjectOptions(request) {
|
|
1153
|
+
request.signal?.throwIfAborted();
|
|
1154
|
+
const normalized = await normalizeInject(request);
|
|
1155
|
+
return {
|
|
1156
|
+
method: normalized.method,
|
|
1157
|
+
url: normalized.url,
|
|
1158
|
+
headers: normalized.headers,
|
|
1159
|
+
remoteAddress: "127.0.0.1",
|
|
1160
|
+
payloadAsStream: true,
|
|
1161
|
+
...normalized.payload === void 0 ? {} : { payload: toBuffer(normalized.payload) }
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
/** Runs a request through a Node.js request listener with `light-my-request`, streaming the body. */
|
|
1165
|
+
async function injectNodeStream(adapter, dispatch, request) {
|
|
1166
|
+
const options = await lightInjectOptions(request);
|
|
1167
|
+
const { inject } = await loadLightMyRequest(adapter);
|
|
1168
|
+
return lightStreamResponse(await inject(dispatch, options), request.signal);
|
|
1169
|
+
}
|
|
1170
|
+
/**
|
|
1171
|
+
* An {@link InjectStreamResponse} of a light-my-request streaming response. Cancelling the body
|
|
1172
|
+
* (or aborting `signal`) destroys the server response: the server sees a client disconnect.
|
|
1173
|
+
*/
|
|
1174
|
+
function lightStreamResponse(response, signal) {
|
|
1175
|
+
const readable = response.stream();
|
|
1176
|
+
const disconnect = () => {
|
|
1177
|
+
response.raw.res.destroy();
|
|
1178
|
+
readable.destroy();
|
|
1179
|
+
};
|
|
1180
|
+
let finished = false;
|
|
1181
|
+
const body = new ReadableStream({
|
|
1182
|
+
start(controller) {
|
|
1183
|
+
readable.on("data", (chunk) => {
|
|
1184
|
+
if (finished) return;
|
|
1185
|
+
controller.enqueue(new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength));
|
|
1186
|
+
if ((controller.desiredSize ?? 0) <= 0) readable.pause();
|
|
1187
|
+
});
|
|
1188
|
+
readable.once("end", () => {
|
|
1189
|
+
if (finished) return;
|
|
1190
|
+
finished = true;
|
|
1191
|
+
controller.close();
|
|
1192
|
+
});
|
|
1193
|
+
readable.once("error", (error) => {
|
|
1194
|
+
if (finished) return;
|
|
1195
|
+
finished = true;
|
|
1196
|
+
controller.error(error);
|
|
1197
|
+
});
|
|
1198
|
+
},
|
|
1199
|
+
pull() {
|
|
1200
|
+
if (readable.isPaused()) readable.resume();
|
|
1201
|
+
},
|
|
1202
|
+
cancel() {
|
|
1203
|
+
finished = true;
|
|
1204
|
+
disconnect();
|
|
1205
|
+
}
|
|
1206
|
+
}, { highWaterMark: 0 });
|
|
1207
|
+
if (signal !== void 0) {
|
|
1208
|
+
if (signal.aborted) disconnect();
|
|
1209
|
+
else signal.addEventListener("abort", disconnect, { once: true });
|
|
1210
|
+
}
|
|
1211
|
+
return injectStreamResponse(response.statusCode, response.headers, body);
|
|
1212
|
+
}
|
|
1213
|
+
/** A `Buffer` view of bytes (tools such as light-my-request JSON-encode other objects). */
|
|
1214
|
+
function toBuffer(payload) {
|
|
1215
|
+
return typeof payload === "string" ? payload : Buffer.from(payload.buffer, payload.byteOffset, payload.byteLength);
|
|
1216
|
+
}
|
|
1217
|
+
/**
|
|
1218
|
+
* Runs a request through a Fetch handler (`(request: Request) => Promise<Response>`), streaming
|
|
1219
|
+
* the body. Cancelling the body (or aborting `signal`) aborts the request signal and cancels the
|
|
1220
|
+
* response body.
|
|
1221
|
+
*/
|
|
1222
|
+
async function injectFetchStream(fetch, request) {
|
|
1223
|
+
const signal = request.signal;
|
|
1224
|
+
signal?.throwIfAborted();
|
|
1225
|
+
const normalized = await normalizeInject(request);
|
|
1226
|
+
const abort = new AbortController();
|
|
1227
|
+
signal?.addEventListener("abort", () => abort.abort(signal.reason), { once: true });
|
|
1228
|
+
const init = {
|
|
1229
|
+
method: normalized.method,
|
|
1230
|
+
headers: normalized.headers,
|
|
1231
|
+
signal: abort.signal
|
|
1232
|
+
};
|
|
1233
|
+
if (normalized.payload !== void 0 && normalized.method !== "GET" && normalized.method !== "HEAD") init.body = normalized.payload;
|
|
1234
|
+
const response = await fetch(new Request(`http://localhost${normalized.url}`, init));
|
|
1235
|
+
const headers = {};
|
|
1236
|
+
for (const [name, value] of response.headers) if (name !== "set-cookie") headers[name] = value;
|
|
1237
|
+
const cookies = response.headers.getSetCookie();
|
|
1238
|
+
if (cookies.length > 0) headers["set-cookie"] = cookies;
|
|
1239
|
+
const reader = response.body?.getReader();
|
|
1240
|
+
const body = new ReadableStream({
|
|
1241
|
+
async pull(controller) {
|
|
1242
|
+
if (reader === void 0) {
|
|
1243
|
+
controller.close();
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
const result = await reader.read();
|
|
1247
|
+
if (result.done) controller.close();
|
|
1248
|
+
else controller.enqueue(result.value);
|
|
1249
|
+
},
|
|
1250
|
+
async cancel(reason) {
|
|
1251
|
+
abort.abort(reason);
|
|
1252
|
+
await reader?.cancel(reason).catch(() => void 0);
|
|
1253
|
+
}
|
|
1254
|
+
}, { highWaterMark: 0 });
|
|
1255
|
+
abort.signal.addEventListener("abort", () => reader?.cancel(abort.signal.reason).catch(() => void 0), { once: true });
|
|
1256
|
+
return injectStreamResponse(response.status, headers, body);
|
|
1257
|
+
}
|
|
1258
|
+
// src/adapters/node-io.ts
|
|
1259
|
+
/** Bytes buffered by a request body stream before the request is paused. */
|
|
1260
|
+
const BODY_HIGH_WATER_MARK = 65536;
|
|
1261
|
+
/** Request headers with array values (only `set-cookie` in Node.js) joined with `, `. */
|
|
1262
|
+
function normalizeNodeHeaders(headers) {
|
|
1263
|
+
for (const name in headers) if (typeof headers[name] !== "string") return copyHeaders(headers);
|
|
1264
|
+
return headers;
|
|
1265
|
+
}
|
|
1266
|
+
function copyHeaders(headers) {
|
|
1267
|
+
const result = {};
|
|
1268
|
+
for (const name in headers) {
|
|
1269
|
+
const value = headers[name];
|
|
1270
|
+
result[name] = Array.isArray(value) ? value.join(", ") : value;
|
|
1271
|
+
}
|
|
1272
|
+
return result;
|
|
1273
|
+
}
|
|
1274
|
+
/**
|
|
1275
|
+
* Whether the body was already read by someone else: an Express body parser (`express.json()`...)
|
|
1276
|
+
* marks the request with `_body`; otherwise a declared body that already ended.
|
|
1277
|
+
*/
|
|
1278
|
+
function consumedElsewhere(request) {
|
|
1279
|
+
if (request._body === true) return true;
|
|
1280
|
+
if (!request.readableEnded) return false;
|
|
1281
|
+
const length = request.headers["content-length"];
|
|
1282
|
+
return request.headers["transfer-encoding"] !== void 0 || length !== void 0 && length.trim() !== "0";
|
|
1283
|
+
}
|
|
1284
|
+
/** The error of a body consumed before Znight read it (an empty body would be silently wrong). */
|
|
1285
|
+
function consumedError() {
|
|
1286
|
+
return /* @__PURE__ */ new Error("The request body was already read by a middleware (an Express body parser such as express.json()?): Znight reads the bodies of its routes itself, do not register body parsers for them");
|
|
1287
|
+
}
|
|
1288
|
+
/** Reads a Node.js request body, enforcing `limit` (on `content-length`, then while streaming). */
|
|
1289
|
+
function readNodeBody(request, limit) {
|
|
1290
|
+
if (exceedsDeclaredLength(request.headers["content-length"], limit)) {
|
|
1291
|
+
request.resume();
|
|
1292
|
+
return Promise.reject(payloadTooLarge(limit));
|
|
1293
|
+
}
|
|
1294
|
+
if (consumedElsewhere(request)) return Promise.reject(consumedError());
|
|
1295
|
+
if (request.readableEnded) return Promise.resolve(/* @__PURE__ */ new Uint8Array(0));
|
|
1296
|
+
if (request.destroyed) return Promise.reject(new BadRequestException("The request was aborted"));
|
|
1297
|
+
return new Promise((resolve, reject) => {
|
|
1298
|
+
const chunks = [];
|
|
1299
|
+
let size = 0;
|
|
1300
|
+
const cleanup = () => {
|
|
1301
|
+
request.off("data", onData);
|
|
1302
|
+
request.off("end", onEnd);
|
|
1303
|
+
request.off("error", onError);
|
|
1304
|
+
request.off("close", onClose);
|
|
1305
|
+
};
|
|
1306
|
+
const onData = (chunk) => {
|
|
1307
|
+
const bytes = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
1308
|
+
size += bytes.byteLength;
|
|
1309
|
+
if (size > limit) {
|
|
1310
|
+
cleanup();
|
|
1311
|
+
request.resume();
|
|
1312
|
+
reject(payloadTooLarge(limit));
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
chunks.push(bytes);
|
|
1316
|
+
};
|
|
1317
|
+
const onEnd = () => {
|
|
1318
|
+
cleanup();
|
|
1319
|
+
resolve(concat(chunks, size));
|
|
1320
|
+
};
|
|
1321
|
+
const onError = (error) => {
|
|
1322
|
+
cleanup();
|
|
1323
|
+
reject(new BadRequestException("Failed to read the request body", void 0, { cause: error }));
|
|
1324
|
+
};
|
|
1325
|
+
const onClose = () => {
|
|
1326
|
+
if (request.complete) return;
|
|
1327
|
+
cleanup();
|
|
1328
|
+
reject(new BadRequestException("The request was aborted"));
|
|
1329
|
+
};
|
|
1330
|
+
request.on("data", onData);
|
|
1331
|
+
request.on("end", onEnd);
|
|
1332
|
+
request.on("error", onError);
|
|
1333
|
+
request.on("close", onClose);
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
/**
|
|
1337
|
+
* A Node.js request body as a Web stream (see `AdapterRequest.streamBody`): the request is paused
|
|
1338
|
+
* while the consumer does not read (backpressure); past `limit` bytes the stream errors with 413 and
|
|
1339
|
+
* the rest of the body is discarded (`request.resume()`), like when the consumer cancels the stream,
|
|
1340
|
+
* so the response can still be written (it carries `connection: close`).
|
|
1341
|
+
*/
|
|
1342
|
+
function nodeBodyStream(request, limit) {
|
|
1343
|
+
if (exceedsDeclaredLength(request.headers["content-length"], limit)) {
|
|
1344
|
+
request.resume();
|
|
1345
|
+
return failedStream(payloadTooLarge(limit));
|
|
1346
|
+
}
|
|
1347
|
+
if (consumedElsewhere(request)) return failedStream(consumedError());
|
|
1348
|
+
if (request.readableEnded) return new ReadableStream({ start: (controller) => controller.close() });
|
|
1349
|
+
if (request.destroyed) return failedStream(new BadRequestException("The request was aborted"));
|
|
1350
|
+
let size = 0;
|
|
1351
|
+
let cleanup = () => void 0;
|
|
1352
|
+
return new ReadableStream({
|
|
1353
|
+
start(controller) {
|
|
1354
|
+
const onData = (chunk) => {
|
|
1355
|
+
const bytes = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
1356
|
+
size += bytes.byteLength;
|
|
1357
|
+
if (size > limit) {
|
|
1358
|
+
cleanup();
|
|
1359
|
+
request.resume();
|
|
1360
|
+
controller.error(payloadTooLarge(limit));
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
controller.enqueue(bytes);
|
|
1364
|
+
if ((controller.desiredSize ?? 0) <= 0) request.pause();
|
|
1365
|
+
};
|
|
1366
|
+
const onEnd = () => {
|
|
1367
|
+
cleanup();
|
|
1368
|
+
controller.close();
|
|
1369
|
+
};
|
|
1370
|
+
const onError = (error) => {
|
|
1371
|
+
cleanup();
|
|
1372
|
+
controller.error(new BadRequestException("Failed to read the request body", void 0, { cause: error }));
|
|
1373
|
+
};
|
|
1374
|
+
const onClose = () => {
|
|
1375
|
+
if (request.complete) return;
|
|
1376
|
+
cleanup();
|
|
1377
|
+
controller.error(new BadRequestException("The request was aborted"));
|
|
1378
|
+
};
|
|
1379
|
+
cleanup = () => {
|
|
1380
|
+
request.off("data", onData);
|
|
1381
|
+
request.off("end", onEnd);
|
|
1382
|
+
request.off("error", onError);
|
|
1383
|
+
request.off("close", onClose);
|
|
1384
|
+
};
|
|
1385
|
+
request.on("data", onData);
|
|
1386
|
+
request.on("end", onEnd);
|
|
1387
|
+
request.on("error", onError);
|
|
1388
|
+
request.on("close", onClose);
|
|
1389
|
+
},
|
|
1390
|
+
pull() {
|
|
1391
|
+
if (request.isPaused()) request.resume();
|
|
1392
|
+
},
|
|
1393
|
+
cancel() {
|
|
1394
|
+
cleanup();
|
|
1395
|
+
request.resume();
|
|
1396
|
+
}
|
|
1397
|
+
}, {
|
|
1398
|
+
highWaterMark: BODY_HIGH_WATER_MARK,
|
|
1399
|
+
size: (chunk) => chunk?.byteLength ?? 0
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
/** Wraps a Node.js request/response pair as an {@link AdapterRequest}. */
|
|
1403
|
+
function nodeAdapterRequest(request, response, options = {}) {
|
|
1404
|
+
let body;
|
|
1405
|
+
return {
|
|
1406
|
+
method: request.method ?? "GET",
|
|
1407
|
+
url: options.url ?? request.url ?? "/",
|
|
1408
|
+
headers: normalizeNodeHeaders(request.headers),
|
|
1409
|
+
ip: options.ip ?? request.socket?.remoteAddress,
|
|
1410
|
+
remoteAddress: request.socket?.remoteAddress,
|
|
1411
|
+
encrypted: isEncrypted(request),
|
|
1412
|
+
raw: options.raw ?? request,
|
|
1413
|
+
rawResponse: options.rawResponse ?? response,
|
|
1414
|
+
readBody: (limit) => {
|
|
1415
|
+
body ??= readNodeBody(request, limit);
|
|
1416
|
+
return body;
|
|
1417
|
+
},
|
|
1418
|
+
streamBody: (limit) => nodeBodyStream(request, limit),
|
|
1419
|
+
rawStatus: () => response.statusCode,
|
|
1420
|
+
onAbort: (listener) => onResponseAbort(response, listener)
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
/**
|
|
1424
|
+
* Calls `listener` once when `response` closes before it was fully written (the client left): on
|
|
1425
|
+
* the next microtask when it is already destroyed, never after a complete response.
|
|
1426
|
+
*/
|
|
1427
|
+
function onResponseAbort(response, listener) {
|
|
1428
|
+
if (response.writableFinished) return;
|
|
1429
|
+
if (response.destroyed) {
|
|
1430
|
+
queueMicrotask(listener);
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1433
|
+
response.once("close", () => {
|
|
1434
|
+
if (!response.writableFinished) listener();
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1437
|
+
/** Whether the request came over TLS (`node:https`, `node:http2` secure servers). */
|
|
1438
|
+
function isEncrypted(request) {
|
|
1439
|
+
return request.socket?.encrypted === true;
|
|
1440
|
+
}
|
|
1441
|
+
/**
|
|
1442
|
+
* Writes a reply to a `ServerResponse`. Resolves when the response is complete (streams included);
|
|
1443
|
+
* never rejects (a failing stream destroys the response).
|
|
1444
|
+
*/
|
|
1445
|
+
async function writeNodeReply(response, reply) {
|
|
1446
|
+
if (reply.raw === true) {
|
|
1447
|
+
discardStream(reply.body instanceof ReadableStream ? reply.body : null);
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1450
|
+
if (response.headersSent || response.destroyed) {
|
|
1451
|
+
discardStream(reply.body instanceof ReadableStream ? reply.body : null);
|
|
1452
|
+
if (!response.writableEnded && !response.destroyed) response.end();
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
try {
|
|
1456
|
+
const headers = withNativeCookies(response.getHeader("set-cookie"), reply.headers);
|
|
1457
|
+
response.writeHead(reply.status, headers);
|
|
1458
|
+
} catch {
|
|
1459
|
+
discardStream(reply.body instanceof ReadableStream ? reply.body : null);
|
|
1460
|
+
if (!response.headersSent) {
|
|
1461
|
+
for (const name of response.getHeaderNames()) response.removeHeader(name);
|
|
1462
|
+
response.statusCode = 500;
|
|
1463
|
+
response.setHeader("content-type", "application/json; charset=utf-8");
|
|
1464
|
+
response.end("{\"statusCode\":500,\"error\":\"Internal Server Error\"}");
|
|
1465
|
+
}
|
|
1466
|
+
return;
|
|
1467
|
+
}
|
|
1468
|
+
const body = reply.body;
|
|
1469
|
+
if (body === null) {
|
|
1470
|
+
response.end();
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
if (reply.eventStream === true && response.isLightMyRequest !== true) response.setTimeout(0);
|
|
1474
|
+
if (body instanceof Uint8Array) {
|
|
1475
|
+
response.end(body);
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
const stream = body instanceof Blob ? body.stream() : body;
|
|
1479
|
+
try {
|
|
1480
|
+
await pipeline(Readable.fromWeb(stream), response);
|
|
1481
|
+
} catch {
|
|
1482
|
+
if (!response.destroyed) response.destroy();
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
/**
|
|
1486
|
+
* `headers` with the `set-cookie` lines already set on the native response (Express middleware
|
|
1487
|
+
* `res.cookie()`, Fastify hooks `reply.header('set-cookie', ...)`) before its own: `headers`
|
|
1488
|
+
* unchanged when either side has none.
|
|
1489
|
+
*/
|
|
1490
|
+
function withNativeCookies(native, headers) {
|
|
1491
|
+
const own = headers["set-cookie"];
|
|
1492
|
+
if (own === void 0 || native === void 0 || typeof native === "number") return headers;
|
|
1493
|
+
const before = typeof native === "string" ? [native] : native;
|
|
1494
|
+
if (before.length === 0) return headers;
|
|
1495
|
+
return {
|
|
1496
|
+
...headers,
|
|
1497
|
+
"set-cookie": [...before, ...typeof own === "string" ? [own] : own]
|
|
1498
|
+
};
|
|
1499
|
+
}
|
|
1500
|
+
/** Last-resort 500 when a handler promise rejected (should not happen: the pipeline maps errors). */
|
|
1501
|
+
function writeNodeFailure(response) {
|
|
1502
|
+
if (response.headersSent) {
|
|
1503
|
+
if (!response.writableEnded) response.destroy();
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1506
|
+
response.statusCode = 500;
|
|
1507
|
+
response.setHeader("content-type", "application/json; charset=utf-8");
|
|
1508
|
+
response.end("{\"statusCode\":500,\"error\":\"Internal Server Error\"}");
|
|
1509
|
+
}
|
|
1510
|
+
/**
|
|
1511
|
+
* Tracks the connections of a `node:http` server to close them on shutdown: idle keep-alive
|
|
1512
|
+
* connections first (graceful), every connection when forced. `server.closeIdleConnections()` /
|
|
1513
|
+
* `closeAllConnections()` are not reliable on every runtime (Bun), so sockets are tracked here.
|
|
1514
|
+
*/
|
|
1515
|
+
var ConnectionTracker = class {
|
|
1516
|
+
/** Socket → number of requests in progress. */
|
|
1517
|
+
#sockets = /* @__PURE__ */ new Map();
|
|
1518
|
+
#closing = false;
|
|
1519
|
+
constructor(server) {
|
|
1520
|
+
server.on("connection", (socket) => {
|
|
1521
|
+
this.#sockets.set(socket, 0);
|
|
1522
|
+
socket.once("close", () => this.#sockets.delete(socket));
|
|
1523
|
+
});
|
|
1524
|
+
server.on("request", (request, response) => {
|
|
1525
|
+
const socket = request.socket;
|
|
1526
|
+
if (!this.#sockets.has(socket)) return;
|
|
1527
|
+
this.#sockets.set(socket, (this.#sockets.get(socket) ?? 0) + 1);
|
|
1528
|
+
response.once("close", () => {
|
|
1529
|
+
const active = this.#sockets.get(socket);
|
|
1530
|
+
if (active === void 0) return;
|
|
1531
|
+
this.#sockets.set(socket, active - 1);
|
|
1532
|
+
if (active - 1 === 0 && this.#closing) socket.end();
|
|
1533
|
+
});
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
/** Ends idle connections now and the others once their requests completed. */
|
|
1537
|
+
closeIdle() {
|
|
1538
|
+
this.#closing = true;
|
|
1539
|
+
for (const [socket, active] of this.#sockets) if (active === 0) socket.end();
|
|
1540
|
+
}
|
|
1541
|
+
/** Destroys every connection. */
|
|
1542
|
+
destroyAll() {
|
|
1543
|
+
this.#closing = true;
|
|
1544
|
+
for (const socket of this.#sockets.keys()) socket.destroy();
|
|
1545
|
+
}
|
|
1546
|
+
};
|
|
1547
|
+
/** Closes a `node:http` server: graceful (idle connections ended), then forced when asked. */
|
|
1548
|
+
function closeServer(server, tracker, closing, force) {
|
|
1549
|
+
const promise = closing ?? new Promise((resolve) => {
|
|
1550
|
+
if (!server.listening) {
|
|
1551
|
+
resolve();
|
|
1552
|
+
return;
|
|
1553
|
+
}
|
|
1554
|
+
server.close(() => resolve());
|
|
1555
|
+
server.closeIdleConnections?.();
|
|
1556
|
+
tracker.closeIdle();
|
|
1557
|
+
});
|
|
1558
|
+
if (force) {
|
|
1559
|
+
server.closeAllConnections?.();
|
|
1560
|
+
tracker.destroyAll();
|
|
1561
|
+
}
|
|
1562
|
+
return promise;
|
|
1563
|
+
}
|
|
1564
|
+
export { UnprocessableEntityException as $, serializeCookie as A, ConflictException as B, decodeCookieValue as C, parseCookieHeader as D, isCookieName as E, isStreamSource as F, InternalServerErrorException as G, GatewayTimeoutException as H, toByteStream as I, NotImplementedException as J, MethodNotAllowedException as K, trackStream as L, captureContext as M, discardStream as N, parseSetCookie as O, isByteStream as P, UnauthorizedException as Q, BadGatewayException as R, checkCookie as S, givenCookieKey as T, GoneException as U, ForbiddenException as V, HttpException as W, ServiceUnavailableException as X, PayloadTooLargeException as Y, TooManyRequestsException as Z, lightStreamResponse as _, nodeBodyStream as a, isBodylessStatus as at, webBodyStream as b, readNodeBody as c, writeNodeReply as d, UnsupportedMediaTypeException as et, bufferInjectStream as f, lightInjectOptions as g, isEventStreamType as h, nodeAdapterRequest as i, toIssueDetail as it, serializeResponseCookie as j, serializeClearCookie as k, withNativeCookies as l, injectNodeStream as m, closeServer as n, isHttpException as nt, normalizeNodeHeaders as o, isValidStatus as ot, injectFetchStream as p, NotFoundException as q, isEncrypted as r, issue as rt, onResponseAbort as s, statusText as st, ConnectionTracker as t, ValidationException as tt, writeNodeFailure as u, readWebStream as v, encodeCookieValue as w, DEFAULT_COOKIE_OPTIONS as x, serverAddress as y, BadRequestException as z };
|
|
1565
|
+
|
|
1566
|
+
//# sourceMappingURL=node-io-BY1ZDLRQ.mjs.map
|