@nifrajs/core 2.14.1 → 3.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.
Files changed (226) hide show
  1. package/README.md +25 -0
  2. package/dist/assurance.d.ts +16 -8
  3. package/dist/assurance.d.ts.map +1 -1
  4. package/dist/assurance.js +33 -0
  5. package/dist/assurance.js.map +1 -1
  6. package/dist/durable-adapters.d.ts.map +1 -1
  7. package/dist/durable-adapters.js +26 -3
  8. package/dist/durable-adapters.js.map +1 -1
  9. package/dist/durable-approval.d.ts +56 -0
  10. package/dist/durable-approval.d.ts.map +1 -0
  11. package/dist/durable-approval.js +233 -0
  12. package/dist/durable-approval.js.map +1 -0
  13. package/dist/durable-effect.d.ts +42 -0
  14. package/dist/durable-effect.d.ts.map +1 -0
  15. package/dist/durable-effect.js +234 -0
  16. package/dist/durable-effect.js.map +1 -0
  17. package/dist/durable-execution.d.ts +8 -346
  18. package/dist/durable-execution.d.ts.map +1 -1
  19. package/dist/durable-execution.js +7 -1150
  20. package/dist/durable-execution.js.map +1 -1
  21. package/dist/durable-saga.d.ts +94 -0
  22. package/dist/durable-saga.d.ts.map +1 -0
  23. package/dist/durable-saga.js +550 -0
  24. package/dist/durable-saga.js.map +1 -0
  25. package/dist/edge-kit.d.ts +27 -0
  26. package/dist/edge-kit.d.ts.map +1 -0
  27. package/dist/edge-kit.js +24 -0
  28. package/dist/edge-kit.js.map +1 -0
  29. package/dist/errors.d.ts +1 -1
  30. package/dist/errors.d.ts.map +1 -1
  31. package/dist/errors.js.map +1 -1
  32. package/dist/index.d.ts +23 -2
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +18 -2
  35. package/dist/index.js.map +1 -1
  36. package/dist/internal/durable-approval-protocol.d.ts +1 -1
  37. package/dist/internal/durable-approval-protocol.d.ts.map +1 -1
  38. package/dist/internal/durable-effect-protocol.d.ts +1 -1
  39. package/dist/internal/durable-effect-protocol.d.ts.map +1 -1
  40. package/dist/internal/durable-saga-protocol.d.ts +1 -1
  41. package/dist/internal/durable-saga-protocol.d.ts.map +1 -1
  42. package/dist/internal/durable-shared.d.ts +26 -0
  43. package/dist/internal/durable-shared.d.ts.map +1 -0
  44. package/dist/internal/durable-shared.js +131 -0
  45. package/dist/internal/durable-shared.js.map +1 -0
  46. package/dist/internal/durable-types.d.ts +159 -0
  47. package/dist/internal/durable-types.d.ts.map +1 -0
  48. package/dist/internal/durable-types.js +2 -0
  49. package/dist/internal/durable-types.js.map +1 -0
  50. package/dist/internal/route-compiler.d.ts +61 -0
  51. package/dist/internal/route-compiler.d.ts.map +1 -0
  52. package/dist/internal/route-compiler.js +121 -0
  53. package/dist/internal/route-compiler.js.map +1 -0
  54. package/dist/internal/route-execution.d.ts +17 -9
  55. package/dist/internal/route-execution.d.ts.map +1 -1
  56. package/dist/internal/route-execution.js +4 -18
  57. package/dist/internal/route-execution.js.map +1 -1
  58. package/dist/internal/route-lanes.d.ts +35 -0
  59. package/dist/internal/route-lanes.d.ts.map +1 -0
  60. package/dist/internal/route-lanes.js +150 -0
  61. package/dist/internal/route-lanes.js.map +1 -0
  62. package/dist/internal/route-program.d.ts +67 -0
  63. package/dist/internal/route-program.d.ts.map +1 -0
  64. package/dist/internal/route-program.js +199 -0
  65. package/dist/internal/route-program.js.map +1 -0
  66. package/dist/response-observer.d.ts +25 -0
  67. package/dist/response-observer.d.ts.map +1 -0
  68. package/dist/response-observer.js +40 -0
  69. package/dist/response-observer.js.map +1 -0
  70. package/dist/security/baseline.d.ts +68 -0
  71. package/dist/security/baseline.d.ts.map +1 -0
  72. package/dist/security/baseline.js +118 -0
  73. package/dist/security/baseline.js.map +1 -0
  74. package/dist/server/bare-error-lane.d.ts +26 -0
  75. package/dist/server/bare-error-lane.d.ts.map +1 -0
  76. package/dist/server/bare-error-lane.js +60 -0
  77. package/dist/server/bare-error-lane.js.map +1 -0
  78. package/dist/server/body-lane.d.ts +13 -0
  79. package/dist/server/body-lane.d.ts.map +1 -0
  80. package/dist/server/body-lane.js +134 -0
  81. package/dist/server/body-lane.js.map +1 -0
  82. package/dist/server/body.d.ts +7 -0
  83. package/dist/server/body.d.ts.map +1 -1
  84. package/dist/server/body.js +7 -0
  85. package/dist/server/body.js.map +1 -1
  86. package/dist/server/bridge-markers.d.ts +25 -0
  87. package/dist/server/bridge-markers.d.ts.map +1 -0
  88. package/dist/server/bridge-markers.js +25 -0
  89. package/dist/server/bridge-markers.js.map +1 -0
  90. package/dist/server/context.d.ts +6 -1
  91. package/dist/server/context.d.ts.map +1 -1
  92. package/dist/server/contract.d.ts +40 -14
  93. package/dist/server/contract.d.ts.map +1 -1
  94. package/dist/server/contract.js +7 -0
  95. package/dist/server/contract.js.map +1 -1
  96. package/dist/server/cookies.d.ts.map +1 -1
  97. package/dist/server/cookies.js +16 -2
  98. package/dist/server/cookies.js.map +1 -1
  99. package/dist/server/header-case.d.ts.map +1 -1
  100. package/dist/server/header-case.js +2 -1
  101. package/dist/server/header-case.js.map +1 -1
  102. package/dist/server/http.d.ts +36 -3
  103. package/dist/server/http.d.ts.map +1 -1
  104. package/dist/server/http.js +49 -3
  105. package/dist/server/http.js.map +1 -1
  106. package/dist/server/install.d.ts +6 -8
  107. package/dist/server/install.d.ts.map +1 -1
  108. package/dist/server/install.js +7 -0
  109. package/dist/server/install.js.map +1 -1
  110. package/dist/server/node-outcome-hook.d.ts +2 -1
  111. package/dist/server/node-outcome-hook.d.ts.map +1 -1
  112. package/dist/server/node-outcome-hook.js.map +1 -1
  113. package/dist/server/node-outcome.d.ts +17 -2
  114. package/dist/server/node-outcome.d.ts.map +1 -1
  115. package/dist/server/node-outcome.js +37 -3
  116. package/dist/server/node-outcome.js.map +1 -1
  117. package/dist/server/plugin.d.ts +2 -2
  118. package/dist/server/plugin.d.ts.map +1 -1
  119. package/dist/server/plugin.js +6 -1
  120. package/dist/server/plugin.js.map +1 -1
  121. package/dist/server/query.d.ts +7 -1
  122. package/dist/server/query.d.ts.map +1 -1
  123. package/dist/server/query.js +8 -3
  124. package/dist/server/query.js.map +1 -1
  125. package/dist/server/registry.d.ts +40 -12
  126. package/dist/server/registry.d.ts.map +1 -1
  127. package/dist/server/request-context.d.ts +2 -13
  128. package/dist/server/request-context.d.ts.map +1 -1
  129. package/dist/server/request-context.js +18 -159
  130. package/dist/server/request-context.js.map +1 -1
  131. package/dist/server/respond.d.ts.map +1 -1
  132. package/dist/server/respond.js +58 -4
  133. package/dist/server/respond.js.map +1 -1
  134. package/dist/server/response-contract-lane.d.ts.map +1 -1
  135. package/dist/server/response-contract-lane.js +5 -2
  136. package/dist/server/response-contract-lane.js.map +1 -1
  137. package/dist/server/response-observer-runtime.d.ts +20 -0
  138. package/dist/server/response-observer-runtime.d.ts.map +1 -0
  139. package/dist/server/response-observer-runtime.js +95 -0
  140. package/dist/server/response-observer-runtime.js.map +1 -0
  141. package/dist/server/runtime-core.d.ts +76 -0
  142. package/dist/server/runtime-core.d.ts.map +1 -1
  143. package/dist/server/runtime-core.js +42 -1
  144. package/dist/server/runtime-core.js.map +1 -1
  145. package/dist/server/server-types.d.ts +6 -1
  146. package/dist/server/server-types.d.ts.map +1 -1
  147. package/dist/server/server.d.ts +158 -116
  148. package/dist/server/server.d.ts.map +1 -1
  149. package/dist/server/server.js +707 -844
  150. package/dist/server/server.js.map +1 -1
  151. package/dist/server/validation.d.ts +5 -0
  152. package/dist/server/validation.d.ts.map +1 -0
  153. package/dist/server/validation.js +13 -0
  154. package/dist/server/validation.js.map +1 -0
  155. package/dist/server/websocket.d.ts +20 -4
  156. package/dist/server/websocket.d.ts.map +1 -1
  157. package/dist/server/websocket.js +81 -11
  158. package/dist/server/websocket.js.map +1 -1
  159. package/dist/server/ws-bun.d.ts +12 -4
  160. package/dist/server/ws-bun.d.ts.map +1 -1
  161. package/dist/server/ws-bun.js +50 -16
  162. package/dist/server/ws-bun.js.map +1 -1
  163. package/dist/server/ws-hook.d.ts +3 -2
  164. package/dist/server/ws-hook.d.ts.map +1 -1
  165. package/dist/server.d.ts +3 -2
  166. package/dist/server.d.ts.map +1 -1
  167. package/dist/server.js +2 -1
  168. package/dist/server.js.map +1 -1
  169. package/dist/single-copy-register.d.ts +2 -0
  170. package/dist/single-copy-register.d.ts.map +1 -0
  171. package/dist/single-copy-register.js +24 -0
  172. package/dist/single-copy-register.js.map +1 -0
  173. package/dist/single-copy.d.ts +156 -0
  174. package/dist/single-copy.d.ts.map +1 -0
  175. package/dist/single-copy.js +472 -0
  176. package/dist/single-copy.js.map +1 -0
  177. package/package.json +28 -3
  178. package/src/assurance.ts +58 -1
  179. package/src/durable-adapters.ts +20 -0
  180. package/src/durable-approval.ts +281 -0
  181. package/src/durable-effect.ts +300 -0
  182. package/src/durable-execution.ts +8 -1627
  183. package/src/durable-saga.ts +744 -0
  184. package/src/edge-kit.ts +27 -0
  185. package/src/errors.ts +1 -0
  186. package/src/index.ts +129 -2
  187. package/src/internal/durable-approval-protocol.ts +1 -1
  188. package/src/internal/durable-effect-protocol.ts +1 -1
  189. package/src/internal/durable-saga-protocol.ts +1 -1
  190. package/src/internal/durable-shared.ts +157 -0
  191. package/src/internal/durable-types.ts +194 -0
  192. package/src/internal/route-compiler.ts +234 -0
  193. package/src/internal/route-execution.ts +53 -96
  194. package/src/internal/route-lanes.ts +221 -0
  195. package/src/internal/route-program.ts +404 -0
  196. package/src/response-observer.ts +64 -0
  197. package/src/security/baseline.ts +172 -0
  198. package/src/server/bare-error-lane.ts +77 -0
  199. package/src/server/body-lane.ts +203 -0
  200. package/src/server/body.ts +8 -0
  201. package/src/server/bridge-markers.ts +24 -0
  202. package/src/server/context.ts +6 -1
  203. package/src/server/contract.ts +70 -20
  204. package/src/server/cookies.ts +16 -2
  205. package/src/server/header-case.ts +3 -1
  206. package/src/server/http.ts +53 -3
  207. package/src/server/install.ts +14 -0
  208. package/src/server/node-outcome-hook.ts +2 -1
  209. package/src/server/node-outcome.ts +44 -3
  210. package/src/server/plugin.ts +11 -5
  211. package/src/server/query.ts +10 -4
  212. package/src/server/registry.ts +71 -16
  213. package/src/server/request-context.ts +13 -216
  214. package/src/server/respond.ts +71 -3
  215. package/src/server/response-contract-lane.ts +6 -2
  216. package/src/server/response-observer-runtime.ts +135 -0
  217. package/src/server/runtime-core.ts +124 -1
  218. package/src/server/server-types.ts +6 -1
  219. package/src/server/server.ts +1446 -1485
  220. package/src/server/validation.ts +19 -0
  221. package/src/server/websocket.ts +127 -17
  222. package/src/server/ws-bun.ts +67 -15
  223. package/src/server/ws-hook.ts +3 -2
  224. package/src/server.ts +8 -2
  225. package/src/single-copy-register.ts +24 -0
  226. package/src/single-copy.ts +526 -0
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Registration-time route compilation.
3
+ *
4
+ * This module owns the policy decisions that turn a route declaration into the immutable facts the
5
+ * execution-plan builder and the route catalog consume. It deliberately runs only from `Server.register`
6
+ * and `Server.registerBatch`; no function here is reachable from the request path.
7
+ *
8
+ * The public Server remains the fluent registration surface. This module is the deep implementation seam
9
+ * behind that surface: body limits, capability and assurance evidence, optional runtimes, decorations, and
10
+ * lane selection are resolved once, together, so adding a new route policy does not require editing the
11
+ * kernel's dispatch code.
12
+ */
13
+ import type { EffectLifecycleObserver } from "../effect-lifecycle.ts"
14
+ import { RouteConfigError } from "../errors.ts"
15
+ import { type CompiledRoutePattern, compileRoutePattern } from "../router/pattern.ts"
16
+ import type { Method } from "../router/router.ts"
17
+ import type { StandardSchemaV1 } from "../schema/standard.ts"
18
+ import { assertByteLimit } from "../server/body.ts"
19
+ import type { RouteSchema } from "../server/context.ts"
20
+ import type { IdempotencyRuntime, ResolvedIdempotency } from "../server/idempotency-lane.ts"
21
+ import type { EffectLedgerRuntime, ResolvedEffectLedger } from "../server/ledger-lane.ts"
22
+ import type { ResponseContractRuntime } from "../server/response-contract-lane.ts"
23
+ import {
24
+ CAPABILITY_GUARD,
25
+ type CapabilityUseEvent,
26
+ createCapabilityGuard,
27
+ normalizeRouteCapabilities,
28
+ type RegisteredCapabilityInterceptor,
29
+ } from "./capability-runtime.ts"
30
+ import {
31
+ type AssuranceDeclaration,
32
+ assuranceDeclarationsOf,
33
+ assuranceEvidenceFor,
34
+ NIFRA_ASSURANCE_IDS,
35
+ validEvidenceId,
36
+ } from "./route-assurance.ts"
37
+ import type {
38
+ RawAfterHandle,
39
+ RawAround,
40
+ RawBeforeHandle,
41
+ RawDerive,
42
+ RawErrorHandler,
43
+ } from "./route-execution.ts"
44
+ import { type RouteLaneSelection, selectRouteLanes } from "./route-lanes.ts"
45
+
46
+ export interface RouteCompilerContext {
47
+ readonly maxBodyBytes: number
48
+ readonly activeAssurance: readonly AssuranceDeclaration[]
49
+ readonly globalAssurance: readonly AssuranceDeclaration[]
50
+ readonly decorations: Record<string, unknown>
51
+ readonly onCapabilityUse: ((event: CapabilityUseEvent) => void) | undefined
52
+ readonly capabilityInterceptors: readonly RegisteredCapabilityInterceptor[]
53
+ readonly capabilityObservers: readonly EffectLifecycleObserver[]
54
+ readonly effectLedgerRuntime: EffectLedgerRuntime | undefined
55
+ readonly idempotencyRuntime: IdempotencyRuntime | undefined
56
+ readonly responseContractRuntime: ResponseContractRuntime | undefined
57
+ readonly derives: readonly RawDerive[]
58
+ readonly beforeHandleHooks: readonly RawBeforeHandle[]
59
+ readonly afterHandleHooks: readonly RawAfterHandle[]
60
+ readonly onErrorHooks: readonly RawErrorHandler[]
61
+ readonly aroundHooks: readonly RawAround[]
62
+ readonly defaultOnValidationError: RouteSchema["onValidationError"] | undefined
63
+ }
64
+
65
+ export interface CompiledRouteOptions {
66
+ readonly pattern: CompiledRoutePattern
67
+ readonly bodyLimit: number | undefined
68
+ readonly capabilities: readonly string[]
69
+ readonly handlerAssurance: readonly AssuranceDeclaration[]
70
+ readonly routeDecorations: Record<PropertyKey, unknown>
71
+ readonly hasDecorations: boolean
72
+ readonly idempotent: ResolvedIdempotency | undefined
73
+ readonly ledgered: ResolvedEffectLedger | undefined
74
+ readonly responseContract:
75
+ | { readonly runtime: ResponseContractRuntime; readonly schema: StandardSchemaV1 }
76
+ | undefined
77
+ readonly lanes: RouteLaneSelection
78
+ readonly routeAssurance: readonly AssuranceDeclaration[]
79
+ }
80
+
81
+ /** Resolve all registration-time policy and lane facts for one route. */
82
+ export function compileRouteOptions(
83
+ context: RouteCompilerContext,
84
+ method: Method,
85
+ path: string,
86
+ schema: RouteSchema | undefined,
87
+ handler: (context: never) => unknown,
88
+ ): CompiledRouteOptions {
89
+ const pattern = compileRoutePattern(path)
90
+ let bodyLimit: number | undefined = context.maxBodyBytes
91
+ const invalidBodyLimit = (message: string): never => {
92
+ throw new RouteConfigError("INVALID_BODY_LIMIT", `route ${method} ${path}: ${message}`)
93
+ }
94
+ if (schema?.bodyLimitReason !== undefined && schema.bodyLimit !== "unlimited") {
95
+ invalidBodyLimit('bodyLimitReason is only valid with bodyLimit: "unlimited"')
96
+ }
97
+ if (schema?.bodyLimit === "unlimited") {
98
+ if (typeof schema.bodyLimitReason !== "string" || schema.bodyLimitReason.trim().length === 0) {
99
+ invalidBodyLimit('bodyLimit: "unlimited" requires a non-empty bodyLimitReason')
100
+ }
101
+ if (schema.body !== undefined) {
102
+ invalidBodyLimit('bodyLimit: "unlimited" cannot be used with a body schema; use a finite cap')
103
+ }
104
+ if (schema.idempotency !== undefined) {
105
+ invalidBodyLimit('bodyLimit: "unlimited" cannot be used with idempotency')
106
+ }
107
+ bodyLimit = undefined
108
+ } else if (schema?.bodyLimit !== undefined) {
109
+ try {
110
+ assertByteLimit(schema.bodyLimit, "route bodyLimit")
111
+ } catch {
112
+ invalidBodyLimit('bodyLimit must be a non-negative safe integer or "unlimited"')
113
+ }
114
+ bodyLimit = schema.bodyLimit
115
+ }
116
+
117
+ const capabilities = normalizeRouteCapabilities(schema?.capabilities)
118
+ const handlerAssurance = assuranceDeclarationsOf(handler as unknown as object)
119
+ const invalidHandlerScope = handlerAssurance.find((declaration) => declaration.scope !== "plugin")
120
+ if (invalidHandlerScope !== undefined) {
121
+ throw new RouteConfigError(
122
+ "INVALID_ASSURANCE",
123
+ `route handler assurance must use plugin scope (received ${invalidHandlerScope.scope})`,
124
+ )
125
+ }
126
+
127
+ const authenticated = assuranceEvidenceFor(
128
+ [...context.activeAssurance, ...handlerAssurance, ...context.globalAssurance],
129
+ method,
130
+ path,
131
+ ).some((evidence) => evidence.id === NIFRA_ASSURANCE_IDS.AUTHENTICATED)
132
+ const routeDecorations: Record<PropertyKey, unknown> = { ...context.decorations }
133
+ if (capabilities.length > 0) {
134
+ routeDecorations[CAPABILITY_GUARD] = createCapabilityGuard(
135
+ capabilities,
136
+ method,
137
+ path,
138
+ context.onCapabilityUse,
139
+ context.idempotencyRuntime?.trackEffect,
140
+ Object.freeze([...context.capabilityInterceptors]),
141
+ Object.freeze([...context.capabilityObservers]),
142
+ )
143
+ }
144
+ const hasDecorations = Reflect.ownKeys(routeDecorations).length > 0
145
+
146
+ if (schema?.idempotency !== undefined && context.idempotencyRuntime === undefined) {
147
+ throw new RouteConfigError(
148
+ "INVALID_IDEMPOTENCY",
149
+ "route declares idempotency but the idempotency plugin is not installed; add .use(idempotency())",
150
+ )
151
+ }
152
+ const idempotent = context.idempotencyRuntime?.resolve(
153
+ schema,
154
+ authenticated,
155
+ context.maxBodyBytes,
156
+ )
157
+ const ledgered = context.effectLedgerRuntime?.resolve(capabilities, method, path)
158
+ const contracted =
159
+ context.responseContractRuntime !== undefined && schema?.response !== undefined
160
+ ? { runtime: context.responseContractRuntime, schema: schema.response }
161
+ : undefined
162
+ const lanes = selectRouteLanes({
163
+ schema,
164
+ hasIdempotency: idempotent !== undefined,
165
+ hasLedger: ledgered !== undefined,
166
+ hasResponseContract: contracted !== undefined,
167
+ hasDecorations,
168
+ derives: context.derives.length,
169
+ beforeHandle: context.beforeHandleHooks.length,
170
+ afterHandle: context.afterHandleHooks.length,
171
+ onError: context.onErrorHooks.length,
172
+ around: context.aroundHooks.length,
173
+ defaultOnValidationError: context.defaultOnValidationError !== undefined,
174
+ })
175
+
176
+ const routeAssurance: AssuranceDeclaration[] = [...context.activeAssurance, ...handlerAssurance]
177
+ if (contracted?.runtime.mode === "enforce" && schema?.response !== undefined) {
178
+ routeAssurance.push(
179
+ Object.freeze({
180
+ id: NIFRA_ASSURANCE_IDS.RESPONSE_CONTRACT,
181
+ source: "response-contract",
182
+ scope: "plugin",
183
+ }),
184
+ )
185
+ }
186
+ for (const id of schema?.assurance ?? []) {
187
+ if (!validEvidenceId(id)) {
188
+ throw new Error(
189
+ `route assurance: invalid evidence id ${JSON.stringify(id)} on ${method} ${path} (use lowercase dot/dash segments)`,
190
+ )
191
+ }
192
+ const declared = { id, source: "declared", scope: "plugin" } as AssuranceDeclaration
193
+ Object.defineProperty(declared, "provenance", {
194
+ value: "declared",
195
+ enumerable: false,
196
+ configurable: false,
197
+ writable: false,
198
+ })
199
+ routeAssurance.push(Object.freeze(declared))
200
+ }
201
+ const unlimitedBody = schema?.bodyLimit === "unlimited"
202
+ if (!unlimitedBody && (schema?.body !== undefined || typeof schema?.bodyLimit === "number")) {
203
+ routeAssurance.push(
204
+ Object.freeze({
205
+ id: NIFRA_ASSURANCE_IDS.BODY_BOUNDED,
206
+ source: "route-schema",
207
+ scope: "plugin",
208
+ }),
209
+ )
210
+ }
211
+ if (schema?.idempotency !== undefined) {
212
+ routeAssurance.push(
213
+ Object.freeze({
214
+ id: NIFRA_ASSURANCE_IDS.IDEMPOTENCY_KEY,
215
+ source: "route-schema",
216
+ scope: "plugin",
217
+ }),
218
+ )
219
+ }
220
+
221
+ return {
222
+ pattern,
223
+ bodyLimit,
224
+ capabilities,
225
+ handlerAssurance,
226
+ routeDecorations,
227
+ hasDecorations,
228
+ idempotent,
229
+ ledgered,
230
+ responseContract: contracted,
231
+ lanes,
232
+ routeAssurance: Object.freeze(routeAssurance),
233
+ }
234
+ }
@@ -19,6 +19,7 @@
19
19
  import type { RequestBudget } from "../budget.ts"
