@daloyjs/core 0.35.0 → 0.35.2

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.
Files changed (188) hide show
  1. package/README.md +145 -29
  2. package/dist/adapters/bun.d.ts +0 -1
  3. package/dist/adapters/bun.js +0 -1
  4. package/dist/adapters/cloudflare.d.ts +0 -1
  5. package/dist/adapters/cloudflare.js +0 -1
  6. package/dist/adapters/deno.d.ts +0 -1
  7. package/dist/adapters/deno.js +0 -1
  8. package/dist/adapters/fastly.d.ts +0 -1
  9. package/dist/adapters/fastly.js +0 -1
  10. package/dist/adapters/lambda.d.ts +0 -1
  11. package/dist/adapters/lambda.js +0 -1
  12. package/dist/adapters/node.d.ts +11 -1
  13. package/dist/adapters/node.js +246 -44
  14. package/dist/adapters/vercel.d.ts +0 -1
  15. package/dist/adapters/vercel.js +0 -1
  16. package/dist/app.d.ts +52 -1
  17. package/dist/app.js +557 -146
  18. package/dist/banner.d.ts +0 -1
  19. package/dist/banner.js +0 -1
  20. package/dist/cli.d.ts +0 -1
  21. package/dist/cli.js +0 -1
  22. package/dist/client.d.ts +0 -1
  23. package/dist/client.js +0 -1
  24. package/dist/combine.d.ts +0 -1
  25. package/dist/combine.js +0 -1
  26. package/dist/compression.d.ts +0 -1
  27. package/dist/compression.js +0 -1
  28. package/dist/config.d.ts +0 -1
  29. package/dist/config.js +0 -1
  30. package/dist/conn-info.d.ts +0 -1
  31. package/dist/conn-info.js +0 -1
  32. package/dist/contract.d.ts +0 -1
  33. package/dist/contract.js +0 -1
  34. package/dist/cookie.d.ts +0 -1
  35. package/dist/cookie.js +0 -1
  36. package/dist/dependency.d.ts +0 -1
  37. package/dist/dependency.js +0 -1
  38. package/dist/discriminator.d.ts +0 -1
  39. package/dist/discriminator.js +0 -1
  40. package/dist/docs.d.ts +0 -1
  41. package/dist/docs.js +0 -1
  42. package/dist/errors.d.ts +0 -1
  43. package/dist/errors.js +6 -3
  44. package/dist/etag.d.ts +0 -1
  45. package/dist/etag.js +0 -1
  46. package/dist/fetch-guard.d.ts +0 -1
  47. package/dist/fetch-guard.js +0 -1
  48. package/dist/hashing.d.ts +0 -1
  49. package/dist/hashing.js +0 -1
  50. package/dist/index.d.ts +0 -1
  51. package/dist/index.js +0 -1
  52. package/dist/ip-restriction.d.ts +0 -1
  53. package/dist/ip-restriction.js +0 -1
  54. package/dist/jwk.d.ts +0 -1
  55. package/dist/jwk.js +0 -1
  56. package/dist/jwt.d.ts +0 -1
  57. package/dist/jwt.js +0 -1
  58. package/dist/load-shedding.d.ts +0 -1
  59. package/dist/load-shedding.js +0 -1
  60. package/dist/logger.d.ts +0 -1
  61. package/dist/logger.js +0 -1
  62. package/dist/middleware.d.ts +0 -1
  63. package/dist/middleware.js +16 -13
  64. package/dist/multipart.d.ts +0 -1
  65. package/dist/multipart.js +0 -1
  66. package/dist/openapi.d.ts +0 -1
  67. package/dist/openapi.js +0 -1
  68. package/dist/rate-limit-redis.d.ts +0 -1
  69. package/dist/rate-limit-redis.js +0 -1
  70. package/dist/router.d.ts +0 -1
  71. package/dist/router.js +0 -1
  72. package/dist/safe-redirect.d.ts +2 -3
  73. package/dist/safe-redirect.js +2 -3
  74. package/dist/sbom.cdx.json +9 -9
  75. package/dist/sbom.spdx.json +5 -5
  76. package/dist/schema.d.ts +0 -1
  77. package/dist/schema.js +0 -1
  78. package/dist/security-schemes.d.ts +0 -1
  79. package/dist/security-schemes.js +0 -1
  80. package/dist/security.d.ts +0 -16
  81. package/dist/security.js +32 -4
  82. package/dist/session.d.ts +0 -1
  83. package/dist/session.js +0 -1
  84. package/dist/streaming.d.ts +0 -1
  85. package/dist/streaming.js +0 -1
  86. package/dist/subdomains.d.ts +0 -1
  87. package/dist/subdomains.js +0 -1
  88. package/dist/time-claims.d.ts +0 -1
  89. package/dist/time-claims.js +0 -1
  90. package/dist/tracing.d.ts +0 -1
  91. package/dist/tracing.js +0 -1
  92. package/dist/types.d.ts +0 -1
  93. package/dist/types.js +0 -1
  94. package/dist/websocket.d.ts +0 -1
  95. package/dist/websocket.js +0 -1
  96. package/package.json +6 -2
  97. package/dist/adapters/bun.d.ts.map +0 -1
  98. package/dist/adapters/bun.js.map +0 -1
  99. package/dist/adapters/cloudflare.d.ts.map +0 -1
  100. package/dist/adapters/cloudflare.js.map +0 -1
  101. package/dist/adapters/deno.d.ts.map +0 -1
  102. package/dist/adapters/deno.js.map +0 -1
  103. package/dist/adapters/fastly.d.ts.map +0 -1
  104. package/dist/adapters/fastly.js.map +0 -1
  105. package/dist/adapters/lambda.d.ts.map +0 -1
  106. package/dist/adapters/lambda.js.map +0 -1
  107. package/dist/adapters/node.d.ts.map +0 -1
  108. package/dist/adapters/node.js.map +0 -1
  109. package/dist/adapters/vercel.d.ts.map +0 -1
  110. package/dist/adapters/vercel.js.map +0 -1
  111. package/dist/app.d.ts.map +0 -1
  112. package/dist/app.js.map +0 -1
  113. package/dist/banner.d.ts.map +0 -1
  114. package/dist/banner.js.map +0 -1
  115. package/dist/cli.d.ts.map +0 -1
  116. package/dist/cli.js.map +0 -1
  117. package/dist/client.d.ts.map +0 -1
  118. package/dist/client.js.map +0 -1
  119. package/dist/combine.d.ts.map +0 -1
  120. package/dist/combine.js.map +0 -1
  121. package/dist/compression.d.ts.map +0 -1
  122. package/dist/compression.js.map +0 -1
  123. package/dist/config.d.ts.map +0 -1
  124. package/dist/config.js.map +0 -1
  125. package/dist/conn-info.d.ts.map +0 -1
  126. package/dist/conn-info.js.map +0 -1
  127. package/dist/contract.d.ts.map +0 -1
  128. package/dist/contract.js.map +0 -1
  129. package/dist/cookie.d.ts.map +0 -1
  130. package/dist/cookie.js.map +0 -1
  131. package/dist/dependency.d.ts.map +0 -1
  132. package/dist/dependency.js.map +0 -1
  133. package/dist/discriminator.d.ts.map +0 -1
  134. package/dist/discriminator.js.map +0 -1
  135. package/dist/docs.d.ts.map +0 -1
  136. package/dist/docs.js.map +0 -1
  137. package/dist/errors.d.ts.map +0 -1
  138. package/dist/errors.js.map +0 -1
  139. package/dist/etag.d.ts.map +0 -1
  140. package/dist/etag.js.map +0 -1
  141. package/dist/fetch-guard.d.ts.map +0 -1
  142. package/dist/fetch-guard.js.map +0 -1
  143. package/dist/hashing.d.ts.map +0 -1
  144. package/dist/hashing.js.map +0 -1
  145. package/dist/index.d.ts.map +0 -1
  146. package/dist/index.js.map +0 -1
  147. package/dist/ip-restriction.d.ts.map +0 -1
  148. package/dist/ip-restriction.js.map +0 -1
  149. package/dist/jwk.d.ts.map +0 -1
  150. package/dist/jwk.js.map +0 -1
  151. package/dist/jwt.d.ts.map +0 -1
  152. package/dist/jwt.js.map +0 -1
  153. package/dist/load-shedding.d.ts.map +0 -1
  154. package/dist/load-shedding.js.map +0 -1
  155. package/dist/logger.d.ts.map +0 -1
  156. package/dist/logger.js.map +0 -1
  157. package/dist/middleware.d.ts.map +0 -1
  158. package/dist/middleware.js.map +0 -1
  159. package/dist/multipart.d.ts.map +0 -1
  160. package/dist/multipart.js.map +0 -1
  161. package/dist/openapi.d.ts.map +0 -1
  162. package/dist/openapi.js.map +0 -1
  163. package/dist/rate-limit-redis.d.ts.map +0 -1
  164. package/dist/rate-limit-redis.js.map +0 -1
  165. package/dist/router.d.ts.map +0 -1
  166. package/dist/router.js.map +0 -1
  167. package/dist/safe-redirect.d.ts.map +0 -1
  168. package/dist/safe-redirect.js.map +0 -1
  169. package/dist/schema.d.ts.map +0 -1
  170. package/dist/schema.js.map +0 -1
  171. package/dist/security-schemes.d.ts.map +0 -1
  172. package/dist/security-schemes.js.map +0 -1
  173. package/dist/security.d.ts.map +0 -1
  174. package/dist/security.js.map +0 -1
  175. package/dist/session.d.ts.map +0 -1
  176. package/dist/session.js.map +0 -1
  177. package/dist/streaming.d.ts.map +0 -1
  178. package/dist/streaming.js.map +0 -1
  179. package/dist/subdomains.d.ts.map +0 -1
  180. package/dist/subdomains.js.map +0 -1
  181. package/dist/time-claims.d.ts.map +0 -1
  182. package/dist/time-claims.js.map +0 -1
  183. package/dist/tracing.d.ts.map +0 -1
  184. package/dist/tracing.js.map +0 -1
  185. package/dist/types.d.ts.map +0 -1
  186. package/dist/types.js.map +0 -1
  187. package/dist/websocket.d.ts.map +0 -1
  188. package/dist/websocket.js.map +0 -1
