@nifrajs/core 5.5.1 → 5.6.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 +153 -0
- package/dist/assurance.d.ts.map +1 -1
- package/dist/assurance.js.map +1 -1
- package/dist/evidence.d.ts +2 -0
- package/dist/evidence.d.ts.map +1 -1
- package/dist/evidence.js +2 -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 +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.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 +14 -1
- package/dist/reflection.d.ts.map +1 -1
- package/dist/reflection.js +34 -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/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/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.js +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/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/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 +1 -3
- package/dist/server/request-context.js.map +1 -1
- package/dist/server/respond.d.ts +1 -3
- package/dist/server/respond.d.ts.map +1 -1
- package/dist/server/respond.js +21 -16
- 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 +135 -8
- package/dist/server/response-contract-lane.js.map +1 -1
- 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 +8 -1
- package/dist/server/runtime-core.js.map +1 -1
- package/dist/server/server-types.d.ts +3 -2
- package/dist/server/server-types.d.ts.map +1 -1
- package/dist/server/server.d.ts +6 -2
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +239 -78
- 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 +5 -4
- package/dist/server/sse.js.map +1 -1
- 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 +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1 -0
- package/dist/server.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/package.json +11 -1
- package/src/assurance.ts +1 -0
- package/src/evidence.ts +4 -0
- package/src/file-type.ts +130 -0
- package/src/index.ts +2 -1
- package/src/internal/route-program.ts +261 -93
- package/src/reflection.ts +53 -7
- package/src/response-observer.ts +6 -0
- package/src/server/bare-error-lane.ts +9 -4
- 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 +1 -1
- package/src/server/http.ts +6 -15
- package/src/server/multipart.ts +232 -100
- 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 +1 -3
- package/src/server/respond.ts +23 -15
- package/src/server/response-contract-lane.ts +130 -8
- package/src/server/runtime-core.ts +10 -1
- package/src/server/server-types.ts +3 -2
- package/src/server/server.ts +265 -76
- package/src/server/sse.ts +6 -5
- 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 +1 -0
- package/src/tool-cli.ts +584 -0
package/dist/server/server.js
CHANGED
|
@@ -6,12 +6,13 @@ 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
13
|
import { emitRequestErrorLog, renderBareError } from "./bare-error-lane.js";
|
|
14
|
-
import {
|
|
14
|
+
import { LAST_BEFORE_HANDLE } from "./before-handler.js";
|
|
15
|
+
import { assertByteLimit, clientBodyErrorCode, hasTrustedBodyFraming, markTransportCap, markTrustedBodyFraming, UNLIMITED_BODY_BYTES, } from "./body.js";
|
|
15
16
|
import { NIFRA_PLATFORM_CLIENT_IP_DERIVED, resolveClientIp, } from "./client-ip.js";
|
|
16
17
|
import { mergeRefusal, recordCopyVersion } from "./core-copies.js";
|
|
17
18
|
import { hasLowercaseHeaderKeysMark, headerKeysAllLowercase, markLowercaseHeaderKeys, } from "./header-case.js";
|
|
@@ -129,6 +130,8 @@ function requireMcpRuntime(runtime) {
|
|
|
129
130
|
// existing importers keep resolving them from the server module.
|
|
130
131
|
export { defineContextPlugin, defineIdentityPlugin, definePlugin, defineRouterPlugin, } from "./plugin.js";
|
|
131
132
|
const DEFAULT_MAX_BODY_BYTES = 1_000_000;
|
|
133
|
+
const BODY_RESPONSE = (response) => response;
|
|
134
|
+
const BODY_ERROR = (error) => Promise.reject(error);
|
|
132
135
|
const DEFAULT_DRAIN_MS = 10_000;
|
|
133
136
|
const DRAIN_POLL_MS = 10;
|
|
134
137
|
const STOP_HOOK_TIMEOUT_MS = 5_000;
|
|
@@ -171,16 +174,106 @@ function hasUnroutedParam(params) {
|
|
|
171
174
|
* Bun, workerd and every `new Request()` parse it first. Only there does `fetch` read the URL up front.
|
|
172
175
|
*/
|
|
173
176
|
const rawRequestTargets = typeof globalThis.Deno !== "undefined";
|
|
174
|
-
/**
|
|
175
|
-
|
|
177
|
+
/**
|
|
178
|
+
* Bun parses the target only when it can read the Host. Otherwise `Request.url` is the target as sent:
|
|
179
|
+
* relative for a Host with a space or a byte past ASCII or for no Host at all, the raw authority
|
|
180
|
+
* for a port past 65535, and `new URL(req.url)` throws in the app. So Bun is held to the Host check
|
|
181
|
+
* Deno runs.
|
|
182
|
+
*/
|
|
183
|
+
const unparsedBunTargets = typeof Bun !== "undefined";
|
|
184
|
+
/**
|
|
185
|
+
* `req` as Bun or workerd would have delivered it: the same request, its URL resolved as they parse it.
|
|
186
|
+
* `undefined` when the `Request` constructor refuses what the wire carried - a TRACE, CONNECT or TRACK,
|
|
187
|
+
* a lower-case `get` with a body - which {@link resolvedSource} routes instead.
|
|
188
|
+
*/
|
|
189
|
+
function withResolvedTarget(req, url) {
|
|
190
|
+
let rebuilt;
|
|
191
|
+
try {
|
|
192
|
+
rebuilt = new Request(url, req);
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
// The same body, so the same framing: a runtime still delimited it.
|
|
198
|
+
if (hasTrustedBodyFraming(req))
|
|
199
|
+
markTrustedBodyFraming(rebuilt);
|
|
200
|
+
return rebuilt;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* A request no `Request` can be rebuilt from, as a source carrying its resolved URL: it is routed on that
|
|
204
|
+
* URL and gets the 404 or 405 the other runtimes give, where rebuilding it threw out of `fetch`. A
|
|
205
|
+
* handler that reads `c.req` gets the request as Deno delivered it.
|
|
206
|
+
*/
|
|
207
|
+
function resolvedSource(req, url) {
|
|
208
|
+
const source = {
|
|
209
|
+
method: req.method,
|
|
210
|
+
url,
|
|
211
|
+
get headers() {
|
|
212
|
+
return req.headers;
|
|
213
|
+
},
|
|
214
|
+
header: (name) => headerOf(req, name),
|
|
215
|
+
get body() {
|
|
216
|
+
return req.body;
|
|
217
|
+
},
|
|
218
|
+
arrayBuffer: () => req.arrayBuffer(),
|
|
219
|
+
json: () => req.json(),
|
|
220
|
+
bytes: () => req.bytes(),
|
|
221
|
+
request: req,
|
|
222
|
+
};
|
|
223
|
+
if (hasTrustedBodyFraming(req))
|
|
224
|
+
markTrustedBodyFraming(source);
|
|
225
|
+
return source;
|
|
226
|
+
}
|
|
176
227
|
// A Host as RFC 9110 allows it: a reg-name or an IP literal, then an optional port. Deno builds
|
|
177
228
|
// `Request.url` by appending the target to the Host as received, so `Host: x/admin#` turned a
|
|
178
229
|
// 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
|
-
|
|
230
|
+
const HOST_AUTHORITY = /^(?:\[[0-9a-f:.%]+\]|[\w.-]+)(?::(\d{1,5}))?$/i;
|
|
231
|
+
// Clients repeat one Host on every request; comparing to the last valid one is ~7x cheaper than the regex.
|
|
232
|
+
let lastValidHost = "";
|
|
233
|
+
// A port past 65535, or an IPv6 literal no URL parser takes (`[1:2]`), is refused as Node refuses it:
|
|
234
|
+
// no URL can be built on either, so resolving a dot segment against one threw out of `fetch`.
|
|
235
|
+
const invalidHost = (host) => {
|
|
236
|
+
if (host === lastValidHost)
|
|
237
|
+
return false;
|
|
238
|
+
const authority = HOST_AUTHORITY.exec(host);
|
|
239
|
+
if (authority === null)
|
|
240
|
+
return true;
|
|
241
|
+
if (authority[1] !== undefined && Number(authority[1]) > 65_535)
|
|
242
|
+
return true;
|
|
243
|
+
if (host.charCodeAt(0) === 91 /* [ */ && !URL.canParse(`http://${host}/`))
|
|
244
|
+
return true;
|
|
245
|
+
lastValidHost = host;
|
|
246
|
+
return false;
|
|
247
|
+
};
|
|
248
|
+
/**
|
|
249
|
+
* What a request is routed on in place of `req.url`, before anything reads it: `true` refuses its
|
|
250
|
+
* Host (400), a URL replaces the target, `false` keeps it. Deno hands the target over as sent, so
|
|
251
|
+
* its dot segments are resolved; so does Bun when HTTP/1.0 leaves the Host out, and its route table
|
|
252
|
+
* matched `/files/..` raw. Such a request is served as `localhost`, as Node and Deno serve it.
|
|
253
|
+
* `headerOf`, not `req.headers`: on Deno that reads the Host alone instead of building every
|
|
254
|
+
* header.
|
|
255
|
+
*/
|
|
256
|
+
const requestTarget = (req) => {
|
|
257
|
+
if (!rawRequestTargets && !unparsedBunTargets)
|
|
258
|
+
return false;
|
|
182
259
|
const host = headerOf(req, "host");
|
|
183
|
-
|
|
260
|
+
if (host !== null && invalidHost(host)) {
|
|
261
|
+
if (rawRequestTargets)
|
|
262
|
+
return true;
|
|
263
|
+
// Bun served it anyway: it could not read the Host, so it left the target unparsed, or it
|
|
264
|
+
// built the URL on it - sent twice (joined with ", ", the URL on the first; RFC 9112 section
|
|
265
|
+
// 3.2 answers 400), percent-encoded (`%61.example` routed as `a.example`), or with a
|
|
266
|
+
// sub-delimiter (`a!b`). A request built in process keeps the URL its caller built, unless Bun
|
|
267
|
+
// would have built it so.
|
|
268
|
+
const routed = URL.parse(req.url)?.host;
|
|
269
|
+
return routed === undefined || URL.parse(`//${host.split(", ")[0]}`, req.url)?.host === routed;
|
|
270
|
+
}
|
|
271
|
+
if (rawRequestTargets)
|
|
272
|
+
return needsResolving(req.url) && resolveDotSegments(req.url);
|
|
273
|
+
// A request built in process carries an absolute URL whatever its headers say.
|
|
274
|
+
return (host === null &&
|
|
275
|
+
req.url.charCodeAt(0) === 47 /* / */ &&
|
|
276
|
+
resolveDotSegments(`http://localhost${req.url}`));
|
|
184
277
|
};
|
|
185
278
|
function normalizeMountPrefix(path) {
|
|
186
279
|
if (!path.startsWith("/") || path.includes("?") || path.includes("#")) {
|
|
@@ -403,6 +496,17 @@ const recordScopedHook = (key, routesAtPush, kind) => {
|
|
|
403
496
|
site: captureCallerSite(),
|
|
404
497
|
});
|
|
405
498
|
};
|
|
499
|
+
/** `next(value)` now, or once `value` settles. */
|
|
500
|
+
const thenMaybe = (value, next) => (value instanceof Promise ? value.then(next) : next(value));
|
|
501
|
+
/** A mount registered after an `authenticate()` stage runs it, so the stage covers something. */
|
|
502
|
+
const noteAuthenticatedMount = (key) => {
|
|
503
|
+
const state = hookAudit.get(key);
|
|
504
|
+
if (state === undefined)
|
|
505
|
+
return;
|
|
506
|
+
for (const site of state.sites)
|
|
507
|
+
if (site.kind === "authenticate")
|
|
508
|
+
site.routesAtPush = -1;
|
|
509
|
+
};
|
|
406
510
|
/** Count routes a `merge()` adopted: they never run this server's order-scoped hooks, so a hook
|
|
407
511
|
* followed only by merges covers nothing. */
|
|
408
512
|
const noteMergedRoutes = (key, count) => {
|
|
@@ -632,9 +736,13 @@ export class Server {
|
|
|
632
736
|
/** Body/raw response hooks need a framework-payload marker; keep that decision per app. */
|
|
633
737
|
responseBodyTag;
|
|
634
738
|
responseBodyOwners;
|
|
635
|
-
|
|
636
|
-
//
|
|
637
|
-
ctx
|
|
739
|
+
// General lifecycles need no extra response bookkeeping when neither controls nor observers
|
|
740
|
+
// use it. Share the fused renderer after the lifecycle, including on body/write routes.
|
|
741
|
+
finalizeResponse = (result, set, ctx) => set === EMPTY_RESPONSE_CONTROLS && this.onResponseHooks.length === 0
|
|
742
|
+
? fusedRespondNoSet(result, this.responseBodyTag, this.staticResponseHeaders, true)
|
|
743
|
+
: toResponse(result, set, this.responseBodyTag, this.staticResponseHeaders,
|
|
744
|
+
// Response hooks write next, and they run for this same request object.
|
|
745
|
+
ctx !== undefined && this.onResponseHooks.length > 0 ? ctx[CONTEXT_REQUEST] : undefined);
|
|
638
746
|
responseRequests;
|
|
639
747
|
/** A request the WebSocket handshake check ran the onRequest hooks on and then passed to HTTP →
|
|
640
748
|
* what the hooks returned, so `fetch` routes that instead of running the hooks a second time. */
|
|
@@ -773,7 +881,10 @@ export class Server {
|
|
|
773
881
|
/**
|
|
774
882
|
* Mount a legacy fetch handler under a literal path prefix. Mounted handlers sit behind typed
|
|
775
883
|
* 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.
|
|
884
|
+
* Mounted responses are outside Nifra's typed route and response-contract checks. An
|
|
885
|
+
* `authenticate()` stage registered before the mount, or a required auth plugin built on one
|
|
886
|
+
* (`bearer()`, `jwt()`, `basicAuth()`, `apiKey()`), refuses a request before the handler sees it;
|
|
887
|
+
* mount before the stage to keep the legacy surface public.
|
|
777
888
|
*
|
|
778
889
|
* It carries HTTP only: a WebSocket upgrade under the prefix reaches the handler as an ordinary
|
|
779
890
|
* request and never becomes a socket. Serve that path's sockets with `app.ws()` routes or a
|
|
@@ -792,7 +903,10 @@ export class Server {
|
|
|
792
903
|
beforeRoutes: false,
|
|
793
904
|
order: this.mountOrder++,
|
|
794
905
|
opaque: options.opaque,
|
|
906
|
+
authStages: [...this.authStages],
|
|
795
907
|
};
|
|
908
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production" && this.authStages.length > 0)
|
|
909
|
+
noteAuthenticatedMount(this);
|
|
796
910
|
this.fetchMounts.push(mount);
|
|
797
911
|
this.fetchMounts.sort(compareMounts);
|
|
798
912
|
return this;
|
|
@@ -846,7 +960,10 @@ export class Server {
|
|
|
846
960
|
order: this.mountOrder++,
|
|
847
961
|
opaque: options.opaque,
|
|
848
962
|
app,
|
|
963
|
+
authStages: [...this.authStages],
|
|
849
964
|
};
|
|
965
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production" && this.authStages.length > 0)
|
|
966
|
+
noteAuthenticatedMount(this);
|
|
850
967
|
// Mounts run before routes: a request under this prefix that no mount on the way strips comes back
|
|
851
968
|
// here unchanged until the stack overflows, which takes the process down. Dev-time only, like the
|
|
852
969
|
// hook audit: a production define folds this away, and the first dev request would hit it anyway.
|
|
@@ -888,7 +1005,8 @@ export class Server {
|
|
|
888
1005
|
* (see {@link ServerOptions.unusedScopedHooks}).
|
|
889
1006
|
*/
|
|
890
1007
|
/**
|
|
891
|
-
* Register a dedicated authentication stage for subsequent routes
|
|
1008
|
+
* Register a dedicated authentication stage for subsequent routes, and for the `mount()` and
|
|
1009
|
+
* `mountFetch()` handlers registered after it: a request it refuses never reaches them.
|
|
892
1010
|
*
|
|
893
1011
|
* Unlike `derive`, this stage receives a body-blind authentication capsule and is compiled before
|
|
894
1012
|
* route validation. Keeping it separate prevents an unrelated derive from accidentally becoming a
|
|
@@ -950,7 +1068,13 @@ export class Server {
|
|
|
950
1068
|
*/
|
|
951
1069
|
beforeHandle(fn) {
|
|
952
1070
|
this.assertConfigurable("beforeHandle()");
|
|
953
|
-
this.beforeHandleHooks
|
|
1071
|
+
const hooks = this.beforeHandleHooks;
|
|
1072
|
+
let at = hooks.length;
|
|
1073
|
+
if (!LAST_BEFORE_HANDLE.has(fn)) {
|
|
1074
|
+
while (at > 0 && LAST_BEFORE_HANDLE.has(hooks[at - 1]))
|
|
1075
|
+
at--;
|
|
1076
|
+
}
|
|
1077
|
+
hooks.splice(at, 0, fn);
|
|
954
1078
|
if (hookAuditRuntime && process.env.NODE_ENV !== "production") {
|
|
955
1079
|
recordScopedHook(this, this.catalog.size + this.wsRoutes.length, "beforeHandle");
|
|
956
1080
|
tagHook(fn, "beforeHandle", this);
|
|
@@ -2029,11 +2153,14 @@ export class Server {
|
|
|
2029
2153
|
fetch(req, platform) {
|
|
2030
2154
|
// Deno's `Request.url` is the target as sent; Bun, workerd and every `new Request()` parse it.
|
|
2031
2155
|
// Route what the parsed URL says, so `/users/../admin` is `/admin` on every runtime.
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2156
|
+
const target = requestTarget(req);
|
|
2157
|
+
if (target === true)
|
|
2158
|
+
return jsonError(400, "invalid_host");
|
|
2159
|
+
if (target !== false) {
|
|
2160
|
+
const rebuilt = withResolvedTarget(req, target);
|
|
2161
|
+
if (rebuilt === undefined)
|
|
2162
|
+
return this.fetchSource(resolvedSource(req, target), platform);
|
|
2163
|
+
req = rebuilt;
|
|
2037
2164
|
}
|
|
2038
2165
|
// An edge deployment whose only ingress is this method declares its requests runtime-framed at
|
|
2039
2166
|
// construction; everything else stays on the delivered-byte check (see `trustBodyFraming`).
|
|
@@ -2151,7 +2278,7 @@ export class Server {
|
|
|
2151
2278
|
// The mount the ordinary path reaches first: every `mount()` runs before any `mountFetch()`,
|
|
2152
2279
|
// whatever their prefix lengths. A 404 fallthrough chain stays on the ordinary path.
|
|
2153
2280
|
const mount = this.firstMountUnder(parts.pathname, true) ?? this.firstMountUnder(parts.pathname, false);
|
|
2154
|
-
if (mount === undefined || mount.fallbackOn404)
|
|
2281
|
+
if (mount === undefined || mount.fallbackOn404 || mount.authStages.length > 0)
|
|
2155
2282
|
return undefined;
|
|
2156
2283
|
const candidate = mount.handler[NODE_NATIVE_MOUNT];
|
|
2157
2284
|
return typeof candidate === "function"
|
|
@@ -2435,12 +2562,16 @@ export class Server {
|
|
|
2435
2562
|
// The adapter answers a pass by handing this same object to `fetch`.
|
|
2436
2563
|
const received = req;
|
|
2437
2564
|
// The handshake routes the path `fetch` would: Host checked, dot segments resolved (see `fetch`).
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2565
|
+
const target = requestTarget(req);
|
|
2566
|
+
if (target === true) {
|
|
2567
|
+
return { kind: "reject", response: this.wrapWebResponse(jsonError(400, "invalid_host")) };
|
|
2568
|
+
}
|
|
2569
|
+
if (target !== false) {
|
|
2570
|
+
// A GET carrying a body cannot be rebuilt; `fetch` routes it as the ordinary request it is.
|
|
2571
|
+
const rebuilt = withResolvedTarget(req, target);
|
|
2572
|
+
if (rebuilt === undefined)
|
|
2573
|
+
return WS_PASS;
|
|
2574
|
+
req = rebuilt;
|
|
2444
2575
|
}
|
|
2445
2576
|
// The handshake's caller is the one `fetch` derives: hooks and `upgrade()` see the trusted IP.
|
|
2446
2577
|
platform = this.trustedPlatform(req, platform);
|
|
@@ -2545,19 +2676,24 @@ export class Server {
|
|
|
2545
2676
|
}
|
|
2546
2677
|
if (selected === undefined)
|
|
2547
2678
|
return WS_PASS;
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
childRequest =
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2679
|
+
const mount = selected;
|
|
2680
|
+
const upgrade = () => {
|
|
2681
|
+
let childRequest = request;
|
|
2682
|
+
if (mount.stripPrefix) {
|
|
2683
|
+
childRequest = stripMountPrefix(request, mount.path);
|
|
2684
|
+
mount.wsPassed ??= new WeakMap();
|
|
2685
|
+
mount.wsPassed.set(request, childRequest);
|
|
2686
|
+
}
|
|
2687
|
+
try {
|
|
2688
|
+
return mount.resolveWebSocketUpgrade(childRequest, platform);
|
|
2689
|
+
}
|
|
2690
|
+
catch {
|
|
2691
|
+
return refuse(jsonError(500, "internal_error"));
|
|
2692
|
+
}
|
|
2693
|
+
};
|
|
2694
|
+
const child = mount.authStages.length === 0
|
|
2695
|
+
? upgrade()
|
|
2696
|
+
: thenMaybe(authenticateMount(mount.authStages, request, platform, this.logger), (refusal) => refusal === undefined ? upgrade() : refuse(refusal));
|
|
2561
2697
|
const settle = (outcome) => outcome.kind === "pass" ||
|
|
2562
2698
|
(outcome.kind === "reject" && outcome.response.status === 404 && selected.fallbackOn404)
|
|
2563
2699
|
? dispatch(selectedIndex + 1)
|
|
@@ -2857,8 +2993,8 @@ export class Server {
|
|
|
2857
2993
|
* routes with validation/hooks keep the full compiled route program, unchanged.
|
|
2858
2994
|
*/
|
|
2859
2995
|
routeAndRun(source, platform, finalize, wrapResponse, onTimeout, webFast) {
|
|
2860
|
-
//
|
|
2861
|
-
//
|
|
2996
|
+
// Native delimiter searches keep path/query splitting out of a character-by-character JS loop.
|
|
2997
|
+
// Keep the slices in locals instead of allocating the public helper's pathname/search pair.
|
|
2862
2998
|
let pathname;
|
|
2863
2999
|
let search;
|
|
2864
3000
|
// Read the source's split ONCE. `urlParts` is an accessor on the Node request sources that
|
|
@@ -2879,22 +3015,11 @@ export class Server {
|
|
|
2879
3015
|
search = "";
|
|
2880
3016
|
}
|
|
2881
3017
|
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
|
-
}
|
|
3018
|
+
const fragment = rawUrl.indexOf("#", start);
|
|
3019
|
+
const searchEnd = fragment === -1 ? rawUrl.length : fragment;
|
|
3020
|
+
const question = rawUrl.indexOf("?", start);
|
|
3021
|
+
const searchStart = question !== -1 && question < searchEnd ? question : -1;
|
|
3022
|
+
const pathEnd = searchStart === -1 ? searchEnd : searchStart;
|
|
2898
3023
|
pathname = rawUrl.slice(start, pathEnd);
|
|
2899
3024
|
search = searchStart === -1 ? "" : rawUrl.slice(searchStart, searchEnd);
|
|
2900
3025
|
}
|
|
@@ -2967,10 +3092,16 @@ export class Server {
|
|
|
2967
3092
|
}
|
|
2968
3093
|
if (mount === undefined)
|
|
2969
3094
|
return fallback ?? jsonError(404, "not_found");
|
|
2970
|
-
const
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
3095
|
+
const selected = mount;
|
|
3096
|
+
const run = () => {
|
|
3097
|
+
const childRequest = selected.stripPrefix
|
|
3098
|
+
? (selected.wsPassed?.get(request) ?? stripMountPrefix(request, selected.path))
|
|
3099
|
+
: request;
|
|
3100
|
+
return selected.handler(childRequest, platform);
|
|
3101
|
+
};
|
|
3102
|
+
const outcome = mount.authStages.length === 0
|
|
3103
|
+
? run()
|
|
3104
|
+
: thenMaybe(authenticateMount(mount.authStages, request, platform, this.logger), (refusal) => refusal ?? run());
|
|
2974
3105
|
if (!replayable || !mount.fallbackOn404)
|
|
2975
3106
|
return outcome;
|
|
2976
3107
|
if (outcome instanceof Promise) {
|
|
@@ -3861,7 +3992,7 @@ export class Server {
|
|
|
3861
3992
|
// The thrown-value contract lives in `bare-error-lane.ts`, one source of truth for both this
|
|
3862
3993
|
// lane and the fused body runner. `responseSet` and the logger are injected because they reach
|
|
3863
3994
|
// 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),
|
|
3995
|
+
return renderBareError(err, ctx, finalize, responseSet, (e, c) => this.logRequestError(e, c), wrapResponse);
|
|
3865
3996
|
}
|
|
3866
3997
|
/**
|
|
3867
3998
|
* Keep the ordinary thrown-error response independent from the caught value. Deliberate Response
|
|
@@ -3939,7 +4070,7 @@ export class Server {
|
|
|
3939
4070
|
}
|
|
3940
4071
|
// @ts-expect-error TS6133 -- invoked structurally by compiled plans (internal/route-execution.ts)
|
|
3941
4072
|
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);
|
|
4073
|
+
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
4074
|
}
|
|
3944
4075
|
/** Validate + run the handler for the bodyOnly path - shared by the inline fast path and the
|
|
3945
4076
|
* streaming fallback. A method (not per-request closures) so the hot path allocates nothing
|
|
@@ -4193,7 +4324,15 @@ export class Server {
|
|
|
4193
4324
|
return finalize(result, responseSet(ctx), ctx);
|
|
4194
4325
|
const checked = contract.runtime.check(contract.definition, result, ctx);
|
|
4195
4326
|
if (checked instanceof Promise) {
|
|
4196
|
-
return checked.then((outcome) =>
|
|
4327
|
+
return checked.then((outcome) => {
|
|
4328
|
+
// The render runs outside the try below: a throw (a BigInt, a cycle) must still reach onError.
|
|
4329
|
+
try {
|
|
4330
|
+
return this.finishContractOutcome(ctx, finalize, wrapResponse, outcome);
|
|
4331
|
+
}
|
|
4332
|
+
catch (err) {
|
|
4333
|
+
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse);
|
|
4334
|
+
}
|
|
4335
|
+
}, (err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse));
|
|
4197
4336
|
}
|
|
4198
4337
|
return this.finishContractOutcome(ctx, finalize, wrapResponse, checked);
|
|
4199
4338
|
}
|
|
@@ -4238,6 +4377,9 @@ export class Server {
|
|
|
4238
4377
|
if (err instanceof Response || isResponseResult(err))
|
|
4239
4378
|
return this.finishLifecycleContract(entry, ctx, finalize, wrapResponse, err);
|
|
4240
4379
|
if (entry.onError.length === 0) {
|
|
4380
|
+
const clientCode = clientBodyErrorCode(err);
|
|
4381
|
+
if (clientCode !== undefined)
|
|
4382
|
+
return wrapResponse(plainError(400, clientCode));
|
|
4241
4383
|
// Never crash the server or leak internals. The client gets a flat 500; the detail goes to the
|
|
4242
4384
|
// (redacting) logger. Body-read failures and around-hook failures land here too.
|
|
4243
4385
|
this.logRequestError(err, ctx);
|
|
@@ -4297,6 +4439,9 @@ export class Server {
|
|
|
4297
4439
|
return wrapResponse(plainError(500, "internal_error"));
|
|
4298
4440
|
}
|
|
4299
4441
|
}
|
|
4442
|
+
const clientCode = clientBodyErrorCode(err);
|
|
4443
|
+
if (clientCode !== undefined)
|
|
4444
|
+
return wrapResponse(plainError(400, clientCode));
|
|
4300
4445
|
this.logRequestError(err, ctx);
|
|
4301
4446
|
return fallback === undefined ? wrapResponse(plainError(500, "internal_error")) : fallback();
|
|
4302
4447
|
}
|
|
@@ -4311,27 +4456,35 @@ export class Server {
|
|
|
4311
4456
|
// second continuation, while retaining the exact bounded-read, content-type, and poisoning
|
|
4312
4457
|
// contracts shared by every body route.
|
|
4313
4458
|
const validate = (parsed) => {
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4459
|
+
// Reject outside the parser's JSON catch so a custom validator throw reaches onError.
|
|
4460
|
+
try {
|
|
4461
|
+
const validation = bodySchema["~standard"].validate(parsed);
|
|
4462
|
+
return validation instanceof Promise
|
|
4463
|
+
? validation.then((result) => this.applyLifecycleValidation(entry, result, ctx, "body"))
|
|
4464
|
+
: this.applyLifecycleValidation(entry, validation, ctx, "body");
|
|
4465
|
+
}
|
|
4466
|
+
catch (error) {
|
|
4467
|
+
return Promise.reject(error);
|
|
4468
|
+
}
|
|
4318
4469
|
};
|
|
4319
|
-
return readBodyFramed(req, bodyLimit ?? UNLIMITED_BODY_BYTES, this.protoPoisoning, validate,
|
|
4470
|
+
return readBodyFramed(req, bodyLimit ?? UNLIMITED_BODY_BYTES, this.protoPoisoning, validate, BODY_RESPONSE, BODY_ERROR, bodySchema, this.formOrigins);
|
|
4320
4471
|
}
|
|
4321
4472
|
/** Apply validation and its recovery hook on the generic lifecycle lane. Recovery is completed
|
|
4322
4473
|
* before derives/beforeHandle run, matching the body-only and query-only execution lanes. */
|
|
4323
4474
|
applyLifecycleValidation(entry, result, ctx, kind) {
|
|
4324
|
-
const assign = (value) => {
|
|
4325
|
-
;
|
|
4326
|
-
ctx[kind] = value;
|
|
4327
|
-
};
|
|
4328
4475
|
if (result.issues === undefined) {
|
|
4329
|
-
|
|
4476
|
+
// biome-ignore lint/plugin/noChainedTypeAssertions lint/plugin/requireSafetyCommentForTypeAssertion: RawContext carries every ProgramValidationKind slot; this instruction assigns its validated value before handlers run.
|
|
4477
|
+
;
|
|
4478
|
+
ctx[kind] = result.value;
|
|
4330
4479
|
return undefined;
|
|
4331
4480
|
}
|
|
4332
4481
|
const hook = entry.schema?.onValidationError ?? this.defaultOnValidationError;
|
|
4333
4482
|
if (hook === undefined)
|
|
4334
4483
|
return plainValidationError(result.issues);
|
|
4484
|
+
const assign = (value) => {
|
|
4485
|
+
;
|
|
4486
|
+
ctx[kind] = value;
|
|
4487
|
+
};
|
|
4335
4488
|
const attempted = hook(result.issues, ctx, kind);
|
|
4336
4489
|
if (attempted instanceof Promise) {
|
|
4337
4490
|
return attempted.then((recovery) => this.finishLifecycleValidationRecovery(entry, kind, result.issues, recovery, assign));
|
|
@@ -4364,13 +4517,19 @@ export class Server {
|
|
|
4364
4517
|
// the HTTP framing, so the JSON lane may retain its native fused `json()` parse without weakening
|
|
4365
4518
|
// the portable/adapted source contract (those sources are never marked).
|
|
4366
4519
|
const markFramed = (request) => markTrustedBodyFraming(request);
|
|
4367
|
-
// The
|
|
4520
|
+
// The direct native lanes bypass `runMatched`, so the route's transport byte cap must be
|
|
4368
4521
|
// marked here too - otherwise a Bun `listen()` fused route would leave direct `c.req` body
|
|
4369
|
-
// reads uncapped. The
|
|
4522
|
+
// reads uncapped. The fallback branches go through `fetchMatched` -> `runMatched`, which marks.
|
|
4370
4523
|
const bodyLimit = entry.schema?.body === undefined || entry.program.authBeforeValidation
|
|
4371
4524
|
? entry.bodyLimit
|
|
4372
4525
|
: undefined;
|
|
4373
|
-
|
|
4526
|
+
// The complete plan already preserves general validation, hooks, recovery, around, and ledgers.
|
|
4527
|
+
// When native admission is safe, use it directly even if the route has no closed-shape fusion.
|
|
4528
|
+
// Idempotency still needs runMatched's request preparation and is excluded from the table.
|
|
4529
|
+
const inner = fused ??
|
|
4530
|
+
(signal && budget
|
|
4531
|
+
? (source, params, search, activeSignal, activeBudget, platform, nativeContext) => entry.execution.run(this, entry, source, params, search, activeSignal, activeBudget, platform, nativeContext, this.finalizeResponse, this.wrapWebResponse)
|
|
4532
|
+
: undefined);
|
|
4374
4533
|
const capped = inner === undefined || bodyLimit === undefined
|
|
4375
4534
|
? inner
|
|
4376
4535
|
: (source, params, search, fusedSignal, fusedBudget, platform, nativeContext) => {
|
|
@@ -4530,7 +4689,9 @@ export class Server {
|
|
|
4530
4689
|
}
|
|
4531
4690
|
const paramNames = pattern.paramNames;
|
|
4532
4691
|
const fused = mayUseFusedNative ? entry.execution.fusedWeb : undefined;
|
|
4533
|
-
|
|
4692
|
+
const handler = this.compileBunNativeHandler(entry, paramNames, fused, unboundedSignal, unboundedBudget, peer, fallback);
|
|
4693
|
+
// The table matched the target as sent; one `fetch` refuses or rewrites goes through it.
|
|
4694
|
+
tableAt(path, pattern.segments)[method] = (request, server) => requestTarget(request) ? fallback(request, server) : handler(request, server);
|
|
4534
4695
|
count += 1;
|
|
4535
4696
|
if (paramNames.length > 0) {
|
|
4536
4697
|
below.add(pattern.segments.length + method);
|