@daloyjs/core 1.0.0-rc.4 → 1.0.0-rc.6
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/README.md +34 -22
- package/dist/adapters/bun.d.ts +20 -2
- package/dist/adapters/bun.js +42 -7
- package/dist/adapters/deno.js +24 -7
- package/dist/adapters/lambda.d.ts +59 -2
- package/dist/adapters/lambda.js +136 -20
- package/dist/adapters/node.d.ts +8 -1
- package/dist/adapters/node.js +117 -46
- package/dist/app.d.ts +30 -4
- package/dist/app.js +187 -45
- package/dist/auto-ban.js +1 -3
- package/dist/bot-guard.js +30 -3
- package/dist/cli.js +9 -6
- package/dist/client.d.ts +36 -7
- package/dist/client.js +7 -0
- package/dist/compression.d.ts +9 -0
- package/dist/compression.js +72 -1
- package/dist/config.js +1 -3
- package/dist/conn-info.d.ts +5 -2
- package/dist/conn-info.js +5 -2
- package/dist/errors.d.ts +12 -3
- package/dist/errors.js +14 -8
- package/dist/etag.js +12 -2
- package/dist/fetch-guard.d.ts +27 -19
- package/dist/fetch-guard.js +50 -8
- package/dist/geo-block.js +4 -9
- package/dist/hashing.js +1 -1
- package/dist/http-signatures.d.ts +4 -1
- package/dist/http-signatures.js +16 -9
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/ip-reputation.js +1 -1
- package/dist/ip-restriction.js +3 -12
- package/dist/jwt.js +12 -14
- package/dist/logger.d.ts +45 -0
- package/dist/logger.js +135 -0
- package/dist/mcp.js +10 -9
- package/dist/middleware.js +33 -3
- package/dist/mtls.js +6 -1
- package/dist/multipart.js +9 -12
- package/dist/openapi.d.ts +1 -1
- package/dist/openapi.js +2 -2
- package/dist/rate-limit-redis.d.ts +4 -4
- package/dist/response-cache.d.ts +179 -21
- package/dist/response-cache.js +338 -29
- package/dist/router.d.ts +2 -2
- package/dist/router.js +24 -9
- package/dist/safe-redirect.d.ts +5 -1
- package/dist/safe-redirect.js +27 -3
- package/dist/sbom.cdx.json +9 -9
- package/dist/sbom.spdx.json +5 -5
- package/dist/security-schemes.js +1 -2
- package/dist/security.d.ts +41 -0
- package/dist/security.js +131 -15
- package/dist/session.d.ts +13 -2
- package/dist/session.js +111 -17
- package/dist/subdomains.js +1 -4
- package/dist/tenancy.d.ts +40 -0
- package/dist/tenancy.js +54 -3
- package/dist/time-claims.js +3 -1
- package/dist/waf.js +124 -32
- package/dist/webhook-delivery.js +19 -3
- package/dist/websocket.d.ts +8 -0
- package/dist/websocket.js +19 -4
- package/package.json +6 -5
package/dist/adapters/node.d.ts
CHANGED
|
@@ -76,7 +76,14 @@ export interface NodeServerOptions {
|
|
|
76
76
|
export interface NodeServerHandle {
|
|
77
77
|
/** The underlying `node:http` `Server` instance, for advanced wiring (extra listeners, address introspection). */
|
|
78
78
|
server: Server;
|
|
79
|
-
/**
|
|
79
|
+
/**
|
|
80
|
+
* Bound TCP port once the server emits `listening`.
|
|
81
|
+
*
|
|
82
|
+
* Before the listener is ready, this is the requested
|
|
83
|
+
* {@link NodeServerOptions.port} (default `3000`). In particular, callers
|
|
84
|
+
* using `port: 0` must await the server's `listening` event before reading
|
|
85
|
+
* this property to receive the OS-assigned ephemeral port.
|
|
86
|
+
*/
|
|
80
87
|
port: number;
|
|
81
88
|
/** Graceful shutdown: drains {@link App.shutdown} hooks, destroys WebSocket sockets, then closes the server. Idempotent. */
|
|
82
89
|
close(): Promise<void>;
|
package/dist/adapters/node.js
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
* Node adapter: translates IncomingMessage/ServerResponse to web-standard
|
|
3
3
|
* Request/Response. Includes graceful shutdown wired to SIGTERM/SIGINT.
|
|
4
4
|
*/
|
|
5
|
-
import { createServer
|
|
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, DALOY_LIGHT_RESPONSE_OK, } from "../app.js";
|
|
7
|
+
import { DALOY_RAW_BODY, DALOY_RAW_STREAM, DALOY_REQUEST_RAW_BODY, DALOY_LIGHT_RESPONSE_OK, DALOY_REQUEST_ABORT, } from "../app.js";
|
|
8
|
+
import { BadRequestError } from "../errors.js";
|
|
8
9
|
import { setClientCertificate, normalizePeerCertificate, } from "../mtls.js";
|
|
10
|
+
import { setConnInfo } from "../conn-info.js";
|
|
9
11
|
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";
|
|
10
12
|
/**
|
|
11
13
|
* Start a Node.js HTTP (and optional WebSocket) server bound to the given {@link App}.
|
|
@@ -72,9 +74,7 @@ export function serve(app, opts = {}) {
|
|
|
72
74
|
// `0` opts out and restores Node's unbounded-ish default (2000).
|
|
73
75
|
const maxHeaderCount = opts.maxHeaderCount;
|
|
74
76
|
server.maxHeadersCount =
|
|
75
|
-
typeof maxHeaderCount === "number" && maxHeaderCount >= 0
|
|
76
|
-
? maxHeaderCount
|
|
77
|
-
: 100;
|
|
77
|
+
typeof maxHeaderCount === "number" && maxHeaderCount >= 0 ? maxHeaderCount : 100;
|
|
78
78
|
// Connection-layer admission control. Reject overflow sockets at accept time
|
|
79
79
|
// rather than queuing them into the event loop under overload.
|
|
80
80
|
if (typeof opts.maxConnections === "number" && opts.maxConnections > 0) {
|
|
@@ -85,11 +85,23 @@ export function serve(app, opts = {}) {
|
|
|
85
85
|
server.on("upgrade", (req, socket, head) => {
|
|
86
86
|
wsSockets.add(socket);
|
|
87
87
|
socket.on("close", () => wsSockets.delete(socket));
|
|
88
|
-
|
|
88
|
+
// Safety net: a rejection here would otherwise be unhandled and, under
|
|
89
|
+
// the production crash-on-unhandledRejection posture, kill the process
|
|
90
|
+
// from a single malformed upgrade request.
|
|
91
|
+
handleUpgrade(app, req, socket, head, trustProxy).catch((err) => {
|
|
92
|
+
app.log.error({ err }, "WebSocket upgrade failed");
|
|
93
|
+
try {
|
|
94
|
+
writeUpgradeError(socket, 400, "Bad Request");
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
/* socket already closed */
|
|
98
|
+
}
|
|
99
|
+
socket.destroy();
|
|
100
|
+
});
|
|
89
101
|
});
|
|
90
102
|
}
|
|
91
|
-
const
|
|
92
|
-
server.listen(
|
|
103
|
+
const requestedPort = opts.port ?? 3000;
|
|
104
|
+
server.listen(requestedPort, opts.hostname ?? "0.0.0.0");
|
|
93
105
|
// Kill idle keep-alive sockets immediately when draining begins.
|
|
94
106
|
// In-flight requests keep their socket because Node's
|
|
95
107
|
// `closeIdleConnections()` is a no-op for sockets with an in-flight request.
|
|
@@ -111,11 +123,21 @@ export function serve(app, opts = {}) {
|
|
|
111
123
|
await new Promise((resolve, reject) => server.close((err) => (err ? reject(err) : resolve())));
|
|
112
124
|
};
|
|
113
125
|
if (opts.handleSignals !== false) {
|
|
114
|
-
const onSignal = (sig) => {
|
|
126
|
+
const onSignal = (sig) => {
|
|
127
|
+
app.log.info({ sig }, "DaloyJS received signal, shutting down");
|
|
128
|
+
void close().then(() => process.exit(0));
|
|
129
|
+
};
|
|
115
130
|
process.once("SIGTERM", () => onSignal("SIGTERM"));
|
|
116
131
|
process.once("SIGINT", () => onSignal("SIGINT"));
|
|
117
132
|
}
|
|
118
|
-
return {
|
|
133
|
+
return {
|
|
134
|
+
server,
|
|
135
|
+
get port() {
|
|
136
|
+
const address = server.address();
|
|
137
|
+
return address !== null && typeof address === "object" ? address.port : requestedPort;
|
|
138
|
+
},
|
|
139
|
+
close,
|
|
140
|
+
};
|
|
119
141
|
}
|
|
120
142
|
/**
|
|
121
143
|
* Default pre-buffer ceiling for the Node adapter. 256 KiB is a compromise:
|
|
@@ -135,6 +157,14 @@ function dispatchToApp(app, req, res, trustProxy, bufferedBody) {
|
|
|
135
157
|
writeAdapterError(res, e);
|
|
136
158
|
return;
|
|
137
159
|
}
|
|
160
|
+
// Fulfil the conn-info contract: the immediate TCP peer, so
|
|
161
|
+
// `getConnInfo` / `resolveClientIp` / `behindProxy` and WAF client-IP
|
|
162
|
+
// attribution work on Node. Never derived from spoofable headers.
|
|
163
|
+
setConnInfo(request, {
|
|
164
|
+
remoteAddress: req.socket.remoteAddress,
|
|
165
|
+
remotePort: req.socket.remotePort,
|
|
166
|
+
tls: req.socket.encrypted === true,
|
|
167
|
+
});
|
|
138
168
|
attachClientCertificate(req, request);
|
|
139
169
|
const responseOrPromise = app.fetch(request);
|
|
140
170
|
if (responseOrPromise instanceof Promise) {
|
|
@@ -249,11 +279,7 @@ function attachClientCertificate(req, request) {
|
|
|
249
279
|
* Node's `connect` event rather than the request listener; it is included here
|
|
250
280
|
* defensively for runtimes/proxies that surface it as a normal request.)
|
|
251
281
|
*/
|
|
252
|
-
const FETCH_FORBIDDEN_METHODS = new Set([
|
|
253
|
-
"CONNECT",
|
|
254
|
-
"TRACE",
|
|
255
|
-
"TRACK",
|
|
256
|
-
]);
|
|
282
|
+
const FETCH_FORBIDDEN_METHODS = new Set(["CONNECT", "TRACE", "TRACK"]);
|
|
257
283
|
/**
|
|
258
284
|
* Refuse a Fetch-forbidden HTTP method with a spec-correct `501 Not
|
|
259
285
|
* Implemented`. `501` is more accurate than `405` here because the method is
|
|
@@ -279,12 +305,15 @@ function writeMethodRefused(res) {
|
|
|
279
305
|
}
|
|
280
306
|
function writeAdapterError(res, e) {
|
|
281
307
|
if (!res.headersSent) {
|
|
282
|
-
|
|
308
|
+
const clientError = e instanceof BadRequestError;
|
|
309
|
+
res.statusCode = clientError ? 400 : 500;
|
|
283
310
|
res.setHeader("content-type", "application/problem+json");
|
|
284
311
|
res.end(JSON.stringify({
|
|
285
|
-
type:
|
|
286
|
-
|
|
287
|
-
|
|
312
|
+
type: clientError
|
|
313
|
+
? "https://daloyjs.dev/errors/bad-request"
|
|
314
|
+
: "https://daloyjs.dev/errors/internal",
|
|
315
|
+
title: clientError ? "Bad Request" : "Internal Server Error",
|
|
316
|
+
status: clientError ? 400 : 500,
|
|
288
317
|
}));
|
|
289
318
|
}
|
|
290
319
|
else {
|
|
@@ -307,9 +336,11 @@ function writeAdapterError(res, e) {
|
|
|
307
336
|
* - `instanceof Request` holds (prototype chain is re-rooted onto
|
|
308
337
|
* `Request.prototype`), and every WHATWG method/getter is overridden here,
|
|
309
338
|
* so nothing hits undici's brand-checked prototype accessors.
|
|
310
|
-
* - `signal` is
|
|
311
|
-
*
|
|
312
|
-
*
|
|
339
|
+
* - `signal` is a lazily-created per-instance `AbortSignal`. The framework
|
|
340
|
+
* aborts it (via the {@link DALOY_REQUEST_ABORT} hook) when the request
|
|
341
|
+
* exceeds `requestTimeoutMs`, so a handler that forwards `ctx.request.signal`
|
|
342
|
+
* to downstream `fetch`/DB calls sees them cancel on timeout. It is still NOT
|
|
343
|
+
* wired to client socket-disconnect — that teardown never fires the signal.
|
|
313
344
|
* - Passing this object directly to `fetch()` is not supported (undici
|
|
314
345
|
* brand-checks its input) — forward with `request.clone()` instead, which
|
|
315
346
|
* returns a real `Request`. This mirrors @hono/node-server's shim.
|
|
@@ -327,7 +358,7 @@ class LightRequest {
|
|
|
327
358
|
#headers;
|
|
328
359
|
#bodyBytes;
|
|
329
360
|
#real;
|
|
330
|
-
#
|
|
361
|
+
#controller;
|
|
331
362
|
constructor(url, method, headers, bodyBytes) {
|
|
332
363
|
this.#url = url;
|
|
333
364
|
this.#method = method;
|
|
@@ -355,10 +386,22 @@ class LightRequest {
|
|
|
355
386
|
return this.#headers;
|
|
356
387
|
}
|
|
357
388
|
get signal() {
|
|
358
|
-
//
|
|
359
|
-
//
|
|
360
|
-
//
|
|
361
|
-
|
|
389
|
+
// Lazily created so only handlers that actually read `signal` pay for the
|
|
390
|
+
// controller. The framework aborts it via the DALOY_REQUEST_ABORT hook
|
|
391
|
+
// below when the request exceeds `requestTimeoutMs`; client
|
|
392
|
+
// socket-disconnect is not wired into it.
|
|
393
|
+
return (this.#controller ??= new AbortController()).signal;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Framework abort hook ({@link DALOY_REQUEST_ABORT}). Invoked by the core on
|
|
397
|
+
* request timeout so `ctx.request.signal` fires for cooperative teardown.
|
|
398
|
+
* A no-op when no handler ever read `signal` — there is no controller to
|
|
399
|
+
* abort and nothing observing it.
|
|
400
|
+
*
|
|
401
|
+
* @param reason - Abort reason surfaced on `signal.reason` (a `TimeoutError`).
|
|
402
|
+
*/
|
|
403
|
+
[DALOY_REQUEST_ABORT](reason) {
|
|
404
|
+
this.#controller?.abort(reason);
|
|
362
405
|
}
|
|
363
406
|
get body() {
|
|
364
407
|
return this.#materialize().body;
|
|
@@ -496,16 +539,18 @@ Object.setPrototypeOf(LightRequest.prototype, Request.prototype);
|
|
|
496
539
|
LightRequest.prototype[DALOY_LIGHT_RESPONSE_OK] = true;
|
|
497
540
|
function toWebRequest(req, trustProxy, bufferedBody) {
|
|
498
541
|
const reqHeaders = req.headers;
|
|
499
|
-
const forwardedHost = trustProxy
|
|
500
|
-
? firstHeader(reqHeaders["x-forwarded-host"])
|
|
501
|
-
: undefined;
|
|
542
|
+
const forwardedHost = trustProxy ? firstHeader(reqHeaders["x-forwarded-host"]) : undefined;
|
|
502
543
|
const host = forwardedHost ?? reqHeaders.host ?? "localhost";
|
|
503
|
-
const forwardedProto = trustProxy
|
|
504
|
-
|
|
505
|
-
: undefined;
|
|
506
|
-
const proto = forwardedProto ??
|
|
507
|
-
(req.socket.encrypted ? "https" : "http");
|
|
544
|
+
const forwardedProto = trustProxy ? firstHeader(reqHeaders["x-forwarded-proto"]) : undefined;
|
|
545
|
+
const proto = forwardedProto ?? (req.socket.encrypted ? "https" : "http");
|
|
508
546
|
const url = `${proto}://${host}${normalizeRequestTarget(req.url)}`;
|
|
547
|
+
// Reject malformed Host / request-target combinations at the adapter
|
|
548
|
+
// boundary instead of letting the invalid URL propagate as a 500 later.
|
|
549
|
+
// URL.canParse applies WHATWG validation without allocating and immediately
|
|
550
|
+
// discarding a URL object on every request.
|
|
551
|
+
if (!URL.canParse(url)) {
|
|
552
|
+
throw new BadRequestError("Invalid request target or Host header");
|
|
553
|
+
}
|
|
509
554
|
// Build headers from `rawHeaders` (a flat [k0,v0,k1,v1,...] array) instead
|
|
510
555
|
// of the parsed `req.headers` object. This matches @hono/node-server's
|
|
511
556
|
// `newHeadersFromIncoming`: one `new Headers([[k,v],...])` constructor
|
|
@@ -578,7 +623,21 @@ function normalizeRequestTarget(target) {
|
|
|
578
623
|
}
|
|
579
624
|
function sendWebResponse(res, out) {
|
|
580
625
|
out.statusCode = res.status;
|
|
581
|
-
|
|
626
|
+
// `Headers.forEach` yields each `Set-Cookie` as a separate callback while
|
|
627
|
+
// `ServerResponse.setHeader` overwrites repeated keys — copying naively
|
|
628
|
+
// keeps only the LAST cookie (e.g. dropping the session cookie when
|
|
629
|
+
// `csrf()` also sets its token cookie). Collect them via `getSetCookie()`
|
|
630
|
+
// and set the array once so every cookie reaches the wire.
|
|
631
|
+
let hasSetCookie = false;
|
|
632
|
+
res.headers.forEach((v, k) => {
|
|
633
|
+
if (k === "set-cookie") {
|
|
634
|
+
hasSetCookie = true;
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
out.setHeader(k, v);
|
|
638
|
+
});
|
|
639
|
+
if (hasSetCookie)
|
|
640
|
+
out.setHeader("set-cookie", res.headers.getSetCookie());
|
|
582
641
|
// Fast-path: response was produced by serializeResult and carries the raw
|
|
583
642
|
// body bytes via the DALOY_RAW_BODY Symbol. Skip arrayBuffer() and the
|
|
584
643
|
// reader-loop microtask churn entirely for buffer-backed responses.
|
|
@@ -598,7 +657,8 @@ function sendWebResponse(res, out) {
|
|
|
598
657
|
// which we must strip so Node falls back to chunked transfer-encoding.
|
|
599
658
|
const rawStream = res[DALOY_RAW_STREAM];
|
|
600
659
|
if (rawStream !== undefined) {
|
|
601
|
-
if (typeof rawStream.pipe === "function" &&
|
|
660
|
+
if (typeof rawStream.pipe === "function" &&
|
|
661
|
+
!(rawStream instanceof ReadableStream)) {
|
|
602
662
|
// Node `Readable` from the handler: skip the Web-stream bridge entirely
|
|
603
663
|
// and `.pipe(out)` like Fastify/Koa/Express do.
|
|
604
664
|
out.removeHeader("content-length");
|
|
@@ -652,16 +712,22 @@ function pumpBody(body, out) {
|
|
|
652
712
|
}
|
|
653
713
|
// ---------- WebSocket upgrade ----------
|
|
654
714
|
async function handleUpgrade(app, req, socket, head, trustProxy) {
|
|
655
|
-
const forwardedHost = trustProxy
|
|
656
|
-
? firstHeader(req.headers["x-forwarded-host"])
|
|
657
|
-
: undefined;
|
|
715
|
+
const forwardedHost = trustProxy ? firstHeader(req.headers["x-forwarded-host"]) : undefined;
|
|
658
716
|
const host = forwardedHost ?? req.headers.host ?? "localhost";
|
|
659
|
-
const forwardedProto = trustProxy
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
717
|
+
const forwardedProto = trustProxy ? firstHeader(req.headers["x-forwarded-proto"]) : undefined;
|
|
718
|
+
const proto = forwardedProto ?? (req.socket.encrypted ? "https" : "http");
|
|
719
|
+
// A malformed `Host` header (e.g. containing a space) reaches this point:
|
|
720
|
+
// Node's HTTP parser accepts it and fires `upgrade`, but WHATWG URL
|
|
721
|
+
// parsing throws. Reject it as the client error it is instead of letting
|
|
722
|
+
// the throw escape the adapter.
|
|
723
|
+
let url;
|
|
724
|
+
try {
|
|
725
|
+
url = new URL(`${proto}://${host}${req.url ?? "/"}`);
|
|
726
|
+
}
|
|
727
|
+
catch {
|
|
728
|
+
writeUpgradeError(socket, 400, "Bad Request");
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
665
731
|
const match = app.webSocketRoutes.find(url.pathname);
|
|
666
732
|
if (!match) {
|
|
667
733
|
writeUpgradeError(socket, 404, "Not Found");
|
|
@@ -682,6 +748,11 @@ async function handleUpgrade(app, req, socket, head, trustProxy) {
|
|
|
682
748
|
method: "GET",
|
|
683
749
|
headers,
|
|
684
750
|
});
|
|
751
|
+
setConnInfo(request, {
|
|
752
|
+
remoteAddress: req.socket.remoteAddress,
|
|
753
|
+
remotePort: req.socket.remotePort,
|
|
754
|
+
tls: req.socket.encrypted === true,
|
|
755
|
+
});
|
|
685
756
|
const ctx = {
|
|
686
757
|
request,
|
|
687
758
|
params: match.params,
|
package/dist/app.d.ts
CHANGED
|
@@ -78,7 +78,12 @@ export interface AppOptions {
|
|
|
78
78
|
bodyLimitBytes?: number;
|
|
79
79
|
/** Reject requests whose Content-Type isn't in this allowlist (when a body schema is declared). */
|
|
80
80
|
allowedContentTypes?: string[];
|
|
81
|
-
/**
|
|
81
|
+
/**
|
|
82
|
+
* Per-request timeout in ms (handler + hooks). Default: 30000. Set 0 to
|
|
83
|
+
* disable. On timeout the framework aborts `ctx.request.signal` (cooperative
|
|
84
|
+
* cancellation of downstream I/O) and responds `408`; see
|
|
85
|
+
* {@link RequestTimeoutError}. It does not forcibly stop CPU-bound work.
|
|
86
|
+
*/
|
|
82
87
|
requestTimeoutMs?: number;
|
|
83
88
|
/**
|
|
84
89
|
* Maximum number of distinct request header fields accepted before the
|
|
@@ -771,6 +776,23 @@ export declare const DALOY_RAW_BODY: unique symbol;
|
|
|
771
776
|
* so first-party adapters can opt in; not part of the userland API surface.
|
|
772
777
|
*/
|
|
773
778
|
export declare const DALOY_REQUEST_RAW_BODY: unique symbol;
|
|
779
|
+
/**
|
|
780
|
+
* Internal Symbol an adapter sets (on its request shim) to expose the request's
|
|
781
|
+
* abort hook: a `(reason: unknown) => void` that aborts the `AbortController`
|
|
782
|
+
* backing `request.signal`. The core invokes it when a request exceeds
|
|
783
|
+
* {@link AppOptions.requestTimeoutMs} so a handler that forwarded
|
|
784
|
+
* `ctx.request.signal` to downstream I/O (`fetch`, a DB driver) sees those
|
|
785
|
+
* calls cancel — cooperative teardown, since single-threaded JS cannot preempt
|
|
786
|
+
* a running handler.
|
|
787
|
+
*
|
|
788
|
+
* The hook is invoked as a method on the request (`this` stays bound to the
|
|
789
|
+
* shim) so it can reach the shim's private controller. Absent on runtimes
|
|
790
|
+
* whose `Request.signal` is managed by the platform (Bun / Deno / Workers) and
|
|
791
|
+
* on direct `app.fetch()` callers, where {@link abortRequest} is a safe no-op
|
|
792
|
+
* and the timeout still resolves as a `408`. Module-public so first-party
|
|
793
|
+
* adapters can opt in; not part of the userland API surface.
|
|
794
|
+
*/
|
|
795
|
+
export declare const DALOY_REQUEST_ABORT: unique symbol;
|
|
774
796
|
/**
|
|
775
797
|
* Internal Symbol set by handlers/serializers to attach a raw stream
|
|
776
798
|
* (Node `Readable` or Web `ReadableStream`) to a `Response`. The Node
|
|
@@ -1154,7 +1176,11 @@ export declare class App<Routes extends readonly RouteDefinition<any, any, any,
|
|
|
1154
1176
|
* auth hook unless it opted out with `mcpRoutes(path, handler, { public: true })`.
|
|
1155
1177
|
* MCP tools are model-controlled and side-effecting, so a public one is a
|
|
1156
1178
|
* high-impact default.
|
|
1157
|
-
* 3. **
|
|
1179
|
+
* 3. **Cache ahead of tenancy** — a `responseCache()` that runs before
|
|
1180
|
+
* `tenancy()` builds its key before the tenant exists in `ctx.state`, so
|
|
1181
|
+
* every tenant collides on one entry and one tenant's response is served to
|
|
1182
|
+
* the next caller (CWE-524).
|
|
1183
|
+
* 4. **Missing CSRF** — when `session()` is installed and any route accepts a
|
|
1158
1184
|
* state-changing method (`POST`/`PUT`/`PATCH`/`DELETE`), a `csrf()` hook
|
|
1159
1185
|
* (or third-party equivalent stamped with {@link CSRF_HOOK_MARKER}) must
|
|
1160
1186
|
* also be present. Skipped when `app({ csrf: "off" })`.
|
|
@@ -1687,8 +1713,8 @@ export declare class App<Routes extends readonly RouteDefinition<any, any, any,
|
|
|
1687
1713
|
* "draining" signal); then the app waits up to `timeoutMs` for in-flight
|
|
1688
1714
|
* requests to settle; finally, {@link App.onClose} cleanups run.
|
|
1689
1715
|
*
|
|
1690
|
-
*
|
|
1691
|
-
* Call it manually from custom runtimes or integration tests.
|
|
1716
|
+
* The Node, Bun, and Deno adapters call this automatically on `SIGINT` /
|
|
1717
|
+
* `SIGTERM`. Call it manually from custom runtimes or integration tests.
|
|
1692
1718
|
*
|
|
1693
1719
|
* @param timeoutMs - Maximum time (ms) to wait for inflight requests. Default: `10_000`.
|
|
1694
1720
|
* @param reason - Optional human-readable reason forwarded to listeners.
|