@nifrajs/core 5.11.0 → 5.12.1
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 +19 -0
- package/dist/edge-kit.d.ts +1 -1
- package/dist/edge-kit.d.ts.map +1 -1
- package/dist/edge-kit.js +1 -1
- package/dist/edge-kit.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/capability-runtime.d.ts.map +1 -1
- package/dist/internal/capability-runtime.js +3 -2
- package/dist/internal/capability-runtime.js.map +1 -1
- package/dist/internal/route-execution.d.ts +6 -4
- package/dist/internal/route-execution.d.ts.map +1 -1
- package/dist/internal/route-execution.js.map +1 -1
- package/dist/internal/route-lanes.d.ts +4 -4
- package/dist/internal/route-lanes.d.ts.map +1 -1
- package/dist/internal/route-lanes.js +37 -37
- package/dist/internal/route-lanes.js.map +1 -1
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +2 -1
- package/dist/manifest.js.map +1 -1
- package/dist/server/body.d.ts +2 -0
- package/dist/server/body.d.ts.map +1 -1
- package/dist/server/body.js +2 -0
- package/dist/server/body.js.map +1 -1
- package/dist/server/core-copies.js +1 -1
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js +8 -4
- package/dist/server/request-context.js.map +1 -1
- package/dist/server/respond.d.ts.map +1 -1
- package/dist/server/respond.js +10 -0
- package/dist/server/respond.js.map +1 -1
- package/dist/server/server-types.d.ts +7 -1
- package/dist/server/server-types.d.ts.map +1 -1
- package/dist/server/server.d.ts +16 -24
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +285 -140
- package/dist/server/server.js.map +1 -1
- package/dist/server/static-response.d.ts +13 -0
- package/dist/server/static-response.d.ts.map +1 -0
- package/dist/server/static-response.js +28 -0
- package/dist/server/static-response.js.map +1 -0
- package/dist/server/websocket.d.ts +23 -7
- package/dist/server/websocket.d.ts.map +1 -1
- package/dist/server/websocket.js +8 -3
- package/dist/server/websocket.js.map +1 -1
- package/dist/server/ws-adapter-view.d.ts +3 -3
- package/dist/server/ws-adapter-view.d.ts.map +1 -1
- package/dist/server/ws-bun.d.ts +4 -4
- package/dist/server/ws-bun.d.ts.map +1 -1
- package/dist/server/ws-bun.js +12 -5
- package/dist/server/ws-bun.js.map +1 -1
- package/dist/server/ws-hook.d.ts +4 -4
- package/dist/server/ws-hook.d.ts.map +1 -1
- package/dist/server.d.ts +2 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1 -0
- package/dist/server.js.map +1 -1
- package/dist/tool-contract.d.ts +5 -0
- package/dist/tool-contract.d.ts.map +1 -1
- package/dist/tool-contract.js +6 -3
- package/dist/tool-contract.js.map +1 -1
- package/dist/ws.d.ts +1 -1
- package/dist/ws.d.ts.map +1 -1
- package/dist/ws.js.map +1 -1
- package/package.json +1 -1
- package/src/edge-kit.ts +1 -1
- package/src/index.ts +3 -1
- package/src/internal/capability-runtime.ts +3 -2
- package/src/internal/route-execution.ts +6 -4
- package/src/internal/route-lanes.ts +44 -44
- package/src/manifest.ts +2 -1
- package/src/server/body.ts +3 -0
- package/src/server/core-copies.ts +1 -1
- package/src/server/request-context.ts +12 -8
- package/src/server/respond.ts +11 -0
- package/src/server/server-types.ts +7 -1
- package/src/server/server.ts +414 -189
- package/src/server/static-response.ts +36 -0
- package/src/server/websocket.ts +29 -8
- package/src/server/ws-adapter-view.ts +3 -3
- package/src/server/ws-bun.ts +15 -9
- package/src/server/ws-hook.ts +4 -3
- package/src/server.ts +2 -0
- package/src/tool-contract.ts +13 -3
- package/src/ws.ts +1 -0
package/src/server/server.ts
CHANGED
|
@@ -81,6 +81,7 @@ import { LAST_BEFORE_HANDLE } from "./before-handler.ts"
|
|
|
81
81
|
import {
|
|
82
82
|
assertByteLimit,
|
|
83
83
|
clientBodyErrorCode,
|
|
84
|
+
DEFAULT_MAX_BODY_BYTES,
|
|
84
85
|
hasTrustedBodyFraming,
|
|
85
86
|
markTransportCap,
|
|
86
87
|
markTrustedBodyFraming,
|
|
@@ -100,7 +101,7 @@ import {
|
|
|
100
101
|
headerKeysAllLowercase,
|
|
101
102
|
markLowercaseHeaderKeys,
|
|
102
103
|
} from "./header-case.ts"
|
|
103
|
-
import { declaredHeaders, headerObjectOf } from "./headers.ts"
|
|
104
|
+
import { declaredHeaderNames, declaredHeaders, headerObjectOf } from "./headers.ts"
|
|
104
105
|
import {
|
|
105
106
|
isRoutableMethod,
|
|
106
107
|
jsonError,
|
|
@@ -167,6 +168,7 @@ import {
|
|
|
167
168
|
type StatusResponse,
|
|
168
169
|
} from "./runtime-core.ts"
|
|
169
170
|
import { normalizeStaticResponseHeaders, type StaticResponseHeaders } from "./static-headers.ts"
|
|
171
|
+
import { createStaticResponse, staticResponseDefinition } from "./static-response.ts"
|
|
170
172
|
import { withTextSlots } from "./text-slots.ts"
|
|
171
173
|
import { plainValidationError } from "./validation.ts"
|
|
172
174
|
|
|
@@ -249,7 +251,7 @@ import type {
|
|
|
249
251
|
} from "./server-types.ts"
|
|
250
252
|
import type { SSEInit, TypedSSEStream } from "./sse.ts"
|
|
251
253
|
import type { SseRuntime } from "./sse-hook.ts"
|
|
252
|
-
import type {
|
|
254
|
+
import type { TopicAdapter, WebSocketHandler, WebSocketUpgradeOutcome } from "./websocket.ts"
|
|
253
255
|
import type { BunWsData } from "./ws-bun.ts"
|
|
254
256
|
import type { WsRuntime } from "./ws-hook.ts"
|
|
255
257
|
|
|
@@ -452,7 +454,8 @@ class BunPeerPlatform implements PeerPlatform {
|
|
|
452
454
|
}
|
|
453
455
|
|
|
454
456
|
type BunNativeHandler = (request: Request, server: BunPeerServer) => MaybePromise<Response>
|
|
455
|
-
type
|
|
457
|
+
type BunNativeRouteValue = BunNativeHandler | Response
|
|
458
|
+
type BunNativeMethodTable = Partial<Record<Method, BunNativeRouteValue>>
|
|
456
459
|
// Bun's route table has a slot for each of these; a QUERY entry is ignored and the request falls
|
|
457
460
|
// through to the table's fetch, so QUERY routes stay on the portable dispatcher.
|
|
458
461
|
const BUN_TABLE_METHODS: readonly Method[] = METHODS.filter((method) => method !== "QUERY")
|
|
@@ -648,7 +651,6 @@ export {
|
|
|
648
651
|
} from "./plugin.ts"
|
|
649
652
|
export type { IdentityPlugin }
|
|
650
653
|
|
|
651
|
-
const DEFAULT_MAX_BODY_BYTES = 1_000_000
|
|
652
654
|
const BODY_RESPONSE = (response: Response | ResponseResult): Response | ResponseResult => response
|
|
653
655
|
const BODY_ERROR = (error: unknown): Promise<never> => Promise.reject(error)
|
|
654
656
|
const DEFAULT_DRAIN_MS = 10_000
|
|
@@ -886,6 +888,98 @@ interface FusedHooks {
|
|
|
886
888
|
readonly decorations: Record<PropertyKey, unknown> | undefined
|
|
887
889
|
}
|
|
888
890
|
|
|
891
|
+
interface FusedValidationStage {
|
|
892
|
+
readonly schema: StandardSchemaV1
|
|
893
|
+
readonly kind: "headers" | "cookies" | "params" | "query"
|
|
894
|
+
readonly headerNames?: readonly string[] | undefined
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
type FusedValidationFields = {
|
|
898
|
+
headers: unknown
|
|
899
|
+
cookies: unknown
|
|
900
|
+
params: unknown
|
|
901
|
+
query: unknown
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/** Compile the non-body validation stages in the same order as the route program. The selected
|
|
905
|
+
* fused lane runs this only for routes whose recovery behavior is the ordinary 422 response. */
|
|
906
|
+
function compileFusedValidationStages(
|
|
907
|
+
schema: RouteSchema | undefined,
|
|
908
|
+
): readonly FusedValidationStage[] {
|
|
909
|
+
if (schema === undefined) return []
|
|
910
|
+
const stages: FusedValidationStage[] = []
|
|
911
|
+
if (schema.headers !== undefined) {
|
|
912
|
+
stages.push({
|
|
913
|
+
schema: schema.headers,
|
|
914
|
+
kind: "headers",
|
|
915
|
+
headerNames: declaredHeaderNames(schema.headers),
|
|
916
|
+
})
|
|
917
|
+
}
|
|
918
|
+
if (schema.cookies !== undefined) {
|
|
919
|
+
stages.push({ schema: schema.cookies, kind: "cookies" })
|
|
920
|
+
}
|
|
921
|
+
if (schema.params !== undefined) {
|
|
922
|
+
stages.push({ schema: schema.params, kind: "params" })
|
|
923
|
+
}
|
|
924
|
+
if (schema.query !== undefined) {
|
|
925
|
+
stages.push({ schema: schema.query, kind: "query" })
|
|
926
|
+
}
|
|
927
|
+
return stages
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
/** Run a registration-compiled input sequence without allocating a lifecycle-program continuation
|
|
931
|
+
* for each synchronous schema. Only an asynchronous validator creates a Promise continuation. */
|
|
932
|
+
function runFusedValidationStages<T>(
|
|
933
|
+
stages: readonly FusedValidationStage[],
|
|
934
|
+
source: RequestSource,
|
|
935
|
+
ctx: RawContext,
|
|
936
|
+
wrapResponse: Wrap<T>,
|
|
937
|
+
onError: (error: unknown) => MaybePromise<T>,
|
|
938
|
+
next: () => MaybePromise<T>,
|
|
939
|
+
startIndex = 0,
|
|
940
|
+
): MaybePromise<T> {
|
|
941
|
+
if (stages.length === 0) return next()
|
|
942
|
+
const validationContext: FusedValidationFields = ctx
|
|
943
|
+
for (let index = startIndex; index < stages.length; index++) {
|
|
944
|
+
const stage = stages[index]!
|
|
945
|
+
let validation: StandardResult<unknown> | Promise<StandardResult<unknown>>
|
|
946
|
+
try {
|
|
947
|
+
const input =
|
|
948
|
+
stage.kind === "headers"
|
|
949
|
+
? stage.headerNames === undefined
|
|
950
|
+
? headerObjectOf(source.headers)
|
|
951
|
+
: declaredHeaders(source.headers, stage.headerNames)
|
|
952
|
+
: stage.kind === "cookies"
|
|
953
|
+
? ctx.cookies
|
|
954
|
+
: stage.kind === "params"
|
|
955
|
+
? ctx.params
|
|
956
|
+
: queryObjectOf(ctx[CONTEXT_SEARCH])
|
|
957
|
+
validation = stage.schema["~standard"].validate(input)
|
|
958
|
+
} catch (error) {
|
|
959
|
+
return onError(error)
|
|
960
|
+
}
|
|
961
|
+
if (validation instanceof Promise) {
|
|
962
|
+
return validation.then((result) => {
|
|
963
|
+
if (result.issues !== undefined) return wrapResponse(plainValidationError(result.issues))
|
|
964
|
+
try {
|
|
965
|
+
validationContext[stage.kind] = result.value
|
|
966
|
+
} catch (error) {
|
|
967
|
+
return onError(error)
|
|
968
|
+
}
|
|
969
|
+
return runFusedValidationStages(stages, source, ctx, wrapResponse, onError, next, index + 1)
|
|
970
|
+
}, onError)
|
|
971
|
+
}
|
|
972
|
+
if (validation.issues !== undefined)
|
|
973
|
+
return wrapResponse(plainValidationError(validation.issues))
|
|
974
|
+
try {
|
|
975
|
+
validationContext[stage.kind] = validation.value
|
|
976
|
+
} catch (error) {
|
|
977
|
+
return onError(error)
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
return next()
|
|
981
|
+
}
|
|
982
|
+
|
|
889
983
|
/** What a fused lifecycle route runs for its hook shape on the Web lane: the first derive, or the
|
|
890
984
|
* handler when the shape has no hooks before it. */
|
|
891
985
|
type FusedWebStart = (ctx: RawContext, route: FusedHooks, index: number) => MaybePromise<Response>
|
|
@@ -1380,17 +1474,15 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1380
1474
|
private wsMountsCheckedAt = -1
|
|
1381
1475
|
/** Whether an app mounted here takes WebSocket upgrades, as of `wsMountsCheckedAt`. */
|
|
1382
1476
|
private wsMountsLive = false
|
|
1383
|
-
/**
|
|
1384
|
-
|
|
1385
|
-
* no-WebSocket app never constructs (or bundles) it. */
|
|
1386
|
-
private topics: TopicRegistry | undefined
|
|
1477
|
+
/** Configured topic adapter, or the in-process default created by the first `app.ws()` route. */
|
|
1478
|
+
private topics: TopicAdapter | undefined
|
|
1387
1479
|
/** True once any `app.ws()` route sets `validateSend: true` - the one case where a broadcast frame
|
|
1388
|
-
* is validated (and possibly dropped) per socket, so `app.publish` must stay on the JS
|
|
1480
|
+
* is validated (and possibly dropped) per socket, so `app.publish` must stay on the JS adapter loop
|
|
1389
1481
|
* rather than delegate to Bun's native `server.publish`. */
|
|
1390
1482
|
private wsHasValidatedSend = false
|
|
1391
1483
|
/** Bun's native topic broadcast, bound by `listen()` in native-pubsub mode; `app.publish` uses it
|
|
1392
|
-
* instead of the JS registry. `undefined` off Bun, before `listen()`, with a
|
|
1393
|
-
* or when a mounted app takes upgrades. */
|
|
1484
|
+
* instead of the JS registry. `undefined` off Bun, before `listen()`, with a custom topic adapter,
|
|
1485
|
+
* a validated-send route, or when a mounted app takes upgrades. */
|
|
1394
1486
|
private nativePublish:
|
|
1395
1487
|
| ((topic: string, data: string | ArrayBufferView | ArrayBuffer) => void)
|
|
1396
1488
|
| undefined
|
|
@@ -1539,7 +1631,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
1539
1631
|
this.catalog = new RouteCatalog()
|
|
1540
1632
|
this.wsRouter = new Router<WsEntry>()
|
|
1541
1633
|
this.wsRoutes = []
|
|
1542
|
-
this.topics =
|
|
1634
|
+
this.topics = options.topicAdapter
|
|
1543
1635
|
const maxBodyBytes = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES
|
|
1544
1636
|
assertByteLimit(maxBodyBytes, "maxBodyBytes")
|
|
1545
1637
|
this.protoPoisoning = options.protoPoisoning ?? "reject"
|
|
@@ -2607,7 +2699,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2607
2699
|
)
|
|
2608
2700
|
}
|
|
2609
2701
|
// A validated-send route makes broadcast bytes route-dependent (drop-on-invalid per socket), so the
|
|
2610
|
-
// whole app forgoes native pub/sub and keeps the JS
|
|
2702
|
+
// whole app forgoes native pub/sub and keeps the JS adapter loop that runs each send through the sender.
|
|
2611
2703
|
if (handler.validateSend === true) this.wsHasValidatedSend = true
|
|
2612
2704
|
this.topics ??= runtime.createTopics()
|
|
2613
2705
|
// A `messageSchema` wraps `message` with validation once, here - every adapter then dispatches
|
|
@@ -2637,18 +2729,18 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2637
2729
|
|
|
2638
2730
|
/**
|
|
2639
2731
|
* Broadcast `data` to every WebSocket connection subscribed to `topic` (via `ws.subscribe(topic)`).
|
|
2640
|
-
*
|
|
2641
|
-
*
|
|
2732
|
+
* Uses the configured {@link ServerOptions.topicAdapter}, or the in-process topic registry by
|
|
2733
|
+
* default. Await the result when a custom adapter publishes asynchronously.
|
|
2642
2734
|
*/
|
|
2643
|
-
publish(topic: string, data: string | ArrayBufferView | ArrayBuffer): void {
|
|
2735
|
+
publish(topic: string, data: string | ArrayBufferView | ArrayBuffer): void | Promise<void> {
|
|
2644
2736
|
// Native-pubsub mode (Bun, no validated-send route): one native broadcast to every subscriber, no
|
|
2645
|
-
// per-connection JS loop. Bound only after `listen()`; before that (or off Bun) the
|
|
2737
|
+
// per-connection JS loop. Bound only after `listen()`; before that (or off Bun) the adapter runs.
|
|
2646
2738
|
if (this.nativePublish !== undefined) {
|
|
2647
2739
|
this.nativePublish(topic, data)
|
|
2648
2740
|
return
|
|
2649
2741
|
}
|
|
2650
|
-
//
|
|
2651
|
-
this.topics?.publish(topic, data)
|
|
2742
|
+
// With no custom adapter and no `app.ws()` route, no registry exists and this is a no-op.
|
|
2743
|
+
return this.topics?.publish(topic, data)
|
|
2652
2744
|
}
|
|
2653
2745
|
|
|
2654
2746
|
private route(
|
|
@@ -2730,6 +2822,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2730
2822
|
schema: RouteSchema | undefined,
|
|
2731
2823
|
handler: (context: never) => unknown,
|
|
2732
2824
|
): CatalogRoute {
|
|
2825
|
+
const staticResponse = staticResponseDefinition(handler)
|
|
2733
2826
|
schema = withTextSlots(schema)
|
|
2734
2827
|
// Route provenance for the CLI's capability walk, which sets NIFRA_ROUTE_SITES while it loads the
|
|
2735
2828
|
// app: the module that registered the route, read where a static scan cannot see the path.
|
|
@@ -2791,9 +2884,12 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2791
2884
|
handler = this.idempotencyRuntime.wrapHandler(handler)
|
|
2792
2885
|
}
|
|
2793
2886
|
const { bare, fusedQuery, fusedBody } = lanes
|
|
2794
|
-
//
|
|
2795
|
-
//
|
|
2796
|
-
const fusedLifecycle =
|
|
2887
|
+
// Composite-input routes and hook-only routes share the lifecycle builders; simple shapes use
|
|
2888
|
+
// their narrower runners.
|
|
2889
|
+
const fusedLifecycle =
|
|
2890
|
+
lanes.fusedLane === "hooks" ||
|
|
2891
|
+
lanes.fusedLane === "inputs" ||
|
|
2892
|
+
(lanes.fusedLane === "query" && schema?.params !== undefined)
|
|
2797
2893
|
// The fused chains keep the arrays they are built from, and `beforeHandle()` inserts into the live
|
|
2798
2894
|
// ones ahead of `beforeHandler()` guards, so the entry and the fused chains share one snapshot of
|
|
2799
2895
|
// this route's.
|
|
@@ -2817,16 +2913,12 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2817
2913
|
? this.buildFusedBodyRunner(
|
|
2818
2914
|
handler as unknown as InternalHandler,
|
|
2819
2915
|
schema?.body as StandardSchemaV1,
|
|
2916
|
+
schema?.params,
|
|
2820
2917
|
hasDecorations ? routeDecorations : undefined,
|
|
2821
2918
|
bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
2822
2919
|
)
|
|
2823
2920
|
: fusedHooks !== undefined
|
|
2824
|
-
? this.buildFusedLifecycleRunner(
|
|
2825
|
-
fusedHooks,
|
|
2826
|
-
schema?.query,
|
|
2827
|
-
schema?.body as StandardSchemaV1 | undefined,
|
|
2828
|
-
bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
2829
|
-
)
|
|
2921
|
+
? this.buildFusedLifecycleRunner(fusedHooks, schema, bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
2830
2922
|
: undefined
|
|
2831
2923
|
const fusedWeb =
|
|
2832
2924
|
bare && this.aroundHooks.length === 0
|
|
@@ -2840,18 +2932,14 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2840
2932
|
? this.buildFusedQueryWeb(
|
|
2841
2933
|
handler as unknown as InternalHandler,
|
|
2842
2934
|
hasDecorations ? routeDecorations : undefined,
|
|
2843
|
-
schema?.query
|
|
2935
|
+
schema?.query,
|
|
2936
|
+
schema?.params,
|
|
2844
2937
|
bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
2845
2938
|
)
|
|
2846
2939
|
: fusedBody
|
|
2847
2940
|
? this.buildFusedBodyWeb(fusedBodyRunner as FusedBodyRunner)
|
|
2848
2941
|
: fusedHooks !== undefined
|
|
2849
|
-
? this.buildFusedLifecycleWeb(
|
|
2850
|
-
fusedHooks,
|
|
2851
|
-
schema?.query,
|
|
2852
|
-
schema?.body as StandardSchemaV1 | undefined,
|
|
2853
|
-
bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
2854
|
-
)
|
|
2942
|
+
? this.buildFusedLifecycleWeb(fusedHooks, schema, bodyLimit ?? UNLIMITED_BODY_BYTES)
|
|
2855
2943
|
: undefined
|
|
2856
2944
|
const program = compileRouteProgram({
|
|
2857
2945
|
schema,
|
|
@@ -2880,6 +2968,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
2880
2968
|
})
|
|
2881
2969
|
const registeredEntry: RouteEntry = {
|
|
2882
2970
|
handler: handler as unknown as InternalHandler,
|
|
2971
|
+
staticResponse,
|
|
2883
2972
|
schema,
|
|
2884
2973
|
bodyLimit,
|
|
2885
2974
|
idempotent,
|
|
@@ -3346,7 +3435,10 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3346
3435
|
const lane = entry.execution.fusedLane
|
|
3347
3436
|
// Type-erase the lane so each `===` branch sees the full union, not the narrowed remainder.
|
|
3348
3437
|
const laneName: string | undefined = lane
|
|
3349
|
-
const lifecycle =
|
|
3438
|
+
const lifecycle =
|
|
3439
|
+
laneName === "hooks" ||
|
|
3440
|
+
laneName === "inputs" ||
|
|
3441
|
+
(laneName === "query" && entry.schema?.params !== undefined)
|
|
3350
3442
|
const hooks: FusedHooks | undefined = lifecycle
|
|
3351
3443
|
? {
|
|
3352
3444
|
handler: entry.handler,
|
|
@@ -3362,14 +3454,14 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3362
3454
|
? this.buildFusedBodyRunner(
|
|
3363
3455
|
entry.handler,
|
|
3364
3456
|
entry.schema?.body as StandardSchemaV1,
|
|
3457
|
+
entry.schema?.params,
|
|
3365
3458
|
entry.hasDecorations ? entry.decorations : undefined,
|
|
3366
3459
|
entry.bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
3367
3460
|
)
|
|
3368
3461
|
: hooks !== undefined
|
|
3369
3462
|
? this.buildFusedLifecycleRunner(
|
|
3370
3463
|
hooks,
|
|
3371
|
-
entry.schema
|
|
3372
|
-
entry.schema?.body as StandardSchemaV1 | undefined,
|
|
3464
|
+
entry.schema,
|
|
3373
3465
|
entry.bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
3374
3466
|
)
|
|
3375
3467
|
: undefined
|
|
@@ -3380,14 +3472,14 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
3380
3472
|
? this.buildFusedQueryWeb(
|
|
3381
3473
|
entry.handler,
|
|
3382
3474
|
entry.hasDecorations ? entry.decorations : undefined,
|
|
3383
|
-
entry.schema?.query
|
|
3475
|
+
entry.schema?.query,
|
|
3476
|
+
entry.schema?.params,
|
|
3384
3477
|
entry.bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
3385
3478
|
)
|
|
3386
3479
|
: hooks !== undefined
|
|
3387
3480
|
? this.buildFusedLifecycleWeb(
|
|
3388
3481
|
hooks,
|
|
3389
|
-
entry.schema
|
|
3390
|
-
entry.schema?.body as StandardSchemaV1 | undefined,
|
|
3482
|
+
entry.schema,
|
|
3391
3483
|
entry.bodyLimit ?? UNLIMITED_BODY_BYTES,
|
|
3392
3484
|
)
|
|
3393
3485
|
: this.buildFusedWeb(
|
|
@@ -4017,8 +4109,8 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
4017
4109
|
const params = match.params === EMPTY_PARAMS ? match.params : decodeRouteParams(match.params)
|
|
4018
4110
|
if (params === null) return refuse(jsonError(400, "malformed_path"))
|
|
4019
4111
|
const { handler, chain } = match.payload
|
|
4020
|
-
// Non-null: a socket route ⇒ ws() ran ⇒ `.use(websocket())` installed the runtime +
|
|
4021
|
-
const pubsub = this.topics as
|
|
4112
|
+
// Non-null: a socket route ⇒ ws() ran ⇒ `.use(websocket())` installed the runtime + adapter.
|
|
4113
|
+
const pubsub = this.topics as TopicAdapter
|
|
4022
4114
|
const runtime = this.wsRuntime as WsRuntime
|
|
4023
4115
|
// CSWSH guard, before any per-connection work or the user's upgrade(): reject a disallowed
|
|
4024
4116
|
// Origin with 403. Browsers don't CORS-protect WS handshakes but do send cookies, so this
|
|
@@ -5179,12 +5271,13 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5179
5271
|
}
|
|
5180
5272
|
}
|
|
5181
5273
|
|
|
5182
|
-
/** Compile the eligible body
|
|
5274
|
+
/** Compile the eligible body route's params → parser → validator → handler continuation once. The
|
|
5183
5275
|
* bounded parser remains shared with the generic lane; only the route-invariant entry lookups and
|
|
5184
5276
|
* lifecycle dispatch disappear from the common synchronous-validator/synchronous-handler case. */
|
|
5185
5277
|
private buildFusedBodyRunner(
|
|
5186
5278
|
handler: InternalHandler,
|
|
5187
5279
|
bodySchema: StandardSchemaV1,
|
|
5280
|
+
paramsSchema: StandardSchemaV1 | undefined,
|
|
5188
5281
|
decorations: Record<PropertyKey, unknown> | undefined,
|
|
5189
5282
|
maxBodyBytes: number,
|
|
5190
5283
|
): FusedBodyRunner {
|
|
@@ -5268,6 +5361,23 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5268
5361
|
}
|
|
5269
5362
|
}
|
|
5270
5363
|
|
|
5364
|
+
const readBody = <T>(
|
|
5365
|
+
source: RequestSource,
|
|
5366
|
+
ctx: RawContext,
|
|
5367
|
+
finalize: (result: unknown, set: CtxSet, ctx: RawContext) => T,
|
|
5368
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
5369
|
+
): Promise<T> =>
|
|
5370
|
+
readBodyFramed(
|
|
5371
|
+
source,
|
|
5372
|
+
maxBodyBytes,
|
|
5373
|
+
this.protoPoisoning,
|
|
5374
|
+
(parsed) => runParsed(parsed, ctx, finalize, wrapResponse),
|
|
5375
|
+
wrapResponse,
|
|
5376
|
+
(err) => logError(err, ctx, finalize, wrapResponse),
|
|
5377
|
+
bodySchema,
|
|
5378
|
+
this.formOrigins,
|
|
5379
|
+
)
|
|
5380
|
+
|
|
5271
5381
|
return <T>(
|
|
5272
5382
|
source: RequestSource,
|
|
5273
5383
|
params: Record<string, string>,
|
|
@@ -5291,18 +5401,30 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5291
5401
|
maxBodyBytes,
|
|
5292
5402
|
this.protoPoisoning,
|
|
5293
5403
|
)
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5404
|
+
if (paramsSchema === undefined) return readBody(source, ctx, finalize, wrapResponse)
|
|
5405
|
+
let validation: StandardResult<unknown> | Promise<StandardResult<unknown>>
|
|
5406
|
+
try {
|
|
5407
|
+
validation = paramsSchema["~standard"].validate(ctx.params)
|
|
5408
|
+
} catch (err) {
|
|
5409
|
+
return logError(err, ctx, finalize, wrapResponse)
|
|
5410
|
+
}
|
|
5411
|
+
if (validation instanceof Promise) {
|
|
5412
|
+
return validation.then(
|
|
5413
|
+
(settled) => {
|
|
5414
|
+
if (settled.issues !== undefined)
|
|
5415
|
+
return wrapResponse(plainValidationError(settled.issues))
|
|
5416
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: the route's params schema defines this validated output; RawContext erases its route-specific type.
|
|
5417
|
+
ctx.params = settled.value as Record<string, string>
|
|
5418
|
+
return readBody(source, ctx, finalize, wrapResponse)
|
|
5419
|
+
},
|
|
5420
|
+
(err) => logError(err, ctx, finalize, wrapResponse),
|
|
5421
|
+
)
|
|
5422
|
+
}
|
|
5423
|
+
if (validation.issues !== undefined)
|
|
5424
|
+
return wrapResponse(plainValidationError(validation.issues))
|
|
5425
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: the route's params schema defines this validated output; RawContext erases its route-specific type.
|
|
5426
|
+
ctx.params = validation.value as Record<string, string>
|
|
5427
|
+
return readBody(source, ctx, finalize, wrapResponse)
|
|
5306
5428
|
}
|
|
5307
5429
|
}
|
|
5308
5430
|
|
|
@@ -5330,17 +5452,13 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5330
5452
|
) as MaybePromise<Response>
|
|
5331
5453
|
}
|
|
5332
5454
|
|
|
5333
|
-
/** The fused Web lane for
|
|
5334
|
-
*
|
|
5335
|
-
* Eligibility is decided at registration (see `fusedQuery` in {@link register}); the semantics here
|
|
5336
|
-
* are exactly `runQueryOnly`'s for that eligible shape - invalid input returns
|
|
5337
|
-
* `validationError(issues)` (recovery hooks disqualify the route from this lane), a thrown
|
|
5338
|
-
* `Response` passes through, anything else logs and returns a flat 500, and an async validator
|
|
5339
|
-
* falls to a then-chain with the same steps. */
|
|
5455
|
+
/** The fused Web lane for routes with path params and/or a query schema: validate + handler + respond
|
|
5456
|
+
* in one closure, with no lifecycle promise when the validators and handler are sync. */
|
|
5340
5457
|
private buildFusedQueryWeb(
|
|
5341
5458
|
handler: InternalHandler,
|
|
5342
5459
|
decorations: Record<PropertyKey, unknown> | undefined,
|
|
5343
|
-
querySchema: StandardSchemaV1,
|
|
5460
|
+
querySchema: StandardSchemaV1 | undefined,
|
|
5461
|
+
paramsSchema: StandardSchemaV1 | undefined,
|
|
5344
5462
|
maxBodyBytes: number,
|
|
5345
5463
|
): FusedWebRunner {
|
|
5346
5464
|
// Same thrown-value contract as the generic lane (`bare-error-lane.ts` via `bareError`); the fused
|
|
@@ -5354,7 +5472,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5354
5472
|
this.wrapWebResponse,
|
|
5355
5473
|
)
|
|
5356
5474
|
const runHandler = (ctx: RawContext, value: unknown): MaybePromise<Response> => {
|
|
5357
|
-
ctx.query = value
|
|
5475
|
+
if (querySchema !== undefined) ctx.query = value
|
|
5358
5476
|
let result: unknown
|
|
5359
5477
|
try {
|
|
5360
5478
|
result = handler(ctx)
|
|
@@ -5369,6 +5487,27 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5369
5487
|
}
|
|
5370
5488
|
return this.respondFused(result, ctx, this.onResponseHooks.length === 0)
|
|
5371
5489
|
}
|
|
5490
|
+
const runQuery = (ctx: RawContext): MaybePromise<Response> => {
|
|
5491
|
+
if (querySchema === undefined) return runHandler(ctx, undefined)
|
|
5492
|
+
let validation: MaybePromise<StandardResult<unknown>>
|
|
5493
|
+
try {
|
|
5494
|
+
validation = querySchema["~standard"].validate(queryObjectOf(ctx[CONTEXT_SEARCH]))
|
|
5495
|
+
} catch (err) {
|
|
5496
|
+
return logError(err, ctx)
|
|
5497
|
+
}
|
|
5498
|
+
if (validation instanceof Promise) {
|
|
5499
|
+
return validation.then(
|
|
5500
|
+
(settled) =>
|
|
5501
|
+
settled.issues !== undefined
|
|
5502
|
+
? this.wrapWebResponse(plainValidationError(settled.issues))
|
|
5503
|
+
: runHandler(ctx, settled.value),
|
|
5504
|
+
(err) => logError(err, ctx),
|
|
5505
|
+
)
|
|
5506
|
+
}
|
|
5507
|
+
if (validation.issues !== undefined)
|
|
5508
|
+
return this.wrapWebResponse(plainValidationError(validation.issues))
|
|
5509
|
+
return runHandler(ctx, validation.value)
|
|
5510
|
+
}
|
|
5372
5511
|
return (source, params, search, signal, budget, platform, nativeContext) => {
|
|
5373
5512
|
const ctx = nativeContext
|
|
5374
5513
|
? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning)
|
|
@@ -5383,24 +5522,30 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5383
5522
|
this.protoPoisoning,
|
|
5384
5523
|
)
|
|
5385
5524
|
if (decorations !== undefined) Object.assign(ctx, decorations)
|
|
5525
|
+
if (paramsSchema === undefined) return runQuery(ctx)
|
|
5386
5526
|
let validation: MaybePromise<StandardResult<unknown>>
|
|
5387
5527
|
try {
|
|
5388
|
-
validation =
|
|
5528
|
+
validation = paramsSchema["~standard"].validate(ctx.params)
|
|
5389
5529
|
} catch (err) {
|
|
5390
5530
|
return logError(err, ctx)
|
|
5391
5531
|
}
|
|
5392
5532
|
if (validation instanceof Promise) {
|
|
5393
5533
|
return validation.then(
|
|
5394
|
-
(settled) =>
|
|
5395
|
-
settled.issues !== undefined
|
|
5396
|
-
|
|
5397
|
-
|
|
5534
|
+
(settled) => {
|
|
5535
|
+
if (settled.issues !== undefined)
|
|
5536
|
+
return this.wrapWebResponse(plainValidationError(settled.issues))
|
|
5537
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: the route's params schema defines this validated output; RawContext erases its route-specific type.
|
|
5538
|
+
ctx.params = settled.value as Record<string, string>
|
|
5539
|
+
return runQuery(ctx)
|
|
5540
|
+
},
|
|
5398
5541
|
(err) => logError(err, ctx),
|
|
5399
5542
|
)
|
|
5400
5543
|
}
|
|
5401
5544
|
if (validation.issues !== undefined)
|
|
5402
5545
|
return this.wrapWebResponse(plainValidationError(validation.issues))
|
|
5403
|
-
|
|
5546
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: the route's params schema defines this validated output; RawContext erases its route-specific type.
|
|
5547
|
+
ctx.params = validation.value as Record<string, string>
|
|
5548
|
+
return runQuery(ctx)
|
|
5404
5549
|
}
|
|
5405
5550
|
}
|
|
5406
5551
|
|
|
@@ -5625,13 +5770,12 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5625
5770
|
/**
|
|
5626
5771
|
* Compile a fused lifecycle route's entry closure: parse (body) → validate → the shared stage chain
|
|
5627
5772
|
* of its hook shape (derive → before → handler → after → respond, see {@link fusedWebStages}) in a
|
|
5628
|
-
* single frame, where the generic program walks staged hook loops. `selectRouteLanes` admits
|
|
5629
|
-
*
|
|
5630
|
-
*
|
|
5631
|
-
*
|
|
5632
|
-
*
|
|
5633
|
-
*
|
|
5634
|
-
* an argument, so a request allocates no step closures.
|
|
5773
|
+
* single frame, where the generic program walks staged hook loops. `selectRouteLanes` admits ordinary
|
|
5774
|
+
* validation stages and derive/before/after/error hooks, while excluding auth-first ordering, custom
|
|
5775
|
+
* validation recovery, outer wrappers and response contracts. The shared input sequence preserves
|
|
5776
|
+
* the program's validation order around body parsing; the hook chain preserves thrown-value, `c.set`
|
|
5777
|
+
* and response-rendering semantics. The route's closure holds its `FusedHooks` and schemas, so a
|
|
5778
|
+
* request allocates no step closures.
|
|
5635
5779
|
*/
|
|
5636
5780
|
/**
|
|
5637
5781
|
* Keep the common one-derive/one-before shape bound to its hooks. Inline its synchronous path to
|
|
@@ -5781,12 +5925,13 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5781
5925
|
|
|
5782
5926
|
private buildFusedLifecycleWeb(
|
|
5783
5927
|
route: FusedHooks,
|
|
5784
|
-
|
|
5785
|
-
bodySchema: StandardSchemaV1 | undefined,
|
|
5928
|
+
schema: RouteSchema | undefined,
|
|
5786
5929
|
maxBodyBytes: number,
|
|
5787
5930
|
): FusedWebRunner {
|
|
5931
|
+
const validationStages = compileFusedValidationStages(schema)
|
|
5932
|
+
const bodySchema = schema?.body
|
|
5788
5933
|
if (
|
|
5789
|
-
|
|
5934
|
+
validationStages.length === 0 &&
|
|
5790
5935
|
bodySchema === undefined &&
|
|
5791
5936
|
route.errors.length === 0 &&
|
|
5792
5937
|
route.decorations === undefined &&
|
|
@@ -5810,30 +5955,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5810
5955
|
this.fusedWebStages(d, b, a),
|
|
5811
5956
|
)
|
|
5812
5957
|
const decorations = route.decorations
|
|
5813
|
-
if (bodySchema
|
|
5814
|
-
const onParsed = (parsed: unknown, ctx: RawContext): MaybePromise<Response> => {
|
|
5815
|
-
let validation: StandardResult<unknown> | Promise<StandardResult<unknown>>
|
|
5816
|
-
try {
|
|
5817
|
-
validation = bodySchema["~standard"].validate(parsed)
|
|
5818
|
-
} catch (err) {
|
|
5819
|
-
return this.fusedWebRouteFail(err, ctx, route)
|
|
5820
|
-
}
|
|
5821
|
-
if (validation instanceof Promise) {
|
|
5822
|
-
return validation.then(
|
|
5823
|
-
(settled) => {
|
|
5824
|
-
if (settled.issues !== undefined)
|
|
5825
|
-
return this.wrapWebResponse(plainValidationError(settled.issues))
|
|
5826
|
-
ctx.body = settled.value
|
|
5827
|
-
return runDerive(ctx, route, 0)
|
|
5828
|
-
},
|
|
5829
|
-
(err) => this.fusedWebRouteFail(err, ctx, route),
|
|
5830
|
-
)
|
|
5831
|
-
}
|
|
5832
|
-
if (validation.issues !== undefined)
|
|
5833
|
-
return this.wrapWebResponse(plainValidationError(validation.issues))
|
|
5834
|
-
ctx.body = validation.value
|
|
5835
|
-
return runDerive(ctx, route, 0)
|
|
5836
|
-
}
|
|
5958
|
+
if (bodySchema === undefined) {
|
|
5837
5959
|
return (source, params, search, signal, budget, platform, nativeContext) => {
|
|
5838
5960
|
const ctx = nativeContext
|
|
5839
5961
|
? RequestContext.native(
|
|
@@ -5855,36 +5977,40 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5855
5977
|
this.protoPoisoning,
|
|
5856
5978
|
)
|
|
5857
5979
|
if (decorations !== undefined) Object.assign(ctx, decorations)
|
|
5858
|
-
return
|
|
5980
|
+
if (validationStages.length === 0) return runDerive(ctx, route, 0)
|
|
5981
|
+
return runFusedValidationStages(
|
|
5982
|
+
validationStages,
|
|
5859
5983
|
source,
|
|
5860
|
-
|
|
5861
|
-
this.protoPoisoning,
|
|
5862
|
-
(parsed) => onParsed(parsed, ctx),
|
|
5984
|
+
ctx,
|
|
5863
5985
|
this.wrapWebResponse,
|
|
5864
5986
|
(err) => this.fusedWebRouteFail(err, ctx, route),
|
|
5865
|
-
|
|
5866
|
-
|
|
5867
|
-
) as MaybePromise<Response>
|
|
5987
|
+
() => runDerive(ctx, route, 0),
|
|
5988
|
+
)
|
|
5868
5989
|
}
|
|
5869
5990
|
}
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
5991
|
+
const queryIndex =
|
|
5992
|
+
schema?.query === undefined ? validationStages.length : validationStages.length - 1
|
|
5993
|
+
const beforeBodyStages = validationStages.slice(0, queryIndex)
|
|
5994
|
+
const afterBodyStages = validationStages.slice(queryIndex)
|
|
5995
|
+
const runAfterBody = (source: RequestSource, ctx: RawContext): MaybePromise<Response> =>
|
|
5996
|
+
afterBodyStages.length === 0
|
|
5997
|
+
? runDerive(ctx, route, 0)
|
|
5998
|
+
: runFusedValidationStages(
|
|
5999
|
+
afterBodyStages,
|
|
5874
6000
|
source,
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
platform,
|
|
5880
|
-
maxBodyBytes,
|
|
5881
|
-
this.protoPoisoning,
|
|
6001
|
+
ctx,
|
|
6002
|
+
this.wrapWebResponse,
|
|
6003
|
+
(err) => this.fusedWebRouteFail(err, ctx, route),
|
|
6004
|
+
() => runDerive(ctx, route, 0),
|
|
5882
6005
|
)
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
|
|
6006
|
+
const onParsed = (
|
|
6007
|
+
parsed: unknown,
|
|
6008
|
+
source: RequestSource,
|
|
6009
|
+
ctx: RawContext,
|
|
6010
|
+
): MaybePromise<Response> => {
|
|
6011
|
+
let validation: StandardResult<unknown> | Promise<StandardResult<unknown>>
|
|
5886
6012
|
try {
|
|
5887
|
-
validation =
|
|
6013
|
+
validation = bodySchema["~standard"].validate(parsed)
|
|
5888
6014
|
} catch (err) {
|
|
5889
6015
|
return this.fusedWebRouteFail(err, ctx, route)
|
|
5890
6016
|
}
|
|
@@ -5893,16 +6019,51 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
5893
6019
|
(settled) => {
|
|
5894
6020
|
if (settled.issues !== undefined)
|
|
5895
6021
|
return this.wrapWebResponse(plainValidationError(settled.issues))
|
|
5896
|
-
ctx.
|
|
5897
|
-
return
|
|
6022
|
+
ctx.body = settled.value
|
|
6023
|
+
return runAfterBody(source, ctx)
|
|
5898
6024
|
},
|
|
5899
6025
|
(err) => this.fusedWebRouteFail(err, ctx, route),
|
|
5900
6026
|
)
|
|
5901
6027
|
}
|
|
5902
6028
|
if (validation.issues !== undefined)
|
|
5903
6029
|
return this.wrapWebResponse(plainValidationError(validation.issues))
|
|
5904
|
-
ctx.
|
|
5905
|
-
return
|
|
6030
|
+
ctx.body = validation.value
|
|
6031
|
+
return runAfterBody(source, ctx)
|
|
6032
|
+
}
|
|
6033
|
+
const readBody = (source: RequestSource, ctx: RawContext): Promise<Response> =>
|
|
6034
|
+
readBodyFramed(
|
|
6035
|
+
source,
|
|
6036
|
+
maxBodyBytes,
|
|
6037
|
+
this.protoPoisoning,
|
|
6038
|
+
(parsed) => onParsed(parsed, source, ctx),
|
|
6039
|
+
this.wrapWebResponse,
|
|
6040
|
+
(err) => this.fusedWebRouteFail(err, ctx, route),
|
|
6041
|
+
bodySchema,
|
|
6042
|
+
this.formOrigins,
|
|
6043
|
+
)
|
|
6044
|
+
return (source, params, search, signal, budget, platform, nativeContext) => {
|
|
6045
|
+
const ctx = nativeContext
|
|
6046
|
+
? RequestContext.native(source, params, search, maxBodyBytes, platform, this.protoPoisoning)
|
|
6047
|
+
: new RequestContext(
|
|
6048
|
+
source,
|
|
6049
|
+
params,
|
|
6050
|
+
search,
|
|
6051
|
+
signal,
|
|
6052
|
+
budget,
|
|
6053
|
+
platform,
|
|
6054
|
+
maxBodyBytes,
|
|
6055
|
+
this.protoPoisoning,
|
|
6056
|
+
)
|
|
6057
|
+
if (decorations !== undefined) Object.assign(ctx, decorations)
|
|
6058
|
+
if (beforeBodyStages.length === 0) return readBody(source, ctx)
|
|
6059
|
+
return runFusedValidationStages(
|
|
6060
|
+
beforeBodyStages,
|
|
6061
|
+
source,
|
|
6062
|
+
ctx,
|
|
6063
|
+
this.wrapWebResponse,
|
|
6064
|
+
(err) => this.fusedWebRouteFail(err, ctx, route),
|
|
6065
|
+
() => readBody(source, ctx),
|
|
6066
|
+
)
|
|
5906
6067
|
}
|
|
5907
6068
|
}
|
|
5908
6069
|
|
|
@@ -6154,44 +6315,17 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6154
6315
|
*/
|
|
6155
6316
|
private buildFusedLifecycleRunner(
|
|
6156
6317
|
route: FusedHooks,
|
|
6157
|
-
|
|
6158
|
-
bodySchema: StandardSchemaV1 | undefined,
|
|
6318
|
+
schema: RouteSchema | undefined,
|
|
6159
6319
|
maxBodyBytes: number,
|
|
6160
6320
|
): FusedBodyRunner {
|
|
6321
|
+
const validationStages = compileFusedValidationStages(schema)
|
|
6322
|
+
const bodySchema = schema?.body
|
|
6161
6323
|
const runDerive = sharedStages(NODE_STAGES, this, route, (d, b, a) =>
|
|
6162
6324
|
this.fusedNodeStages(d, b, a),
|
|
6163
6325
|
)
|
|
6164
6326
|
const decorations = route.decorations
|
|
6165
6327
|
|
|
6166
|
-
if (bodySchema
|
|
6167
|
-
const onParsed = <T>(
|
|
6168
|
-
parsed: unknown,
|
|
6169
|
-
ctx: RawContext,
|
|
6170
|
-
finalize: (result: unknown, set: CtxSet, ctx: RawContext) => T,
|
|
6171
|
-
wrapResponse: (response: Response | ResponseResult) => T,
|
|
6172
|
-
): MaybePromise<T> => {
|
|
6173
|
-
let validation: StandardResult<unknown> | Promise<StandardResult<unknown>>
|
|
6174
|
-
try {
|
|
6175
|
-
validation = bodySchema["~standard"].validate(parsed)
|
|
6176
|
-
} catch (err) {
|
|
6177
|
-
return this.fusedFail(err, ctx, route, finalize, wrapResponse)
|
|
6178
|
-
}
|
|
6179
|
-
if (validation instanceof Promise) {
|
|
6180
|
-
return validation.then(
|
|
6181
|
-
(settled) => {
|
|
6182
|
-
if (settled.issues !== undefined)
|
|
6183
|
-
return wrapResponse(plainValidationError(settled.issues))
|
|
6184
|
-
ctx.body = settled.value
|
|
6185
|
-
return runDerive(ctx, route, finalize, wrapResponse, 0)
|
|
6186
|
-
},
|
|
6187
|
-
(err) => this.fusedFail(err, ctx, route, finalize, wrapResponse),
|
|
6188
|
-
)
|
|
6189
|
-
}
|
|
6190
|
-
if (validation.issues !== undefined)
|
|
6191
|
-
return wrapResponse(plainValidationError(validation.issues))
|
|
6192
|
-
ctx.body = validation.value
|
|
6193
|
-
return runDerive(ctx, route, finalize, wrapResponse, 0)
|
|
6194
|
-
}
|
|
6328
|
+
if (bodySchema === undefined) {
|
|
6195
6329
|
return <T>(
|
|
6196
6330
|
source: RequestSource,
|
|
6197
6331
|
params: Record<string, string>,
|
|
@@ -6223,41 +6357,84 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6223
6357
|
this.protoPoisoning,
|
|
6224
6358
|
)
|
|
6225
6359
|
if (decorations !== undefined) Object.assign(ctx, decorations)
|
|
6226
|
-
return
|
|
6360
|
+
if (validationStages.length === 0) return runDerive(ctx, route, finalize, wrapResponse, 0)
|
|
6361
|
+
return runFusedValidationStages(
|
|
6362
|
+
validationStages,
|
|
6227
6363
|
source,
|
|
6228
|
-
|
|
6229
|
-
this.protoPoisoning,
|
|
6230
|
-
(parsed) => onParsed(parsed, ctx, finalize, wrapResponse),
|
|
6364
|
+
ctx,
|
|
6231
6365
|
wrapResponse,
|
|
6232
6366
|
(err) => this.fusedFail(err, ctx, route, finalize, wrapResponse),
|
|
6233
|
-
|
|
6234
|
-
this.formOrigins,
|
|
6367
|
+
() => runDerive(ctx, route, finalize, wrapResponse, 0),
|
|
6235
6368
|
)
|
|
6236
6369
|
}
|
|
6237
6370
|
}
|
|
6238
6371
|
|
|
6239
|
-
const
|
|
6240
|
-
|
|
6372
|
+
const queryIndex =
|
|
6373
|
+
schema?.query === undefined ? validationStages.length : validationStages.length - 1
|
|
6374
|
+
const beforeBodyStages = validationStages.slice(0, queryIndex)
|
|
6375
|
+
const afterBodyStages = validationStages.slice(queryIndex)
|
|
6376
|
+
const runAfterBody = <T>(
|
|
6377
|
+
source: RequestSource,
|
|
6378
|
+
ctx: RawContext,
|
|
6379
|
+
finalize: Finalize<T>,
|
|
6380
|
+
wrapResponse: Wrap<T>,
|
|
6381
|
+
): MaybePromise<T> =>
|
|
6382
|
+
afterBodyStages.length === 0
|
|
6383
|
+
? runDerive(ctx, route, finalize, wrapResponse, 0)
|
|
6384
|
+
: runFusedValidationStages(
|
|
6385
|
+
afterBodyStages,
|
|
6386
|
+
source,
|
|
6387
|
+
ctx,
|
|
6388
|
+
wrapResponse,
|
|
6389
|
+
(err) => this.fusedFail(err, ctx, route, finalize, wrapResponse),
|
|
6390
|
+
() => runDerive(ctx, route, finalize, wrapResponse, 0),
|
|
6391
|
+
)
|
|
6392
|
+
|
|
6393
|
+
const onParsed = <T>(
|
|
6394
|
+
parsed: unknown,
|
|
6395
|
+
source: RequestSource,
|
|
6241
6396
|
ctx: RawContext,
|
|
6242
6397
|
finalize: (result: unknown, set: CtxSet, ctx: RawContext) => T,
|
|
6243
6398
|
wrapResponse: (response: Response | ResponseResult) => T,
|
|
6244
6399
|
): MaybePromise<T> => {
|
|
6400
|
+
let validation: StandardResult<unknown> | Promise<StandardResult<unknown>>
|
|
6401
|
+
try {
|
|
6402
|
+
validation = bodySchema["~standard"].validate(parsed)
|
|
6403
|
+
} catch (err) {
|
|
6404
|
+
return this.fusedFail(err, ctx, route, finalize, wrapResponse)
|
|
6405
|
+
}
|
|
6245
6406
|
if (validation instanceof Promise) {
|
|
6246
6407
|
return validation.then(
|
|
6247
6408
|
(settled) => {
|
|
6248
6409
|
if (settled.issues !== undefined)
|
|
6249
6410
|
return wrapResponse(plainValidationError(settled.issues))
|
|
6250
|
-
ctx.
|
|
6251
|
-
return
|
|
6411
|
+
ctx.body = settled.value
|
|
6412
|
+
return runAfterBody(source, ctx, finalize, wrapResponse)
|
|
6252
6413
|
},
|
|
6253
6414
|
(err) => this.fusedFail(err, ctx, route, finalize, wrapResponse),
|
|
6254
6415
|
)
|
|
6255
6416
|
}
|
|
6256
6417
|
if (validation.issues !== undefined)
|
|
6257
6418
|
return wrapResponse(plainValidationError(validation.issues))
|
|
6258
|
-
ctx.
|
|
6259
|
-
return
|
|
6419
|
+
ctx.body = validation.value
|
|
6420
|
+
return runAfterBody(source, ctx, finalize, wrapResponse)
|
|
6260
6421
|
}
|
|
6422
|
+
const readBody = <T>(
|
|
6423
|
+
source: RequestSource,
|
|
6424
|
+
ctx: RawContext,
|
|
6425
|
+
finalize: (result: unknown, set: CtxSet, ctx: RawContext) => T,
|
|
6426
|
+
wrapResponse: (response: Response | ResponseResult) => T,
|
|
6427
|
+
): Promise<T> =>
|
|
6428
|
+
readBodyFramed(
|
|
6429
|
+
source,
|
|
6430
|
+
maxBodyBytes,
|
|
6431
|
+
this.protoPoisoning,
|
|
6432
|
+
(parsed) => onParsed(parsed, source, ctx, finalize, wrapResponse),
|
|
6433
|
+
wrapResponse,
|
|
6434
|
+
(err) => this.fusedFail(err, ctx, route, finalize, wrapResponse),
|
|
6435
|
+
bodySchema,
|
|
6436
|
+
this.formOrigins,
|
|
6437
|
+
)
|
|
6261
6438
|
|
|
6262
6439
|
return <T>(
|
|
6263
6440
|
source: RequestSource,
|
|
@@ -6283,14 +6460,15 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
6283
6460
|
this.protoPoisoning,
|
|
6284
6461
|
)
|
|
6285
6462
|
if (decorations !== undefined) Object.assign(ctx, decorations)
|
|
6286
|
-
if (
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
|
|
6292
|
-
|
|
6293
|
-
|
|
6463
|
+
if (beforeBodyStages.length === 0) return readBody(source, ctx, finalize, wrapResponse)
|
|
6464
|
+
return runFusedValidationStages(
|
|
6465
|
+
beforeBodyStages,
|
|
6466
|
+
source,
|
|
6467
|
+
ctx,
|
|
6468
|
+
wrapResponse,
|
|
6469
|
+
(err) => this.fusedFail(err, ctx, route, finalize, wrapResponse),
|
|
6470
|
+
() => readBody(source, ctx, finalize, wrapResponse),
|
|
6471
|
+
)
|
|
6294
6472
|
}
|
|
6295
6473
|
}
|
|
6296
6474
|
|
|
@@ -7300,6 +7478,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
7300
7478
|
fallback: BunNativeHandler,
|
|
7301
7479
|
peer: PeerPlatform,
|
|
7302
7480
|
platformFor?: (request: Request, server: BunPeerServer) => Platform<EnvOf<Ctx>>,
|
|
7481
|
+
allowStaticResponses = true,
|
|
7303
7482
|
): BunNativeRoutes | undefined {
|
|
7304
7483
|
const hooked = this.onRequestHooks.length > 0 && !this.bunNativeRequestHooksSafe
|
|
7305
7484
|
if (
|
|
@@ -7320,6 +7499,19 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
7320
7499
|
// The capacity gate must wrap every request; the fused lane bypasses fetchMatched, so enabling
|
|
7321
7500
|
// admission drops fusion (native matching stays) and routes through the gated matched lane.
|
|
7322
7501
|
this.capacityGate === undefined
|
|
7502
|
+
// A response value skips every request callback, so every policy that could inspect it must be absent.
|
|
7503
|
+
const mayUseStaticResponse =
|
|
7504
|
+
allowStaticResponses &&
|
|
7505
|
+
!this.validateHost &&
|
|
7506
|
+
!this.normalizeTarget &&
|
|
7507
|
+
this.requestTimeoutMs === 0 &&
|
|
7508
|
+
!this.acceptInboundDeadlines &&
|
|
7509
|
+
this.capacityGate === undefined &&
|
|
7510
|
+
this.onRequestHooks.length === 0 &&
|
|
7511
|
+
this.onResponseHooks.length === 0 &&
|
|
7512
|
+
this.onResponseFinalizedHooks.length === 0 &&
|
|
7513
|
+
this.staticResponseHeaders === undefined &&
|
|
7514
|
+
this.responseBodyTag === undefined
|
|
7323
7515
|
const unboundedSignal = mayUseFusedNative ? getNeverAbortSignal() : undefined
|
|
7324
7516
|
const unboundedBudget = mayUseFusedNative ? getUnboundedRequestBudget() : undefined
|
|
7325
7517
|
let count = 0
|
|
@@ -7385,7 +7577,8 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
7385
7577
|
return routes[key]
|
|
7386
7578
|
}
|
|
7387
7579
|
for (let i = 0; i < all.length; i++) {
|
|
7388
|
-
const
|
|
7580
|
+
const route = all[i]!
|
|
7581
|
+
const { method, path, pattern, entry } = route
|
|
7389
7582
|
const run = lead[i]
|
|
7390
7583
|
// The route is served from the table unless its path cannot be there, Bun's table has no slot
|
|
7391
7584
|
// for its method, or a path outside the table outranks it: Bun would hand this route a request
|
|
@@ -7402,6 +7595,34 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
7402
7595
|
) {
|
|
7403
7596
|
continue
|
|
7404
7597
|
}
|
|
7598
|
+
const staticResponse = entry.staticResponse
|
|
7599
|
+
if (
|
|
7600
|
+
mayUseStaticResponse &&
|
|
7601
|
+
staticResponse !== undefined &&
|
|
7602
|
+
(method === "GET" || method === "HEAD" || method === "POST") &&
|
|
7603
|
+
// Bun 1.3 can misframe null-body status codes in its native static table.
|
|
7604
|
+
staticResponse.status !== 204 &&
|
|
7605
|
+
staticResponse.status !== 205 &&
|
|
7606
|
+
staticResponse.status !== 304 &&
|
|
7607
|
+
pattern.paramNames.length === 0 &&
|
|
7608
|
+
entry.schema === undefined &&
|
|
7609
|
+
entry.idempotent === undefined &&
|
|
7610
|
+
entry.ledgered === undefined &&
|
|
7611
|
+
entry.responseContract === undefined &&
|
|
7612
|
+
entry.derives.length === 0 &&
|
|
7613
|
+
entry.authStages.length === 0 &&
|
|
7614
|
+
!entry.hasDecorations &&
|
|
7615
|
+
entry.beforeHandle.length === 0 &&
|
|
7616
|
+
entry.afterHandle.length === 0 &&
|
|
7617
|
+
entry.onError.length === 0 &&
|
|
7618
|
+
entry.around.length === 0 &&
|
|
7619
|
+
route.assurance.length === 0 &&
|
|
7620
|
+
(route.descriptor.capabilities?.length ?? 0) === 0
|
|
7621
|
+
) {
|
|
7622
|
+
tableAt(path, pattern.segments)[method as Method] = createStaticResponse(staticResponse)
|
|
7623
|
+
count += 1
|
|
7624
|
+
continue
|
|
7625
|
+
}
|
|
7405
7626
|
const paramNames = pattern.paramNames
|
|
7406
7627
|
const fused = mayUseFusedNative ? entry.execution.fusedWeb : undefined
|
|
7407
7628
|
const handler = this.compileBunNativeHandler(
|
|
@@ -7519,8 +7740,8 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
7519
7740
|
// matched route's handler, set by `server.upgrade`.
|
|
7520
7741
|
// With WS routes, the dispatcher comes from the installed `.use(websocket())` runtime - non-null
|
|
7521
7742
|
// because a socket route means ws() ran, and ws() requires the runtime at registration.
|
|
7522
|
-
// Native pub/sub when the app has WS routes and none validate outbound
|
|
7523
|
-
// `app.publish` go through Bun's own (uWebSockets) broadcast
|
|
7743
|
+
// Native pub/sub when the app has WS routes, no custom adapter, and none validate outbound
|
|
7744
|
+
// frames: `ws.subscribe` and `app.publish` go through Bun's own (uWebSockets) broadcast.
|
|
7524
7745
|
// A mounted app counts only while it has a WebSocket route (see `[GET_WS_RUNTIME]`), so composing
|
|
7525
7746
|
// one without any needs no runtime here.
|
|
7526
7747
|
const wsRuntime = this[GET_WS_RUNTIME]()
|
|
@@ -7531,11 +7752,14 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
7531
7752
|
)
|
|
7532
7753
|
}
|
|
7533
7754
|
// Native pub/sub is safe only when every upgrade belongs to this app. A mounted child that takes
|
|
7534
|
-
// upgrades owns a different
|
|
7535
|
-
// that
|
|
7755
|
+
// upgrades owns a different topic adapter, so force the per-connection JS dispatcher and carry
|
|
7756
|
+
// that adapter in the upgrade outcome instead of accidentally broadcasting child sockets through
|
|
7536
7757
|
// the parent.
|
|
7537
7758
|
const nativePubsub =
|
|
7538
|
-
this.wsRoutes.length > 0 &&
|
|
7759
|
+
this.wsRoutes.length > 0 &&
|
|
7760
|
+
this.options.topicAdapter === undefined &&
|
|
7761
|
+
!this.mountsTakeUpgrades() &&
|
|
7762
|
+
!this.wsHasValidatedSend
|
|
7539
7763
|
const wsHandlers = wsRuntime?.bunHandlers(
|
|
7540
7764
|
this.topics ?? wsRuntime.createTopics(),
|
|
7541
7765
|
nativePubsub,
|
|
@@ -7599,6 +7823,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
7599
7823
|
(request, server) =>
|
|
7600
7824
|
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: `fallback` hands `fetch` the same platform, which holds no environment.
|
|
7601
7825
|
new BunPeerPlatform(server, request, setIdleTimeout) as Platform<EnvOf<Ctx>>,
|
|
7826
|
+
options?.static === undefined,
|
|
7602
7827
|
)
|
|
7603
7828
|
const first = options?.static
|
|
7604
7829
|
const before = <S, A>(
|
|
@@ -7619,7 +7844,7 @@ export class Server<R extends Registry = EmptyRegistry, Ctx = EmptyContext, Hook
|
|
|
7619
7844
|
const methods = nativeRoutes[path]!
|
|
7620
7845
|
for (const method of BUN_TABLE_METHODS) {
|
|
7621
7846
|
const handler = methods[method]
|
|
7622
|
-
if (handler
|
|
7847
|
+
if (typeof handler === "function") methods[method] = before(handler)
|
|
7623
7848
|
}
|
|
7624
7849
|
}
|
|
7625
7850
|
}
|