@nifrajs/core 3.3.0 → 4.0.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 (331) hide show
  1. package/README.md +26 -0
  2. package/dist/assurance.d.ts +7 -0
  3. package/dist/assurance.d.ts.map +1 -1
  4. package/dist/assurance.js +39 -3
  5. package/dist/assurance.js.map +1 -1
  6. package/dist/capabilities.d.ts +20 -2
  7. package/dist/capabilities.d.ts.map +1 -1
  8. package/dist/capabilities.js +47 -15
  9. package/dist/capabilities.js.map +1 -1
  10. package/dist/diff.d.ts +6 -1
  11. package/dist/diff.d.ts.map +1 -1
  12. package/dist/diff.js +36 -2
  13. package/dist/diff.js.map +1 -1
  14. package/dist/durable-adapters.d.ts.map +1 -1
  15. package/dist/durable-adapters.js +16 -3
  16. package/dist/durable-adapters.js.map +1 -1
  17. package/dist/durable-effect.d.ts.map +1 -1
  18. package/dist/durable-effect.js +6 -3
  19. package/dist/durable-effect.js.map +1 -1
  20. package/dist/edge-kit.d.ts +2 -0
  21. package/dist/edge-kit.d.ts.map +1 -1
  22. package/dist/edge-kit.js +2 -0
  23. package/dist/edge-kit.js.map +1 -1
  24. package/dist/effect-lifecycle.d.ts.map +1 -1
  25. package/dist/effect-lifecycle.js +2 -1
  26. package/dist/effect-lifecycle.js.map +1 -1
  27. package/dist/effect-scope.d.ts.map +1 -1
  28. package/dist/effect-scope.js +1 -1
  29. package/dist/effect-scope.js.map +1 -1
  30. package/dist/errors.d.ts +1 -1
  31. package/dist/errors.d.ts.map +1 -1
  32. package/dist/errors.js.map +1 -1
  33. package/dist/evidence.d.ts +36 -1
  34. package/dist/evidence.d.ts.map +1 -1
  35. package/dist/evidence.js +219 -13
  36. package/dist/evidence.js.map +1 -1
  37. package/dist/idempotency.d.ts +31 -0
  38. package/dist/idempotency.d.ts.map +1 -1
  39. package/dist/idempotency.js +48 -9
  40. package/dist/idempotency.js.map +1 -1
  41. package/dist/index.d.ts +9 -6
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +6 -3
  44. package/dist/index.js.map +1 -1
  45. package/dist/internal/code-unit-order.d.ts +7 -0
  46. package/dist/internal/code-unit-order.d.ts.map +1 -0
  47. package/dist/internal/code-unit-order.js +9 -0
  48. package/dist/internal/code-unit-order.js.map +1 -0
  49. package/dist/internal/durable-saga-protocol.d.ts.map +1 -1
  50. package/dist/internal/durable-saga-protocol.js +2 -0
  51. package/dist/internal/durable-saga-protocol.js.map +1 -1
  52. package/dist/internal/route-catalog.d.ts +6 -6
  53. package/dist/internal/route-catalog.d.ts.map +1 -1
  54. package/dist/internal/route-catalog.js +20 -18
  55. package/dist/internal/route-catalog.js.map +1 -1
  56. package/dist/internal/route-compiler.d.ts +7 -6
  57. package/dist/internal/route-compiler.d.ts.map +1 -1
  58. package/dist/internal/route-compiler.js +36 -4
  59. package/dist/internal/route-compiler.js.map +1 -1
  60. package/dist/internal/route-execution.d.ts +7 -3
  61. package/dist/internal/route-execution.d.ts.map +1 -1
  62. package/dist/internal/route-execution.js.map +1 -1
  63. package/dist/internal/route-lanes.d.ts +1 -0
  64. package/dist/internal/route-lanes.d.ts.map +1 -1
  65. package/dist/internal/route-lanes.js +34 -29
  66. package/dist/internal/route-lanes.js.map +1 -1
  67. package/dist/internal/route-program.d.ts +8 -2
  68. package/dist/internal/route-program.d.ts.map +1 -1
  69. package/dist/internal/route-program.js +222 -20
  70. package/dist/internal/route-program.js.map +1 -1
  71. package/dist/ledger.d.ts.map +1 -1
  72. package/dist/ledger.js +8 -5
  73. package/dist/ledger.js.map +1 -1
  74. package/dist/manifest.d.ts +1 -1
  75. package/dist/manifest.d.ts.map +1 -1
  76. package/dist/manifest.js +52 -46
  77. package/dist/manifest.js.map +1 -1
  78. package/dist/mount.d.ts +36 -0
  79. package/dist/mount.d.ts.map +1 -1
  80. package/dist/mount.js +32 -0
  81. package/dist/mount.js.map +1 -1
  82. package/dist/reflection.d.ts +26 -0
  83. package/dist/reflection.d.ts.map +1 -1
  84. package/dist/reflection.js +65 -1
  85. package/dist/reflection.js.map +1 -1
  86. package/dist/response-observer.d.ts +1 -1
  87. package/dist/response-observer.d.ts.map +1 -1
  88. package/dist/response-observer.js.map +1 -1
  89. package/dist/router/overlap.d.ts +23 -0
  90. package/dist/router/overlap.d.ts.map +1 -0
  91. package/dist/router/overlap.js +224 -0
  92. package/dist/router/overlap.js.map +1 -0
  93. package/dist/router/pattern.d.ts +117 -2
  94. package/dist/router/pattern.d.ts.map +1 -1
  95. package/dist/router/pattern.js +283 -23
  96. package/dist/router/pattern.js.map +1 -1
  97. package/dist/router/router.d.ts +18 -4
  98. package/dist/router/router.d.ts.map +1 -1
  99. package/dist/router/router.js +51 -44
  100. package/dist/router/router.js.map +1 -1
  101. package/dist/server/auth.d.ts +53 -0
  102. package/dist/server/auth.d.ts.map +1 -0
  103. package/dist/server/auth.js +9 -0
  104. package/dist/server/auth.js.map +1 -0
  105. package/dist/server/bare-error-lane.d.ts +12 -7
  106. package/dist/server/bare-error-lane.d.ts.map +1 -1
  107. package/dist/server/bare-error-lane.js +13 -11
  108. package/dist/server/bare-error-lane.js.map +1 -1
  109. package/dist/server/body-lane.d.ts +20 -2
  110. package/dist/server/body-lane.d.ts.map +1 -1
  111. package/dist/server/body-lane.js +26 -7
  112. package/dist/server/body-lane.js.map +1 -1
  113. package/dist/server/body-parser.d.ts +68 -0
  114. package/dist/server/body-parser.d.ts.map +1 -0
  115. package/dist/server/body-parser.js +172 -0
  116. package/dist/server/body-parser.js.map +1 -0
  117. package/dist/server/body.d.ts.map +1 -1
  118. package/dist/server/body.js +62 -7
  119. package/dist/server/body.js.map +1 -1
  120. package/dist/server/client-ip.d.ts +8 -0
  121. package/dist/server/client-ip.d.ts.map +1 -1
  122. package/dist/server/client-ip.js +8 -0
  123. package/dist/server/client-ip.js.map +1 -1
  124. package/dist/server/context.d.ts +71 -6
  125. package/dist/server/context.d.ts.map +1 -1
  126. package/dist/server/contract.d.ts +15 -5
  127. package/dist/server/contract.d.ts.map +1 -1
  128. package/dist/server/contract.js +25 -4
  129. package/dist/server/contract.js.map +1 -1
  130. package/dist/server/cookies.d.ts +4 -1
  131. package/dist/server/cookies.d.ts.map +1 -1
  132. package/dist/server/cookies.js +40 -2
  133. package/dist/server/cookies.js.map +1 -1
  134. package/dist/server/core-copies.d.ts +2 -0
  135. package/dist/server/core-copies.d.ts.map +1 -0
  136. package/dist/server/core-copies.js +20 -0
  137. package/dist/server/core-copies.js.map +1 -0
  138. package/dist/server/edge.d.ts.map +1 -1
  139. package/dist/server/edge.js +2 -0
  140. package/dist/server/edge.js.map +1 -1
  141. package/dist/server/http.d.ts +11 -0
  142. package/dist/server/http.d.ts.map +1 -1
  143. package/dist/server/http.js +36 -0
  144. package/dist/server/http.js.map +1 -1
  145. package/dist/server/idempotency-lane.d.ts +6 -0
  146. package/dist/server/idempotency-lane.d.ts.map +1 -1
  147. package/dist/server/idempotency-lane.js +130 -73
  148. package/dist/server/idempotency-lane.js.map +1 -1
  149. package/dist/server/install.d.ts +4 -0
  150. package/dist/server/install.d.ts.map +1 -1
  151. package/dist/server/install.js +4 -0
  152. package/dist/server/install.js.map +1 -1
  153. package/dist/server/methods.d.ts +45 -0
  154. package/dist/server/methods.d.ts.map +1 -0
  155. package/dist/server/methods.js +37 -0
  156. package/dist/server/methods.js.map +1 -0
  157. package/dist/server/multipart.d.ts +63 -0
  158. package/dist/server/multipart.d.ts.map +1 -0
  159. package/dist/server/multipart.js +374 -0
  160. package/dist/server/multipart.js.map +1 -0
  161. package/dist/server/node-outcome-hook.d.ts +20 -0
  162. package/dist/server/node-outcome-hook.d.ts.map +1 -1
  163. package/dist/server/node-outcome-hook.js +15 -6
  164. package/dist/server/node-outcome-hook.js.map +1 -1
  165. package/dist/server/not-found-answer.d.ts +40 -0
  166. package/dist/server/not-found-answer.d.ts.map +1 -0
  167. package/dist/server/not-found-answer.js +56 -0
  168. package/dist/server/not-found-answer.js.map +1 -0
  169. package/dist/server/not-found.d.ts +23 -0
  170. package/dist/server/not-found.d.ts.map +1 -0
  171. package/dist/server/not-found.js +81 -0
  172. package/dist/server/not-found.js.map +1 -0
  173. package/dist/server/plugin.d.ts +26 -1
  174. package/dist/server/plugin.d.ts.map +1 -1
  175. package/dist/server/plugin.js.map +1 -1
  176. package/dist/server/proto-guard.d.ts +18 -5
  177. package/dist/server/proto-guard.d.ts.map +1 -1
  178. package/dist/server/proto-guard.js +114 -9
  179. package/dist/server/proto-guard.js.map +1 -1
  180. package/dist/server/query.d.ts +7 -0
  181. package/dist/server/query.d.ts.map +1 -1
  182. package/dist/server/query.js +47 -0
  183. package/dist/server/query.js.map +1 -1
  184. package/dist/server/registry.d.ts +13 -2
  185. package/dist/server/registry.d.ts.map +1 -1
  186. package/dist/server/request-context.d.ts +11 -0
  187. package/dist/server/request-context.d.ts.map +1 -1
  188. package/dist/server/request-context.js +33 -19
  189. package/dist/server/request-context.js.map +1 -1
  190. package/dist/server/request-lineage.d.ts +8 -0
  191. package/dist/server/request-lineage.d.ts.map +1 -0
  192. package/dist/server/request-lineage.js +14 -0
  193. package/dist/server/request-lineage.js.map +1 -0
  194. package/dist/server/request-target.d.ts +31 -0
  195. package/dist/server/request-target.d.ts.map +1 -0
  196. package/dist/server/request-target.js +44 -0
  197. package/dist/server/request-target.js.map +1 -0
  198. package/dist/server/respond.d.ts +1 -1
  199. package/dist/server/respond.d.ts.map +1 -1
  200. package/dist/server/respond.js +16 -4
  201. package/dist/server/respond.js.map +1 -1
  202. package/dist/server/response-contract-lane.d.ts +19 -1
  203. package/dist/server/response-contract-lane.d.ts.map +1 -1
  204. package/dist/server/response-contract-lane.js +59 -14
  205. package/dist/server/response-contract-lane.js.map +1 -1
  206. package/dist/server/response-observer-runtime.d.ts +9 -5
  207. package/dist/server/response-observer-runtime.d.ts.map +1 -1
  208. package/dist/server/response-observer-runtime.js +10 -6
  209. package/dist/server/response-observer-runtime.js.map +1 -1
  210. package/dist/server/runtime-core.d.ts +2 -0
  211. package/dist/server/runtime-core.d.ts.map +1 -1
  212. package/dist/server/runtime-core.js.map +1 -1
  213. package/dist/server/server-types.d.ts +55 -3
  214. package/dist/server/server-types.d.ts.map +1 -1
  215. package/dist/server/server.d.ts +194 -19
  216. package/dist/server/server.d.ts.map +1 -1
  217. package/dist/server/server.js +1342 -274
  218. package/dist/server/server.js.map +1 -1
  219. package/dist/server/sse.d.ts.map +1 -1
  220. package/dist/server/sse.js +10 -7
  221. package/dist/server/sse.js.map +1 -1
  222. package/dist/server/static-headers.d.ts +3 -0
  223. package/dist/server/static-headers.d.ts.map +1 -1
  224. package/dist/server/static-headers.js +19 -1
  225. package/dist/server/static-headers.js.map +1 -1
  226. package/dist/server/transport-lane.d.ts +8 -0
  227. package/dist/server/transport-lane.d.ts.map +1 -1
  228. package/dist/server/transport-lane.js +9 -8
  229. package/dist/server/transport-lane.js.map +1 -1
  230. package/dist/server/validation.d.ts.map +1 -1
  231. package/dist/server/validation.js +5 -1
  232. package/dist/server/validation.js.map +1 -1
  233. package/dist/server/websocket.d.ts +10 -3
  234. package/dist/server/websocket.d.ts.map +1 -1
  235. package/dist/server/websocket.js +7 -4
  236. package/dist/server/websocket.js.map +1 -1
  237. package/dist/server/ws-bun.d.ts +2 -0
  238. package/dist/server/ws-bun.d.ts.map +1 -1
  239. package/dist/server/ws-bun.js +6 -3
  240. package/dist/server/ws-bun.js.map +1 -1
  241. package/dist/server/ws-hook.d.ts +2 -1
  242. package/dist/server/ws-hook.d.ts.map +1 -1
  243. package/dist/server.d.ts +8 -5
  244. package/dist/server.d.ts.map +1 -1
  245. package/dist/server.js +5 -2
  246. package/dist/server.js.map +1 -1
  247. package/dist/single-copy-register.js +5 -0
  248. package/dist/single-copy-register.js.map +1 -1
  249. package/dist/single-copy.d.ts +46 -3
  250. package/dist/single-copy.d.ts.map +1 -1
  251. package/dist/single-copy.js +125 -12
  252. package/dist/single-copy.js.map +1 -1
  253. package/dist/tool-contract.d.ts +23 -0
  254. package/dist/tool-contract.d.ts.map +1 -1
  255. package/dist/tool-contract.js +37 -3
  256. package/dist/tool-contract.js.map +1 -1
  257. package/dist/transport-codec.d.ts.map +1 -1
  258. package/dist/transport-codec.js +2 -1
  259. package/dist/transport-codec.js.map +1 -1
  260. package/package.json +27 -2
  261. package/src/assurance.ts +52 -2
  262. package/src/capabilities.ts +81 -22
  263. package/src/diff.ts +54 -7
  264. package/src/durable-adapters.ts +20 -3
  265. package/src/durable-effect.ts +10 -3
  266. package/src/edge-kit.ts +7 -0
  267. package/src/effect-lifecycle.ts +5 -1
  268. package/src/effect-scope.ts +2 -1
  269. package/src/errors.ts +3 -0
  270. package/src/evidence.ts +302 -12
  271. package/src/idempotency.ts +80 -9
  272. package/src/index.ts +41 -3
  273. package/src/internal/code-unit-order.ts +8 -0
  274. package/src/internal/durable-saga-protocol.ts +2 -0
  275. package/src/internal/route-catalog.ts +19 -18
  276. package/src/internal/route-compiler.ts +62 -13
  277. package/src/internal/route-execution.ts +13 -3
  278. package/src/internal/route-lanes.ts +37 -28
  279. package/src/internal/route-program.ts +434 -20
  280. package/src/ledger.ts +11 -5
  281. package/src/manifest.ts +52 -46
  282. package/src/mount.ts +62 -0
  283. package/src/reflection.ts +84 -1
  284. package/src/response-observer.ts +1 -0
  285. package/src/router/overlap.ts +258 -0
  286. package/src/router/pattern.ts +349 -25
  287. package/src/router/router.ts +76 -51
  288. package/src/server/auth.ts +75 -0
  289. package/src/server/bare-error-lane.ts +12 -11
  290. package/src/server/body-lane.ts +46 -6
  291. package/src/server/body-parser.ts +241 -0
  292. package/src/server/body.ts +82 -9
  293. package/src/server/client-ip.ts +9 -0
  294. package/src/server/context.ts +219 -12
  295. package/src/server/contract.ts +54 -9
  296. package/src/server/cookies.ts +34 -2
  297. package/src/server/core-copies.ts +21 -0
  298. package/src/server/edge.ts +2 -0
  299. package/src/server/http.ts +34 -0
  300. package/src/server/idempotency-lane.ts +147 -72
  301. package/src/server/install.ts +6 -0
  302. package/src/server/methods.ts +143 -0
  303. package/src/server/multipart.ts +424 -0
  304. package/src/server/node-outcome-hook.ts +40 -7
  305. package/src/server/not-found-answer.ts +109 -0
  306. package/src/server/not-found.ts +121 -0
  307. package/src/server/plugin.ts +45 -1
  308. package/src/server/proto-guard.ts +128 -9
  309. package/src/server/query.ts +49 -0
  310. package/src/server/registry.ts +17 -2
  311. package/src/server/request-context.ts +44 -16
  312. package/src/server/request-lineage.ts +15 -0
  313. package/src/server/request-target.ts +46 -0
  314. package/src/server/respond.ts +14 -3
  315. package/src/server/response-contract-lane.ts +97 -16
  316. package/src/server/response-observer-runtime.ts +37 -8
  317. package/src/server/runtime-core.ts +2 -0
  318. package/src/server/server-types.ts +61 -2
  319. package/src/server/server.ts +1798 -355
  320. package/src/server/sse.ts +10 -8
  321. package/src/server/static-headers.ts +22 -1
  322. package/src/server/transport-lane.ts +17 -9
  323. package/src/server/validation.ts +7 -1
  324. package/src/server/websocket.ts +23 -9
  325. package/src/server/ws-bun.ts +8 -3
  326. package/src/server/ws-hook.ts +2 -1
  327. package/src/server.ts +39 -2
  328. package/src/single-copy-register.ts +5 -0
  329. package/src/single-copy.ts +160 -12
  330. package/src/tool-contract.ts +66 -3
  331. package/src/transport-codec.ts +2 -1
