@nifrajs/core 5.5.1 → 5.7.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/CHANGELOG.md +246 -0
- package/README.md +5 -0
- package/dist/assurance.d.ts.map +1 -1
- package/dist/assurance.js.map +1 -1
- package/dist/binary.d.ts +15 -1
- package/dist/binary.d.ts.map +1 -1
- package/dist/binary.js +1 -0
- package/dist/binary.js.map +1 -1
- package/dist/edge-kit.d.ts +1 -0
- package/dist/edge-kit.d.ts.map +1 -1
- package/dist/edge-kit.js +1 -0
- package/dist/edge-kit.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/evidence.d.ts +4 -0
- package/dist/evidence.d.ts.map +1 -1
- package/dist/evidence.js +4 -0
- package/dist/evidence.js.map +1 -1
- package/dist/file-type.d.ts +30 -0
- package/dist/file-type.d.ts.map +1 -0
- package/dist/file-type.js +118 -0
- package/dist/file-type.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/hook-origins.d.ts +2 -0
- package/dist/internal/hook-origins.d.ts.map +1 -1
- package/dist/internal/hook-origins.js +12 -0
- package/dist/internal/hook-origins.js.map +1 -1
- package/dist/internal/route-program.d.ts +11 -0
- package/dist/internal/route-program.d.ts.map +1 -1
- package/dist/internal/route-program.js +133 -85
- package/dist/internal/route-program.js.map +1 -1
- package/dist/reflection.d.ts +19 -1
- package/dist/reflection.d.ts.map +1 -1
- package/dist/reflection.js +46 -7
- package/dist/reflection.js.map +1 -1
- package/dist/response-observer.d.ts +1 -0
- package/dist/response-observer.d.ts.map +1 -1
- package/dist/response-observer.js +1 -0
- package/dist/response-observer.js.map +1 -1
- package/dist/server/answer-step.d.ts +27 -0
- package/dist/server/answer-step.d.ts.map +1 -0
- package/dist/server/answer-step.js +33 -0
- package/dist/server/answer-step.js.map +1 -0
- package/dist/server/bare-error-lane.d.ts +4 -1
- package/dist/server/bare-error-lane.d.ts.map +1 -1
- package/dist/server/bare-error-lane.js +9 -3
- package/dist/server/bare-error-lane.js.map +1 -1
- package/dist/server/base64-answers.d.ts +5 -0
- package/dist/server/base64-answers.d.ts.map +1 -0
- package/dist/server/base64-answers.js +132 -0
- package/dist/server/base64-answers.js.map +1 -0
- package/dist/server/before-handler.d.ts +10 -0
- package/dist/server/before-handler.d.ts.map +1 -0
- package/dist/server/before-handler.js +12 -0
- package/dist/server/before-handler.js.map +1 -0
- package/dist/server/body-lane.d.ts.map +1 -1
- package/dist/server/body-lane.js +8 -2
- package/dist/server/body-lane.js.map +1 -1
- package/dist/server/body-parser.d.ts.map +1 -1
- package/dist/server/body-parser.js +2 -5
- package/dist/server/body-parser.js.map +1 -1
- package/dist/server/body.d.ts +24 -8
- package/dist/server/body.d.ts.map +1 -1
- package/dist/server/body.js +86 -33
- package/dist/server/body.js.map +1 -1
- package/dist/server/core-copies.d.ts.map +1 -1
- package/dist/server/core-copies.js +5 -2
- package/dist/server/core-copies.js.map +1 -1
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +6 -17
- package/dist/server/http.js.map +1 -1
- package/dist/server/install.d.ts +2 -0
- package/dist/server/install.d.ts.map +1 -1
- package/dist/server/install.js +2 -0
- package/dist/server/install.js.map +1 -1
- package/dist/server/json-response.d.ts +3 -0
- package/dist/server/json-response.d.ts.map +1 -0
- package/dist/server/json-response.js +14 -0
- package/dist/server/json-response.js.map +1 -0
- package/dist/server/json-text.d.ts +16 -0
- package/dist/server/json-text.d.ts.map +1 -0
- package/dist/server/json-text.js +34 -0
- package/dist/server/json-text.js.map +1 -0
- package/dist/server/multipart.d.ts +3 -0
- package/dist/server/multipart.d.ts.map +1 -1
- package/dist/server/multipart.js +226 -97
- package/dist/server/multipart.js.map +1 -1
- package/dist/server/node-outcome.d.ts +5 -0
- package/dist/server/node-outcome.d.ts.map +1 -1
- package/dist/server/node-outcome.js +3 -4
- package/dist/server/node-outcome.js.map +1 -1
- package/dist/server/proto-guard.d.ts.map +1 -1
- package/dist/server/proto-guard.js +39 -4
- package/dist/server/proto-guard.js.map +1 -1
- package/dist/server/query.d.ts.map +1 -1
- package/dist/server/query.js +8 -14
- package/dist/server/query.js.map +1 -1
- package/dist/server/repeated-prefix.d.ts +17 -0
- package/dist/server/repeated-prefix.d.ts.map +1 -0
- package/dist/server/repeated-prefix.js +17 -0
- package/dist/server/repeated-prefix.js.map +1 -0
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js +5 -5
- package/dist/server/request-context.js.map +1 -1
- package/dist/server/respond.d.ts +3 -4
- package/dist/server/respond.d.ts.map +1 -1
- package/dist/server/respond.js +45 -37
- package/dist/server/respond.js.map +1 -1
- package/dist/server/response-contract-lane.d.ts +12 -2
- package/dist/server/response-contract-lane.d.ts.map +1 -1
- package/dist/server/response-contract-lane.js +137 -9
- package/dist/server/response-contract-lane.js.map +1 -1
- package/dist/server/route-template.d.ts +10 -0
- package/dist/server/route-template.d.ts.map +1 -0
- package/dist/server/route-template.js +21 -0
- package/dist/server/route-template.js.map +1 -0
- package/dist/server/runtime-core.d.ts +7 -0
- package/dist/server/runtime-core.d.ts.map +1 -1
- package/dist/server/runtime-core.js +10 -2
- package/dist/server/runtime-core.js.map +1 -1
- package/dist/server/server-types.d.ts +17 -8
- package/dist/server/server-types.d.ts.map +1 -1
- package/dist/server/server.d.ts +27 -12
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +326 -108
- package/dist/server/server.js.map +1 -1
- package/dist/server/sse.d.ts +1 -1
- package/dist/server/sse.d.ts.map +1 -1
- package/dist/server/sse.js +7 -5
- package/dist/server/sse.js.map +1 -1
- package/dist/server/text-slots.d.ts +8 -0
- package/dist/server/text-slots.d.ts.map +1 -0
- package/dist/server/text-slots.js +19 -0
- package/dist/server/text-slots.js.map +1 -0
- package/dist/server/webhook.d.ts +1 -1
- package/dist/server/webhook.d.ts.map +1 -1
- package/dist/server/webhook.js +24 -19
- package/dist/server/webhook.js.map +1 -1
- package/dist/server/websocket.d.ts +18 -3
- package/dist/server/websocket.d.ts.map +1 -1
- package/dist/server/websocket.js +45 -5
- package/dist/server/websocket.js.map +1 -1
- package/dist/server/ws-bun.d.ts.map +1 -1
- package/dist/server/ws-bun.js +7 -4
- package/dist/server/ws-bun.js.map +1 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -0
- package/dist/server.js.map +1 -1
- package/dist/single-copy.d.ts +5 -0
- package/dist/single-copy.d.ts.map +1 -1
- package/dist/single-copy.js +26 -17
- package/dist/single-copy.js.map +1 -1
- package/dist/tool-cli.d.ts +64 -0
- package/dist/tool-cli.d.ts.map +1 -0
- package/dist/tool-cli.js +495 -0
- package/dist/tool-cli.js.map +1 -0
- package/dist/transport-codec.d.ts.map +1 -1
- package/dist/transport-codec.js +2 -1
- package/dist/transport-codec.js.map +1 -1
- package/package.json +11 -1
- package/src/assurance.ts +1 -0
- package/src/binary.ts +17 -0
- package/src/edge-kit.ts +1 -0
- package/src/errors.ts +1 -0
- package/src/evidence.ts +8 -0
- package/src/file-type.ts +130 -0
- package/src/index.ts +3 -1
- package/src/internal/hook-origins.ts +10 -0
- package/src/internal/route-program.ts +261 -93
- package/src/reflection.ts +71 -7
- package/src/response-observer.ts +6 -0
- package/src/server/answer-step.ts +68 -0
- package/src/server/bare-error-lane.ts +9 -4
- package/src/server/base64-answers.ts +138 -0
- package/src/server/before-handler.ts +14 -0
- package/src/server/body-lane.ts +19 -9
- package/src/server/body-parser.ts +1 -5
- package/src/server/body.ts +96 -36
- package/src/server/core-copies.ts +5 -2
- package/src/server/http.ts +6 -15
- package/src/server/install.ts +3 -0
- package/src/server/json-response.ts +13 -0
- package/src/server/json-text.ts +34 -0
- package/src/server/multipart.ts +232 -100
- package/src/server/node-outcome.ts +4 -5
- package/src/server/proto-guard.ts +39 -4
- package/src/server/query.ts +7 -13
- package/src/server/repeated-prefix.ts +25 -0
- package/src/server/request-context.ts +5 -5
- package/src/server/respond.ts +47 -30
- package/src/server/response-contract-lane.ts +132 -9
- package/src/server/route-template.ts +24 -0
- package/src/server/runtime-core.ts +12 -2
- package/src/server/server-types.ts +18 -6
- package/src/server/server.ts +361 -112
- package/src/server/sse.ts +8 -6
- package/src/server/text-slots.ts +21 -0
- package/src/server/webhook.ts +29 -24
- package/src/server/websocket.ts +57 -8
- package/src/server/ws-bun.ts +7 -3
- package/src/server.ts +2 -0
- package/src/single-copy.ts +31 -15
- package/src/tool-cli.ts +584 -0
- package/src/transport-codec.ts +3 -1
package/dist/server/server.js
CHANGED
|
@@ -6,12 +6,15 @@ import { assuranceDeclarationsOf, assuranceEvidenceFor, NIFRA_ASSURANCE_IDS, } f
|
|
|
6
6
|
import { RouteCatalog } from "../internal/route-catalog.js";
|
|
7
7
|
import { adoptRouteRuntimes, compileRouteOptions } from "../internal/route-compiler.js";
|
|
8
8
|
import { compileRouteExecutionPlan, } from "../internal/route-execution.js";
|
|
9
|
-
import { compileRouteProgram, executeRouteProgram } from "../internal/route-program.js";
|
|
9
|
+
import { authenticateMount, compileRouteProgram, executeRouteProgram, } from "../internal/route-program.js";
|
|
10
10
|
import { isSameOriginRequest } from "../internal/same-origin.js";
|
|
11
11
|
import { decodeRouteParams, expandOptionalParams, } from "../router/pattern.js";
|
|
12
12
|
import { EMPTY_PARAMS, METHODS, Router } from "../router/router.js";
|
|
13
|
+
import { withAnswerSteps } from "./answer-step.js";
|
|
14
|
+
import { isAuthenticatedRequest, markAuthenticatedRequest, } from "./auth.js";
|
|
13
15
|
import { emitRequestErrorLog, renderBareError } from "./bare-error-lane.js";
|
|
14
|
-
import {
|
|
16
|
+
import { LAST_BEFORE_HANDLE } from "./before-handler.js";
|
|
17
|
+
import { assertByteLimit, clientBodyErrorCode, hasTrustedBodyFraming, markTransportCap, markTrustedBodyFraming, UNLIMITED_BODY_BYTES, } from "./body.js";
|
|
15
18
|
import { NIFRA_PLATFORM_CLIENT_IP_DERIVED, resolveClientIp, } from "./client-ip.js";
|
|
16
19
|
import { mergeRefusal, recordCopyVersion } from "./core-copies.js";
|
|
17
20
|
import { hasLowercaseHeaderKeysMark, headerKeysAllLowercase, markLowercaseHeaderKeys, } from "./header-case.js";
|
|
@@ -25,9 +28,10 @@ import { needsResolving, resolveDotSegments } from "./request-target.js";
|
|
|
25
28
|
import { applyStaticResponseHeaders, buildStaticResponseHeaders, claimHandedOver, fusedRespond, fusedRespondNoSet, handedBackResponse, recordResidentTexts, toResponse, writableResponse, } from "./respond.js";
|
|
26
29
|
import { CONTEXT_REQUEST, CONTEXT_SEARCH, CONTEXT_SET, EMPTY_RESPONSE_CONTROLS, getNeverAbortSignal, getUnboundedRequestBudget, headerOf, isResponseResult, requestOf, } from "./runtime-core.js";
|
|
27
30
|
import { normalizeStaticResponseHeaders } from "./static-headers.js";
|
|
31
|
+
import { withTextSlots } from "./text-slots.js";
|
|
28
32
|
import { plainValidationError } from "./validation.js";
|
|
29
33
|
import { NIFRA_BACKEND_WS_RUNTIME } from "../mount.js";
|
|
30
|
-
import { GET_WS_RUNTIME, INSTALL_EFFECT_LEDGER, INSTALL_IDEMPOTENCY, INSTALL_MCP, INSTALL_NODE_DIRECT, INSTALL_NOT_FOUND, INSTALL_RESPONSE_CONTRACT, INSTALL_RESPONSE_OBSERVER, INSTALL_SSE, INSTALL_WS, NODE_NATIVE_MOUNT, REPORT_ERROR, RESOLVE_NODE_MOUNT, } from "./install.js";
|
|
34
|
+
import { GET_WS_RUNTIME, INSTALL_EFFECT_LEDGER, INSTALL_IDEMPOTENCY, INSTALL_MCP, INSTALL_NODE_DIRECT, INSTALL_NOT_FOUND, INSTALL_RESPONSE_CONTRACT, INSTALL_RESPONSE_OBSERVER, INSTALL_SSE, INSTALL_WS, NODE_NATIVE_MOUNT, REPORT_ERROR, RESOLVE_NODE_MOUNT, SERVER_LIFECYCLE, } from "./install.js";
|
|
31
35
|
import { jsonLogger } from "./logger.js";
|
|
32
36
|
// CORS uses this marker to say its request hook only handles OPTIONS preflight. Bun can keep
|
|
33
37
|
// ordinary registered routes on its native table and send unmatched OPTIONS requests through the
|
|
@@ -129,6 +133,8 @@ function requireMcpRuntime(runtime) {
|
|
|
129
133
|
// existing importers keep resolving them from the server module.
|
|
130
134
|
export { defineContextPlugin, defineIdentityPlugin, definePlugin, defineRouterPlugin, } from "./plugin.js";
|
|
131
135
|
const DEFAULT_MAX_BODY_BYTES = 1_000_000;
|
|
136
|
+
const BODY_RESPONSE = (response) => response;
|
|
137
|
+
const BODY_ERROR = (error) => Promise.reject(error);
|
|
132
138
|
const DEFAULT_DRAIN_MS = 10_000;
|
|
133
139
|
const DRAIN_POLL_MS = 10;
|
|
134
140
|
const STOP_HOOK_TIMEOUT_MS = 5_000;
|
|
@@ -171,16 +177,106 @@ function hasUnroutedParam(params) {
|
|
|
171
177
|
* Bun, workerd and every `new Request()` parse it first. Only there does `fetch` read the URL up front.
|
|
172
178
|
*/
|
|
173
179
|
const rawRequestTargets = typeof globalThis.Deno !== "undefined";
|
|
174
|
-
/**
|
|
175
|
-
|
|
180
|
+
/**
|
|
181
|
+
* Bun parses the target only when it can read the Host. Otherwise `Request.url` is the target as sent:
|
|
182
|
+
* relative for a Host with a space or a byte past ASCII or for no Host at all, the raw authority
|
|
183
|
+
* for a port past 65535, and `new URL(req.url)` throws in the app. So Bun is held to the Host check
|
|
184
|
+
* Deno runs.
|
|
185
|
+
*/
|
|
186
|
+
const unparsedBunTargets = typeof Bun !== "undefined";
|
|
187
|
+
/**
|
|
188
|
+
* `req` as Bun or workerd would have delivered it: the same request, its URL resolved as they parse it.
|
|
189
|
+
* `undefined` when the `Request` constructor refuses what the wire carried - a TRACE, CONNECT or TRACK,
|
|
190
|
+
* a lower-case `get` with a body - which {@link resolvedSource} routes instead.
|
|
191
|
+
*/
|
|
192
|
+
function withResolvedTarget(req, url) {
|
|
193
|
+
let rebuilt;
|
|
194
|
+
try {
|
|
195
|
+
rebuilt = new Request(url, req);
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
// The same body, so the same framing: a runtime still delimited it.
|
|
201
|
+
if (hasTrustedBodyFraming(req))
|
|
202
|
+
markTrustedBodyFraming(rebuilt);
|
|
203
|
+
return rebuilt;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* A request no `Request` can be rebuilt from, as a source carrying its resolved URL: it is routed on that
|
|
207
|
+
* URL and gets the 404 or 405 the other runtimes give, where rebuilding it threw out of `fetch`. A
|
|
208
|
+
* handler that reads `c.req` gets the request as Deno delivered it.
|
|
209
|
+
*/
|
|
210
|
+
function resolvedSource(req, url) {
|
|
211
|
+
const source = {
|
|
212
|
+
method: req.method,
|
|
213
|
+
url,
|
|
214
|
+
get headers() {
|
|
215
|
+
return req.headers;
|
|
216
|
+
},
|
|
217
|
+
header: (name) => headerOf(req, name),
|
|
218
|
+
get body() {
|
|
219
|
+
return req.body;
|
|
220
|
+
},
|
|
221
|
+
arrayBuffer: () => req.arrayBuffer(),
|
|
222
|
+
json: () => req.json(),
|
|
223
|
+
bytes: () => req.bytes(),
|
|
224
|
+
request: req,
|
|
225
|
+
};
|
|
226
|
+
if (hasTrustedBodyFraming(req))
|
|
227
|
+
markTrustedBodyFraming(source);
|
|
228
|
+
return source;
|
|
229
|
+
}
|
|
176
230
|
// A Host as RFC 9110 allows it: a reg-name or an IP literal, then an optional port. Deno builds
|
|
177
231
|
// `Request.url` by appending the target to the Host as received, so `Host: x/admin#` turned a
|
|
178
232
|
// request for `/public` into one for `/admin`. Node refuses such a Host before routing; so does this.
|
|
179
|
-
const HOST_AUTHORITY = /^(?:\[[0-9a-f:.%]+\]|[\w.-]+)(
|
|
180
|
-
//
|
|
181
|
-
|
|
233
|
+
const HOST_AUTHORITY = /^(?:\[[0-9a-f:.%]+\]|[\w.-]+)(?::(\d{1,5}))?$/i;
|
|
234
|
+
// Clients repeat one Host on every request; comparing to the last valid one is ~7x cheaper than the regex.
|
|
235
|
+
let lastValidHost = "";
|
|
236
|
+
// A port past 65535, or an IPv6 literal no URL parser takes (`[1:2]`), is refused as Node refuses it:
|
|
237
|
+
// no URL can be built on either, so resolving a dot segment against one threw out of `fetch`.
|
|
238
|
+
const invalidHost = (host) => {
|
|
239
|
+
if (host === lastValidHost)
|
|
240
|
+
return false;
|
|
241
|
+
const authority = HOST_AUTHORITY.exec(host);
|
|
242
|
+
if (authority === null)
|
|
243
|
+
return true;
|
|
244
|
+
if (authority[1] !== undefined && Number(authority[1]) > 65_535)
|
|
245
|
+
return true;
|
|
246
|
+
if (host.charCodeAt(0) === 91 /* [ */ && !URL.canParse(`http://${host}/`))
|
|
247
|
+
return true;
|
|
248
|
+
lastValidHost = host;
|
|
249
|
+
return false;
|
|
250
|
+
};
|
|
251
|
+
/**
|
|
252
|
+
* What a request is routed on in place of `req.url`, before anything reads it: `true` refuses its
|
|
253
|
+
* Host (400), a URL replaces the target, `false` keeps it. Deno hands the target over as sent, so
|
|
254
|
+
* its dot segments are resolved; so does Bun when HTTP/1.0 leaves the Host out, and its route table
|
|
255
|
+
* matched `/files/..` raw. Such a request is served as `localhost`, as Node and Deno serve it.
|
|
256
|
+
* `headerOf`, not `req.headers`: on Deno that reads the Host alone instead of building every
|
|
257
|
+
* header.
|
|
258
|
+
*/
|
|
259
|
+
const requestTarget = (req) => {
|
|
260
|
+
if (!rawRequestTargets && !unparsedBunTargets)
|
|
261
|
+
return false;
|
|
182
262
|
const host = headerOf(req, "host");
|
|
183
|
-
|
|
263
|
+
if (host !== null && invalidHost(host)) {
|
|
264
|
+
if (rawRequestTargets)
|
|
265
|
+
return true;
|
|
266
|
+
// Bun served it anyway: it could not read the Host, so it left the target unparsed, or it
|
|
267
|
+
// built the URL on it - sent twice (joined with ", ", the URL on the first; RFC 9112 section
|
|
268
|
+
// 3.2 answers 400), percent-encoded (`%61.example` routed as `a.example`), or with a
|
|
269
|
+
// sub-delimiter (`a!b`). A request built in process keeps the URL its caller built, unless Bun
|
|
270
|
+
// would have built it so.
|
|
271
|
+
const routed = URL.parse(req.url)?.host;
|
|
272
|
+
return routed === undefined || URL.parse(`//${host.split(", ")[0]}`, req.url)?.host === routed;
|
|
273
|
+
}
|
|
274
|
+
if (rawRequestTargets)
|
|
275
|
+
return needsResolving(req.url) && resolveDotSegments(req.url);
|
|
276
|
+
// A request built in process carries an absolute URL whatever its headers say.
|
|
277
|
+
return (host === null &&
|
|
278
|
+
req.url.charCodeAt(0) === 47 /* / */ &&
|
|
279
|
+
resolveDotSegments(`http://localhost${req.url}`));
|
|
184
280
|
};
|
|
185
281
|
function normalizeMountPrefix(path) {
|
|
186
282
|
if (!path.startsWith("/") || path.includes("?") || path.includes("#")) {
|
|
@@ -210,13 +306,17 @@ function mountPriorityOf(priority) {
|
|
|
210
306
|
function compareMounts(a, b) {
|
|
211
307
|
return b.priority - a.priority || b.path.length - a.path.length || a.order - b.order;
|
|
212
308
|
}
|
|
309
|
+
/** Rebase a mounted request's path without losing the authentication mark used by child caches. */
|
|
213
310
|
function stripMountPrefix(request, prefix) {
|
|
214
311
|
if (prefix === "/")
|
|
215
312
|
return request;
|
|
216
313
|
const url = new URL(request.url);
|
|
217
314
|
const rest = url.pathname.slice(prefix.length);
|
|
218
315
|
url.pathname = rest === "" ? "/" : rest;
|
|
219
|
-
|
|
316
|
+
const childRequest = new Request(url.href, request);
|
|
317
|
+
if (isAuthenticatedRequest(request))
|
|
318
|
+
markAuthenticatedRequest(childRequest);
|
|
319
|
+
return childRequest;
|
|
220
320
|
}
|
|
221
321
|
function responseSet(ctx) {
|
|
222
322
|
return ctx[CONTEXT_SET]() ?? EMPTY_RESPONSE_CONTROLS;
|
|
@@ -403,6 +503,17 @@ const recordScopedHook = (key, routesAtPush, kind) => {
|
|
|
403
503
|
site: captureCallerSite(),
|
|
404
504
|
});
|
|
405
505
|
};
|
|
506
|
+
/** `next(value)` now, or once `value` settles. */
|
|
507
|
+
const thenMaybe = (value, next) => (value instanceof Promise ? value.then(next) : next(value));
|
|
508
|
+
/** A mount registered after an `authenticate()` stage runs it, so the stage covers something. */
|
|
509
|
+
const noteAuthenticatedMount = (key) => {
|
|
510
|
+
const state = hookAudit.get(key);
|
|
511
|
+
if (state === undefined)
|
|
512
|
+
return;
|
|
513
|
+
for (const site of state.sites)
|
|
514
|
+
if (site.kind === "authenticate")
|
|
515
|
+
site.routesAtPush = -1;
|
|
516
|
+
};
|
|
406
517
|
/** Count routes a `merge()` adopted: they never run this server's order-scoped hooks, so a hook
|
|
407
518
|
* followed only by merges covers nothing. */
|
|
408
519
|
const noteMergedRoutes = (key, count) => {
|
|
@@ -555,6 +666,7 @@ export class Server {
|
|
|
555
666
|
deadlineAdmissionOptions;
|
|
556
667
|
gracefulSignals;
|
|
557
668
|
stopHooks;
|
|
669
|
+
stoppingHooks;
|
|
558
670
|
fetchMounts;
|
|
559
671
|
mountOrder;
|
|
560
672
|
/** Number of composed mounts that must run before the parent route table. Keeps legacy mounts off the hot path. */
|
|
@@ -632,9 +744,13 @@ export class Server {
|
|
|
632
744
|
/** Body/raw response hooks need a framework-payload marker; keep that decision per app. */
|
|
633
745
|
responseBodyTag;
|
|
634
746
|
responseBodyOwners;
|
|
635
|
-
|
|
636
|
-
//
|
|
637
|
-
ctx
|
|
747
|
+
// General lifecycles need no extra response bookkeeping when neither controls nor observers
|
|
748
|
+
// use it. Share the fused renderer after the lifecycle, including on body/write routes.
|
|
749
|
+
finalizeResponse = (result, set, ctx) => set === EMPTY_RESPONSE_CONTROLS && this.onResponseHooks.length === 0
|
|
750
|
+
? fusedRespondNoSet(result, this.responseBodyTag, this.staticResponseHeaders, true)
|
|
751
|
+
: toResponse(result, set, this.responseBodyTag, this.staticResponseHeaders,
|
|
752
|
+
// Response hooks write next, and they run for this same request object.
|
|
753
|
+
ctx !== undefined && this.onResponseHooks.length > 0 ? ctx[CONTEXT_REQUEST] : undefined, this.onResponseHooks.length === 0);
|
|
638
754
|
responseRequests;
|
|
639
755
|
/** A request the WebSocket handshake check ran the onRequest hooks on and then passed to HTTP →
|
|
640
756
|
* what the hooks returned, so `fetch` routes that instead of running the hooks a second time. */
|
|
@@ -700,6 +816,7 @@ export class Server {
|
|
|
700
816
|
this.gracefulSignals =
|
|
701
817
|
options.gracefulSignals === true ? {} : options.gracefulSignals || undefined;
|
|
702
818
|
this.stopHooks = [];
|
|
819
|
+
this.stoppingHooks = [];
|
|
703
820
|
this.fetchMounts = [];
|
|
704
821
|
this.mountOrder = 0;
|
|
705
822
|
this.preRouteMountCount = 0;
|
|
@@ -770,10 +887,25 @@ export class Server {
|
|
|
770
887
|
this.stopHooks.push(fn);
|
|
771
888
|
return this;
|
|
772
889
|
}
|
|
890
|
+
/**
|
|
891
|
+
* Register a callback awaited as soon as `stop()` begins, while the server still serves: readiness
|
|
892
|
+
* should fail from here, and work that outlives a request (a poller, a queue consumer) should stop
|
|
893
|
+
* taking more. Hung callbacks are bounded.
|
|
894
|
+
*/
|
|
895
|
+
onStopping(fn) {
|
|
896
|
+
this.assertConfigurable("onStopping()");
|
|
897
|
+
if (typeof fn !== "function")
|
|
898
|
+
throw new TypeError("onStopping callback must be a function");
|
|
899
|
+
this.stoppingHooks.push(fn);
|
|
900
|
+
return this;
|
|
901
|
+
}
|
|
773
902
|
/**
|
|
774
903
|
* Mount a legacy fetch handler under a literal path prefix. Mounted handlers sit behind typed
|
|
775
904
|
* routes, so routes can move one at a time while the remaining legacy surface stays live.
|
|
776
|
-
* Mounted responses are outside Nifra's typed route and response-contract checks.
|
|
905
|
+
* Mounted responses are outside Nifra's typed route and response-contract checks. An
|
|
906
|
+
* `authenticate()` stage registered before the mount, or a required auth plugin built on one
|
|
907
|
+
* (`bearer()`, `jwt()`, `basicAuth()`, `apiKey()`), refuses a request before the handler sees it;
|
|
908
|
+
* mount before the stage to keep the legacy surface public.
|
|
777
909
|
*
|
|
778
910
|
* It carries HTTP only: a WebSocket upgrade under the prefix reaches the handler as an ordinary
|
|
779
911
|
* request and never becomes a socket. Serve that path's sockets with `app.ws()` routes or a
|
|
@@ -792,7 +924,10 @@ export class Server {
|
|
|
792
924
|
beforeRoutes: false,
|
|
793
925
|
order: this.mountOrder++,
|
|
794
926
|
opaque: options.opaque,
|
|
927
|
+
authStages: [...this.authStages],
|
|
795
928
|
};
|
|
929
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production" && this.authStages.length > 0)
|
|
930
|
+
noteAuthenticatedMount(this);
|
|
796
931
|
this.fetchMounts.push(mount);
|
|
797
932
|
this.fetchMounts.sort(compareMounts);
|
|
798
933
|
return this;
|
|
@@ -846,7 +981,10 @@ export class Server {
|
|
|
846
981
|
order: this.mountOrder++,
|
|
847
982
|
opaque: options.opaque,
|
|
848
983
|
app,
|
|
984
|
+
authStages: [...this.authStages],
|
|
849
985
|
};
|
|
986
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production" && this.authStages.length > 0)
|
|
987
|
+
noteAuthenticatedMount(this);
|
|
850
988
|
// Mounts run before routes: a request under this prefix that no mount on the way strips comes back
|
|
851
989
|
// here unchanged until the stack overflows, which takes the process down. Dev-time only, like the
|
|
852
990
|
// hook audit: a production define folds this away, and the first dev request would hit it anyway.
|
|
@@ -888,7 +1026,8 @@ export class Server {
|
|
|
888
1026
|
* (see {@link ServerOptions.unusedScopedHooks}).
|
|
889
1027
|
*/
|
|
890
1028
|
/**
|
|
891
|
-
* Register a dedicated authentication stage for subsequent routes
|
|
1029
|
+
* Register a dedicated authentication stage for subsequent routes, and for the `mount()` and
|
|
1030
|
+
* `mountFetch()` handlers registered after it: a request it refuses never reaches them.
|
|
892
1031
|
*
|
|
893
1032
|
* Unlike `derive`, this stage receives a body-blind authentication capsule and is compiled before
|
|
894
1033
|
* route validation. Keeping it separate prevents an unrelated derive from accidentally becoming a
|
|
@@ -950,7 +1089,13 @@ export class Server {
|
|
|
950
1089
|
*/
|
|
951
1090
|
beforeHandle(fn) {
|
|
952
1091
|
this.assertConfigurable("beforeHandle()");
|
|
953
|
-
this.beforeHandleHooks
|
|
1092
|
+
const hooks = this.beforeHandleHooks;
|
|
1093
|
+
let at = hooks.length;
|
|
1094
|
+
if (!LAST_BEFORE_HANDLE.has(fn)) {
|
|
1095
|
+
while (at > 0 && LAST_BEFORE_HANDLE.has(hooks[at - 1]))
|
|
1096
|
+
at--;
|
|
1097
|
+
}
|
|
1098
|
+
hooks.splice(at, 0, fn);
|
|
954
1099
|
if (hookAuditRuntime && process.env.NODE_ENV !== "production") {
|
|
955
1100
|
recordScopedHook(this, this.catalog.size + this.wsRoutes.length, "beforeHandle");
|
|
956
1101
|
tagHook(fn, "beforeHandle", this);
|
|
@@ -1451,6 +1596,7 @@ export class Server {
|
|
|
1451
1596
|
throw new RouteConfigError("INVALID_PATH", `group(${JSON.stringify(prefix)}) route path must start with "/": ${JSON.stringify(path)}`);
|
|
1452
1597
|
}
|
|
1453
1598
|
prepareRoute(method, path, schema, handler) {
|
|
1599
|
+
schema = withTextSlots(schema);
|
|
1454
1600
|
// Route provenance for the CLI's capability walk, which sets NIFRA_ROUTE_SITES while it loads the
|
|
1455
1601
|
// app: the module that registered the route, read where a static scan cannot see the path.
|
|
1456
1602
|
if (hookAuditRuntime &&
|
|
@@ -1480,6 +1626,15 @@ export class Server {
|
|
|
1480
1626
|
defaultOnValidationError: this.defaultOnValidationError,
|
|
1481
1627
|
}, method, path, schema, handler);
|
|
1482
1628
|
const { pattern, bodyLimit, capabilities, routeDecorations, hasDecorations, idempotent, ledgered, responseContract, lanes, routeAssurance, } = compiled;
|
|
1629
|
+
// A schema that writes its answer its own way (`t.bytes()`) wraps every stage that can answer.
|
|
1630
|
+
const stages = withAnswerSteps(schema, method, path, {
|
|
1631
|
+
handler,
|
|
1632
|
+
beforeHandle: this.beforeHandleHooks,
|
|
1633
|
+
afterHandle: this.afterHandleHooks,
|
|
1634
|
+
onError: this.onErrorHooks,
|
|
1635
|
+
});
|
|
1636
|
+
handler = stages.handler;
|
|
1637
|
+
const { beforeHandle, afterHandle, onError } = stages;
|
|
1483
1638
|
if (idempotent !== undefined && this.idempotencyRuntime !== undefined) {
|
|
1484
1639
|
handler = this.idempotencyRuntime.wrapHandler(handler);
|
|
1485
1640
|
}
|
|
@@ -1498,11 +1653,11 @@ export class Server {
|
|
|
1498
1653
|
const fusedBodyRunner = fusedBody
|
|
1499
1654
|
? this.buildFusedBodyRunner(handler, schema?.body, hasDecorations ? routeDecorations : undefined, bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
1500
1655
|
: isFusedBodyLifecycle
|
|
1501
|
-
? this.buildFusedBodyDeriveBeforeAfterRunner(handler, this.derives[0],
|
|
1656
|
+
? this.buildFusedBodyDeriveBeforeAfterRunner(handler, this.derives[0], beforeHandle[0], afterHandle[0], schema?.body, bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
1502
1657
|
: isFusedLifecycle && lifecycleHookLane === "derive-before"
|
|
1503
|
-
? this.buildFusedDeriveBeforeRunner(handler, this.derives[0],
|
|
1658
|
+
? this.buildFusedDeriveBeforeRunner(handler, this.derives[0], beforeHandle[0], undefined, schema?.query, bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
1504
1659
|
: isFusedLifecycle && lifecycleHookLane === "derive-before-after"
|
|
1505
|
-
? this.buildFusedDeriveBeforeRunner(handler, this.derives[0],
|
|
1660
|
+
? this.buildFusedDeriveBeforeRunner(handler, this.derives[0], beforeHandle[0], afterHandle[0], schema?.query, bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
1506
1661
|
: undefined;
|
|
1507
1662
|
const fusedWeb = bare && this.aroundHooks.length === 0
|
|
1508
1663
|
? this.buildFusedWeb(handler, hasDecorations ? routeDecorations : undefined, isContextlessNoArgArrow(handler), bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
@@ -1511,20 +1666,20 @@ export class Server {
|
|
|
1511
1666
|
: fusedBody
|
|
1512
1667
|
? this.buildFusedBodyWeb(fusedBodyRunner)
|
|
1513
1668
|
: isFusedBodyLifecycle
|
|
1514
|
-
? this.buildFusedBodyDeriveBeforeAfter(handler, this.derives[0],
|
|
1669
|
+
? this.buildFusedBodyDeriveBeforeAfter(handler, this.derives[0], beforeHandle[0], afterHandle[0], schema?.body, bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
1515
1670
|
: isFusedLifecycle && lifecycleHookLane === "derive-before"
|
|
1516
|
-
? this.buildFusedDeriveBefore(handler, this.derives[0],
|
|
1671
|
+
? this.buildFusedDeriveBefore(handler, this.derives[0], beforeHandle[0], schema?.query, bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
1517
1672
|
: isFusedLifecycle && lifecycleHookLane === "derive-before-after"
|
|
1518
|
-
? this.buildFusedDeriveBeforeAfter(handler, this.derives[0],
|
|
1673
|
+
? this.buildFusedDeriveBeforeAfter(handler, this.derives[0], beforeHandle[0], afterHandle[0], schema?.query, bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
1519
1674
|
: undefined;
|
|
1520
1675
|
const program = compileRouteProgram({
|
|
1521
1676
|
schema,
|
|
1522
1677
|
handler: handler,
|
|
1523
1678
|
derives: this.derives,
|
|
1524
1679
|
authStages: compiled.authStages,
|
|
1525
|
-
beforeHandle
|
|
1526
|
-
afterHandle
|
|
1527
|
-
onError
|
|
1680
|
+
beforeHandle,
|
|
1681
|
+
afterHandle,
|
|
1682
|
+
onError,
|
|
1528
1683
|
decorations: routeDecorations,
|
|
1529
1684
|
hasDecorations,
|
|
1530
1685
|
bodySchema: schema?.body,
|
|
@@ -1552,9 +1707,9 @@ export class Server {
|
|
|
1552
1707
|
authStages: [...compiled.authStages],
|
|
1553
1708
|
decorations: routeDecorations,
|
|
1554
1709
|
hasDecorations,
|
|
1555
|
-
beforeHandle: [...
|
|
1556
|
-
afterHandle: [...
|
|
1557
|
-
onError: [...
|
|
1710
|
+
beforeHandle: [...beforeHandle],
|
|
1711
|
+
afterHandle: [...afterHandle],
|
|
1712
|
+
onError: [...onError],
|
|
1558
1713
|
lifecycleHookLane: lanes.lifecycleHookLane,
|
|
1559
1714
|
around: [...this.aroundHooks],
|
|
1560
1715
|
execution,
|
|
@@ -1940,6 +2095,7 @@ export class Server {
|
|
|
1940
2095
|
// Cleanup the source registered (a pool, a queue consumer) belongs to the server that now runs
|
|
1941
2096
|
// its routes; dropping it would leak the resource past `stop()`.
|
|
1942
2097
|
this.stopHooks.push(...source.stopHooks);
|
|
2098
|
+
this.stoppingHooks.push(...source.stoppingHooks);
|
|
1943
2099
|
this.mcpResourceList.push(...source.mcpResourceList);
|
|
1944
2100
|
this.mcpPromptList.push(...source.mcpPromptList);
|
|
1945
2101
|
}
|
|
@@ -2029,11 +2185,14 @@ export class Server {
|
|
|
2029
2185
|
fetch(req, platform) {
|
|
2030
2186
|
// Deno's `Request.url` is the target as sent; Bun, workerd and every `new Request()` parse it.
|
|
2031
2187
|
// Route what the parsed URL says, so `/users/../admin` is `/admin` on every runtime.
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2188
|
+
const target = requestTarget(req);
|
|
2189
|
+
if (target === true)
|
|
2190
|
+
return jsonError(400, "invalid_host");
|
|
2191
|
+
if (target !== false) {
|
|
2192
|
+
const rebuilt = withResolvedTarget(req, target);
|
|
2193
|
+
if (rebuilt === undefined)
|
|
2194
|
+
return this.fetchSource(resolvedSource(req, target), platform);
|
|
2195
|
+
req = rebuilt;
|
|
2037
2196
|
}
|
|
2038
2197
|
// An edge deployment whose only ingress is this method declares its requests runtime-framed at
|
|
2039
2198
|
// construction; everything else stays on the delivered-byte check (see `trustBodyFraming`).
|
|
@@ -2151,7 +2310,7 @@ export class Server {
|
|
|
2151
2310
|
// The mount the ordinary path reaches first: every `mount()` runs before any `mountFetch()`,
|
|
2152
2311
|
// whatever their prefix lengths. A 404 fallthrough chain stays on the ordinary path.
|
|
2153
2312
|
const mount = this.firstMountUnder(parts.pathname, true) ?? this.firstMountUnder(parts.pathname, false);
|
|
2154
|
-
if (mount === undefined || mount.fallbackOn404)
|
|
2313
|
+
if (mount === undefined || mount.fallbackOn404 || mount.authStages.length > 0)
|
|
2155
2314
|
return undefined;
|
|
2156
2315
|
const candidate = mount.handler[NODE_NATIVE_MOUNT];
|
|
2157
2316
|
return typeof candidate === "function"
|
|
@@ -2435,12 +2594,16 @@ export class Server {
|
|
|
2435
2594
|
// The adapter answers a pass by handing this same object to `fetch`.
|
|
2436
2595
|
const received = req;
|
|
2437
2596
|
// The handshake routes the path `fetch` would: Host checked, dot segments resolved (see `fetch`).
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2597
|
+
const target = requestTarget(req);
|
|
2598
|
+
if (target === true) {
|
|
2599
|
+
return { kind: "reject", response: this.wrapWebResponse(jsonError(400, "invalid_host")) };
|
|
2600
|
+
}
|
|
2601
|
+
if (target !== false) {
|
|
2602
|
+
// A GET carrying a body cannot be rebuilt; `fetch` routes it as the ordinary request it is.
|
|
2603
|
+
const rebuilt = withResolvedTarget(req, target);
|
|
2604
|
+
if (rebuilt === undefined)
|
|
2605
|
+
return WS_PASS;
|
|
2606
|
+
req = rebuilt;
|
|
2444
2607
|
}
|
|
2445
2608
|
// The handshake's caller is the one `fetch` derives: hooks and `upgrade()` see the trusted IP.
|
|
2446
2609
|
platform = this.trustedPlatform(req, platform);
|
|
@@ -2545,26 +2708,35 @@ export class Server {
|
|
|
2545
2708
|
}
|
|
2546
2709
|
if (selected === undefined)
|
|
2547
2710
|
return WS_PASS;
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
childRequest =
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2711
|
+
const mount = selected;
|
|
2712
|
+
const upgrade = () => {
|
|
2713
|
+
let childRequest = request;
|
|
2714
|
+
if (mount.stripPrefix) {
|
|
2715
|
+
childRequest = stripMountPrefix(request, mount.path);
|
|
2716
|
+
mount.wsPassed ??= new WeakMap();
|
|
2717
|
+
mount.wsPassed.set(request, childRequest);
|
|
2718
|
+
}
|
|
2719
|
+
try {
|
|
2720
|
+
return mount.resolveWebSocketUpgrade(childRequest, platform);
|
|
2721
|
+
}
|
|
2722
|
+
catch {
|
|
2723
|
+
return refuse(jsonError(500, "internal_error"));
|
|
2724
|
+
}
|
|
2725
|
+
};
|
|
2561
2726
|
const settle = (outcome) => outcome.kind === "pass" ||
|
|
2562
2727
|
(outcome.kind === "reject" && outcome.response.status === 404 && selected.fallbackOn404)
|
|
2563
2728
|
? dispatch(selectedIndex + 1)
|
|
2564
2729
|
: outcome.kind === "reject"
|
|
2565
2730
|
? refuse(outcome.response)
|
|
2566
2731
|
: outcome;
|
|
2567
|
-
|
|
2732
|
+
const run = () => {
|
|
2733
|
+
const child = upgrade();
|
|
2734
|
+
return child instanceof Promise ? child.then(settle) : settle(child);
|
|
2735
|
+
};
|
|
2736
|
+
// Only a child's miss may fall through, never an authentication refusal.
|
|
2737
|
+
return mount.authStages.length === 0
|
|
2738
|
+
? run()
|
|
2739
|
+
: thenMaybe(authenticateMount(mount.authStages, request, platform, this.logger), (refusal) => (refusal === undefined ? run() : refuse(refusal)));
|
|
2568
2740
|
};
|
|
2569
2741
|
return dispatch(0);
|
|
2570
2742
|
};
|
|
@@ -2857,8 +3029,8 @@ export class Server {
|
|
|
2857
3029
|
* routes with validation/hooks keep the full compiled route program, unchanged.
|
|
2858
3030
|
*/
|
|
2859
3031
|
routeAndRun(source, platform, finalize, wrapResponse, onTimeout, webFast) {
|
|
2860
|
-
//
|
|
2861
|
-
//
|
|
3032
|
+
// Native delimiter searches keep path/query splitting out of a character-by-character JS loop.
|
|
3033
|
+
// Keep the slices in locals instead of allocating the public helper's pathname/search pair.
|
|
2862
3034
|
let pathname;
|
|
2863
3035
|
let search;
|
|
2864
3036
|
// Read the source's split ONCE. `urlParts` is an accessor on the Node request sources that
|
|
@@ -2879,22 +3051,11 @@ export class Server {
|
|
|
2879
3051
|
search = "";
|
|
2880
3052
|
}
|
|
2881
3053
|
else {
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
if (c === 63 /* ? */ && searchStart === -1) {
|
|
2888
|
-
pathEnd = i;
|
|
2889
|
-
searchStart = i;
|
|
2890
|
-
}
|
|
2891
|
-
else if (c === 35 /* # */) {
|
|
2892
|
-
if (searchStart === -1)
|
|
2893
|
-
pathEnd = i;
|
|
2894
|
-
searchEnd = i;
|
|
2895
|
-
break;
|
|
2896
|
-
}
|
|
2897
|
-
}
|
|
3054
|
+
const fragment = rawUrl.indexOf("#", start);
|
|
3055
|
+
const searchEnd = fragment === -1 ? rawUrl.length : fragment;
|
|
3056
|
+
const question = rawUrl.indexOf("?", start);
|
|
3057
|
+
const searchStart = question !== -1 && question < searchEnd ? question : -1;
|
|
3058
|
+
const pathEnd = searchStart === -1 ? searchEnd : searchStart;
|
|
2898
3059
|
pathname = rawUrl.slice(start, pathEnd);
|
|
2899
3060
|
search = searchStart === -1 ? "" : rawUrl.slice(searchStart, searchEnd);
|
|
2900
3061
|
}
|
|
@@ -2967,16 +3128,28 @@ export class Server {
|
|
|
2967
3128
|
}
|
|
2968
3129
|
if (mount === undefined)
|
|
2969
3130
|
return fallback ?? jsonError(404, "not_found");
|
|
2970
|
-
const
|
|
2971
|
-
|
|
2972
|
-
:
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
3131
|
+
const selected = mount;
|
|
3132
|
+
const run = () => {
|
|
3133
|
+
const reused = selected.stripPrefix ? selected.wsPassed?.get(request) : undefined;
|
|
3134
|
+
const childRequest = selected.stripPrefix
|
|
3135
|
+
? (reused ?? stripMountPrefix(request, selected.path))
|
|
3136
|
+
: request;
|
|
3137
|
+
// HTTP authentication may have marked the original since the WebSocket pass.
|
|
3138
|
+
if (reused !== undefined && isAuthenticatedRequest(request)) {
|
|
3139
|
+
markAuthenticatedRequest(reused);
|
|
3140
|
+
}
|
|
3141
|
+
const outcome = selected.handler(childRequest, platform);
|
|
3142
|
+
if (!replayable || !selected.fallbackOn404)
|
|
3143
|
+
return outcome;
|
|
3144
|
+
if (outcome instanceof Promise) {
|
|
3145
|
+
return outcome.then((response) => response.status === 404 ? dispatch(index + 1, response) : response);
|
|
3146
|
+
}
|
|
3147
|
+
return outcome.status === 404 ? dispatch(index + 1, outcome) : outcome;
|
|
3148
|
+
};
|
|
3149
|
+
// Keep gate refusals outside the child's 404 fallthrough path.
|
|
3150
|
+
return mount.authStages.length === 0
|
|
3151
|
+
? run()
|
|
3152
|
+
: thenMaybe(authenticateMount(mount.authStages, request, platform, this.logger), (refusal) => refusal ?? run());
|
|
2980
3153
|
};
|
|
2981
3154
|
return dispatch(first);
|
|
2982
3155
|
}
|
|
@@ -3861,7 +4034,7 @@ export class Server {
|
|
|
3861
4034
|
// The thrown-value contract lives in `bare-error-lane.ts`, one source of truth for both this
|
|
3862
4035
|
// lane and the fused body runner. `responseSet` and the logger are injected because they reach
|
|
3863
4036
|
// into class state; the closure is allocated only on the throw path, never on the hot lane.
|
|
3864
|
-
return renderBareError(err, ctx, finalize, responseSet, (e, c) => this.logRequestError(e, c),
|
|
4037
|
+
return renderBareError(err, ctx, finalize, responseSet, (e, c) => this.logRequestError(e, c), wrapResponse);
|
|
3865
4038
|
}
|
|
3866
4039
|
/**
|
|
3867
4040
|
* Keep the ordinary thrown-error response independent from the caught value. Deliberate Response
|
|
@@ -3939,7 +4112,7 @@ export class Server {
|
|
|
3939
4112
|
}
|
|
3940
4113
|
// @ts-expect-error TS6133 -- invoked structurally by compiled plans (internal/route-execution.ts)
|
|
3941
4114
|
runBodyOnly(entry, source, ctx, finalize, wrapResponse) {
|
|
3942
|
-
return readBodyFramed(source, entry.bodyLimit ?? UNLIMITED_BODY_BYTES, this.protoPoisoning, (parsed) => this.finishBodyOnly(entry, parsed, ctx, finalize, wrapResponse), wrapResponse, (err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse), entry.schema?.body);
|
|
4115
|
+
return readBodyFramed(source, entry.bodyLimit ?? UNLIMITED_BODY_BYTES, this.protoPoisoning, (parsed) => this.finishBodyOnly(entry, parsed, ctx, finalize, wrapResponse), wrapResponse, (err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse), entry.schema?.body, this.formOrigins);
|
|
3943
4116
|
}
|
|
3944
4117
|
/** Validate + run the handler for the bodyOnly path - shared by the inline fast path and the
|
|
3945
4118
|
* streaming fallback. A method (not per-request closures) so the hot path allocates nothing
|
|
@@ -4193,7 +4366,15 @@ export class Server {
|
|
|
4193
4366
|
return finalize(result, responseSet(ctx), ctx);
|
|
4194
4367
|
const checked = contract.runtime.check(contract.definition, result, ctx);
|
|
4195
4368
|
if (checked instanceof Promise) {
|
|
4196
|
-
return checked.then((outcome) =>
|
|
4369
|
+
return checked.then((outcome) => {
|
|
4370
|
+
// The render runs outside the try below: a throw (a BigInt, a cycle) must still reach onError.
|
|
4371
|
+
try {
|
|
4372
|
+
return this.finishContractOutcome(ctx, finalize, wrapResponse, outcome);
|
|
4373
|
+
}
|
|
4374
|
+
catch (err) {
|
|
4375
|
+
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse);
|
|
4376
|
+
}
|
|
4377
|
+
}, (err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse));
|
|
4197
4378
|
}
|
|
4198
4379
|
return this.finishContractOutcome(ctx, finalize, wrapResponse, checked);
|
|
4199
4380
|
}
|
|
@@ -4238,6 +4419,9 @@ export class Server {
|
|
|
4238
4419
|
if (err instanceof Response || isResponseResult(err))
|
|
4239
4420
|
return this.finishLifecycleContract(entry, ctx, finalize, wrapResponse, err);
|
|
4240
4421
|
if (entry.onError.length === 0) {
|
|
4422
|
+
const clientCode = clientBodyErrorCode(err);
|
|
4423
|
+
if (clientCode !== undefined)
|
|
4424
|
+
return wrapResponse(plainError(400, clientCode));
|
|
4241
4425
|
// Never crash the server or leak internals. The client gets a flat 500; the detail goes to the
|
|
4242
4426
|
// (redacting) logger. Body-read failures and around-hook failures land here too.
|
|
4243
4427
|
this.logRequestError(err, ctx);
|
|
@@ -4297,6 +4481,9 @@ export class Server {
|
|
|
4297
4481
|
return wrapResponse(plainError(500, "internal_error"));
|
|
4298
4482
|
}
|
|
4299
4483
|
}
|
|
4484
|
+
const clientCode = clientBodyErrorCode(err);
|
|
4485
|
+
if (clientCode !== undefined)
|
|
4486
|
+
return wrapResponse(plainError(400, clientCode));
|
|
4300
4487
|
this.logRequestError(err, ctx);
|
|
4301
4488
|
return fallback === undefined ? wrapResponse(plainError(500, "internal_error")) : fallback();
|
|
4302
4489
|
}
|
|
@@ -4311,27 +4498,35 @@ export class Server {
|
|
|
4311
4498
|
// second continuation, while retaining the exact bounded-read, content-type, and poisoning
|
|
4312
4499
|
// contracts shared by every body route.
|
|
4313
4500
|
const validate = (parsed) => {
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4501
|
+
// Reject outside the parser's JSON catch so a custom validator throw reaches onError.
|
|
4502
|
+
try {
|
|
4503
|
+
const validation = bodySchema["~standard"].validate(parsed);
|
|
4504
|
+
return validation instanceof Promise
|
|
4505
|
+
? validation.then((result) => this.applyLifecycleValidation(entry, result, ctx, "body"))
|
|
4506
|
+
: this.applyLifecycleValidation(entry, validation, ctx, "body");
|
|
4507
|
+
}
|
|
4508
|
+
catch (error) {
|
|
4509
|
+
return Promise.reject(error);
|
|
4510
|
+
}
|
|
4318
4511
|
};
|
|
4319
|
-
return readBodyFramed(req, bodyLimit ?? UNLIMITED_BODY_BYTES, this.protoPoisoning, validate,
|
|
4512
|
+
return readBodyFramed(req, bodyLimit ?? UNLIMITED_BODY_BYTES, this.protoPoisoning, validate, BODY_RESPONSE, BODY_ERROR, bodySchema, this.formOrigins);
|
|
4320
4513
|
}
|
|
4321
4514
|
/** Apply validation and its recovery hook on the generic lifecycle lane. Recovery is completed
|
|
4322
4515
|
* before derives/beforeHandle run, matching the body-only and query-only execution lanes. */
|
|
4323
4516
|
applyLifecycleValidation(entry, result, ctx, kind) {
|
|
4324
|
-
const assign = (value) => {
|
|
4325
|
-
;
|
|
4326
|
-
ctx[kind] = value;
|
|
4327
|
-
};
|
|
4328
4517
|
if (result.issues === undefined) {
|
|
4329
|
-
|
|
4518
|
+
// biome-ignore lint/plugin/noChainedTypeAssertions lint/plugin/requireSafetyCommentForTypeAssertion: RawContext carries every ProgramValidationKind slot; this instruction assigns its validated value before handlers run.
|
|
4519
|
+
;
|
|
4520
|
+
ctx[kind] = result.value;
|
|
4330
4521
|
return undefined;
|
|
4331
4522
|
}
|
|
4332
4523
|
const hook = entry.schema?.onValidationError ?? this.defaultOnValidationError;
|
|
4333
4524
|
if (hook === undefined)
|
|
4334
4525
|
return plainValidationError(result.issues);
|
|
4526
|
+
const assign = (value) => {
|
|
4527
|
+
;
|
|
4528
|
+
ctx[kind] = value;
|
|
4529
|
+
};
|
|
4335
4530
|
const attempted = hook(result.issues, ctx, kind);
|
|
4336
4531
|
if (attempted instanceof Promise) {
|
|
4337
4532
|
return attempted.then((recovery) => this.finishLifecycleValidationRecovery(entry, kind, result.issues, recovery, assign));
|
|
@@ -4364,13 +4559,19 @@ export class Server {
|
|
|
4364
4559
|
// the HTTP framing, so the JSON lane may retain its native fused `json()` parse without weakening
|
|
4365
4560
|
// the portable/adapted source contract (those sources are never marked).
|
|
4366
4561
|
const markFramed = (request) => markTrustedBodyFraming(request);
|
|
4367
|
-
// The
|
|
4562
|
+
// The direct native lanes bypass `runMatched`, so the route's transport byte cap must be
|
|
4368
4563
|
// marked here too - otherwise a Bun `listen()` fused route would leave direct `c.req` body
|
|
4369
|
-
// reads uncapped. The
|
|
4564
|
+
// reads uncapped. The fallback branches go through `fetchMatched` -> `runMatched`, which marks.
|
|
4370
4565
|
const bodyLimit = entry.schema?.body === undefined || entry.program.authBeforeValidation
|
|
4371
4566
|
? entry.bodyLimit
|
|
4372
4567
|
: undefined;
|
|
4373
|
-
|
|
4568
|
+
// The complete plan already preserves general validation, hooks, recovery, around, and ledgers.
|
|
4569
|
+
// When native admission is safe, use it directly even if the route has no closed-shape fusion.
|
|
4570
|
+
// Idempotency still needs runMatched's request preparation and is excluded from the table.
|
|
4571
|
+
const inner = fused ??
|
|
4572
|
+
(signal && budget
|
|
4573
|
+
? (source, params, search, activeSignal, activeBudget, platform, nativeContext) => entry.execution.run(this, entry, source, params, search, activeSignal, activeBudget, platform, nativeContext, this.finalizeResponse, this.wrapWebResponse)
|
|
4574
|
+
: undefined);
|
|
4374
4575
|
const capped = inner === undefined || bodyLimit === undefined
|
|
4375
4576
|
? inner
|
|
4376
4577
|
: (source, params, search, fusedSignal, fusedBudget, platform, nativeContext) => {
|
|
@@ -4530,7 +4731,9 @@ export class Server {
|
|
|
4530
4731
|
}
|
|
4531
4732
|
const paramNames = pattern.paramNames;
|
|
4532
4733
|
const fused = mayUseFusedNative ? entry.execution.fusedWeb : undefined;
|
|
4533
|
-
|
|
4734
|
+
const handler = this.compileBunNativeHandler(entry, paramNames, fused, unboundedSignal, unboundedBudget, peer, fallback);
|
|
4735
|
+
// The table matched the target as sent; one `fetch` refuses or rewrites goes through it.
|
|
4736
|
+
tableAt(path, pattern.segments)[method] = (request, server) => requestTarget(request) ? fallback(request, server) : handler(request, server);
|
|
4534
4737
|
count += 1;
|
|
4535
4738
|
if (paramNames.length > 0) {
|
|
4536
4739
|
below.add(pattern.segments.length + method);
|
|
@@ -4713,11 +4916,12 @@ export class Server {
|
|
|
4713
4916
|
* The Bun semantics: poll `pendingRequests` (awaiting
|
|
4714
4917
|
* `stop()`'s promise drops in-flight requests), and decide graceful-vs-forced in
|
|
4715
4918
|
* ONE call (Bun can't escalate an already-graceful `stop()` to a forced close).
|
|
4716
|
-
* New connections may be accepted during the drain window;
|
|
4717
|
-
*
|
|
4718
|
-
*
|
|
4919
|
+
* New connections may be accepted during the drain window; `delayMs` is the time a load balancer
|
|
4920
|
+
* gets to stop routing here first. The `onStopping` hooks run before that delay, the `onStop`
|
|
4921
|
+
* hooks after the drain. Open WebSockets get a 1001 close when the drain starts and the same
|
|
4922
|
+
* window to finish it; a socket opened after `stop()` began is closed at once.
|
|
4719
4923
|
*/
|
|
4720
|
-
async stop({ drainMs = DEFAULT_DRAIN_MS } = {}) {
|
|
4924
|
+
async stop({ drainMs = DEFAULT_DRAIN_MS, delayMs = 0 } = {}) {
|
|
4721
4925
|
const onSignal = this.signalHandler;
|
|
4722
4926
|
if (onSignal !== undefined) {
|
|
4723
4927
|
this.signalHandler = undefined;
|
|
@@ -4727,6 +4931,9 @@ export class Server {
|
|
|
4727
4931
|
if (server === undefined)
|
|
4728
4932
|
return;
|
|
4729
4933
|
this.bunServer = undefined;
|
|
4934
|
+
await this[SERVER_LIFECYCLE]("stopping");
|
|
4935
|
+
if (delayMs > 0)
|
|
4936
|
+
await Bun.sleep(delayMs);
|
|
4730
4937
|
const sockets = this.bunSockets;
|
|
4731
4938
|
this.bunSockets = undefined;
|
|
4732
4939
|
if (sockets !== undefined)
|
|
@@ -4738,14 +4945,25 @@ export class Server {
|
|
|
4738
4945
|
await Bun.sleep(DRAIN_POLL_MS);
|
|
4739
4946
|
}
|
|
4740
4947
|
server.stop(busy()); // force-close iff stragglers remain past the deadline
|
|
4741
|
-
|
|
4948
|
+
await this[SERVER_LIFECYCLE]("stopped");
|
|
4949
|
+
}
|
|
4950
|
+
/**
|
|
4951
|
+
* @internal Run the `onStopping` or the `onStop` hooks, each bounded. A runtime adapter that drains
|
|
4952
|
+
* its own listener calls it on either side of the drain, as `stop()` does here.
|
|
4953
|
+
*/
|
|
4954
|
+
async [SERVER_LIFECYCLE](phase) {
|
|
4955
|
+
const hooks = phase === "stopping" ? this.stoppingHooks : this.stopHooks;
|
|
4956
|
+
if (hooks.length === 0)
|
|
4742
4957
|
return;
|
|
4743
|
-
|
|
4744
|
-
const settled = Promise.allSettled(callbacks);
|
|
4958
|
+
let timer;
|
|
4745
4959
|
await Promise.race([
|
|
4746
|
-
|
|
4747
|
-
new Promise((resolve) =>
|
|
4960
|
+
Promise.allSettled(hooks.map((hook) => Promise.resolve().then(hook))),
|
|
4961
|
+
new Promise((resolve) => {
|
|
4962
|
+
timer = setTimeout(resolve, STOP_HOOK_TIMEOUT_MS);
|
|
4963
|
+
}),
|
|
4748
4964
|
]);
|
|
4965
|
+
// A pending bound would hold the event loop open after every hook settled.
|
|
4966
|
+
clearTimeout(timer);
|
|
4749
4967
|
}
|
|
4750
4968
|
installSignalHandlers(drain) {
|
|
4751
4969
|
// Drain, then let the process exit naturally - the stopped server no longer
|