@@ -4,30 +4,33 @@
4
4
  */
5
5
  import { createServer, } from "node:http";
6
6
  import { Readable } from "node:stream";
7
+ import { DALOY_RAW_BODY, DALOY_RAW_STREAM, DALOY_REQUEST_RAW_BODY } from "../app.js";
7
8
  import { FrameSink, encodeFrame, encodeClosePayload, encodeSendPayload, validateUpgrade, validateSelectedSubprotocol, checkWebSocketOrigin, WS_OPCODE, WS_CLOSE_CODE, WS_READY_STATE, WS_MAX_CONTROL_PAYLOAD, WebSocketProtocolError, WebSocketPayloadTooLargeError, } from "../websocket.js";
8
9
  /** Start a Node.js HTTP (and optional WebSocket) server bound to the given {@link App}. */
9
10
  export function serve(app, opts = {}) {
10
11
  const trustProxy = opts.trustProxy === true;
11
- const server = createServer({ maxHeaderSize: opts.maxHeaderBytes ?? 16 * 1024 }, async (req, res) => {
12
- try {
13
- const request = await toWebRequest(req, trustProxy);
14
- const response = await app.fetch(request);
15
- await sendWebResponse(response, res);
12
+ const bufferedBodyMaxBytes = typeof opts.bufferedBodyMaxBytes === "number" && opts.bufferedBodyMaxBytes >= 0
13
+ ? opts.bufferedBodyMaxBytes
14
+ : DEFAULT_BUFFERED_BODY_MAX_BYTES;
15
+ const server = createServer({ maxHeaderSize: opts.maxHeaderBytes ?? 16 * 1024 }, (req, res) => {
16
+ // GET/HEAD: no body work, dispatch directly. Keep this first so the GET
17
+ // hot path doesn't pay for any of the buffering bookkeeping below.
18
+ const method = req.method;
19
+ if (method === "GET" || method === "HEAD" || method === undefined) {
20
+ dispatchToApp(app, req, res, trustProxy, undefined);
21
+ return;
16
22
  }
17
- catch (e) {
18
- if (!res.headersSent) {
19
- res.statusCode = 500;
20
- res.setHeader("content-type", "application/problem+json");
21
- res.end(JSON.stringify({
22
- type: "https://daloyjs.dev/errors/internal",
23
- title: "Internal Server Error",
24
- status: 500,
25
- }));
26
- }
27
- else {
28
- res.destroy(e);
29
- }
23
+ // POST/PUT/PATCH/DELETE with a small known content-length: pre-buffer
24
+ // bytes from the Node socket directly so the Request constructor gets a
25
+ // Uint8Array body instead of `Readable.toWeb(req)`. This skips the
26
+ // WHATWG-stream adapter that dominates POST throughput on Node.
27
+ const cl = req.headers["content-length"];
28
+ const n = cl ? Number(cl) : NaN;
29
+ if (Number.isFinite(n) && n >= 0 && n <= bufferedBodyMaxBytes) {
30
+ bufferRequestBody(req, n).then((bytes) => dispatchToApp(app, req, res, trustProxy, bytes), (e) => writeAdapterError(res, e));
31
+ return;
30
32
  }
33
+ dispatchToApp(app, req, res, trustProxy, undefined);
31
34
  });
32
35
  server.requestTimeout = opts.connectionTimeoutMs ?? 30_000;
33
36
  server.headersTimeout = opts.connectionTimeoutMs ?? 30_000;
@@ -69,33 +72,176 @@ export function serve(app, opts = {}) {
69
72
  }
70
73
  return { server, port, close };
71
74
  }
72
- async function toWebRequest(req, trustProxy) {
75
+ /**
76
+ * Default pre-buffer ceiling for the Node adapter. 256 KiB is a compromise:
77
+ * large enough that the vast majority of JSON / form requests stay on the
78
+ * fast (Uint8Array) path, small enough that N concurrent in-flight bodies
79
+ * don't pin huge amounts of memory. Override via
80
+ * {@link NodeServerOptions.bufferedBodyMaxBytes}. The actual security cap
81
+ * on body size remains `App.bodyLimitBytes`.
82
+ */
83
+ const DEFAULT_BUFFERED_BODY_MAX_BYTES = 256 * 1024;
84
+ function dispatchToApp(app, req, res, trustProxy, bufferedBody) {
85
+ let request;
86
+ try {
87
+ request = toWebRequest(req, trustProxy, bufferedBody);
88
+ }
89
+ catch (e) {
90
+ writeAdapterError(res, e);
91
+ return;
92
+ }
93
+ const responseOrPromise = app.fetch(request);
94
+ if (responseOrPromise instanceof Promise) {
95
+ responseOrPromise.then((response) => {
96
+ try {
97
+ const sent = sendWebResponse(response, res);
98
+ if (sent instanceof Promise)
99
+ sent.catch((e) => writeAdapterError(res, e));
100
+ }
101
+ catch (e) {
102
+ writeAdapterError(res, e);
103
+ }
104
+ }, (e) => writeAdapterError(res, e));
105
+ }
106
+ else {
107
+ try {
108
+ const sent = sendWebResponse(responseOrPromise, res);
109
+ if (sent instanceof Promise)
110
+ sent.catch((e) => writeAdapterError(res, e));
111
+ }
112
+ catch (e) {
113
+ writeAdapterError(res, e);
114
+ }
115
+ }
116
+ }
117
+ function bufferRequestBody(req, expected) {
118
+ return new Promise((resolve, reject) => {
119
+ // Pre-allocate to the declared Content-Length. The caller has already
120
+ // checked `expected <= BUFFERED_BODY_MAX_BYTES` (1 MiB) so this
121
+ // allocation is bounded and DoS-safe. Skipping the intermediate
122
+ // `chunks: Buffer[]` array + `Buffer.concat` avoids one full-body
123
+ // copy per request — significant at 1 MiB bodies under load.
124
+ // Use `Buffer.alloc` (zero-filled) rather than `Buffer.allocUnsafe`:
125
+ // the unsafe variant returns uninitialized memory and is forbidden by
126
+ // `verify:no-unsafe-buffer`. Any unwritten tail is sliced off below.
127
+ const out = expected > 0 ? Buffer.alloc(expected) : null;
128
+ let received = 0;
129
+ let settled = false;
130
+ const onData = (chunk) => {
131
+ if (settled)
132
+ return;
133
+ const next = received + chunk.length;
134
+ if (next > expected) {
135
+ settled = true;
136
+ cleanup();
137
+ req.destroy();
138
+ reject(new Error("Request body exceeded declared Content-Length"));
139
+ return;
140
+ }
141
+ // out is non-null here because next > 0 implies expected > 0.
142
+ chunk.copy(out, received);
143
+ received = next;
144
+ };
145
+ const onEnd = () => {
146
+ if (settled)
147
+ return;
148
+ settled = true;
149
+ cleanup();
150
+ if (received === 0) {
151
+ resolve(new Uint8Array(0));
152
+ return;
153
+ }
154
+ // Trust Content-Length: if the client under-delivered we still
155
+ // resolve the prefix actually received (matches prior behavior).
156
+ const buf = received === expected ? out : out.subarray(0, received);
157
+ resolve(new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength));
158
+ };
159
+ const onErr = (err) => {
160
+ if (settled)
161
+ return;
162
+ settled = true;
163
+ cleanup();
164
+ reject(err);
165
+ };
166
+ const cleanup = () => {
167
+ req.off("data", onData);
168
+ req.off("end", onEnd);
169
+ req.off("error", onErr);
170
+ req.off("aborted", onErr);
171
+ };
172
+ req.on("data", onData);
173
+ req.on("end", onEnd);
174
+ req.on("error", onErr);
175
+ req.on("aborted", onErr);
176
+ });
177
+ }
178
+ function writeAdapterError(res, e) {
179
+ if (!res.headersSent) {
180
+ res.statusCode = 500;
181
+ res.setHeader("content-type", "application/problem+json");
182
+ res.end(JSON.stringify({
183
+ type: "https://daloyjs.dev/errors/internal",
184
+ title: "Internal Server Error",
185
+ status: 500,
186
+ }));
187
+ }
188
+ else {
189
+ res.destroy(e);
190
+ }
191
+ }
192
+ function toWebRequest(req, trustProxy, bufferedBody) {
193
+ const reqHeaders = req.headers;
73
194
  const forwardedHost = trustProxy
74
- ? firstHeader(req.headers["x-forwarded-host"])
195
+ ? firstHeader(reqHeaders["x-forwarded-host"])
75
196
  : undefined;
76
- const host = forwardedHost ?? req.headers.host ?? "localhost";
197
+ const host = forwardedHost ?? reqHeaders.host ?? "localhost";
77
198
  const forwardedProto = trustProxy
78
- ? firstHeader(req.headers["x-forwarded-proto"])
199
+ ? firstHeader(reqHeaders["x-forwarded-proto"])
79
200
  : undefined;
80
201
  const proto = forwardedProto ??
81
202
  (req.socket.encrypted ? "https" : "http");
82
203
  const url = `${proto}://${host}${req.url ?? "/"}`;
83
- const headers = new Headers();
84
- for (const [k, v] of Object.entries(req.headers)) {
85
- if (v === undefined)
204
+ // Build headers from `rawHeaders` (a flat [k0,v0,k1,v1,...] array) instead
205
+ // of the parsed `req.headers` object. This matches @hono/node-server's
206
+ // `newHeadersFromIncoming`: one `new Headers([[k,v],...])` constructor
207
+ // call rather than N `headers.set()` calls. It is also stricter for
208
+ // duplicate-Host smuggling — Node coalesces some singleton headers down
209
+ // to the first value on `req.headers`, but `rawHeaders` preserves every
210
+ // occurrence so `assertNoDuplicateSingletonHeaders` actually sees them.
211
+ // Skip HTTP/2 pseudo-headers (leading ':') defensively, even though
212
+ // node:http's createServer is HTTP/1.1 only today.
213
+ const rawHeaders = req.rawHeaders;
214
+ const headerPairs = [];
215
+ for (let i = 0; i < rawHeaders.length; i += 2) {
216
+ const k = rawHeaders[i];
217
+ if (k.charCodeAt(0) === 58 /* ':' */)
86
218
  continue;
87
- if (Array.isArray(v))
88
- headers.set(k, v.join(", "));
89
- else
90
- headers.set(k, String(v));
219
+ headerPairs.push([k, rawHeaders[i + 1]]);
91
220
  }
221
+ const headers = new Headers(headerPairs);
92
222
  const method = req.method ?? "GET";
93
- const init = { method, headers };
94
- if (method !== "GET" && method !== "HEAD") {
95
- init.body = Readable.toWeb(req);
96
- init.duplex = "half";
223
+ if (method === "GET" || method === "HEAD") {
224
+ return new Request(url, { method, headers });
225
+ }
226
+ if (bufferedBody !== undefined) {
227
+ const req2 = new Request(url, {
228
+ method,
229
+ headers,
230
+ body: bufferedBody,
231
+ });
232
+ // Stash the validated bytes so readBodyLimited (and any other internal
233
+ // body reader) can skip the WHATWG ReadableStream reader loop. The
234
+ // adapter has already enforced BUFFERED_BODY_MAX_BYTES + Content-Length
235
+ // here; readBodyLimited re-checks against the caller's limit.
236
+ req2[DALOY_REQUEST_RAW_BODY] = bufferedBody;
237
+ return req2;
97
238
  }
98
- return new Request(url, init);
239
+ return new Request(url, {
240
+ method,
241
+ headers,
242
+ body: Readable.toWeb(req),
243
+ duplex: "half",
244
+ });
99
245
  }
100
246
  function firstHeader(v) {
101
247
  if (v === undefined)
@@ -106,22 +252,79 @@ function firstHeader(v) {
106
252
  const comma = raw.indexOf(",");
107
253
  return (comma === -1 ? raw : raw.slice(0, comma)).trim() || undefined;
108
254
  }
109
- async function sendWebResponse(res, out) {
255
+ function sendWebResponse(res, out) {
110
256
  out.statusCode = res.status;
111
257
  res.headers.forEach((v, k) => out.setHeader(k, v));
258
+ // Fast-path: response was produced by serializeResult and carries the raw
259
+ // body bytes via the DALOY_RAW_BODY Symbol. Skip arrayBuffer() and the
260
+ // reader-loop microtask churn entirely for buffer-backed responses.
261
+ const raw = res[DALOY_RAW_BODY];
262
+ if (raw !== undefined) {
263
+ if (raw === null) {
264
+ out.end();
265
+ }
266
+ else {
267
+ out.end(raw);
268
+ }
269
+ return;
270
+ }
271
+ // Fast-path: handler returned a raw stream. Check before `!res.body` —
272
+ // a Node Readable is stashed alongside a null Response body, and the
273
+ // `new Response(null)` constructor also auto-sets `content-length: 0`
274
+ // which we must strip so Node falls back to chunked transfer-encoding.
275
+ const rawStream = res[DALOY_RAW_STREAM];
276
+ if (rawStream !== undefined) {
277
+ if (typeof rawStream.pipe === "function" && !(rawStream instanceof ReadableStream)) {
278
+ // Node `Readable` from the handler: skip the Web-stream bridge entirely
279
+ // and `.pipe(out)` like Fastify/Koa/Express do.
280
+ out.removeHeader("content-length");
281
+ return new Promise((resolve, reject) => {
282
+ const r = rawStream;
283
+ const onError = (err) => {
284
+ r.destroy();
285
+ reject(err);
286
+ };
287
+ r.once("error", onError);
288
+ out.once("error", onError);
289
+ out.once("finish", () => resolve());
290
+ r.pipe(out);
291
+ });
292
+ }
293
+ return pumpBody(rawStream, out);
294
+ }
112
295
  if (!res.body) {
113
296
  out.end();
114
297
  return;
115
298
  }
116
- const reader = res.body.getReader();
117
- while (true) {
118
- const { done, value } = await reader.read();
119
- if (done)
120
- break;
121
- if (value)
122
- out.write(value);
299
+ const ct = out.getHeader("content-type");
300
+ const isStream = ct && typeof ct === "string" && ct.startsWith("text/event-stream");
301
+ const rawLength = out.getHeader("content-length");
302
+ const contentLength = typeof rawLength === "number"
303
+ ? rawLength
304
+ : typeof rawLength === "string"
305
+ ? Number(rawLength)
306
+ : Number.NaN;
307
+ if (!isStream && Number.isFinite(contentLength) && contentLength <= 64 * 1024) {
308
+ return res.arrayBuffer().then((ab) => {
309
+ out.end(Buffer.from(ab));
310
+ });
123
311
  }
124
- out.end();
312
+ return pumpBody(res.body, out);
313
+ }
314
+ function pumpBody(body, out) {
315
+ // Delegate to Node's native pipe: it honors backpressure and avoids the
316
+ // per-chunk microtask overhead of an explicit `await reader.read()` loop.
317
+ return new Promise((resolve, reject) => {
318
+ const readable = Readable.fromWeb(body);
319
+ const onError = (err) => {
320
+ readable.destroy();
321
+ reject(err);
322
+ };
323
+ readable.once("error", onError);
324
+ out.once("error", onError);
325
+ out.once("finish", () => resolve());
326
+ readable.pipe(out);
327
+ });
125
328
  }
126
329
  // ---------- WebSocket upgrade ----------
127
330
  async function handleUpgrade(app, req, socket, head, trustProxy) {
@@ -390,4 +593,3 @@ class NodeWebSocketConnection {
390
593
  }
391
594
  }
392
595
  }
393
- //# sourceMappingURL=node.js.map
@@ -43,4 +43,3 @@ export declare const toEdgeHandler: typeof toWebHandler;
43
43
  */
44
44
  export declare function toRouteHandlers(app: App): RouteHandlers;
45
45
  export {};
46
- //# sourceMappingURL=vercel.d.ts.map
@@ -23,4 +23,3 @@ export function toRouteHandlers(app) {
23
23
  out[method] = handler;
24
24
  return out;
25
25
  }
26
- //# sourceMappingURL=vercel.js.map
package/dist/app.d.ts CHANGED
@@ -471,6 +471,34 @@ export interface IntrospectedRoute {
471
471
  */
472
472
  meta?: import("./types.js").RouteMeta;
473
473
  }
474
+ /**
475
+ * Internal Symbol used to stash the raw Uint8Array (or null) body on a
476
+ * `Response` produced by {@link serializeResult}. Adapters that want to
477
+ * avoid the cost of `await response.arrayBuffer()` for buffer-backed
478
+ * responses can read this property and write the bytes directly. The
479
+ * Symbol is intentionally module-private (not exported) to keep this an
480
+ * implementation detail — userland code should never depend on it.
481
+ */
482
+ export declare const DALOY_RAW_BODY: unique symbol;
483
+ /**
484
+ * Internal Symbol used by adapters to stash a pre-buffered request body on
485
+ * the `Request` instance. When set, {@link readBodyLimited} skips the
486
+ * `ReadableStream` reader loop and returns the cached bytes directly after
487
+ * re-checking them against the caller-supplied limit. Adapters MUST only
488
+ * attach bytes they have already validated against the configured
489
+ * {@link AppOptions.bodyLimitBytes}; the limit re-check in
490
+ * `readBodyLimited` is defense-in-depth, not the primary cap. Module-public
491
+ * so first-party adapters can opt in; not part of the userland API surface.
492
+ */
493
+ export declare const DALOY_REQUEST_RAW_BODY: unique symbol;
494
+ /**
495
+ * Internal Symbol set by handlers/serializers to attach a raw stream
496
+ * (Node `Readable` or Web `ReadableStream`) to a `Response`. The Node
497
+ * adapter pipes the stream straight to the socket, skipping the
498
+ * Web-stream reader bridge. Module-public so first-party adapters can
499
+ * opt in; userland code should not depend on it.
500
+ */
501
+ export declare const DALOY_RAW_STREAM: unique symbol;
474
502
  /**
475
503
  * Contract-first HTTP application.
476
504
  *
@@ -526,6 +554,14 @@ export declare class App {
526
554
  /** Public registry: enables OpenAPI gen, typed-client gen, dead-route detection. */
527
555
  readonly routes: RouteDefinition<any, any, any, any>[];
528
556
  private router;
557
+ /**
558
+ * Memoized result of `isProduction()`. The inputs (`options.env`,
559
+ * `options.production`, `process.env.NODE_ENV`) cannot change between
560
+ * the moment a route is dispatched and the moment its error is rendered,
561
+ * so reading `process.env.NODE_ENV` on every error response is wasted
562
+ * work in the hot path. Computed lazily on first read.
563
+ */
564
+ private _productionCache;
529
565
  /** WebSocket route registry. Adapters look up handlers via `app.webSocketRoutes.find()`. */
530
566
  readonly webSocketRoutes: WebSocketRegistry;
531
567
  private prefix;
@@ -536,6 +572,13 @@ export declare class App {
536
572
  private routeSecurityMarkers;
537
573
  /** Decorator bag merged into ctx.state on every request. */
538
574
  private decorations;
575
+ /**
576
+ * Count of own keys on {@link decorations}. Tracked alongside the bag so the
577
+ * dispatch hot path can take a `count === 0` fast path and skip the
578
+ * `Object.assign` spread on the common case (no `app.decorate()` calls).
579
+ * Updated only when {@link decorate} mutates the bag.
580
+ */
581
+ private decorationsCount;
539
582
  private installedPlugins;
540
583
  private closeHooks;
541
584
  private closeHooksRun;
@@ -570,6 +613,15 @@ export declare class App {
570
613
  * dashboards see the misconfiguration without flooding on every retry.
571
614
  */
572
615
  private trustProxyWarned;
616
+ /**
617
+ * Cached merge of `options.hooks` only. Used on the cold 404/405 path
618
+ * and as the baseline for cross-origin guard decisions when no route
619
+ * matches.
620
+ */
621
+ private _globalHooksCache;
622
+ private _globalCorsAllowsCache;
623
+ private get globalHooks();
624
+ private get globalCorsAllows();
573
625
  constructor(options?: AppOptions);
574
626
  /**
575
627
  * Validate {@link AppOptions.disconnectStatusCode}.
@@ -1058,4 +1110,3 @@ export declare function createApp(options?: AppOptions): App;
1058
1110
  * @internal
1059
1111
  */
1060
1112
  export declare function _resetPackageJsonCacheForTests(): void;
1061
- //# sourceMappingURL=app.d.ts.map