package/src/index.ts CHANGED
@@ -16,14 +16,25 @@
16
16
  * read its own package.json at runtime. `scripts/version.ts` rewrites it on every release bump and
17
17
  * `check:publish` asserts it equals `@nifrajs/core`'s package version.
18
18
  */
19
- export const VERSION = "3.3.0" as const
19
+ export const VERSION = "4.0.0" as const
20
20
 
21
21
  export type Version = typeof VERSION
22
22
 
23
23
  export { FrameworkError, RouteConfigError, type RouteConfigErrorCode } from "./errors.ts"
24
24
  export { FRAMEWORK_NAME, type FrameworkName } from "./internal/brand.ts"
25
25
  export { isSameOriginRequest } from "./internal/same-origin.ts"
26
- export { METHODS, type Method, Router, type RouterMatch } from "./router/router.ts"
26
+ export {
27
+ ROUTE_PATTERN_OVERLAP_MAX_STATES,
28
+ RoutePatternOverlapLimitError,
29
+ routePatternOverlap,
30
+ } from "./router/overlap.ts"
31
+ export {
32
+ METHODS,
33
+ type Method,
34
+ type RouteMethod,
35
+ Router,
36
+ type RouterMatch,
37
+ } from "./router/router.ts"
27
38
  export type {
28
39
  InferInput,
29
40
  InferOutput,
@@ -33,11 +44,24 @@ export type {
33
44
  StandardTypes,
34
45
  ValidationOutcome,
35
46
  } from "./schema/standard.ts"
47
+ export {
48
+ type AuthenticationFailure,
49
+ type AuthenticationFailureReason,
50
+ type AuthenticationInput,
51
+ type AuthenticationResult,
52
+ type AuthenticationStage,
53
+ type AuthenticationSuccess,
54
+ type AuthHeaders,
55
+ type AuthMaybePromise,
56
+ authenticated,
57
+ rejected,
58
+ } from "./server/auth.ts"
36
59
  export type {
37
60
  Context,
38
61
  Params,
39
62
  Platform,
40
63
  Prettify,
64
+ RequestPath,
41
65
  ResponseControls,
42
66
  RouteSchema,
43
67
  } from "./server/context.ts"
@@ -45,6 +69,7 @@ export {
45
69
  type CookieOptions,
46
70
  type CookieSecret,
47
71
  cookieNamePrefix,
72
+ hasDuplicateCookie,
48
73
  parseCookies,
49
74
  serializeCookie,
50
75
  signValue,
@@ -82,7 +107,15 @@ export {
82
107
  toVercelHandler,
83
108
  type VercelHandler,
84
109
  } from "./server/platform-adapters.ts"
85
- export type { Registry, ResponseMapFor, RouteInfo, RouteInfoFor } from "./server/registry.ts"
110
+ export type {
111
+ JoinRoutePath,
112
+ PrefixRegistry,
113
+ Registry,
114
+ ResponseMapFor,
115
+ RouteInfo,
116
+ RouteInfoFor,
117
+ } from "./server/registry.ts"
118
+ export { replacedRequestOf } from "./server/request-lineage.ts"
86
119
  export {
87
120
  type PlainRender,
88
121
  type ResponseResult,
@@ -101,13 +134,18 @@ export {
101
134
  defineRouterPlugin,
102
135
  type Handler,
103
136
  type IdentityPlugin,
137
+ type ListenTlsOptions,
104
138
  type McpPromptDescriptor,
105
139
  type McpResourceDescriptor,
106
140
  type Middleware,
141
+ type MountableApp,
142
+ type MountFetchOptions,
143
+ type MountOptions,
107
144
  type NifraFeatureVersion,
108
145
  type NifraPlugin,
109
146
  type NodeRequestContext,
110
147
  type NodeRequestHook,
148
+ type NodeResponseBodyHook,
111
149
  type NodeResponseContext,
112
150
  type NodeResponseHook,
113
151
  type NodeServeOutcome,
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Code-unit order, the same on every runtime and in every locale. `localeCompare` answers by the
3
+ * runtime's locale and ICU data, so an artifact that is hashed, signed, or committed sorts with this
4
+ * instead: its bytes must not depend on the machine that built it.
5
+ */
6
+ export function codeUnitOrder(a: string, b: string): number {
7
+ return a < b ? -1 : a > b ? 1 : 0
8
+ }
@@ -22,6 +22,8 @@ function transitionSaga(
22
22
  ): SagaRecord | undefined {
23
23
  if (
24
24
  input.record.version !== input.version + 1 ||
25
+ // A record filed under another saga's key could never load again, and scans would act on its id.
26
+ input.record.sagaId !== input.sagaId ||
25
27
  (current !== undefined &&
26
28
  (current.sagaId !== input.sagaId || current.version !== input.version))
27
29
  ) {
@@ -16,7 +16,7 @@
16
16
  * registration.
17
17
  */
18
18
  import type { CompiledRoutePattern } from "../router/pattern.ts"
19
- import { type Method, Router, type RouterMatch } from "../router/router.ts"
19
+ import { Router, type RouterMatch } from "../router/router.ts"
20
20
  import type { RouteDescriptor } from "../server/server-types.ts"
21
21
  import type { AssuranceDeclaration } from "./route-assurance.ts"
22
22
  import type { RouteEntry } from "./route-execution.ts"
@@ -24,7 +24,7 @@ import type { RouteEntry } from "./route-execution.ts"
24
24
  /** One canonical runtime route fact. The catalog owns matching, reflection, assurance, tool metadata,
25
25
  * replay, and native compilation input so batch registration has one commit point. */
26
26
  export interface CatalogRoute {
27
- readonly method: Method
27
+ readonly method: string
28
28
  readonly path: string
29
29
  readonly pattern: CompiledRoutePattern
30
30
  readonly entry: RouteEntry
@@ -33,10 +33,10 @@ export interface CatalogRoute {
33
33
  }
34
34
 
35
35
  /**
36
- * Runtime route catalog. Single-route registration mutates directly; multi-route registration replays
37
- * the existing catalog plus the candidate batch into a staged router, then swaps the complete state only
38
- * after every route validates. Failed `implement()`/`merge()` batches therefore leave matching and
39
- * reflection unchanged.
36
+ * Runtime route catalog. Single-route registration mutates directly. A batch adds its routes the same
37
+ * way and, if one is rejected, restores the catalog to what it was before the batch. A failed
38
+ * `implement()`/`merge()` batch therefore leaves matching and reflection unchanged, and a batch that
39
+ * validates costs what its routes cost one by one.
40
40
  *
41
41
  * Coverage note: this file reports 90% functions with every method exercised. `bun test --coverage`
42
42
  * (1.3.14) counts one synthetic function per class that it never marks hit - a one-method class with a
@@ -58,18 +58,19 @@ export class RouteCatalog {
58
58
  }
59
59
 
60
60
  addBatch(routes: readonly CatalogRoute[]): void {
61
- if (routes.length === 0) return
62
- const nextRecords = this.records.concat(routes)
63
- const nextDescriptors = this.descriptors.concat(routes.map(({ descriptor }) => descriptor))
64
- const nextAssurancePresent =
65
- this.assurancePresent || routes.some((route) => route.assurance.length > 0)
66
- const staged = new Router<RouteEntry>()
67
- for (const route of this.records) staged.add(route.method, route.pattern, route.entry)
68
- for (const route of routes) staged.add(route.method, route.pattern, route.entry)
69
- this.matcher = staged
70
- this.records = nextRecords
71
- this.descriptors = nextDescriptors
72
- this.assurancePresent = nextAssurancePresent
61
+ const kept = this.records.length
62
+ const assurancePresent = this.assurancePresent
63
+ try {
64
+ for (const route of routes) this.add(route)
65
+ } catch (error) {
66
+ // The rejected route may have left nodes in the trie, so the matcher is not patched: it is
67
+ // rebuilt from the routes that were there before the batch. They validated once already.
68
+ this.records.length = this.descriptors.length = kept
69
+ this.assurancePresent = assurancePresent
70
+ this.matcher = new Router<RouteEntry>()
71
+ for (const route of this.records) this.matcher.add(route.method, route.pattern, route.entry)
72
+ throw error
73
+ }
73
74
  }
74
75
 
75
76
  find(method: string, path: string): RouterMatch<RouteEntry> {
@@ -13,13 +13,14 @@
13
13
  import type { EffectLifecycleObserver } from "../effect-lifecycle.ts"
14
14
  import { RouteConfigError } from "../errors.ts"
15
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
16
  import { assertByteLimit } from "../server/body.ts"
19
17
  import type { RouteSchema } from "../server/context.ts"
20
18
  import type { IdempotencyRuntime, ResolvedIdempotency } from "../server/idempotency-lane.ts"
21
19
  import type { EffectLedgerRuntime, ResolvedEffectLedger } from "../server/ledger-lane.ts"
22
- import type { ResponseContractRuntime } from "../server/response-contract-lane.ts"
20
+ import type {
21
+ ResponseContractDefinition,
22
+ ResponseContractRuntime,
23
+ } from "../server/response-contract-lane.ts"
23
24
  import {
24
25
  CAPABILITY_GUARD,
25
26
  type CapabilityUseEvent,
@@ -37,6 +38,7 @@ import {
37
38
  import type {
38
39
  RawAfterHandle,
39
40
  RawAround,
41
+ RawAuthStage,
40
42
  RawBeforeHandle,
41
43
  RawDerive,
42
44
  RawErrorHandler,
@@ -55,6 +57,7 @@ export interface RouteCompilerContext {
55
57
  readonly idempotencyRuntime: IdempotencyRuntime | undefined
56
58
  readonly responseContractRuntime: ResponseContractRuntime | undefined
57
59
  readonly derives: readonly RawDerive[]
60
+ readonly authStages: readonly RawAuthStage[]
58
61
  readonly beforeHandleHooks: readonly RawBeforeHandle[]
59
62
  readonly afterHandleHooks: readonly RawAfterHandle[]
60
63
  readonly onErrorHooks: readonly RawErrorHandler[]
@@ -72,16 +75,18 @@ export interface CompiledRouteOptions {
72
75
  readonly idempotent: ResolvedIdempotency | undefined
73
76
  readonly ledgered: ResolvedEffectLedger | undefined
74
77
  readonly responseContract:
75
- | { readonly runtime: ResponseContractRuntime; readonly schema: StandardSchemaV1 }
78
+ | { readonly runtime: ResponseContractRuntime; readonly definition: ResponseContractDefinition }
76
79
  | undefined
77
80
  readonly lanes: RouteLaneSelection
78
81
  readonly routeAssurance: readonly AssuranceDeclaration[]
82
+ readonly authStages: readonly RawAuthStage[]
83
+ readonly authenticated: boolean
79
84
  }
80
85
 
81
86
  /** Resolve all registration-time policy and lane facts for one route. */
82
87
  export function compileRouteOptions(
83
88
  context: RouteCompilerContext,
84
- method: Method,
89
+ method: string,
85
90
  path: string,
86
91
  schema: RouteSchema | undefined,
87
92
  handler: (context: never) => unknown,
@@ -91,6 +96,25 @@ export function compileRouteOptions(
91
96
  const invalidBodyLimit = (message: string): never => {
92
97
  throw new RouteConfigError("INVALID_BODY_LIMIT", `route ${method} ${path}: ${message}`)
93
98
  }
99
+ if (schema?.wire !== undefined && schema.wire !== "raw") {
100
+ throw new RouteConfigError("INVALID_WIRE", `route ${method} ${path}: wire must be "raw"`)
101
+ }
102
+ if (schema?.wire === "raw" && schema.body !== undefined) {
103
+ throw new RouteConfigError(
104
+ "INVALID_WIRE",
105
+ `route ${method} ${path}: wire: "raw" cannot be combined with a body schema`,
106
+ )
107
+ }
108
+ if (
109
+ schema?.validationOrder !== undefined &&
110
+ schema.validationOrder !== "validate-before-auth" &&
111
+ schema.validationOrder !== "auth-before-validation"
112
+ ) {
113
+ throw new RouteConfigError(
114
+ "INVALID_VALIDATION_ORDER",
115
+ `route ${method} ${path}: validationOrder is invalid`,
116
+ )
117
+ }
94
118
  if (schema?.bodyLimitReason !== undefined && schema.bodyLimit !== "unlimited") {
95
119
  invalidBodyLimit('bodyLimitReason is only valid with bodyLimit: "unlimited"')
96
120
  }
@@ -124,11 +148,13 @@ export function compileRouteOptions(
124
148
  )
125
149
  }
126
150
 
127
- const authenticated = assuranceEvidenceFor(
128
- [...context.activeAssurance, ...handlerAssurance, ...context.globalAssurance],
129
- method,
130
- path,
131
- ).some((evidence) => evidence.id === NIFRA_ASSURANCE_IDS.AUTHENTICATED)
151
+ const authenticated =
152
+ context.authStages.length > 0 ||
153
+ assuranceEvidenceFor(
154
+ [...context.activeAssurance, ...handlerAssurance, ...context.globalAssurance],
155
+ method,
156
+ path,
157
+ ).some((evidence) => evidence.id === NIFRA_ASSURANCE_IDS.AUTHENTICATED)
132
158
  const routeDecorations: Record<PropertyKey, unknown> = { ...context.decorations }
133
159
  if (capabilities.length > 0) {
134
160
  routeDecorations[CAPABILITY_GUARD] = createCapabilityGuard(
@@ -155,12 +181,20 @@ export function compileRouteOptions(
155
181
  context.maxBodyBytes,
156
182
  )
157
183
  const ledgered = context.effectLedgerRuntime?.resolve(capabilities, method, path)
184
+ const responseDefinition =
185
+ schema?.response !== undefined || schema?.errors !== undefined
186
+ ? {
187
+ ...(schema.response === undefined ? {} : { response: schema.response }),
188
+ ...(schema.errors === undefined ? {} : { errors: schema.errors }),
189
+ }
190
+ : undefined
158
191
  const contracted =
159
- context.responseContractRuntime !== undefined && schema?.response !== undefined
160
- ? { runtime: context.responseContractRuntime, schema: schema.response }
192
+ context.responseContractRuntime !== undefined && responseDefinition !== undefined
193
+ ? { runtime: context.responseContractRuntime, definition: responseDefinition }
161
194
  : undefined
162
195
  const lanes = selectRouteLanes({
163
196
  schema,
197
+ hasAuth: context.authStages.length > 0,
164
198
  hasIdempotency: idempotent !== undefined,
165
199
  hasLedger: ledgered !== undefined,
166
200
  hasResponseContract: contracted !== undefined,
@@ -174,7 +208,7 @@ export function compileRouteOptions(
174
208
  })
175
209
 
176
210
  const routeAssurance: AssuranceDeclaration[] = [...context.activeAssurance, ...handlerAssurance]
177
- if (contracted?.runtime.mode === "enforce" && schema?.response !== undefined) {
211
+ if (contracted?.runtime.mode === "enforce") {
178
212
  routeAssurance.push(
179
213
  Object.freeze({
180
214
  id: NIFRA_ASSURANCE_IDS.RESPONSE_CONTRACT,
@@ -217,6 +251,19 @@ export function compileRouteOptions(
217
251
  }),
218
252
  )
219
253
  }
254
+ if (context.authStages.length > 0) {
255
+ // `authenticate()` is an actual runtime enforcement stage, not a route assertion. Publish its
256
+ // proof directly so a core-authenticated route cannot accidentally become assurance
257
+ // classification-only when it is not using an integration plugin such as better-auth.
258
+ routeAssurance.push(
259
+ Object.freeze({
260
+ id: NIFRA_ASSURANCE_IDS.AUTHENTICATED,
261
+ source: "nifra.authenticate",
262
+ scope: "plugin",
263
+ provenance: "runtime" as const,
264
+ }),
265
+ )
266
+ }
220
267
 
221
268
  return {
222
269
  pattern,
@@ -230,5 +277,7 @@ export function compileRouteOptions(
230
277
  responseContract: contracted,
231
278
  lanes,
232
279
  routeAssurance: Object.freeze(routeAssurance),
280
+ authStages: Object.freeze([...context.authStages]),
281
+ authenticated,
233
282
  }
234
283
  }
@@ -18,7 +18,7 @@
18
18
  */
19
19
  import type { RequestBudget } from "../budget.ts"
20
20
  import type { RequestLedger } from "../ledger.ts"
21
- import type { StandardSchemaV1 } from "../schema/standard.ts"
21
+ import type { AuthenticationStage } from "../server/auth.ts"
22
22
  import { UNLIMITED_BODY_BYTES } from "../server/body.ts"
23
23
  import type { Platform, RouteSchema } from "../server/context.ts"
24
24
  import type { ResolvedIdempotency } from "../server/idempotency-lane.ts"
@@ -26,7 +26,10 @@ import type { EffectLedgerRuntime, ResolvedEffectLedger } from "../server/ledger
26
26
  import type { ProtoPoisoning } from "../server/proto-guard.ts"
27
27
  import type { Registry } from "../server/registry.ts"
28
28
  import { RequestContext } from "../server/request-context.ts"
29
- import type { ResponseContractRuntime } from "../server/response-contract-lane.ts"
29
+ import type {
30
+ ResponseContractDefinition,
31
+ ResponseContractRuntime,
32
+ } from "../server/response-contract-lane.ts"
30
33
  import type { HandlerResult, ResponseResult } from "../server/runtime-core.ts"
31
34
  import type { CtxSet, MaybePromise, RawContext, RequestSource, Server } from "../server/server.ts"
32
35
  import type { RouteProgram } from "./route-program.ts"
@@ -35,6 +38,8 @@ export type InternalHandler = (ctx: RawContext) => MaybePromise<HandlerResult>
35
38
 
36
39
  /** A `derive` computes per-request context extensions; stored path-erased. */
37
40
  export type RawDerive = (ctx: RawContext) => MaybePromise<object>
41
+ /** A route-scoped authentication provider, stored path-erased after registration. */
42
+ export type RawAuthStage = AuthenticationStage<unknown, unknown>
38
43
  export type RawBeforeHandle = (ctx: RawContext) => MaybePromise<unknown>
39
44
  export type RawAfterHandle = (result: unknown, ctx: RawContext) => MaybePromise<unknown>
40
45
  export type RawErrorHandler = (error: unknown, ctx: RawContext) => MaybePromise<unknown>
@@ -311,10 +316,15 @@ export interface RouteEntry {
311
316
  * at registration; `undefined` = not checked (the default, and the only state in which the route can
312
317
  * still take the fused/native lanes). */
313
318
  readonly responseContract:
314
- | { readonly runtime: ResponseContractRuntime; readonly schema: StandardSchemaV1 }
319
+ | {
320
+ readonly runtime: ResponseContractRuntime
321
+ readonly definition: ResponseContractDefinition
322
+ }
315
323
  | undefined
316
324
  /** Per-request context extensions captured at registration (order-scoped). */
317
325
  readonly derives: ReadonlyArray<RawDerive>
326
+ /** Dedicated auth stages run before validation; ordinary derives keep their existing order. */
327
+ readonly authStages: ReadonlyArray<RawAuthStage>
318
328
  /** Static context extensions captured at registration. */
319
329
  readonly decorations: Record<PropertyKey, unknown>
320
330
  /** Whether {@link decorations} has any keys - precomputed so the hot path skips a no-op
@@ -31,6 +31,7 @@ export interface RouteLaneSelection {
31
31
 
32
32
  export function selectRouteLanes(options: {
33
33
  readonly schema: RouteSchema | undefined
34
+ readonly hasAuth?: boolean
34
35
  readonly hasIdempotency: boolean
35
36
  readonly hasLedger: boolean
36
37
  readonly hasResponseContract: boolean
@@ -56,9 +57,15 @@ export function selectRouteLanes(options: {
56
57
  defaultOnValidationError,
57
58
  } = options
58
59
 
60
+ const authBeforeValidation =
61
+ options.hasAuth === true || schema?.validationOrder === "auth-before-validation"
62
+ // No fused lane validates params, headers, or cookies: any of those schemas keeps the stage runner.
63
+ const plain =
64
+ schema?.params === undefined && schema?.headers === undefined && schema?.cookies === undefined
65
+
59
66
  const bare =
60
- schema?.params === undefined &&
61
- schema?.headers === undefined &&
67
+ !authBeforeValidation &&
68
+ plain &&
62
69
  schema?.body === undefined &&
63
70
  schema?.query === undefined &&
64
71
  !hasIdempotency &&
@@ -76,11 +83,11 @@ export function selectRouteLanes(options: {
76
83
  // exactly `validationError(issues)`, so any recovery semantics keep the generic lane.
77
84
  const fusedQuery =
78
85
  !bare &&
86
+ !authBeforeValidation &&
79
87
  !hasResponseContract &&
80
88
  schema?.query !== undefined &&
81
89
  schema.body === undefined &&
82
- schema.params === undefined &&
83
- schema.headers === undefined &&
90
+ plain &&
84
91
  schema.onValidationError === undefined &&
85
92
  !defaultOnValidationError &&
86
93
  !hasIdempotency &&
@@ -92,11 +99,11 @@ export function selectRouteLanes(options: {
92
99
  around === 0
93
100
 
94
101
  const bodyOnly =
102
+ !authBeforeValidation &&
95
103
  !hasResponseContract &&
96
104
  schema?.body !== undefined &&
97
105
  schema.query === undefined &&
98
- schema.params === undefined &&
99
- schema.headers === undefined &&
106
+ plain &&
100
107
  derives === 0 &&
101
108
  beforeHandle === 0 &&
102
109
  afterHandle === 0 &&
@@ -104,6 +111,7 @@ export function selectRouteLanes(options: {
104
111
 
105
112
  const fusedBody =
106
113
  !bare &&
114
+ !authBeforeValidation &&
107
115
  bodyOnly &&
108
116
  schema.onValidationError === undefined &&
109
117
  !defaultOnValidationError &&
@@ -111,28 +119,29 @@ export function selectRouteLanes(options: {
111
119
  !hasLedger &&
112
120
  around === 0
113
121
 
114
- const lane: RouteExecutionLane = bare
115
- ? "bare"
116
- : bodyOnly
117
- ? "body"
118
- : !hasResponseContract &&
119
- schema?.body === undefined &&
120
- schema?.query !== undefined &&
121
- schema.params === undefined &&
122
- schema.headers === undefined &&
123
- derives === 0 &&
124
- beforeHandle === 0 &&
125
- afterHandle === 0 &&
126
- onError === 0
127
- ? "query"
128
- : "lifecycle"
122
+ const lane: RouteExecutionLane = authBeforeValidation
123
+ ? "lifecycle"
124
+ : bare
125
+ ? "bare"
126
+ : bodyOnly
127
+ ? "body"
128
+ : !hasResponseContract &&
129
+ schema?.body === undefined &&
130
+ schema?.query !== undefined &&
131
+ plain &&
132
+ derives === 0 &&
133
+ beforeHandle === 0 &&
134
+ afterHandle === 0 &&
135
+ onError === 0
136
+ ? "query"
137
+ : "lifecycle"
129
138
 
130
139
  // Lifecycle routes with no params schema are the common middleware shape: derive/before plus an
131
140
  // optional query or body schema. Select their complete validation stage at registration so the
132
141
  // request path never re-checks params/body presence. Parameter-schema routes retain the generic
133
142
  // lifecycle runner until their more involved recovery matrix is selected explicitly.
134
143
  const lifecycleLane: LifecycleExecutionLane =
135
- lane !== "lifecycle" || schema?.params !== undefined || schema?.headers !== undefined
144
+ lane !== "lifecycle" || !plain
136
145
  ? undefined
137
146
  : schema?.body !== undefined
138
147
  ? schema.query !== undefined
@@ -145,21 +154,21 @@ export function selectRouteLanes(options: {
145
154
  // The realistic middleware shape is commonly exactly one synchronous-or-async derive followed by
146
155
  // one before hook. Keep the generic runner for every route that can observe decorations, after hooks,
147
156
  // error hooks, or response contracts; this lane only removes the two per-request hook-loop dispatches
148
- // and preserves the same async continuations and error handling. A params or headers schema routes
149
- // back to the generic lane because the fused builder has no `params` / `headers` validation step
157
+ // and preserves the same async continuations and error handling. A params, headers, or cookies schema
158
+ // routes back to the generic lane because the fused builder has no validation step for them
150
159
  // baked in - adding them would be more code than it saves. A `body` schema is the same story
151
160
  // because the fused derive-before / derive-before-after builders don't parse the body: routes with a
152
161
  // body schema are routed to the `body-derive-before` / `body-derive-before-after` lane below.
153
162
  const lifecycleHookLane =
154
163
  lane === "lifecycle" &&
164
+ !authBeforeValidation &&
155
165
  !hasResponseContract &&
156
166
  !hasDecorations &&
157
167
  !hasIdempotency &&
158
168
  !hasLedger &&
159
169
  schema?.onValidationError === undefined &&
160
170
  !defaultOnValidationError &&
161
- schema?.params === undefined &&
162
- schema?.headers === undefined &&
171
+ plain &&
163
172
  schema?.body === undefined &&
164
173
  derives === 1 &&
165
174
  beforeHandle === 1 &&
@@ -177,10 +186,10 @@ export function selectRouteLanes(options: {
177
186
  // win `fusedBody` already gives for the no-hooks body shape.
178
187
  const fusedBodyLifecycle =
179
188
  lane === "lifecycle" &&
189
+ !authBeforeValidation &&
180
190
  schema?.body !== undefined &&
181
191
  schema?.query === undefined &&
182
- schema?.params === undefined &&
183
- schema?.headers === undefined &&
192
+ plain &&
184
193
  schema?.onValidationError === undefined &&
185
194
  !defaultOnValidationError &&
186
195
  !hasResponseContract &&