20
20
  import type { RequestLedger } from "../ledger.ts"
21
21
  import type { StandardSchemaV1 } from "../schema/standard.ts"
22
+ import { UNLIMITED_BODY_BYTES } from "../server/body.ts"
22
23
  import type { Platform, RouteSchema } from "../server/context.ts"
23
24
  import type { ResolvedIdempotency } from "../server/idempotency-lane.ts"
24
25
  import type { EffectLedgerRuntime, ResolvedEffectLedger } from "../server/ledger-lane.ts"
@@ -26,8 +27,9 @@ import type { ProtoPoisoning } from "../server/proto-guard.ts"
26
27
  import type { Registry } from "../server/registry.ts"
27
28
  import { RequestContext } from "../server/request-context.ts"
28
29
  import type { ResponseContractRuntime } from "../server/response-contract-lane.ts"
29
- import type { HandlerResult } from "../server/runtime-core.ts"
30
+ import type { HandlerResult, ResponseResult } from "../server/runtime-core.ts"
30
31
  import type { CtxSet, MaybePromise, RawContext, RequestSource, Server } from "../server/server.ts"
32
+ import type { RouteProgram } from "./route-program.ts"
31
33
 
32
34
  export type InternalHandler = (ctx: RawContext) => MaybePromise<HandlerResult>
