@nifrajs/core 5.0.0 → 5.2.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 +2384 -0
- package/dist/assurance.d.ts +15 -1
- package/dist/assurance.d.ts.map +1 -1
- package/dist/assurance.js +97 -71
- package/dist/assurance.js.map +1 -1
- package/dist/capabilities.d.ts +2 -0
- package/dist/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +1 -1
- package/dist/capabilities.js.map +1 -1
- package/dist/edge-kit.d.ts +1 -1
- package/dist/edge-kit.d.ts.map +1 -1
- package/dist/edge-kit.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/internal/hook-origins.d.ts +17 -1
- package/dist/internal/hook-origins.d.ts.map +1 -1
- package/dist/internal/hook-origins.js +20 -0
- package/dist/internal/hook-origins.js.map +1 -1
- package/dist/internal/route-compiler.d.ts +9 -0
- package/dist/internal/route-compiler.d.ts.map +1 -1
- package/dist/internal/route-compiler.js +95 -13
- package/dist/internal/route-compiler.js.map +1 -1
- package/dist/internal/route-program.d.ts +2 -0
- package/dist/internal/route-program.d.ts.map +1 -1
- package/dist/internal/route-program.js +22 -17
- package/dist/internal/route-program.js.map +1 -1
- package/dist/internal/same-origin.d.ts +11 -1
- package/dist/internal/same-origin.d.ts.map +1 -1
- package/dist/internal/same-origin.js +8 -0
- package/dist/internal/same-origin.js.map +1 -1
- package/dist/reflection.d.ts +23 -2
- package/dist/reflection.d.ts.map +1 -1
- package/dist/reflection.js +57 -1
- package/dist/reflection.js.map +1 -1
- package/dist/server/body-lane.d.ts +8 -2
- package/dist/server/body-lane.d.ts.map +1 -1
- package/dist/server/body-lane.js +27 -2
- package/dist/server/body-lane.js.map +1 -1
- package/dist/server/client-ip.js +6 -5
- package/dist/server/client-ip.js.map +1 -1
- package/dist/server/context.d.ts +7 -4
- package/dist/server/context.d.ts.map +1 -1
- package/dist/server/cookies.d.ts.map +1 -1
- package/dist/server/cookies.js +13 -1
- package/dist/server/cookies.js.map +1 -1
- package/dist/server/core-copies.d.ts +7 -1
- package/dist/server/core-copies.d.ts.map +1 -1
- package/dist/server/core-copies.js +37 -1
- package/dist/server/core-copies.js.map +1 -1
- package/dist/server/edge.d.ts.map +1 -1
- package/dist/server/edge.js +2 -1
- package/dist/server/edge.js.map +1 -1
- package/dist/server/headers.d.ts +9 -0
- package/dist/server/headers.d.ts.map +1 -1
- package/dist/server/headers.js +26 -0
- package/dist/server/headers.js.map +1 -1
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +5 -1
- package/dist/server/http.js.map +1 -1
- package/dist/server/response-contract-lane.d.ts +57 -7
- package/dist/server/response-contract-lane.d.ts.map +1 -1
- package/dist/server/response-contract-lane.js +394 -23
- package/dist/server/response-contract-lane.js.map +1 -1
- package/dist/server/server-types.d.ts +23 -9
- package/dist/server/server-types.d.ts.map +1 -1
- package/dist/server/server.d.ts +40 -5
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +188 -43
- package/dist/server/server.js.map +1 -1
- package/dist/server/websocket.d.ts +11 -0
- package/dist/server/websocket.d.ts.map +1 -1
- package/dist/server/websocket.js +26 -12
- package/dist/server/websocket.js.map +1 -1
- package/dist/server/ws-bun.d.ts +7 -1
- package/dist/server/ws-bun.d.ts.map +1 -1
- package/dist/server/ws-bun.js +42 -2
- package/dist/server/ws-bun.js.map +1 -1
- package/dist/server/ws-hook.d.ts +9 -3
- package/dist/server/ws-hook.d.ts.map +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1 -1
- package/dist/server.js.map +1 -1
- package/dist/single-copy.d.ts.map +1 -1
- package/dist/single-copy.js +81 -10
- package/dist/single-copy.js.map +1 -1
- package/dist/wire.d.ts +5 -0
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js +5 -0
- package/dist/wire.js.map +1 -1
- package/dist/ws.d.ts.map +1 -1
- package/dist/ws.js +2 -1
- package/dist/ws.js.map +1 -1
- package/package.json +3 -2
- package/src/assurance.ts +120 -81
- package/src/capabilities.ts +3 -1
- package/src/edge-kit.ts +1 -1
- package/src/index.ts +2 -2
- package/src/internal/hook-origins.ts +35 -0
- package/src/internal/route-compiler.ts +107 -16
- package/src/internal/route-program.ts +39 -20
- package/src/internal/same-origin.ts +14 -1
- package/src/reflection.ts +61 -1
- package/src/server/body-lane.ts +36 -1
- package/src/server/client-ip.ts +5 -5
- package/src/server/context.ts +7 -4
- package/src/server/cookies.ts +12 -1
- package/src/server/core-copies.ts +40 -0
- package/src/server/edge.ts +2 -1
- package/src/server/headers.ts +39 -0
- package/src/server/http.ts +7 -1
- package/src/server/response-contract-lane.ts +491 -24
- package/src/server/server-types.ts +23 -9
- package/src/server/server.ts +249 -35
- package/src/server/websocket.ts +39 -13
- package/src/server/ws-bun.ts +48 -1
- package/src/server/ws-hook.ts +9 -2
- package/src/server.ts +1 -1
- package/src/single-copy.ts +90 -9
- package/src/wire.ts +14 -0
- package/src/ws.ts +2 -0
|
@@ -59,10 +59,18 @@ export interface ServerOptions {
|
|
|
59
59
|
* `__proto__` key, or a `constructor` carrying a `prototype`, is the payload shape that turns a
|
|
60
60
|
* later innocent merge/assign into prototype pollution. `"reject"` (default) answers the same
|
|
61
61
|
* flat 400 as malformed JSON; `"strip"` removes the offending keys and continues; `"ignore"`
|
|
62
|
-
* parses as-is (only for routes that never merge body input into other objects).
|
|
63
|
-
*
|
|
62
|
+
* parses as-is (only for routes that never merge body input into other objects). Every guarded
|
|
63
|
+
* payload pays one iterative pass over its parsed object nodes; `"ignore"` skips it.
|
|
64
64
|
*/
|
|
65
65
|
readonly protoPoisoning?: "reject" | "strip" | "ignore"
|
|
66
|
+
/**
|
|
67
|
+
* Other sites whose HTML forms may post to a body-schema route: exact origins, or a function that
|
|
68
|
+
* receives the `Origin`. A urlencoded, multipart or `text/plain` body is one a form on any site can
|
|
69
|
+
* send with the visitor's cookies and no preflight, so one whose `Origin` names another site is
|
|
70
|
+
* refused with `403` before it is read. Same-origin posts and requests without an `Origin`, such
|
|
71
|
+
* as server-to-server calls, pass. JSON is not affected: a browser preflights it, so CORS decides.
|
|
72
|
+
*/
|
|
73
|
+
readonly formOrigins?: ReadonlyArray<string> | ((origin: string) => boolean)
|
|
66
74
|
/**
|
|
67
75
|
* Declare that every `Request` reaching `app.fetch` was framed by a runtime HTTP parser, so its
|
|
68
76
|
* `Content-Length` is an exact transport boundary rather than a hint. Set this ONLY when the
|
|
@@ -81,9 +89,12 @@ export interface ServerOptions {
|
|
|
81
89
|
* untrusted input.
|
|
82
90
|
*/
|
|
83
91
|
readonly trustBodyFraming?: boolean
|
|
84
|
-
/**
|
|
85
|
-
*
|
|
86
|
-
* Default: `maxBodyBytes` (1 MB).
|
|
92
|
+
/**
|
|
93
|
+
* Max inbound WebSocket message size (bytes). A larger message never reaches the handler and the
|
|
94
|
+
* socket closes with `1009`. Default: `maxBodyBytes` (1 MB). Bun and Node stop buffering at the cap,
|
|
95
|
+
* so a huge frame is never held in memory; on Bun a mounted app's smaller cap, and on Deno and
|
|
96
|
+
* Workers every cap, is checked after the runtime has buffered the whole message.
|
|
97
|
+
*/
|
|
87
98
|
readonly wsMaxPayloadBytes?: number
|
|
88
99
|
/** Per-request timeout (ms): a slower request gets a 503 and `ctx.signal` aborts. 0 disables (default). */
|
|
89
100
|
readonly requestTimeoutMs?: number
|
|
@@ -153,10 +164,13 @@ export interface ServerOptions {
|
|
|
153
164
|
*/
|
|
154
165
|
readonly onValidationError?: RouteSchema["onValidationError"]
|
|
155
166
|
/**
|
|
156
|
-
* What to do when an order-scoped hook (`derive`/`beforeHandle`/`around
|
|
157
|
-
* `afterHandle`/`onError`/`decorate`) covers no route because it was added after
|
|
158
|
-
* registered. `register()` snapshots the current chain into each route as it is
|
|
159
|
-
* appended after every route reaches nothing and is silently dead. `
|
|
167
|
+
* What to do when an order-scoped hook (`derive`/`authenticate`/`beforeHandle`/`around`/
|
|
168
|
+
* `aroundCapability`/`afterHandle`/`onError`/`decorate`) covers no route because it was added after
|
|
169
|
+
* the last route was registered. `register()` snapshots the current chain into each route as it is
|
|
170
|
+
* declared, so a hook appended after every route reaches nothing and is silently dead. A `merge()`d
|
|
171
|
+
* route keeps the chain of the server that declared it, so a hook followed only by merges is dead
|
|
172
|
+
* too. The same report covers `responseContract()` and `effectLedger()` installed after routes they
|
|
173
|
+
* would have covered (a declared response, declared capabilities). `"warn"` (default) logs once at
|
|
160
174
|
* seal (first `listen()`/`fetch`/`resolveNode`); `"error"` throws a `FrameworkError`
|
|
161
175
|
* (`UNUSED_SCOPED_HOOKS`); `"off"` skips the bookkeeping entirely, including the one-frame stack
|
|
162
176
|
* capture. App-global hooks (`onRequest`/`onResponse`/...) are unaffected in every mode.
|
package/src/server/server.ts
CHANGED
|
@@ -23,6 +23,8 @@ import {
|
|
|
23
23
|
leavePlugin,
|
|
24
24
|
tagAdoption,
|
|
25
25
|
tagHook,
|
|
26
|
+
tagMergeAuthGap,
|
|
27
|
+
tagRouteSite,
|
|
26
28
|
tagScoped,
|
|
27
29
|
tagStatic,
|
|
28
30
|
} from "../internal/hook-origins.ts"
|
|
@@ -30,9 +32,10 @@ import {
|
|
|
30
32
|
type AssuranceDeclaration,
|
|
31
33
|
assuranceDeclarationsOf,
|
|
32
34
|
assuranceEvidenceFor,
|
|
35
|
+
NIFRA_ASSURANCE_IDS,
|
|
33
36
|
} from "../internal/route-assurance.ts"
|
|
34
37
|
import { type CatalogRoute, RouteCatalog } from "../internal/route-catalog.ts"
|
|
35
|
-
import { compileRouteOptions } from "../internal/route-compiler.ts"
|
|
38
|
+
import { adoptRouteRuntimes, compileRouteOptions } from "../internal/route-compiler.ts"
|
|
36
39
|
import {
|
|
37
40
|
compileRouteExecutionPlan,
|
|
38
41
|
type FusedBodyRunner,
|
|
@@ -67,19 +70,20 @@ import {
|
|
|
67
70
|
type RawBodyReaders,
|
|
68
71
|
UNLIMITED_BODY_BYTES,
|
|
69
72
|
} from "./body.ts"
|
|
73
|
+
import type { FormOrigins } from "./body-lane.ts"
|
|
70
74
|
import {
|
|
71
75
|
type ClientIpTrust,
|
|
72
76
|
NIFRA_PLATFORM_CLIENT_IP_DERIVED,
|
|
73
77
|
resolveClientIp,
|
|
74
78
|
} from "./client-ip.ts"
|
|
75
79
|
import type { Context, Platform, ResponseControls, RouteSchema } from "./context.ts"
|
|
76
|
-
import "./core-copies.ts"
|
|
80
|
+
import { mergeRefusal, recordCopyVersion } from "./core-copies.ts"
|
|
77
81
|
import {
|
|
78
82
|
hasLowercaseHeaderKeysMark,
|
|
79
83
|
headerKeysAllLowercase,
|
|
80
84
|
markLowercaseHeaderKeys,
|
|
81
85
|
} from "./header-case.ts"
|
|
82
|
-
import { headerObjectOf } from "./headers.ts"
|
|
86
|
+
import { declaredHeaders, headerObjectOf } from "./headers.ts"
|
|
83
87
|
import {
|
|
84
88
|
isRoutableMethod,
|
|
85
89
|
jsonError,
|
|
@@ -786,6 +790,7 @@ const RESPONSE_TIMEOUT = (): Response => jsonError(503, "request_timeout")
|
|
|
786
790
|
// node-compat `process`, so `typeof process` does NOT gate it out there, yet reading `process.env`
|
|
787
791
|
// throws `NotCapable` without `--allow-env`. The flag short-circuits before the env read on Deno (audit
|
|
788
792
|
// off) while a production define still folds `<flag> && false` to `false` on every other runtime.
|
|
793
|
+
// It must live in this module: imported from another, Bun leaves `<flag>,` at every folded guard.
|
|
789
794
|
const hookAuditRuntime = typeof (globalThis as { Deno?: unknown }).Deno === "undefined"
|
|
790
795
|
|
|
791
796
|
// This module's own path, captured once from a sentinel stack. Every internal frame of an
|
|
@@ -796,9 +801,10 @@ const SERVER_MODULE_PATH = /* @__PURE__ */ ((): string | undefined => {
|
|
|
796
801
|
return frame?.match(/\(?([^()\s]+):\d+:\d+\)?\s*$/)?.[1]
|
|
797
802
|
})()
|
|
798
803
|
|
|
799
|
-
/** One `file:line:col` frame for the caller of an order-scoped hook method
|
|
800
|
-
* report. Only ever called behind
|
|
801
|
-
* non-trivial cost) is never constructed
|
|
804
|
+
/** One `file:line:col` frame for the caller of an order-scoped hook method or a route registration,
|
|
805
|
+
* for the unused-hook report and route provenance. Only ever called behind a development guard that
|
|
806
|
+
* also requires the feature to be on, so the `Error` (the sole non-trivial cost) is never constructed
|
|
807
|
+
* otherwise. */
|
|
802
808
|
const captureCallerSite = (): string | undefined => {
|
|
803
809
|
const stack = new Error().stack
|
|
804
810
|
if (stack === undefined) return undefined
|
|
@@ -822,9 +828,19 @@ interface HookAuditState {
|
|
|
822
828
|
readonly hasCustomLogger: boolean
|
|
823
829
|
readonly sites: {
|
|
824
830
|
readonly kind: string
|
|
831
|
+
/** Routes that captured the chain so far: merged routes excluded, they keep their own. */
|
|
825
832
|
readonly routesAtPush: number
|
|
833
|
+
readonly mergedAtPush: number
|
|
826
834
|
readonly site: string | undefined
|
|
827
835
|
}[]
|
|
836
|
+
/** Registration-time plugins installed after routes they would have covered. */
|
|
837
|
+
readonly lateInstalls: {
|
|
838
|
+
readonly plugin: string
|
|
839
|
+
readonly site: string | undefined
|
|
840
|
+
readonly routes: readonly string[]
|
|
841
|
+
}[]
|
|
842
|
+
/** Routes adopted by `merge()`, which keep the chain of the server that declared them. */
|
|
843
|
+
merged: number
|
|
828
844
|
checked: boolean
|
|
829
845
|
}
|
|
830
846
|
const hookAudit = /* @__PURE__ */ new WeakMap<object, HookAuditState>()
|
|
@@ -859,7 +875,14 @@ const beginHookAudit = (
|
|
|
859
875
|
policy: "warn" | "error" | "off",
|
|
860
876
|
hasCustomLogger: boolean,
|
|
861
877
|
): void => {
|
|
862
|
-
hookAudit.set(key, {
|
|
878
|
+
hookAudit.set(key, {
|
|
879
|
+
policy,
|
|
880
|
+
hasCustomLogger,
|
|
881
|
+
sites: [],
|
|
882
|
+
lateInstalls: [],
|
|
883
|
+
merged: 0,
|
|
884
|
+
checked: false,
|
|
885
|
+
})
|
|
863
886
|
}
|
|
864
887
|
|
|
865
888
|
/** Record one order-scoped hook push: the route count at that moment (so `routesAtPush === total` at
|
|
@@ -876,7 +899,62 @@ const recordScopedHook = (key: object, routesAtPush: number, kind: string): void
|
|
|
876
899
|
}
|
|
877
900
|
const state = hookAudit.get(key)
|
|
878
901
|
if (state === undefined || state.policy === "off") return
|
|
879
|
-
state.sites.push({
|
|
902
|
+
state.sites.push({
|
|
903
|
+
kind,
|
|
904
|
+
routesAtPush: routesAtPush - state.merged,
|
|
905
|
+
mergedAtPush: state.merged,
|
|
906
|
+
site: captureCallerSite(),
|
|
907
|
+
})
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/** Count routes a `merge()` adopted: they never run this server's order-scoped hooks, so a hook
|
|
911
|
+
* followed only by merges covers nothing. */
|
|
912
|
+
const noteMergedRoutes = (key: object, count: number): void => {
|
|
913
|
+
const state = hookAudit.get(key)
|
|
914
|
+
if (state !== undefined) state.merged += count
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/** Record a `merge()` whose server authenticates none of its routes while this server's order-scoped
|
|
918
|
+
* auth - `authenticate()` stages, or `authenticated` evidence scoped to subsequent routes - would
|
|
919
|
+
* cover some of them. A server that authenticates any of its own routes chose its own auth, so its
|
|
920
|
+
* unauthenticated routes are taken as public on purpose. `nifra check` reports what this records. */
|
|
921
|
+
const noteMergeAuthGap = (
|
|
922
|
+
target: object,
|
|
923
|
+
authStages: readonly RawAuthStage[],
|
|
924
|
+
active: readonly AssuranceDeclaration[],
|
|
925
|
+
routes: readonly CatalogRoute[],
|
|
926
|
+
sourceGlobal: readonly AssuranceDeclaration[],
|
|
927
|
+
): void => {
|
|
928
|
+
const parentAuth = active.filter((item) => item.id === NIFRA_ASSURANCE_IDS.AUTHENTICATED)
|
|
929
|
+
if (authStages.length === 0 && parentAuth.length === 0) return
|
|
930
|
+
const authenticates = (route: CatalogRoute): boolean =>
|
|
931
|
+
route.assurance.some((item) => item.id === NIFRA_ASSURANCE_IDS.AUTHENTICATED) ||
|
|
932
|
+
assuranceEvidenceFor(sourceGlobal, route.method, route.path).some(
|
|
933
|
+
(item) => item.id === NIFRA_ASSURANCE_IDS.AUTHENTICATED,
|
|
934
|
+
)
|
|
935
|
+
if (routes.some(authenticates)) return
|
|
936
|
+
const covered = routes.filter(
|
|
937
|
+
(route) =>
|
|
938
|
+
authStages.length > 0 ||
|
|
939
|
+
assuranceEvidenceFor(parentAuth, route.method, route.path).length > 0,
|
|
940
|
+
)
|
|
941
|
+
if (covered.length === 0) return
|
|
942
|
+
tagMergeAuthGap(target, {
|
|
943
|
+
site: captureCallerSite(),
|
|
944
|
+
auth: [
|
|
945
|
+
...authStages.map((stage) => `authenticate(${stage.id})`),
|
|
946
|
+
...new Set(parentAuth.map((item) => item.source)),
|
|
947
|
+
],
|
|
948
|
+
routes: covered.map((route) => `${route.method} ${route.path}`),
|
|
949
|
+
})
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/** Record a registration-time plugin (`responseContract()`, `effectLedger()`) installed after routes
|
|
953
|
+
* it would have covered, named as `METHOD /path`. Reported with the dead hooks at seal. */
|
|
954
|
+
const recordLateInstall = (key: object, plugin: string, routes: readonly string[]): void => {
|
|
955
|
+
const state = hookAudit.get(key)
|
|
956
|
+
if (state === undefined || state.policy === "off" || routes.length === 0) return
|
|
957
|
+
state.lateInstalls.push({ plugin, site: captureCallerSite(), routes })
|
|
880
958
|
}
|
|
881
959
|
|
|
882
960
|
/** Hook provenance for an adoption (dev only, see `internal/hook-origins.ts`): which scope each
|
|
@@ -913,20 +991,46 @@ const sealHookAudit = (
|
|
|
913
991
|
if (state === undefined || state.checked) return
|
|
914
992
|
state.checked = true
|
|
915
993
|
if (state.policy === "off" || total + sockets === 0) return
|
|
994
|
+
const own = total - state.merged
|
|
916
995
|
const dead = state.sites.filter(
|
|
917
996
|
(hook) =>
|
|
918
997
|
hook.routesAtPush ===
|
|
919
998
|
(hook.kind === "around" || hook.kind === "aroundCapability" || hook.kind === "afterHandle"
|
|
920
|
-
?
|
|
921
|
-
:
|
|
999
|
+
? own
|
|
1000
|
+
: own + sockets),
|
|
922
1001
|
)
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
1002
|
+
const parts: string[] = []
|
|
1003
|
+
if (dead.length > 0) {
|
|
1004
|
+
const mergedAfter = (hook: (typeof dead)[number]): boolean => state.merged > hook.mergedAtPush
|
|
1005
|
+
const detail = dead
|
|
1006
|
+
.map(
|
|
1007
|
+
(hook) =>
|
|
1008
|
+
` ${hook.kind}() at ${hook.site ?? "<unknown>"}${mergedAfter(hook) ? " (only merge()d routes follow it)" : ""}`,
|
|
1009
|
+
)
|
|
1010
|
+
.join("\n")
|
|
1011
|
+
parts.push(
|
|
1012
|
+
`[nifra] ${dead.length} order-scoped hook(s) apply to no route - they were added after the ` +
|
|
1013
|
+
`last route was registered, and register() snapshots the chain into each route as it is ` +
|
|
1014
|
+
`declared:\n${detail}\nMove them before the routes they should cover. App-global hooks ` +
|
|
1015
|
+
`(onRequest/onResponse) are not order-scoped and can be added at any time.` +
|
|
1016
|
+
(dead.some(mergedAfter)
|
|
1017
|
+
? " A merge()d route keeps the hooks of the server that declared it and never runs this " +
|
|
1018
|
+
"server's: wire the hook on the merged server, or declare those routes in a group()."
|
|
1019
|
+
: ""),
|
|
1020
|
+
)
|
|
1021
|
+
}
|
|
1022
|
+
for (const late of state.lateInstalls) {
|
|
1023
|
+
const shown = late.routes.slice(0, 5).join(", ")
|
|
1024
|
+
const more = late.routes.length > 5 ? ` and ${late.routes.length - 5} more` : ""
|
|
1025
|
+
parts.push(
|
|
1026
|
+
`[nifra] ${late.plugin}() at ${late.site ?? "<unknown>"} was installed after ` +
|
|
1027
|
+
`${late.routes.length} route(s) it would have covered: ${shown}${more}. It is decided per ` +
|
|
1028
|
+
`route at registration and merge(), so those routes run without it. Install it before the ` +
|
|
1029
|
+
`routes and merge() calls it should cover.`,
|
|
1030
|
+
)
|
|
1031
|
+
}
|
|
1032
|
+
if (parts.length === 0) return
|
|
1033
|
+
const message = parts.join("\n")
|
|
930
1034
|
if (state.policy === "error") throw new FrameworkError("UNUSED_SCOPED_HOOKS", message)
|
|
931
1035
|
if (state.hasCustomLogger) logger.warn(message)
|
|
932
1036
|
else console.warn(message)
|
|
@@ -982,6 +1086,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
982
1086
|
| undefined
|
|
983
1087
|
private readonly maxBodyBytes: number
|
|
984
1088
|
private readonly protoPoisoning: "reject" | "strip" | "ignore"
|
|
1089
|
+
private readonly formOrigins: FormOrigins | undefined
|
|
985
1090
|
/** `trustBodyFraming`: mark every `app.fetch` request as runtime-framed (see ServerOptions). */
|
|
986
1091
|
private readonly trustBodyFraming: boolean
|
|
987
1092
|
private readonly wsMaxPayloadBytes: number
|
|
@@ -1117,6 +1222,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1117
1222
|
const maxBodyBytes = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES
|
|
1118
1223
|
assertByteLimit(maxBodyBytes, "maxBodyBytes")
|
|
1119
1224
|
this.protoPoisoning = options.protoPoisoning ?? "reject"
|
|
1225
|
+
this.formOrigins = options.formOrigins
|
|
1120
1226
|
this.trustBodyFraming = options.trustBodyFraming ?? false
|
|
1121
1227
|
const wsMaxPayloadBytes = options.wsMaxPayloadBytes ?? maxBodyBytes
|
|
1122
1228
|
assertByteLimit(wsMaxPayloadBytes, "wsMaxPayloadBytes")
|
|
@@ -1174,6 +1280,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1174
1280
|
// for the bundler to eliminate; a non-bundled production run skips it the same way.
|
|
1175
1281
|
if (hookAuditRuntime && process.env.NODE_ENV !== "production") {
|
|
1176
1282
|
beginHookAudit(this, options.unusedScopedHooks ?? "warn", options.logger !== undefined)
|
|
1283
|
+
recordCopyVersion()
|
|
1177
1284
|
}
|
|
1178
1285
|
this.derives = []
|
|
1179
1286
|
this.authStages = []
|
|
@@ -1229,6 +1336,10 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1229
1336
|
* Mount a legacy fetch handler under a literal path prefix. Mounted handlers sit behind typed
|
|
1230
1337
|
* routes, so routes can move one at a time while the remaining legacy surface stays live.
|
|
1231
1338
|
* Mounted responses are outside Nifra's typed route and response-contract checks.
|
|
1339
|
+
*
|
|
1340
|
+
* It carries HTTP only: a WebSocket upgrade under the prefix reaches the handler as an ordinary
|
|
1341
|
+
* request and never becomes a socket. Serve that path's sockets with `app.ws()` routes or a
|
|
1342
|
+
* mounted child app, or keep the legacy server running beside this one for them.
|
|
1232
1343
|
*/
|
|
1233
1344
|
mountFetch(
|
|
1234
1345
|
path: string,
|
|
@@ -1366,6 +1477,11 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1366
1477
|
* Unlike `derive`, this stage receives a body-blind authentication capsule and is compiled before
|
|
1367
1478
|
* route validation. Keeping it separate prevents an unrelated derive from accidentally becoming a
|
|
1368
1479
|
* security gate and lets the route compiler retain a protected fast lane.
|
|
1480
|
+
*
|
|
1481
|
+
* A stage that throws, or returns anything but `authenticated(principal)` or `rejected(...)`, fails
|
|
1482
|
+
* closed with 503 `{ ok: false, error: "auth_unavailable" }`. The cause goes where a handler's error
|
|
1483
|
+
* goes: to `onError`, whose response replaces the 503, and when no hook answers, to the logger. A
|
|
1484
|
+
* thrown `Response` or `status(...)` is sent as thrown, as it is from a handler.
|
|
1369
1485
|
*/
|
|
1370
1486
|
authenticate<Principal>(
|
|
1371
1487
|
stage: AuthenticationStage<Principal, EnvOf<Ctx>>,
|
|
@@ -1384,8 +1500,10 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1384
1500
|
throw new TypeError('authenticate() stage mode must be "sync" or "async"')
|
|
1385
1501
|
}
|
|
1386
1502
|
this.authStages.push(stage as unknown as RawAuthStage)
|
|
1387
|
-
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
1503
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production") {
|
|
1504
|
+
recordScopedHook(this, this.catalog.size + this.wsRoutes.length, "authenticate")
|
|
1388
1505
|
tagHook(stage, "authenticate", this)
|
|
1506
|
+
}
|
|
1389
1507
|
return this as unknown as Server<R, Ctx & { readonly principal: Principal }, HookOutput>
|
|
1390
1508
|
}
|
|
1391
1509
|
|
|
@@ -1502,9 +1620,10 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1502
1620
|
return this as unknown as Server<R, Ctx, HookOutput | HookOutputOf<ReturnType<H>>>
|
|
1503
1621
|
}
|
|
1504
1622
|
|
|
1505
|
-
/** Handle a thrown error; a non-`undefined` return becomes the response (else the default 500
|
|
1506
|
-
* Order-scoped: captured by routes
|
|
1507
|
-
* route reaches nothing (see
|
|
1623
|
+
/** Handle a thrown error; a non-`undefined` return becomes the response (else the default 500, or 503
|
|
1624
|
+
* `auth_unavailable` for a failed {@link authenticate} stage). Order-scoped: captured by routes
|
|
1625
|
+
* declared *after* this call, none before; one added after the last route reaches nothing (see
|
|
1626
|
+
* {@link ServerOptions.unusedScopedHooks}). */
|
|
1508
1627
|
onError<H extends (error: unknown, context: Context & Ctx) => MaybePromise<unknown>>(
|
|
1509
1628
|
fn: H,
|
|
1510
1629
|
): Server<R, Ctx, HookOutput | HookOutputOf<ReturnType<H>>> {
|
|
@@ -2258,6 +2377,16 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2258
2377
|
schema: RouteSchema | undefined,
|
|
2259
2378
|
handler: (context: never) => unknown,
|
|
2260
2379
|
): CatalogRoute {
|
|
2380
|
+
// Route provenance for the CLI's capability walk, which sets NIFRA_ROUTE_SITES while it loads the
|
|
2381
|
+
// app: the module that registered the route, read where a static scan cannot see the path.
|
|
2382
|
+
if (
|
|
2383
|
+
hookAuditRuntime &&
|
|
2384
|
+
process.env.NODE_ENV !== "production" &&
|
|
2385
|
+
process.env.NIFRA_ROUTE_SITES === "1"
|
|
2386
|
+
) {
|
|
2387
|
+
const site = captureCallerSite()
|
|
2388
|
+
if (site !== undefined) tagRouteSite(this, `${method.toUpperCase()} ${path}`, site)
|
|
2389
|
+
}
|
|
2261
2390
|
const compiled = compileRouteOptions(
|
|
2262
2391
|
{
|
|
2263
2392
|
maxBodyBytes: this.maxBodyBytes,
|
|
@@ -2458,6 +2587,19 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2458
2587
|
[INSTALL_RESPONSE_CONTRACT](runtime: ResponseContractRuntime): void {
|
|
2459
2588
|
this.assertConfigurable("responseContract()")
|
|
2460
2589
|
this.responseContractRuntime = runtime
|
|
2590
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
2591
|
+
recordLateInstall(
|
|
2592
|
+
this,
|
|
2593
|
+
"responseContract",
|
|
2594
|
+
this.catalog
|
|
2595
|
+
.entries()
|
|
2596
|
+
.filter(
|
|
2597
|
+
({ entry }) =>
|
|
2598
|
+
entry.responseContract === undefined &&
|
|
2599
|
+
(entry.schema?.response !== undefined || entry.schema?.errors !== undefined),
|
|
2600
|
+
)
|
|
2601
|
+
.map((route) => `${route.method} ${route.path}`),
|
|
2602
|
+
)
|
|
2461
2603
|
}
|
|
2462
2604
|
|
|
2463
2605
|
/** @internal Symbol-keyed install seam for the response observer plugin. */
|
|
@@ -2586,6 +2728,31 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2586
2728
|
* registered before a `derive`). Fail closed: a path+method collision throws
|
|
2587
2729
|
* `RouteConfigError` at merge time, and a group with WebSocket routes or mounts is refused
|
|
2588
2730
|
* (register those on the parent).
|
|
2731
|
+
*
|
|
2732
|
+
* What this server's `.use()`, made BEFORE the merge, covers on a merged route versus a
|
|
2733
|
+
* `group()` route:
|
|
2734
|
+
*
|
|
2735
|
+
* | installed on this server | group() route | merge()d route |
|
|
2736
|
+
* | ------------------------------------------ | ------------- | ------------------------------ |
|
|
2737
|
+
* | derive/decorate/beforeHandle/afterHandle, | yes | no - wire it on the merged |
|
|
2738
|
+
* | onError/around, authenticate, route-scoped | | server |
|
|
2739
|
+
* | assurance | | |
|
|
2740
|
+
* | onRequest/onResponse/responseHeaders hooks | yes | yes (app-wide) |
|
|
2741
|
+
* | responseContract() | yes | yes, when the route declares a |
|
|
2742
|
+
* | | | response and its own server |
|
|
2743
|
+
* | | | installed no contract |
|
|
2744
|
+
* | effectLedger() | yes | yes, when the route declares |
|
|
2745
|
+
* | | | capabilities and has no ledger |
|
|
2746
|
+
* | idempotency() | yes | the merged server's own; a |
|
|
2747
|
+
* | | | route declaring it without one |
|
|
2748
|
+
* | | | is refused at registration |
|
|
2749
|
+
*
|
|
2750
|
+
* The response contract and the ledger can only add a check or a record, so a merged route is
|
|
2751
|
+
* recompiled under them. A runtime installed AFTER the merge covers neither kind of route.
|
|
2752
|
+
*
|
|
2753
|
+
* In development, {@link ServerOptions.unusedScopedHooks} reports a hook followed only by merges and
|
|
2754
|
+
* a contract or ledger installed after the routes it would cover; `nifra check` reports a merge whose
|
|
2755
|
+
* server authenticates none of its routes under this server's `authenticate()` (NF-C043).
|
|
2589
2756
|
*/
|
|
2590
2757
|
merge<R2 extends Registry, Ctx2, HookOutput2>(
|
|
2591
2758
|
other: Server<R2, Ctx2, HookOutput2>,
|
|
@@ -2594,7 +2761,11 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2594
2761
|
if (!(other instanceof Server)) {
|
|
2595
2762
|
// A server from another copy of this package reaches this copy's private request state through
|
|
2596
2763
|
// symbols it does not share, and would fail per request. The boot warning names the copies.
|
|
2597
|
-
throw new TypeError(
|
|
2764
|
+
throw new TypeError(
|
|
2765
|
+
hookAuditRuntime && process.env.NODE_ENV !== "production"
|
|
2766
|
+
? mergeRefusal()
|
|
2767
|
+
: "merge() requires a server() from this copy of @nifrajs/core",
|
|
2768
|
+
)
|
|
2598
2769
|
}
|
|
2599
2770
|
if (this.routePrefix !== "") {
|
|
2600
2771
|
// The merged routes were compiled under their own paths; adopting them here would publish them
|
|
@@ -2611,7 +2782,17 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2611
2782
|
"merge() does not carry WebSocket routes or mounts - register them on the parent server",
|
|
2612
2783
|
)
|
|
2613
2784
|
}
|
|
2785
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
2786
|
+
noteMergeAuthGap(
|
|
2787
|
+
this,
|
|
2788
|
+
this.authStages,
|
|
2789
|
+
this.activeAssurance,
|
|
2790
|
+
source.catalog.entries(),
|
|
2791
|
+
source.globalAssurance,
|
|
2792
|
+
)
|
|
2614
2793
|
this.adopt(source, undefined, 0)
|
|
2794
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
2795
|
+
noteMergedRoutes(this, source.catalog.size)
|
|
2615
2796
|
return this as unknown as Server<R & R2, Ctx, HookOutput>
|
|
2616
2797
|
}
|
|
2617
2798
|
|
|
@@ -2630,7 +2811,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2630
2811
|
* The builder receives a scope that INHERITS this server's route chain as it stands at the call
|
|
2631
2812
|
* (`derive`/`decorate`/`authenticate`/`beforeHandle`/`afterHandle`/`onError`/`around`, assurance,
|
|
2632
2813
|
* capability interceptors, installed runtimes, applied plugins), exactly as a route declared here
|
|
2633
|
-
* would. A plugin this server already applied is in effect for the group: the group's `use()` of
|
|
2814
|
+
* would. {@link merge} documents how that differs for a merged server. A plugin this server already applied is in effect for the group: the group's `use()` of
|
|
2634
2815
|
* that same instance does nothing, and its `use()` of another instance under the same name throws a
|
|
2635
2816
|
* `RouteConfigError` (`PLUGIN_RECONFIGURED`), because that instance could never replace the parent's.
|
|
2636
2817
|
* A plugin the group applies still shares the parent's copy of a dependency it `use()`s itself.
|
|
@@ -2738,7 +2919,13 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2738
2919
|
})
|
|
2739
2920
|
: (route: CatalogRoute): CatalogRoute => route
|
|
2740
2921
|
this.catalog.addBatch(
|
|
2741
|
-
sourceRoutes.map((route) =>
|
|
2922
|
+
sourceRoutes.map((route) =>
|
|
2923
|
+
this.bindFusedRuntime(
|
|
2924
|
+
foldedAssurance(
|
|
2925
|
+
adoptRouteRuntimes(route, this.responseContractRuntime, this.effectLedgerRuntime),
|
|
2926
|
+
),
|
|
2927
|
+
),
|
|
2928
|
+
),
|
|
2742
2929
|
)
|
|
2743
2930
|
// Resolved idempotency/ledger route entries carry their own store/sink configuration, while the
|
|
2744
2931
|
// runtime object supplies the generic execution machinery. If the parent already has a runtime,
|
|
@@ -3547,6 +3734,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3547
3734
|
pubsub,
|
|
3548
3735
|
attach: runtime.attach,
|
|
3549
3736
|
maxPayloadBytes: this.wsMaxPayloadBytes,
|
|
3737
|
+
sender: runtime.sender,
|
|
3550
3738
|
}
|
|
3551
3739
|
}
|
|
3552
3740
|
const wrap = (response: Response | ResponseResult): WebSocketUpgradeOutcome =>
|
|
@@ -3659,9 +3847,8 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3659
3847
|
if (o.kind === "pass")
|
|
3660
3848
|
return this.fetch(req, bunPeerPlatform(server, req) as Platform<EnvOf<Ctx>>)
|
|
3661
3849
|
if (o.kind === "reject") return o.response
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
})
|
|
3850
|
+
// A copy: the Bun socket keeps per-connection state on it, and a hand-built outcome may be shared.
|
|
3851
|
+
return server.upgrade(req, { data: { ...o } })
|
|
3665
3852
|
? undefined
|
|
3666
3853
|
: jsonError(426, "upgrade_required")
|
|
3667
3854
|
}
|
|
@@ -4288,6 +4475,18 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4288
4475
|
[INSTALL_EFFECT_LEDGER](runtime: EffectLedgerRuntime): void {
|
|
4289
4476
|
this.assertConfigurable("effectLedger()")
|
|
4290
4477
|
this.effectLedgerRuntime = runtime
|
|
4478
|
+
if (hookAuditRuntime && process.env.NODE_ENV !== "production")
|
|
4479
|
+
recordLateInstall(
|
|
4480
|
+
this,
|
|
4481
|
+
"effectLedger",
|
|
4482
|
+
this.catalog
|
|
4483
|
+
.entries()
|
|
4484
|
+
.filter(
|
|
4485
|
+
({ entry, descriptor }) =>
|
|
4486
|
+
entry.ledgered === undefined && (descriptor.capabilities?.length ?? 0) > 0,
|
|
4487
|
+
)
|
|
4488
|
+
.map((route) => `${route.method} ${route.path}`),
|
|
4489
|
+
)
|
|
4291
4490
|
}
|
|
4292
4491
|
|
|
4293
4492
|
/** The narrowest bare route: a syntactic `() => ...` handler cannot observe the context argument, so
|
|
@@ -4689,6 +4888,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4689
4888
|
wrapResponse,
|
|
4690
4889
|
(err) => logError(err, ctx, finalize, wrapResponse),
|
|
4691
4890
|
bodySchema,
|
|
4891
|
+
this.formOrigins,
|
|
4692
4892
|
)
|
|
4693
4893
|
}
|
|
4694
4894
|
}
|
|
@@ -5103,6 +5303,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5103
5303
|
(response) => this.wrapWebResponse(response),
|
|
5104
5304
|
(err) => Promise.resolve(logError(err, ctx)),
|
|
5105
5305
|
bodySchema,
|
|
5306
|
+
this.formOrigins,
|
|
5106
5307
|
) as MaybePromise<Response>
|
|
5107
5308
|
}
|
|
5108
5309
|
}
|
|
@@ -5534,6 +5735,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5534
5735
|
wrapResponse,
|
|
5535
5736
|
(err) => logError(err, ctx, finalize, wrapResponse),
|
|
5536
5737
|
bodySchema,
|
|
5738
|
+
this.formOrigins,
|
|
5537
5739
|
)
|
|
5538
5740
|
}
|
|
5539
5741
|
}
|
|
@@ -5607,7 +5809,9 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5607
5809
|
if (stage.kind === "body") return this.readProgramBody(entry, source, ctx)
|
|
5608
5810
|
const input =
|
|
5609
5811
|
stage.kind === "headers"
|
|
5610
|
-
?
|
|
5812
|
+
? stage.names === undefined
|
|
5813
|
+
? headerObjectOf(source.headers)
|
|
5814
|
+
: declaredHeaders(source.headers, stage.names)
|
|
5611
5815
|
: stage.kind === "query"
|
|
5612
5816
|
? queryObjectOf(ctx[CONTEXT_SEARCH])
|
|
5613
5817
|
: ctx[stage.kind]
|
|
@@ -5636,8 +5840,9 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5636
5840
|
ctx: RawContext,
|
|
5637
5841
|
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
5638
5842
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
5843
|
+
fallback?: () => T,
|
|
5639
5844
|
): MaybePromise<T> {
|
|
5640
|
-
return this.handleLifecycleError(entry, error, ctx, finalize, wrapResponse)
|
|
5845
|
+
return this.handleLifecycleError(entry, error, ctx, finalize, wrapResponse, fallback)
|
|
5641
5846
|
}
|
|
5642
5847
|
|
|
5643
5848
|
/** Finish a successful program result through response-contract enforcement and finalization. */
|
|
@@ -6107,6 +6312,8 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6107
6312
|
ctx: RawContext,
|
|
6108
6313
|
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
6109
6314
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
6315
|
+
// The answer when no hook does; the default 500 unless a stage owns a different fail-closed status.
|
|
6316
|
+
fallback?: () => T,
|
|
6110
6317
|
): MaybePromise<T> {
|
|
6111
6318
|
// A *thrown* Response is deliberate control flow, not an error - a guard throws a redirect/401,
|
|
6112
6319
|
// an action throws an error page. Send it as the same Response returned would be sent (Remix/
|
|
@@ -6122,9 +6329,9 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6122
6329
|
// Never crash the server or leak internals. The client gets a flat 500; the detail goes to the
|
|
6123
6330
|
// (redacting) logger. Body-read failures and around-hook failures land here too.
|
|
6124
6331
|
this.logRequestError(err, ctx)
|
|
6125
|
-
return wrapResponse(plainError(500, "internal_error"))
|
|
6332
|
+
return fallback === undefined ? wrapResponse(plainError(500, "internal_error")) : fallback()
|
|
6126
6333
|
}
|
|
6127
|
-
return this.runErrorHooks(entry, err, ctx, finalize, wrapResponse)
|
|
6334
|
+
return this.runErrorHooks(entry, err, ctx, finalize, wrapResponse, fallback)
|
|
6128
6335
|
}
|
|
6129
6336
|
|
|
6130
6337
|
/** The `onError` chain, split off {@link handleLifecycleError} so its `await` costs only the routes
|
|
@@ -6135,6 +6342,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6135
6342
|
ctx: RawContext,
|
|
6136
6343
|
finalize: (result: unknown, set: CtxSet, ctx?: RawContext) => T,
|
|
6137
6344
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
6345
|
+
fallback?: () => T,
|
|
6138
6346
|
): Promise<T> {
|
|
6139
6347
|
// Indexed for the same reason as the `beforeHandle` chain above: the iterator would outlive
|
|
6140
6348
|
// the `await` and stay heap-allocated on both engines.
|
|
@@ -6155,7 +6363,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6155
6363
|
}
|
|
6156
6364
|
}
|
|
6157
6365
|
this.logRequestError(err, ctx)
|
|
6158
|
-
return wrapResponse(plainError(500, "internal_error"))
|
|
6366
|
+
return fallback === undefined ? wrapResponse(plainError(500, "internal_error")) : fallback()
|
|
6159
6367
|
}
|
|
6160
6368
|
|
|
6161
6369
|
/** Log an unhandled request error to the (redacting) logger - shared by the compiled route program and the
|
|
@@ -6189,6 +6397,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6189
6397
|
(response) => response,
|
|
6190
6398
|
(error) => Promise.reject(error),
|
|
6191
6399
|
bodySchema,
|
|
6400
|
+
this.formOrigins,
|
|
6192
6401
|
)
|
|
6193
6402
|
}
|
|
6194
6403
|
|
|
@@ -6554,7 +6763,11 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6554
6763
|
// the parent.
|
|
6555
6764
|
const nativePubsub =
|
|
6556
6765
|
this.wsRoutes.length > 0 && !this.mountsTakeUpgrades() && !this.wsHasValidatedSend
|
|
6557
|
-
const wsHandlers = wsRuntime?.bunHandlers(
|
|
6766
|
+
const wsHandlers = wsRuntime?.bunHandlers(
|
|
6767
|
+
this.topics ?? wsRuntime.createTopics(),
|
|
6768
|
+
nativePubsub,
|
|
6769
|
+
this,
|
|
6770
|
+
)
|
|
6558
6771
|
// Spread rather than pass `hostname: undefined` (or an undefined idle timeout or TLS) - Bun treats
|
|
6559
6772
|
// an explicit undefined as a value on some option paths, and omitting is what selects its own
|
|
6560
6773
|
// defaults, 0.0.0.0 among them.
|
|
@@ -6602,8 +6815,9 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6602
6815
|
// `unknown` params bridge a TS structural-variance quirk. Round-trip covered by websocket.test.ts.
|
|
6603
6816
|
websocket: {
|
|
6604
6817
|
// Cap inbound frames so a huge message can't be buffered/JSON-parsed into memory; the runtime
|
|
6605
|
-
// closes an over-cap connection before the handler runs. Default 1 MB (maxBodyBytes).
|
|
6606
|
-
|
|
6818
|
+
// closes an over-cap connection before the handler runs. Default 1 MB (maxBodyBytes). One
|
|
6819
|
+
// limit per listener: the largest cap among this app and the apps mounted on it.
|
|
6820
|
+
maxPayloadLength: wsHandlers.maxPayloadLength,
|
|
6607
6821
|
open: (ws) => wsHandlers.open(ws),
|
|
6608
6822
|
message: (ws, message) => wsHandlers.message(ws, message),
|
|
6609
6823
|
close: (ws, code, reason) => wsHandlers.close(ws, code, reason),
|