@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/src/server/server.ts
CHANGED
|
@@ -50,7 +50,11 @@ import {
|
|
|
50
50
|
type RouteEntry,
|
|
51
51
|
} from "../internal/route-execution.ts"
|
|
52
52
|
import type { ProgramValidationKind, RouteProgramStage } from "../internal/route-program.ts"
|
|
53
|
-
import {
|
|
53
|
+
import {
|
|
54
|
+
authenticateMount,
|
|
55
|
+
compileRouteProgram,
|
|
56
|
+
executeRouteProgram,
|
|
57
|
+
} from "../internal/route-program.ts"
|
|
54
58
|
import { isSameOriginRequest } from "../internal/same-origin.ts"
|
|
55
59
|
import type { ResponseObserverPlugin } from "../response-observer.ts"
|
|
56
60
|
import {
|
|
@@ -65,10 +69,18 @@ import type {
|
|
|
65
69
|
StandardResult,
|
|
66
70
|
StandardSchemaV1,
|
|
67
71
|
} from "../schema/standard.ts"
|
|
68
|
-
import
|
|
72
|
+
import { withAnswerSteps } from "./answer-step.ts"
|
|
73
|
+
import {
|
|
74
|
+
type AuthenticationStage,
|
|
75
|
+
isAuthenticatedRequest,
|
|
76
|
+
markAuthenticatedRequest,
|
|
77
|
+
} from "./auth.ts"
|
|
69
78
|
import { emitRequestErrorLog, renderBareError } from "./bare-error-lane.ts"
|
|
79
|
+
import { LAST_BEFORE_HANDLE } from "./before-handler.ts"
|
|
70
80
|
import {
|
|
71
81
|
assertByteLimit,
|
|
82
|
+
clientBodyErrorCode,
|
|
83
|
+
hasTrustedBodyFraming,
|
|
72
84
|
markTransportCap,
|
|
73
85
|
markTrustedBodyFraming,
|
|
74
86
|
type RawBodyReaders,
|
|
@@ -151,6 +163,7 @@ import {
|
|
|
151
163
|
type StatusResponse,
|
|
152
164
|
} from "./runtime-core.ts"
|
|
153
165
|
import { normalizeStaticResponseHeaders, type StaticResponseHeaders } from "./static-headers.ts"
|
|
166
|
+
import { withTextSlots } from "./text-slots.ts"
|
|
154
167
|
import { plainValidationError } from "./validation.ts"
|
|
155
168
|
|
|
156
169
|
// NodeServeOutcome (the nifra<->node bridge render form) now lives in `./node-outcome.ts`; re-exported
|
|
@@ -184,6 +197,7 @@ import {
|
|
|
184
197
|
NODE_NATIVE_MOUNT,
|
|
185
198
|
REPORT_ERROR,
|
|
186
199
|
RESOLVE_NODE_MOUNT,
|
|
200
|
+
SERVER_LIFECYCLE,
|
|
187
201
|
} from "./install.ts"
|
|
188
202
|
import type { EffectLedgerRuntime } from "./ledger-lane.ts"
|
|
189
203
|
import { jsonLogger, type Logger } from "./logger.ts"
|
|
@@ -226,6 +240,7 @@ import type {
|
|
|
226
240
|
RunningServer,
|
|
227
241
|
ServerOptions,
|
|
228
242
|
StopHook,
|
|
243
|
+
StopOptions,
|
|
229
244
|
ToolAnnotations,
|
|
230
245
|
} from "./server-types.ts"
|
|
231
246
|
import type { SSEInit, TypedSSEStream } from "./sse.ts"
|
|
@@ -378,6 +393,8 @@ interface FetchMount<Env = unknown> {
|
|
|
378
393
|
readonly fallbackOn404: boolean
|
|
379
394
|
readonly beforeRoutes: boolean
|
|
380
395
|
readonly order: number
|
|
396
|
+
/** The `authenticate()` stages registered before this mount, which gate it as they gate a route. */
|
|
397
|
+
readonly authStages: readonly RawAuthStage[]
|
|
381
398
|
/** Why this child is not analyzed (`MountOptions.opaque`), checked by reflection. */
|
|
382
399
|
readonly opaque: string | OpaqueMountDeclaration | undefined
|
|
383
400
|
/** The mounted app (`mount()` only), for reflection to tell a composed child from an opaque one. */
|
|
@@ -563,6 +580,7 @@ export type {
|
|
|
563
580
|
RunningServer,
|
|
564
581
|
ServerOptions,
|
|
565
582
|
StopHook,
|
|
583
|
+
StopOptions,
|
|
566
584
|
ToolAnnotations,
|
|
567
585
|
}
|
|
568
586
|
|
|
@@ -588,6 +606,8 @@ export {
|
|
|
588
606
|
export type { IdentityPlugin }
|
|
589
607
|
|
|
590
608
|
const DEFAULT_MAX_BODY_BYTES = 1_000_000
|
|
609
|
+
const BODY_RESPONSE = (response: Response | ResponseResult): Response | ResponseResult => response
|
|
610
|
+
const BODY_ERROR = (error: unknown): Promise<never> => Promise.reject(error)
|
|
591
611
|
const DEFAULT_DRAIN_MS = 10_000
|
|
592
612
|
const DRAIN_POLL_MS = 10
|
|
593
613
|
const STOP_HOOK_TIMEOUT_MS = 5_000
|
|
@@ -637,18 +657,104 @@ function hasUnroutedParam(params: Record<string, string>): boolean {
|
|
|
637
657
|
*/
|
|
638
658
|
const rawRequestTargets = typeof (globalThis as { Deno?: unknown }).Deno !== "undefined"
|
|
639
659
|
|
|
640
|
-
/**
|
|
641
|
-
|
|
660
|
+
/**
|
|
661
|
+
* Bun parses the target only when it can read the Host. Otherwise `Request.url` is the target as sent:
|
|
662
|
+
* relative for a Host with a space or a byte past ASCII or for no Host at all, the raw authority
|
|
663
|
+
* for a port past 65535, and `new URL(req.url)` throws in the app. So Bun is held to the Host check
|
|
664
|
+
* Deno runs.
|
|
665
|
+
*/
|
|
666
|
+
const unparsedBunTargets = typeof Bun !== "undefined"
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* `req` as Bun or workerd would have delivered it: the same request, its URL resolved as they parse it.
|
|
670
|
+
* `undefined` when the `Request` constructor refuses what the wire carried - a TRACE, CONNECT or TRACK,
|
|
671
|
+
* a lower-case `get` with a body - which {@link resolvedSource} routes instead.
|
|
672
|
+
*/
|
|
673
|
+
function withResolvedTarget(req: Request, url: string): Request | undefined {
|
|
674
|
+
let rebuilt: Request
|
|
675
|
+
try {
|
|
676
|
+
rebuilt = new Request(url, req)
|
|
677
|
+
} catch {
|
|
678
|
+
return undefined
|
|
679
|
+
}
|
|
680
|
+
// The same body, so the same framing: a runtime still delimited it.
|
|
681
|
+
if (hasTrustedBodyFraming(req)) markTrustedBodyFraming(rebuilt)
|
|
682
|
+
return rebuilt
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* A request no `Request` can be rebuilt from, as a source carrying its resolved URL: it is routed on that
|
|
687
|
+
* URL and gets the 404 or 405 the other runtimes give, where rebuilding it threw out of `fetch`. A
|
|
688
|
+
* handler that reads `c.req` gets the request as Deno delivered it.
|
|
689
|
+
*/
|
|
690
|
+
function resolvedSource(req: Request, url: string): RequestSource {
|
|
691
|
+
const source: RequestSource = {
|
|
692
|
+
method: req.method,
|
|
693
|
+
url,
|
|
694
|
+
get headers() {
|
|
695
|
+
return req.headers
|
|
696
|
+
},
|
|
697
|
+
header: (name) => headerOf(req, name),
|
|
698
|
+
get body() {
|
|
699
|
+
return req.body
|
|
700
|
+
},
|
|
701
|
+
arrayBuffer: () => req.arrayBuffer(),
|
|
702
|
+
json: () => req.json(),
|
|
703
|
+
bytes: () => req.bytes(),
|
|
704
|
+
request: req,
|
|
705
|
+
}
|
|
706
|
+
if (hasTrustedBodyFraming(req)) markTrustedBodyFraming(source)
|
|
707
|
+
return source
|
|
708
|
+
}
|
|
642
709
|
|
|
643
710
|
// A Host as RFC 9110 allows it: a reg-name or an IP literal, then an optional port. Deno builds
|
|
644
711
|
// `Request.url` by appending the target to the Host as received, so `Host: x/admin#` turned a
|
|
645
712
|
// request for `/public` into one for `/admin`. Node refuses such a Host before routing; so does this.
|
|
646
|
-
const HOST_AUTHORITY = /^(?:\[[0-9a-f:.%]+\]|[\w.-]+)(
|
|
713
|
+
const HOST_AUTHORITY = /^(?:\[[0-9a-f:.%]+\]|[\w.-]+)(?::(\d{1,5}))?$/i
|
|
714
|
+
|
|
715
|
+
// Clients repeat one Host on every request; comparing to the last valid one is ~7x cheaper than the regex.
|
|
716
|
+
let lastValidHost = ""
|
|
717
|
+
|
|
718
|
+
// A port past 65535, or an IPv6 literal no URL parser takes (`[1:2]`), is refused as Node refuses it:
|
|
719
|
+
// no URL can be built on either, so resolving a dot segment against one threw out of `fetch`.
|
|
720
|
+
const invalidHost = (host: string): boolean => {
|
|
721
|
+
if (host === lastValidHost) return false
|
|
722
|
+
const authority = HOST_AUTHORITY.exec(host)
|
|
723
|
+
if (authority === null) return true
|
|
724
|
+
if (authority[1] !== undefined && Number(authority[1]) > 65_535) return true
|
|
725
|
+
if (host.charCodeAt(0) === 91 /* [ */ && !URL.canParse(`http://${host}/`)) return true
|
|
726
|
+
lastValidHost = host
|
|
727
|
+
return false
|
|
728
|
+
}
|
|
647
729
|
|
|
648
|
-
|
|
649
|
-
|
|
730
|
+
/**
|
|
731
|
+
* What a request is routed on in place of `req.url`, before anything reads it: `true` refuses its
|
|
732
|
+
* Host (400), a URL replaces the target, `false` keeps it. Deno hands the target over as sent, so
|
|
733
|
+
* its dot segments are resolved; so does Bun when HTTP/1.0 leaves the Host out, and its route table
|
|
734
|
+
* matched `/files/..` raw. Such a request is served as `localhost`, as Node and Deno serve it.
|
|
735
|
+
* `headerOf`, not `req.headers`: on Deno that reads the Host alone instead of building every
|
|
736
|
+
* header.
|
|
737
|
+
*/
|
|
738
|
+
const requestTarget = (req: Request): string | boolean => {
|
|
739
|
+
if (!rawRequestTargets && !unparsedBunTargets) return false
|
|
650
740
|
const host = headerOf(req, "host")
|
|
651
|
-
|
|
741
|
+
if (host !== null && invalidHost(host)) {
|
|
742
|
+
if (rawRequestTargets) return true
|
|
743
|
+
// Bun served it anyway: it could not read the Host, so it left the target unparsed, or it
|
|
744
|
+
// built the URL on it - sent twice (joined with ", ", the URL on the first; RFC 9112 section
|
|
745
|
+
// 3.2 answers 400), percent-encoded (`%61.example` routed as `a.example`), or with a
|
|
746
|
+
// sub-delimiter (`a!b`). A request built in process keeps the URL its caller built, unless Bun
|
|
747
|
+
// would have built it so.
|
|
748
|
+
const routed = URL.parse(req.url)?.host
|
|
749
|
+
return routed === undefined || URL.parse(`//${host.split(", ")[0]}`, req.url)?.host === routed
|
|
750
|
+
}
|
|
751
|
+
if (rawRequestTargets) return needsResolving(req.url) && resolveDotSegments(req.url)
|
|
752
|
+
// A request built in process carries an absolute URL whatever its headers say.
|
|
753
|
+
return (
|
|
754
|
+
host === null &&
|
|
755
|
+
req.url.charCodeAt(0) === 47 /* / */ &&
|
|
756
|
+
resolveDotSegments(`http://localhost${req.url}`)
|
|
757
|
+
)
|
|
652
758
|
}
|
|
653
759
|
|
|
654
760
|
function normalizeMountPrefix(path: string): string {
|
|
@@ -681,12 +787,15 @@ function compareMounts<Env>(a: FetchMount<Env>, b: FetchMount<Env>): number {
|
|
|
681
787
|
return b.priority - a.priority || b.path.length - a.path.length || a.order - b.order
|
|
682
788
|
}
|
|
683
789
|
|
|
790
|
+
/** Rebase a mounted request's path without losing the authentication mark used by child caches. */
|
|
684
791
|
function stripMountPrefix(request: Request, prefix: string): Request {
|
|
685
792
|
if (prefix === "/") return request
|
|
686
793
|
const url = new URL(request.url)
|
|
687
794
|
const rest = url.pathname.slice(prefix.length)
|
|
688
795
|
url.pathname = rest === "" ? "/" : rest
|
|
689
|
-
|
|
796
|
+
const childRequest = new Request(url.href, request)
|
|
797
|
+
if (isAuthenticatedRequest(request)) markAuthenticatedRequest(childRequest)
|
|
798
|
+
return childRequest
|
|
690
799
|
}
|
|
691
800
|
|
|
692
801
|
/** `ctx.set` carrying the lazy backings (`_headers`, `_cookies`) so `toResponse` can skip allocating
|
|
@@ -881,8 +990,9 @@ interface HookAuditState {
|
|
|
881
990
|
readonly hasCustomLogger: boolean
|
|
882
991
|
readonly sites: {
|
|
883
992
|
readonly kind: string
|
|
884
|
-
/** Routes that captured the chain so far: merged routes excluded, they keep their own.
|
|
885
|
-
|
|
993
|
+
/** Routes that captured the chain so far: merged routes excluded, they keep their own. -1 once a
|
|
994
|
+
* mount captured it, which no route count matches. */
|
|
995
|
+
routesAtPush: number
|
|
886
996
|
readonly mergedAtPush: number
|
|
887
997
|
readonly site: string | undefined
|
|
888
998
|
}[]
|
|
@@ -964,6 +1074,19 @@ const recordScopedHook = (key: object, routesAtPush: number, kind: string): void
|
|
|
964
1074
|
})
|
|
965
1075
|
}
|
|
966
1076
|
|
|
1077
|
+
/** `next(value)` now, or once `value` settles. */
|
|
1078
|
+
const thenMaybe = <T, U>(
|
|
1079
|
+
value: MaybePromise<T>,
|
|
1080
|
+
next: (value: T) => MaybePromise<U>,
|
|
1081
|
+
): MaybePromise<U> => (value instanceof Promise ? value.then(next) : next(value))
|
|
1082
|
+
|
|
1083
|
+
/** A mount registered after an `authenticate()` stage runs it, so the stage covers something. */
|
|
1084
|
+
const noteAuthenticatedMount = (key: object): void => {
|
|
1085
|
+
const state = hookAudit.get(key)
|
|
1086
|
+
if (state === undefined) return
|
|
1087
|
+
for (const site of state.sites) if (site.kind === "authenticate") site.routesAtPush = -1
|
|
1088
|
+
}
|
|
1089
|
+
|
|
967
1090
|
/** Count routes a `merge()` adopted: they never run this server's order-scoped hooks, so a hook
|
|
968
1091
|
* followed only by merges covers nothing. */
|
|
969
1092
|
const noteMergedRoutes = (key: object, count: number): void => {
|
|
@@ -1158,8 +1281,9 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1158
1281
|
private readonly acceptInboundDeadlines: boolean
|
|
1159
1282
|
private readonly maxInboundDeadlineMs: number
|
|
1160
1283
|
private readonly deadlineAdmissionOptions: DeadlineAdmissionOptions
|
|
1161
|
-
private readonly gracefulSignals:
|
|
1284
|
+
private readonly gracefulSignals: StopOptions | undefined
|
|
1162
1285
|
private readonly stopHooks: StopHook[]
|
|
1286
|
+
private readonly stoppingHooks: StopHook[]
|
|
1163
1287
|
private readonly fetchMounts: FetchMount<EnvOf<Ctx>>[]
|
|
1164
1288
|
private mountOrder: number
|
|
1165
1289
|
/** Number of composed mounts that must run before the parent route table. Keeps legacy mounts off the hot path. */
|
|
@@ -1242,15 +1366,20 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1242
1366
|
/** Body/raw response hooks need a framework-payload marker; keep that decision per app. */
|
|
1243
1367
|
private responseBodyTag: object | undefined
|
|
1244
1368
|
private readonly responseBodyOwners: Set<object>
|
|
1369
|
+
// General lifecycles need no extra response bookkeeping when neither controls nor observers
|
|
1370
|
+
// use it. Share the fused renderer after the lifecycle, including on body/write routes.
|
|
1245
1371
|
private readonly finalizeResponse = (result: unknown, set: CtxSet, ctx?: RawContext): Response =>
|
|
1246
|
-
|
|
1247
|
-
result
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1372
|
+
set === EMPTY_RESPONSE_CONTROLS && this.onResponseHooks.length === 0
|
|
1373
|
+
? fusedRespondNoSet(result, this.responseBodyTag, this.staticResponseHeaders, true)
|
|
1374
|
+
: toResponse(
|
|
1375
|
+
result as HandlerResult,
|
|
1376
|
+
set,
|
|
1377
|
+
this.responseBodyTag,
|
|
1378
|
+
this.staticResponseHeaders,
|
|
1379
|
+
// Response hooks write next, and they run for this same request object.
|
|
1380
|
+
ctx !== undefined && this.onResponseHooks.length > 0 ? ctx[CONTEXT_REQUEST] : undefined,
|
|
1381
|
+
this.onResponseHooks.length === 0,
|
|
1382
|
+
)
|
|
1254
1383
|
private readonly responseRequests: WeakMap<Request, Request>
|
|
1255
1384
|
/** A request the WebSocket handshake check ran the onRequest hooks on and then passed to HTTP →
|
|
1256
1385
|
* what the hooks returned, so `fetch` routes that instead of running the hooks a second time. */
|
|
@@ -1318,6 +1447,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1318
1447
|
this.gracefulSignals =
|
|
1319
1448
|
options.gracefulSignals === true ? {} : options.gracefulSignals || undefined
|
|
1320
1449
|
this.stopHooks = []
|
|
1450
|
+
this.stoppingHooks = []
|
|
1321
1451
|
this.fetchMounts = []
|
|
1322
1452
|
this.mountOrder = 0
|
|
1323
1453
|
this.preRouteMountCount = 0
|
|
@@ -1393,10 +1523,25 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1393
1523
|
return this
|
|
1394
1524
|
}
|
|
1395
1525
|
|
|
1526
|
+
/**
|
|
1527
|
+
* Register a callback awaited as soon as `stop()` begins, while the server still serves: readiness
|
|
1528
|
+
* should fail from here, and work that outlives a request (a poller, a queue consumer) should stop
|
|
1529
|
+
* taking more. Hung callbacks are bounded.
|
|
1530
|
+
*/
|
|
1531
|
+
onStopping(fn: StopHook): this {
|
|
1532
|
+
this.assertConfigurable("onStopping()")
|
|
1533
|
+
if (typeof fn !== "function") throw new TypeError("onStopping callback must be a function")
|
|
1534
|
+
this.stoppingHooks.push(fn)
|
|
1535
|
+
return this
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1396
1538
|
/**
|
|
1397
1539
|
* Mount a legacy fetch handler under a literal path prefix. Mounted handlers sit behind typed
|
|
1398
1540
|
* routes, so routes can move one at a time while the remaining legacy surface stays live.
|
|
1399
|
-
* Mounted responses are outside Nifra's typed route and response-contract checks.
|
|
1541
|
+
* Mounted responses are outside Nifra's typed route and response-contract checks. An
|
|
1542
|
+
* `authenticate()` stage registered before the mount, or a required auth plugin built on one
|
|
1543
|
+
* (`bearer()`, `jwt()`, `basicAuth()`, `apiKey()`), refuses a request before the handler sees it;
|
|
1544
|
+
* mount before the stage to keep the legacy surface public.
|
|
1400
1545
|
*
|
|
1401
1546
|
* It carries HTTP only: a WebSocket upgrade under the prefix reaches the handler as an ordinary
|
|
1402
1547
|
* request and never becomes a socket. Serve that path's sockets with `app.ws()` routes or a
|
|
@@ -1418,7 +1563,10 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1418
1563
|
beforeRoutes: false,
|
|
1419
1564
|
order: this.mountOrder++,
|
|
1420
1565
|
opaque: options.opaque,
|
|
1566
|
+
authStages: [...this.authStages],
|
|
1421
1567
|
}
|
|
1568
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production" && this.authStages.length > 0)
|
|
1569
|
+
noteAuthenticatedMount(this)
|
|
1422
1570
|
this.fetchMounts.push(mount)
|
|
1423
1571
|
this.fetchMounts.sort(compareMounts)
|
|
1424
1572
|
return this
|
|
@@ -1474,7 +1622,10 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1474
1622
|
order: this.mountOrder++,
|
|
1475
1623
|
opaque: options.opaque,
|
|
1476
1624
|
app,
|
|
1625
|
+
authStages: [...this.authStages],
|
|
1477
1626
|
}
|
|
1627
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production" && this.authStages.length > 0)
|
|
1628
|
+
noteAuthenticatedMount(this)
|
|
1478
1629
|
// Mounts run before routes: a request under this prefix that no mount on the way strips comes back
|
|
1479
1630
|
// here unchanged until the stack overflows, which takes the process down. Dev-time only, like the
|
|
1480
1631
|
// hook audit: a production define folds this away, and the first dev request would hit it anyway.
|
|
@@ -1533,7 +1684,8 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1533
1684
|
* (see {@link ServerOptions.unusedScopedHooks}).
|
|
1534
1685
|
*/
|
|
1535
1686
|
/**
|
|
1536
|
-
* Register a dedicated authentication stage for subsequent routes
|
|
1687
|
+
* Register a dedicated authentication stage for subsequent routes, and for the `mount()` and
|
|
1688
|
+
* `mountFetch()` handlers registered after it: a request it refuses never reaches them.
|
|
1537
1689
|
*
|
|
1538
1690
|
* Unlike `derive`, this stage receives a body-blind authentication capsule and is compiled before
|
|
1539
1691
|
* route validation. Keeping it separate prevents an unrelated derive from accidentally becoming a
|
|
@@ -1603,7 +1755,12 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1603
1755
|
fn: H,
|
|
1604
1756
|
): Server<R, Ctx, HookOutput | HookOutputOf<ReturnType<H>>> {
|
|
1605
1757
|
this.assertConfigurable("beforeHandle()")
|
|
1606
|
-
this.beforeHandleHooks
|
|
1758
|
+
const hooks = this.beforeHandleHooks
|
|
1759
|
+
let at = hooks.length
|
|
1760
|
+
if (!LAST_BEFORE_HANDLE.has(fn)) {
|
|
1761
|
+
while (at > 0 && LAST_BEFORE_HANDLE.has(hooks[at - 1]!)) at--
|
|
1762
|
+
}
|
|
1763
|
+
hooks.splice(at, 0, fn as unknown as RawBeforeHandle)
|
|
1607
1764
|
if (hookAuditRuntime && process.env.NODE_ENV !== "production") {
|
|
1608
1765
|
recordScopedHook(this, this.catalog.size + this.wsRoutes.length, "beforeHandle")
|
|
1609
1766
|
tagHook(fn, "beforeHandle", this)
|
|
@@ -2440,6 +2597,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2440
2597
|
schema: RouteSchema | undefined,
|
|
2441
2598
|
handler: (context: never) => unknown,
|
|
2442
2599
|
): CatalogRoute {
|
|
2600
|
+
schema = withTextSlots(schema)
|
|
2443
2601
|
// Route provenance for the CLI's capability walk, which sets NIFRA_ROUTE_SITES while it loads the
|
|
2444
2602
|
// app: the module that registered the route, read where a static scan cannot see the path.
|
|
2445
2603
|
if (
|
|
@@ -2487,6 +2645,15 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2487
2645
|
lanes,
|
|
2488
2646
|
routeAssurance,
|
|
2489
2647
|
} = compiled
|
|
2648
|
+
// A schema that writes its answer its own way (`t.bytes()`) wraps every stage that can answer.
|
|
2649
|
+
const stages = withAnswerSteps(schema, method, path, {
|
|
2650
|
+
handler,
|
|
2651
|
+
beforeHandle: this.beforeHandleHooks,
|
|
2652
|
+
afterHandle: this.afterHandleHooks,
|
|
2653
|
+
onError: this.onErrorHooks,
|
|
2654
|
+
})
|
|
2655
|
+
handler = stages.handler
|
|
2656
|
+
const { beforeHandle, afterHandle, onError } = stages
|
|
2490
2657
|
if (idempotent !== undefined && this.idempotencyRuntime !== undefined) {
|
|
2491
2658
|
handler = this.idempotencyRuntime.wrapHandler(handler)
|
|
2492
2659
|
}
|
|
@@ -2514,8 +2681,8 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2514
2681
|
? this.buildFusedBodyDeriveBeforeAfterRunner(
|
|
2515
2682
|
handler as unknown as InternalHandler,
|
|
2516
2683
|
this.derives[0]!,
|
|
2517
|
-
|
|
2518
|
-
|
|
2684
|
+
beforeHandle[0]!,
|
|
2685
|
+
afterHandle[0],
|
|
2519
2686
|
schema?.body as StandardSchemaV1,
|
|
2520
2687
|
bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
2521
2688
|
)
|
|
@@ -2523,7 +2690,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2523
2690
|
? this.buildFusedDeriveBeforeRunner(
|
|
2524
2691
|
handler as unknown as InternalHandler,
|
|
2525
2692
|
this.derives[0]!,
|
|
2526
|
-
|
|
2693
|
+
beforeHandle[0]!,
|
|
2527
2694
|
undefined,
|
|
2528
2695
|
schema?.query,
|
|
2529
2696
|
bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
@@ -2532,8 +2699,8 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2532
2699
|
? this.buildFusedDeriveBeforeRunner(
|
|
2533
2700
|
handler as unknown as InternalHandler,
|
|
2534
2701
|
this.derives[0]!,
|
|
2535
|
-
|
|
2536
|
-
|
|
2702
|
+
beforeHandle[0]!,
|
|
2703
|
+
afterHandle[0],
|
|
2537
2704
|
schema?.query,
|
|
2538
2705
|
bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
2539
2706
|
)
|
|
@@ -2559,8 +2726,8 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2559
2726
|
? this.buildFusedBodyDeriveBeforeAfter(
|
|
2560
2727
|
handler as unknown as InternalHandler,
|
|
2561
2728
|
this.derives[0]!,
|
|
2562
|
-
|
|
2563
|
-
|
|
2729
|
+
beforeHandle[0]!,
|
|
2730
|
+
afterHandle[0],
|
|
2564
2731
|
schema?.body as StandardSchemaV1,
|
|
2565
2732
|
bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
2566
2733
|
)
|
|
@@ -2568,7 +2735,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2568
2735
|
? this.buildFusedDeriveBefore(
|
|
2569
2736
|
handler as unknown as InternalHandler,
|
|
2570
2737
|
this.derives[0]!,
|
|
2571
|
-
|
|
2738
|
+
beforeHandle[0]!,
|
|
2572
2739
|
schema?.query,
|
|
2573
2740
|
bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
2574
2741
|
)
|
|
@@ -2576,8 +2743,8 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2576
2743
|
? this.buildFusedDeriveBeforeAfter(
|
|
2577
2744
|
handler as unknown as InternalHandler,
|
|
2578
2745
|
this.derives[0]!,
|
|
2579
|
-
|
|
2580
|
-
|
|
2746
|
+
beforeHandle[0]!,
|
|
2747
|
+
afterHandle[0]!,
|
|
2581
2748
|
schema?.query,
|
|
2582
2749
|
bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
2583
2750
|
)
|
|
@@ -2587,9 +2754,9 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2587
2754
|
handler: handler as unknown as InternalHandler,
|
|
2588
2755
|
derives: this.derives,
|
|
2589
2756
|
authStages: compiled.authStages,
|
|
2590
|
-
beforeHandle
|
|
2591
|
-
afterHandle
|
|
2592
|
-
onError
|
|
2757
|
+
beforeHandle,
|
|
2758
|
+
afterHandle,
|
|
2759
|
+
onError,
|
|
2593
2760
|
decorations: routeDecorations,
|
|
2594
2761
|
hasDecorations,
|
|
2595
2762
|
bodySchema: schema?.body,
|
|
@@ -2618,9 +2785,9 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2618
2785
|
authStages: [...compiled.authStages],
|
|
2619
2786
|
decorations: routeDecorations,
|
|
2620
2787
|
hasDecorations,
|
|
2621
|
-
beforeHandle: [...
|
|
2622
|
-
afterHandle: [...
|
|
2623
|
-
onError: [...
|
|
2788
|
+
beforeHandle: [...beforeHandle],
|
|
2789
|
+
afterHandle: [...afterHandle],
|
|
2790
|
+
onError: [...onError],
|
|
2624
2791
|
lifecycleHookLane: lanes.lifecycleHookLane,
|
|
2625
2792
|
around: [...this.aroundHooks],
|
|
2626
2793
|
execution,
|
|
@@ -3060,6 +3227,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3060
3227
|
// Cleanup the source registered (a pool, a queue consumer) belongs to the server that now runs
|
|
3061
3228
|
// its routes; dropping it would leak the resource past `stop()`.
|
|
3062
3229
|
this.stopHooks.push(...source.stopHooks)
|
|
3230
|
+
this.stoppingHooks.push(...source.stoppingHooks)
|
|
3063
3231
|
this.mcpResourceList.push(...source.mcpResourceList)
|
|
3064
3232
|
this.mcpPromptList.push(...source.mcpPromptList)
|
|
3065
3233
|
}
|
|
@@ -3223,9 +3391,12 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3223
3391
|
fetch(req: Request, platform?: Platform<EnvOf<Ctx>>): MaybePromise<Response> {
|
|
3224
3392
|
// Deno's `Request.url` is the target as sent; Bun, workerd and every `new Request()` parse it.
|
|
3225
3393
|
// Route what the parsed URL says, so `/users/../admin` is `/admin` on every runtime.
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3394
|
+
const target = requestTarget(req)
|
|
3395
|
+
if (target === true) return jsonError(400, "invalid_host")
|
|
3396
|
+
if (target !== false) {
|
|
3397
|
+
const rebuilt = withResolvedTarget(req, target)
|
|
3398
|
+
if (rebuilt === undefined) return this.fetchSource(resolvedSource(req, target), platform)
|
|
3399
|
+
req = rebuilt
|
|
3229
3400
|
}
|
|
3230
3401
|
// An edge deployment whose only ingress is this method declares its requests runtime-framed at
|
|
3231
3402
|
// construction; everything else stays on the delivered-byte check (see `trustBodyFraming`).
|
|
@@ -3387,7 +3558,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3387
3558
|
// whatever their prefix lengths. A 404 fallthrough chain stays on the ordinary path.
|
|
3388
3559
|
const mount =
|
|
3389
3560
|
this.firstMountUnder(parts.pathname, true) ?? this.firstMountUnder(parts.pathname, false)
|
|
3390
|
-
if (mount === undefined || mount.fallbackOn404) return undefined
|
|
3561
|
+
if (mount === undefined || mount.fallbackOn404 || mount.authStages.length > 0) return undefined
|
|
3391
3562
|
const candidate = (mount.handler as unknown as Record<symbol, unknown>)[NODE_NATIVE_MOUNT]
|
|
3392
3563
|
return typeof candidate === "function"
|
|
3393
3564
|
? {
|
|
@@ -3724,11 +3895,15 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3724
3895
|
// The adapter answers a pass by handing this same object to `fetch`.
|
|
3725
3896
|
const received = req
|
|
3726
3897
|
// The handshake routes the path `fetch` would: Host checked, dot segments resolved (see `fetch`).
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3898
|
+
const target = requestTarget(req)
|
|
3899
|
+
if (target === true) {
|
|
3900
|
+
return { kind: "reject", response: this.wrapWebResponse(jsonError(400, "invalid_host")) }
|
|
3901
|
+
}
|
|
3902
|
+
if (target !== false) {
|
|
3903
|
+
// A GET carrying a body cannot be rebuilt; `fetch` routes it as the ordinary request it is.
|
|
3904
|
+
const rebuilt = withResolvedTarget(req, target)
|
|
3905
|
+
if (rebuilt === undefined) return WS_PASS
|
|
3906
|
+
req = rebuilt
|
|
3732
3907
|
}
|
|
3733
3908
|
// The handshake's caller is the one `fetch` derives: hooks and `upgrade()` see the trusted IP.
|
|
3734
3909
|
platform = this.trustedPlatform(req, platform)
|
|
@@ -3844,17 +4019,19 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3844
4019
|
}
|
|
3845
4020
|
}
|
|
3846
4021
|
if (selected === undefined) return WS_PASS
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
childRequest =
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
4022
|
+
const mount = selected
|
|
4023
|
+
const upgrade = (): MaybePromise<WebSocketUpgradeOutcome> => {
|
|
4024
|
+
let childRequest = request
|
|
4025
|
+
if (mount.stripPrefix) {
|
|
4026
|
+
childRequest = stripMountPrefix(request, mount.path)
|
|
4027
|
+
mount.wsPassed ??= new WeakMap()
|
|
4028
|
+
mount.wsPassed.set(request, childRequest)
|
|
4029
|
+
}
|
|
4030
|
+
try {
|
|
4031
|
+
return mount.resolveWebSocketUpgrade!(childRequest, platform)
|
|
4032
|
+
} catch {
|
|
4033
|
+
return refuse(jsonError(500, "internal_error"))
|
|
4034
|
+
}
|
|
3858
4035
|
}
|
|
3859
4036
|
const settle = (outcome: WebSocketUpgradeOutcome): MaybePromise<WebSocketUpgradeOutcome> =>
|
|
3860
4037
|
outcome.kind === "pass" ||
|
|
@@ -3863,7 +4040,17 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3863
4040
|
: outcome.kind === "reject"
|
|
3864
4041
|
? refuse(outcome.response)
|
|
3865
4042
|
: outcome
|
|
3866
|
-
|
|
4043
|
+
const run = (): MaybePromise<WebSocketUpgradeOutcome> => {
|
|
4044
|
+
const child = upgrade()
|
|
4045
|
+
return child instanceof Promise ? child.then(settle) : settle(child)
|
|
4046
|
+
}
|
|
4047
|
+
// Only a child's miss may fall through, never an authentication refusal.
|
|
4048
|
+
return mount.authStages.length === 0
|
|
4049
|
+
? run()
|
|
4050
|
+
: thenMaybe(
|
|
4051
|
+
authenticateMount(mount.authStages, request, platform, this.logger),
|
|
4052
|
+
(refusal) => (refusal === undefined ? run() : refuse(refusal)),
|
|
4053
|
+
)
|
|
3867
4054
|
}
|
|
3868
4055
|
return dispatch(0)
|
|
3869
4056
|
}
|
|
@@ -4252,8 +4439,8 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4252
4439
|
onTimeout: () => T,
|
|
4253
4440
|
webFast: boolean,
|
|
4254
4441
|
): MaybePromise<T> {
|
|
4255
|
-
//
|
|
4256
|
-
//
|
|
4442
|
+
// Native delimiter searches keep path/query splitting out of a character-by-character JS loop.
|
|
4443
|
+
// Keep the slices in locals instead of allocating the public helper's pathname/search pair.
|
|
4257
4444
|
let pathname: string
|
|
4258
4445
|
let search: string
|
|
4259
4446
|
// Read the source's split ONCE. `urlParts` is an accessor on the Node request sources that
|
|
@@ -4272,20 +4459,11 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4272
4459
|
pathname = "/"
|
|
4273
4460
|
search = ""
|
|
4274
4461
|
} else {
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
if (c === 63 /* ? */ && searchStart === -1) {
|
|
4281
|
-
pathEnd = i
|
|
4282
|
-
searchStart = i
|
|
4283
|
-
} else if (c === 35 /* # */) {
|
|
4284
|
-
if (searchStart === -1) pathEnd = i
|
|
4285
|
-
searchEnd = i
|
|
4286
|
-
break
|
|
4287
|
-
}
|
|
4288
|
-
}
|
|
4462
|
+
const fragment = rawUrl.indexOf("#", start)
|
|
4463
|
+
const searchEnd = fragment === -1 ? rawUrl.length : fragment
|
|
4464
|
+
const question = rawUrl.indexOf("?", start)
|
|
4465
|
+
const searchStart = question !== -1 && question < searchEnd ? question : -1
|
|
4466
|
+
const pathEnd = searchStart === -1 ? searchEnd : searchStart
|
|
4289
4467
|
pathname = rawUrl.slice(start, pathEnd)
|
|
4290
4468
|
search = searchStart === -1 ? "" : rawUrl.slice(searchStart, searchEnd)
|
|
4291
4469
|
}
|
|
@@ -4377,17 +4555,32 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4377
4555
|
}
|
|
4378
4556
|
}
|
|
4379
4557
|
if (mount === undefined) return fallback ?? jsonError(404, "not_found")
|
|
4380
|
-
const
|
|
4381
|
-
|
|
4382
|
-
:
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4558
|
+
const selected = mount
|
|
4559
|
+
const run = (): MaybePromise<Response> => {
|
|
4560
|
+
const reused = selected.stripPrefix ? selected.wsPassed?.get(request) : undefined
|
|
4561
|
+
const childRequest = selected.stripPrefix
|
|
4562
|
+
? (reused ?? stripMountPrefix(request, selected.path))
|
|
4563
|
+
: request
|
|
4564
|
+
// HTTP authentication may have marked the original since the WebSocket pass.
|
|
4565
|
+
if (reused !== undefined && isAuthenticatedRequest(request)) {
|
|
4566
|
+
markAuthenticatedRequest(reused)
|
|
4567
|
+
}
|
|
4568
|
+
const outcome = selected.handler(childRequest, platform)
|
|
4569
|
+
if (!replayable || !selected.fallbackOn404) return outcome
|
|
4570
|
+
if (outcome instanceof Promise) {
|
|
4571
|
+
return outcome.then((response) =>
|
|
4572
|
+
response.status === 404 ? dispatch(index + 1, response) : response,
|
|
4573
|
+
)
|
|
4574
|
+
}
|
|
4575
|
+
return outcome.status === 404 ? dispatch(index + 1, outcome) : outcome
|
|
4389
4576
|
}
|
|
4390
|
-
|
|
4577
|
+
// Keep gate refusals outside the child's 404 fallthrough path.
|
|
4578
|
+
return mount.authStages.length === 0
|
|
4579
|
+
? run()
|
|
4580
|
+
: thenMaybe(
|
|
4581
|
+
authenticateMount(mount.authStages, request, platform, this.logger),
|
|
4582
|
+
(refusal) => refusal ?? run(),
|
|
4583
|
+
)
|
|
4391
4584
|
}
|
|
4392
4585
|
return dispatch(first)
|
|
4393
4586
|
}
|
|
@@ -5857,7 +6050,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5857
6050
|
finalize,
|
|
5858
6051
|
responseSet,
|
|
5859
6052
|
(e, c) => this.logRequestError(e, c),
|
|
5860
|
-
|
|
6053
|
+
wrapResponse,
|
|
5861
6054
|
)
|
|
5862
6055
|
}
|
|
5863
6056
|
|
|
@@ -5993,6 +6186,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5993
6186
|
wrapResponse,
|
|
5994
6187
|
(err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse),
|
|
5995
6188
|
entry.schema?.body,
|
|
6189
|
+
this.formOrigins,
|
|
5996
6190
|
)
|
|
5997
6191
|
}
|
|
5998
6192
|
|
|
@@ -6365,7 +6559,14 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6365
6559
|
const checked = contract.runtime.check(contract.definition, result, ctx)
|
|
6366
6560
|
if (checked instanceof Promise) {
|
|
6367
6561
|
return checked.then(
|
|
6368
|
-
(outcome) =>
|
|
6562
|
+
(outcome) => {
|
|
6563
|
+
// The render runs outside the try below: a throw (a BigInt, a cycle) must still reach onError.
|
|
6564
|
+
try {
|
|
6565
|
+
return this.finishContractOutcome(ctx, finalize, wrapResponse, outcome)
|
|
6566
|
+
} catch (err) {
|
|
6567
|
+
return this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse)
|
|
6568
|
+
}
|
|
6569
|
+
},
|
|
6369
6570
|
(err) => this.handleLifecycleError(entry, err, ctx, finalize, wrapResponse),
|
|
6370
6571
|
)
|
|
6371
6572
|
}
|
|
@@ -6428,6 +6629,8 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6428
6629
|
if (err instanceof Response || isResponseResult(err))
|
|
6429
6630
|
return this.finishLifecycleContract(entry, ctx, finalize, wrapResponse, err)
|
|
6430
6631
|
if (entry.onError.length === 0) {
|
|
6632
|
+
const clientCode = clientBodyErrorCode(err)
|
|
6633
|
+
if (clientCode !== undefined) return wrapResponse(plainError(400, clientCode))
|
|
6431
6634
|
// Never crash the server or leak internals. The client gets a flat 500; the detail goes to the
|
|
6432
6635
|
// (redacting) logger. Body-read failures and around-hook failures land here too.
|
|
6433
6636
|
this.logRequestError(err, ctx)
|
|
@@ -6516,6 +6719,8 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6516
6719
|
return wrapResponse(plainError(500, "internal_error"))
|
|
6517
6720
|
}
|
|
6518
6721
|
}
|
|
6722
|
+
const clientCode = clientBodyErrorCode(err)
|
|
6723
|
+
if (clientCode !== undefined) return wrapResponse(plainError(400, clientCode))
|
|
6519
6724
|
this.logRequestError(err, ctx)
|
|
6520
6725
|
return fallback === undefined ? wrapResponse(plainError(500, "internal_error")) : fallback()
|
|
6521
6726
|
}
|
|
@@ -6538,18 +6743,23 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6538
6743
|
// second continuation, while retaining the exact bounded-read, content-type, and poisoning
|
|
6539
6744
|
// contracts shared by every body route.
|
|
6540
6745
|
const validate = (parsed: unknown): MaybePromise<Response | ResponseResult | undefined> => {
|
|
6541
|
-
|
|
6542
|
-
|
|
6543
|
-
|
|
6544
|
-
|
|
6746
|
+
// Reject outside the parser's JSON catch so a custom validator throw reaches onError.
|
|
6747
|
+
try {
|
|
6748
|
+
const validation = bodySchema["~standard"].validate(parsed)
|
|
6749
|
+
return validation instanceof Promise
|
|
6750
|
+
? validation.then((result) => this.applyLifecycleValidation(entry, result, ctx, "body"))
|
|
6751
|
+
: this.applyLifecycleValidation(entry, validation, ctx, "body")
|
|
6752
|
+
} catch (error) {
|
|
6753
|
+
return Promise.reject(error)
|
|
6754
|
+
}
|
|
6545
6755
|
}
|
|
6546
6756
|
return readBodyFramed(
|
|
6547
6757
|
req,
|
|
6548
6758
|
bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
6549
6759
|
this.protoPoisoning,
|
|
6550
6760
|
validate,
|
|
6551
|
-
|
|
6552
|
-
|
|
6761
|
+
BODY_RESPONSE,
|
|
6762
|
+
BODY_ERROR,
|
|
6553
6763
|
bodySchema,
|
|
6554
6764
|
this.formOrigins,
|
|
6555
6765
|
)
|
|
@@ -6563,15 +6773,16 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6563
6773
|
ctx: RawContext,
|
|
6564
6774
|
kind: ProgramValidationKind,
|
|
6565
6775
|
): MaybePromise<Response | ResponseResult | undefined> {
|
|
6566
|
-
const assign = (value: unknown): void => {
|
|
6567
|
-
;(ctx as unknown as Record<ProgramValidationKind, unknown>)[kind] = value
|
|
6568
|
-
}
|
|
6569
6776
|
if (result.issues === undefined) {
|
|
6570
|
-
|
|
6777
|
+
// biome-ignore lint/plugin/noChainedTypeAssertions lint/plugin/requireSafetyCommentForTypeAssertion: RawContext carries every ProgramValidationKind slot; this instruction assigns its validated value before handlers run.
|
|
6778
|
+
;(ctx as unknown as Record<ProgramValidationKind, unknown>)[kind] = result.value
|
|
6571
6779
|
return undefined
|
|
6572
6780
|
}
|
|
6573
6781
|
const hook = entry.schema?.onValidationError ?? this.defaultOnValidationError
|
|
6574
6782
|
if (hook === undefined) return plainValidationError(result.issues)
|
|
6783
|
+
const assign = (value: unknown): void => {
|
|
6784
|
+
;(ctx as unknown as Record<ProgramValidationKind, unknown>)[kind] = value
|
|
6785
|
+
}
|
|
6575
6786
|
const attempted = hook(result.issues, ctx as unknown as Context, kind)
|
|
6576
6787
|
if (attempted instanceof Promise) {
|
|
6577
6788
|
return attempted.then((recovery) =>
|
|
@@ -6618,14 +6829,34 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6618
6829
|
// the HTTP framing, so the JSON lane may retain its native fused `json()` parse without weakening
|
|
6619
6830
|
// the portable/adapted source contract (those sources are never marked).
|
|
6620
6831
|
const markFramed = (request: Request): void => markTrustedBodyFraming(request)
|
|
6621
|
-
// The
|
|
6832
|
+
// The direct native lanes bypass `runMatched`, so the route's transport byte cap must be
|
|
6622
6833
|
// marked here too - otherwise a Bun `listen()` fused route would leave direct `c.req` body
|
|
6623
|
-
// reads uncapped. The
|
|
6834
|
+
// reads uncapped. The fallback branches go through `fetchMatched` -> `runMatched`, which marks.
|
|
6624
6835
|
const bodyLimit =
|
|
6625
6836
|
entry.schema?.body === undefined || entry.program.authBeforeValidation
|
|
6626
6837
|
? entry.bodyLimit
|
|
6627
6838
|
: undefined
|
|
6628
|
-
|
|
6839
|
+
// The complete plan already preserves general validation, hooks, recovery, around, and ledgers.
|
|
6840
|
+
// When native admission is safe, use it directly even if the route has no closed-shape fusion.
|
|
6841
|
+
// Idempotency still needs runMatched's request preparation and is excluded from the table.
|
|
6842
|
+
const inner: FusedWebRunner | undefined =
|
|
6843
|
+
fused ??
|
|
6844
|
+
(signal && budget
|
|
6845
|
+
? (source, params, search, activeSignal, activeBudget, platform, nativeContext) =>
|
|
6846
|
+
entry.execution.run(
|
|
6847
|
+
this,
|
|
6848
|
+
entry,
|
|
6849
|
+
source,
|
|
6850
|
+
params,
|
|
6851
|
+
search,
|
|
6852
|
+
activeSignal,
|
|
6853
|
+
activeBudget,
|
|
6854
|
+
platform,
|
|
6855
|
+
nativeContext,
|
|
6856
|
+
this.finalizeResponse,
|
|
6857
|
+
this.wrapWebResponse,
|
|
6858
|
+
)
|
|
6859
|
+
: undefined)
|
|
6629
6860
|
const capped: FusedWebRunner | undefined =
|
|
6630
6861
|
inner === undefined || bodyLimit === undefined
|
|
6631
6862
|
? inner
|
|
@@ -6808,7 +7039,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6808
7039
|
}
|
|
6809
7040
|
const paramNames = pattern.paramNames
|
|
6810
7041
|
const fused = mayUseFusedNative ? entry.execution.fusedWeb : undefined
|
|
6811
|
-
|
|
7042
|
+
const handler = this.compileBunNativeHandler(
|
|
6812
7043
|
entry,
|
|
6813
7044
|
paramNames,
|
|
6814
7045
|
fused,
|
|
@@ -6817,6 +7048,9 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6817
7048
|
peer,
|
|
6818
7049
|
fallback,
|
|
6819
7050
|
)
|
|
7051
|
+
// The table matched the target as sent; one `fetch` refuses or rewrites goes through it.
|
|
7052
|
+
tableAt(path, pattern.segments)[method as Method] = (request, server) =>
|
|
7053
|
+
requestTarget(request) ? fallback(request, server) : handler(request, server)
|
|
6820
7054
|
count += 1
|
|
6821
7055
|
if (paramNames.length > 0) {
|
|
6822
7056
|
below.add(pattern.segments.length + method)
|
|
@@ -7016,11 +7250,12 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
7016
7250
|
* The Bun semantics: poll `pendingRequests` (awaiting
|
|
7017
7251
|
* `stop()`'s promise drops in-flight requests), and decide graceful-vs-forced in
|
|
7018
7252
|
* ONE call (Bun can't escalate an already-graceful `stop()` to a forced close).
|
|
7019
|
-
* New connections may be accepted during the drain window;
|
|
7020
|
-
*
|
|
7021
|
-
*
|
|
7253
|
+
* New connections may be accepted during the drain window; `delayMs` is the time a load balancer
|
|
7254
|
+
* gets to stop routing here first. The `onStopping` hooks run before that delay, the `onStop`
|
|
7255
|
+
* hooks after the drain. Open WebSockets get a 1001 close when the drain starts and the same
|
|
7256
|
+
* window to finish it; a socket opened after `stop()` began is closed at once.
|
|
7022
7257
|
*/
|
|
7023
|
-
async stop({ drainMs = DEFAULT_DRAIN_MS
|
|
7258
|
+
async stop({ drainMs = DEFAULT_DRAIN_MS, delayMs = 0 }: StopOptions = {}): Promise<void> {
|
|
7024
7259
|
const onSignal = this.signalHandler
|
|
7025
7260
|
if (onSignal !== undefined) {
|
|
7026
7261
|
this.signalHandler = undefined
|
|
@@ -7029,6 +7264,8 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
7029
7264
|
const server = this.bunServer
|
|
7030
7265
|
if (server === undefined) return
|
|
7031
7266
|
this.bunServer = undefined
|
|
7267
|
+
await this[SERVER_LIFECYCLE]("stopping")
|
|
7268
|
+
if (delayMs > 0) await Bun.sleep(delayMs)
|
|
7032
7269
|
const sockets = this.bunSockets
|
|
7033
7270
|
this.bunSockets = undefined
|
|
7034
7271
|
if (sockets !== undefined) for (const ws of sockets) ws.close(1001)
|
|
@@ -7038,16 +7275,28 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
7038
7275
|
await Bun.sleep(DRAIN_POLL_MS)
|
|
7039
7276
|
}
|
|
7040
7277
|
server.stop(busy()) // force-close iff stragglers remain past the deadline
|
|
7041
|
-
|
|
7042
|
-
|
|
7043
|
-
|
|
7278
|
+
await this[SERVER_LIFECYCLE]("stopped")
|
|
7279
|
+
}
|
|
7280
|
+
|
|
7281
|
+
/**
|
|
7282
|
+
* @internal Run the `onStopping` or the `onStop` hooks, each bounded. A runtime adapter that drains
|
|
7283
|
+
* its own listener calls it on either side of the drain, as `stop()` does here.
|
|
7284
|
+
*/
|
|
7285
|
+
async [SERVER_LIFECYCLE](phase: "stopping" | "stopped"): Promise<void> {
|
|
7286
|
+
const hooks = phase === "stopping" ? this.stoppingHooks : this.stopHooks
|
|
7287
|
+
if (hooks.length === 0) return
|
|
7288
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
7044
7289
|
await Promise.race([
|
|
7045
|
-
|
|
7046
|
-
new Promise<void>((resolve) =>
|
|
7290
|
+
Promise.allSettled(hooks.map((hook) => Promise.resolve().then(hook))),
|
|
7291
|
+
new Promise<void>((resolve) => {
|
|
7292
|
+
timer = setTimeout(resolve, STOP_HOOK_TIMEOUT_MS)
|
|
7293
|
+
}),
|
|
7047
7294
|
])
|
|
7295
|
+
// A pending bound would hold the event loop open after every hook settled.
|
|
7296
|
+
clearTimeout(timer)
|
|
7048
7297
|
}
|
|
7049
7298
|
|
|
7050
|
-
private installSignalHandlers(drain:
|
|
7299
|
+
private installSignalHandlers(drain: StopOptions): void {
|
|
7051
7300
|
// Drain, then let the process exit naturally - the stopped server no longer
|
|
7052
7301
|
// holds the event loop open. Opt-in (`gracefulSignals`), so taking over the
|
|
7053
7302
|
// signals is consented; we don't force `process.exit`.
|