33
35
 
@@ -49,7 +51,7 @@ export type RouteExecutionRunner = <T, R extends Registry, Ctx>(
49
51
  platform: Platform | undefined,
50
52
  nativeContext: boolean,
51
53
  finalize: (result: unknown, set: CtxSet) => T,
52
- wrapResponse: (response: Response) => T,
54
+ wrapResponse: (response: Response | ResponseResult) => T,
53
55
  ) => MaybePromise<T>
54
56
 
55
57
  export type ContextRouteRunner = <T, R extends Registry, Ctx>(
@@ -58,7 +60,7 @@ export type ContextRouteRunner = <T, R extends Registry, Ctx>(
58
60
  source: RequestSource,
59
61
  ctx: RawContext,
60
62
  finalize: (result: unknown, set: CtxSet) => T,
61
- wrapResponse: (response: Response) => T,
63
+ wrapResponse: (response: Response | ResponseResult) => T,
62
64
  ) => MaybePromise<T>
63
65
 
64
66
  /** The kernel's private execution surface, mirrored structurally. The runner methods are private on
@@ -76,66 +78,40 @@ interface RouteExecutionRuntime {
76
78
  budget: RequestBudget,
77
79
  platform: Platform | undefined,
78
80
  finalize: (result: unknown, set: CtxSet) => T,
79
- wrapResponse: (response: Response) => T,
81
+ wrapResponse: (response: Response | ResponseResult) => T,
80
82
  ): MaybePromise<T>
81
83
  runBare<T>(
82
84
  entry: RouteEntry,
83
85
  ctx: RawContext,
84
86
  finalize: (result: unknown, set: CtxSet) => T,
85
- wrapResponse: (response: Response) => T,
87
+ wrapResponse: (response: Response | ResponseResult) => T,
86
88
  ): MaybePromise<T>
87
89
  runBodyOnly<T>(
88
90
  entry: RouteEntry,
89
91
  source: RequestSource,
90
92
  ctx: RawContext,
91
93
  finalize: (result: unknown, set: CtxSet) => T,
92
- wrapResponse: (response: Response) => T,
94
+ wrapResponse: (response: Response | ResponseResult) => T,
93
95
  ): MaybePromise<T>
94
96
  runQueryOnly<T>(
95
97
  entry: RouteEntry,
96
98
  ctx: RawContext,
97
99
  finalize: (result: unknown, set: CtxSet) => T,
98
- wrapResponse: (response: Response) => T,
100
+ wrapResponse: (response: Response | ResponseResult) => T,
99
101
  ): MaybePromise<T>
100
- runLifecycleHooks<T>(
101
- entry: RouteEntry,
102
- ctx: RawContext,
103
- finalize: (result: unknown, set: CtxSet) => T,
104
- wrapResponse: (response: Response) => T,
105
- ): MaybePromise<T>
106
- runLifecycleQuery<T>(
107
- entry: RouteEntry,
108
- ctx: RawContext,
109
- finalize: (result: unknown, set: CtxSet) => T,
110
- wrapResponse: (response: Response) => T,
111
- ): MaybePromise<T>
112
- runLifecycleBody<T>(
102
+ runProgram<T>(
113
103
  entry: RouteEntry,
114
104
  source: RequestSource,
115
105
  ctx: RawContext,
116
106
  finalize: (result: unknown, set: CtxSet) => T,
117
- wrapResponse: (response: Response) => T,
118
- ): MaybePromise<T>
119
- runLifecycleBodyQuery<T>(
120
- entry: RouteEntry,
121
- source: RequestSource,
122
- ctx: RawContext,
123
- finalize: (result: unknown, set: CtxSet) => T,
124
- wrapResponse: (response: Response) => T,
125
- ): MaybePromise<T>
126
- runLifecycle<T>(
127
- entry: RouteEntry,
128
- source: RequestSource,
129
- ctx: RawContext,
130
- finalize: (result: unknown, set: CtxSet) => T,
131
- wrapResponse: (response: Response) => T,
107
+ wrapResponse: (response: Response | ResponseResult) => T,
132
108
  ): MaybePromise<T>
133
109
  runWithAround<T>(
134
110
  entry: RouteEntry,
135
111
  ctx: RawContext,
136
112
  run: () => MaybePromise<T>,
137
113
  finalize: (result: unknown, set: CtxSet) => T,
138
- wrapResponse: (response: Response) => T,
114
+ wrapResponse: (response: Response | ResponseResult) => T,
139
115
  ): MaybePromise<T>
140
116
  readonly maxBodyBytes: number
141
117
  readonly protoPoisoning: ProtoPoisoning
@@ -157,13 +133,19 @@ export function compileRouteExecutionPlan(options: {
157
133
  readonly contextless: boolean
158
134
  readonly hasAround: boolean
159
135
  readonly hasLedger: boolean
160
- readonly lifecycleLane: LifecycleExecutionLane
161
136
  readonly fusedWeb: FusedWebRunner | undefined
162
137
  readonly fusedBody: FusedBodyRunner | undefined
163
- readonly fusedLane: "bare" | "body" | "query" | undefined
138
+ readonly fusedLane:
139
+ | "bare"
140
+ | "body"
141
+ | "query"
142
+ | "derive-before"
143
+ | "derive-before-after"
144
+ | "body-derive-before"
145
+ | "body-derive-before-after"
146
+ | undefined
164
147
  }): RouteExecutionPlan {
165
- const { lane, contextless, hasAround, hasLedger, lifecycleLane, fusedWeb, fusedBody, fusedLane } =
166
- options
148
+ const { lane, contextless, hasAround, hasLedger, fusedWeb, fusedBody, fusedLane } = options
167
149
 
168
150
  if (contextless) {
169
151
  const run: RouteExecutionRunner = (
@@ -190,7 +172,7 @@ export function compileRouteExecutionPlan(options: {
190
172
  finalize,
191
173
  wrapResponse,
192
174
  )
193
- return Object.freeze({ run, fusedWeb, fusedBody, fusedLane })
175
+ return Object.freeze({ run, fusedWeb, fusedBody, fusedLane }) as RouteExecutionPlan
194
176
  }
195
177
 
196
178
  let inner: ContextRouteRunner
@@ -219,55 +201,14 @@ export function compileRouteExecutionPlan(options: {
219
201
  )
220
202
  break
221
203
  default:
222
- switch (lifecycleLane) {
223
- case "hooks":
224
- inner = (runtime, entry, _source, ctx, finalize, wrapResponse) =>
225
- (runtime as unknown as RouteExecutionRuntime).runLifecycleHooks(
226
- entry,
227
- ctx,
228
- finalize,
229
- wrapResponse,
230
- )
231
- break
232
- case "query":
233
- inner = (runtime, entry, _source, ctx, finalize, wrapResponse) =>
234
- (runtime as unknown as RouteExecutionRuntime).runLifecycleQuery(
235
- entry,
236
- ctx,
237
- finalize,
238
- wrapResponse,
239
- )
240
- break
241
- case "body":
242
- inner = (runtime, entry, source, ctx, finalize, wrapResponse) =>
243
- (runtime as unknown as RouteExecutionRuntime).runLifecycleBody(
244
- entry,
245
- source,
246
- ctx,
247
- finalize,
248
- wrapResponse,
249
- )
250
- break
251
- case "body-query":
252
- inner = (runtime, entry, source, ctx, finalize, wrapResponse) =>
253
- (runtime as unknown as RouteExecutionRuntime).runLifecycleBodyQuery(
254
- entry,
255
- source,
256
- ctx,
257
- finalize,
258
- wrapResponse,
259
- )
260
- break
261
- default:
262
- inner = (runtime, entry, source, ctx, finalize, wrapResponse) =>
263
- (runtime as unknown as RouteExecutionRuntime).runLifecycle(
264
- entry,
265
- source,
266
- ctx,
267
- finalize,
268
- wrapResponse,
269
- )
270
- }
204
+ inner = (runtime, entry, source, ctx, finalize, wrapResponse) =>
205
+ (runtime as unknown as RouteExecutionRuntime).runProgram(
206
+ entry,
207
+ source,
208
+ ctx,
209
+ finalize,
210
+ wrapResponse,
211
+ )
271
212
  }
272
213
 
273
214
  const execute: ContextRouteRunner = hasAround
@@ -295,7 +236,7 @@ export function compileRouteExecutionPlan(options: {
295
236
  wrapResponse,
296
237
  ) => {
297
238
  const server = runtime as unknown as RouteExecutionRuntime
298
- const maxBodyBytes = entry.bodyLimit ?? server.maxBodyBytes
239
+ const maxBodyBytes = entry.bodyLimit ?? UNLIMITED_BODY_BYTES
299
240
  const ctx = nativeContext
300
241
  ? RequestContext.native(source, params, search, maxBodyBytes, platform, server.protoPoisoning)
301
242
  : new RequestContext(
@@ -328,7 +269,7 @@ export function compileRouteExecutionPlan(options: {
328
269
  }
329
270
  return outcome
330
271
  }
331
- return Object.freeze({ run, fusedWeb, fusedBody, fusedLane })
272
+ return Object.freeze({ run, fusedWeb, fusedBody, fusedLane }) as RouteExecutionPlan
332
273
  }
333
274
 
334
275
  /** Registration-compiled route behavior. Every adapter invokes the same runner; the optional fused
@@ -340,13 +281,27 @@ export interface RouteExecutionPlan {
340
281
  /** Which builder produced {@link fusedWeb} - a merge rebinds the closure to the executing server
341
282
  * and must rebuild it with the SAME semantics (a query-fused route rebuilt as bare would skip its
342
283
  * validation). `undefined` iff `fusedWeb` is. */
343
- readonly fusedLane: "bare" | "body" | "query" | undefined
284
+ readonly fusedLane:
285
+ | "bare"
286
+ | "body"
287
+ | "query"
288
+ | "derive-before"
289
+ | "derive-before-after"
290
+ | "body-derive-before"
291
+ | "body-derive-before-after"
292
+ | undefined
344
293
  }
345
294
 
346
295
  export interface RouteEntry {
347
296
  readonly handler: InternalHandler
348
297
  readonly schema: RouteSchema | undefined
349
- /** Effective transport body cap; `undefined` is an explicit streaming/upload exemption. */
298
+ /**
299
+ * Effective transport body cap, already resolved at registration: the route's own
300
+ * `schema.bodyLimit` when it declared one, the server's `maxBodyBytes` otherwise. `undefined` is
301
+ * an explicit streaming/upload exemption (`bodyLimit: "unlimited"`) and means NO cap - readers
302
+ * must fall back to {@link UNLIMITED_BODY_BYTES}, never to the server default, which would be a
303
+ * tighter bound than the route asked for.
304
+ */
350
305
  readonly bodyLimit: number | undefined
351
306
  /** Resolved idempotency config; `undefined` = off (the dedupe lane is never entered). */
352
307
  readonly idempotent: ResolvedIdempotency | undefined
@@ -370,11 +325,13 @@ export interface RouteEntry {
370
325
  readonly afterHandle: ReadonlyArray<RawAfterHandle>
371
326
  readonly onError: ReadonlyArray<RawErrorHandler>
372
327
  /** Registration-specialized hook shape for the common derive + before middleware route. */
373
- readonly lifecycleHookLane: "derive-before" | undefined
328
+ readonly lifecycleHookLane: "derive-before" | "derive-before-after" | undefined
374
329
  /** Wraps the matched route lifecycle. Empty for the common no-around path. */
375
330
  readonly around: ReadonlyArray<RawAround>
376
331
  /** The single immutable execution decision consumed by portable, Node-direct, and Bun-native paths. */
377
332
  readonly execution: RouteExecutionPlan
333
+ /** One immutable general lifecycle program compiled at registration. */
334
+ readonly program: RouteProgram
378
335
  }
379
336
 
380
337
  /** The fused Web lane: same inputs `routeAndRun` would hand the generic path, a `Response` out. */
@@ -399,5 +356,5 @@ export type FusedBodyRunner = <T>(
399
356
  platform: Platform | undefined,
400
357
  nativeContext: boolean,
401
358
  finalize: (result: unknown, set: CtxSet, ctx: RawContext) => T,
402
- wrapResponse: (response: Response) => T,
359
+ wrapResponse: (response: Response | ResponseResult) => T,
403
360
  ) => MaybePromise<T>