@nifrajs/core 3.5.0 → 4.0.2

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 (325) 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 +2 -1
  11. package/dist/diff.d.ts.map +1 -1
  12. package/dist/diff.js +26 -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 +24 -1
  34. package/dist/evidence.d.ts.map +1 -1
  35. package/dist/evidence.js +168 -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 +40 -8
  40. package/dist/idempotency.js.map +1 -1
  41. package/dist/index.d.ts +8 -5
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +5 -2
  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.map +1 -1
  75. package/dist/manifest.js +41 -37
  76. package/dist/manifest.js.map +1 -1
  77. package/dist/mount.d.ts +36 -0
  78. package/dist/mount.d.ts.map +1 -1
  79. package/dist/mount.js +32 -0
  80. package/dist/mount.js.map +1 -1
  81. package/dist/reflection.d.ts +26 -0
  82. package/dist/reflection.d.ts.map +1 -1
  83. package/dist/reflection.js +65 -1
  84. package/dist/reflection.js.map +1 -1
  85. package/dist/response-observer.d.ts +1 -1
  86. package/dist/response-observer.d.ts.map +1 -1
  87. package/dist/response-observer.js.map +1 -1
  88. package/dist/router/overlap.d.ts +23 -0
  89. package/dist/router/overlap.d.ts.map +1 -0
  90. package/dist/router/overlap.js +224 -0
  91. package/dist/router/overlap.js.map +1 -0
  92. package/dist/router/pattern.d.ts +117 -2
  93. package/dist/router/pattern.d.ts.map +1 -1
  94. package/dist/router/pattern.js +283 -23
  95. package/dist/router/pattern.js.map +1 -1
  96. package/dist/router/router.d.ts +18 -4
  97. package/dist/router/router.d.ts.map +1 -1
  98. package/dist/router/router.js +51 -44
  99. package/dist/router/router.js.map +1 -1
  100. package/dist/server/auth.d.ts +53 -0
  101. package/dist/server/auth.d.ts.map +1 -0
  102. package/dist/server/auth.js +9 -0
  103. package/dist/server/auth.js.map +1 -0
  104. package/dist/server/bare-error-lane.d.ts +12 -7
  105. package/dist/server/bare-error-lane.d.ts.map +1 -1
  106. package/dist/server/bare-error-lane.js +13 -11
  107. package/dist/server/bare-error-lane.js.map +1 -1
  108. package/dist/server/body-lane.d.ts +20 -2
  109. package/dist/server/body-lane.d.ts.map +1 -1
  110. package/dist/server/body-lane.js +26 -7
  111. package/dist/server/body-lane.js.map +1 -1
  112. package/dist/server/body-parser.d.ts +68 -0
  113. package/dist/server/body-parser.d.ts.map +1 -0
  114. package/dist/server/body-parser.js +172 -0
  115. package/dist/server/body-parser.js.map +1 -0
  116. package/dist/server/body.d.ts.map +1 -1
  117. package/dist/server/body.js +62 -7
  118. package/dist/server/body.js.map +1 -1
  119. package/dist/server/client-ip.d.ts +8 -0
  120. package/dist/server/client-ip.d.ts.map +1 -1
  121. package/dist/server/client-ip.js +8 -0
  122. package/dist/server/client-ip.js.map +1 -1
  123. package/dist/server/context.d.ts +71 -6
  124. package/dist/server/context.d.ts.map +1 -1
  125. package/dist/server/contract.d.ts +15 -5
  126. package/dist/server/contract.d.ts.map +1 -1
  127. package/dist/server/contract.js +25 -4
  128. package/dist/server/contract.js.map +1 -1
  129. package/dist/server/core-copies.d.ts +2 -0
  130. package/dist/server/core-copies.d.ts.map +1 -0
  131. package/dist/server/core-copies.js +20 -0
  132. package/dist/server/core-copies.js.map +1 -0
  133. package/dist/server/edge.d.ts.map +1 -1
  134. package/dist/server/edge.js +2 -0
  135. package/dist/server/edge.js.map +1 -1
  136. package/dist/server/http.d.ts +11 -0
  137. package/dist/server/http.d.ts.map +1 -1
  138. package/dist/server/http.js +36 -0
  139. package/dist/server/http.js.map +1 -1
  140. package/dist/server/idempotency-lane.d.ts +6 -0
  141. package/dist/server/idempotency-lane.d.ts.map +1 -1
  142. package/dist/server/idempotency-lane.js +130 -73
  143. package/dist/server/idempotency-lane.js.map +1 -1
  144. package/dist/server/install.d.ts +4 -0
  145. package/dist/server/install.d.ts.map +1 -1
  146. package/dist/server/install.js +4 -0
  147. package/dist/server/install.js.map +1 -1
  148. package/dist/server/methods.d.ts +45 -0
  149. package/dist/server/methods.d.ts.map +1 -0
  150. package/dist/server/methods.js +37 -0
  151. package/dist/server/methods.js.map +1 -0
  152. package/dist/server/multipart.d.ts +63 -0
  153. package/dist/server/multipart.d.ts.map +1 -0
  154. package/dist/server/multipart.js +374 -0
  155. package/dist/server/multipart.js.map +1 -0
  156. package/dist/server/node-outcome-hook.d.ts +20 -0
  157. package/dist/server/node-outcome-hook.d.ts.map +1 -1
  158. package/dist/server/node-outcome-hook.js +15 -6
  159. package/dist/server/node-outcome-hook.js.map +1 -1
  160. package/dist/server/not-found-answer.d.ts +40 -0
  161. package/dist/server/not-found-answer.d.ts.map +1 -0
  162. package/dist/server/not-found-answer.js +56 -0
  163. package/dist/server/not-found-answer.js.map +1 -0
  164. package/dist/server/not-found.d.ts +23 -0
  165. package/dist/server/not-found.d.ts.map +1 -0
  166. package/dist/server/not-found.js +81 -0
  167. package/dist/server/not-found.js.map +1 -0
  168. package/dist/server/plugin.d.ts +26 -1
  169. package/dist/server/plugin.d.ts.map +1 -1
  170. package/dist/server/plugin.js.map +1 -1
  171. package/dist/server/proto-guard.d.ts +18 -5
  172. package/dist/server/proto-guard.d.ts.map +1 -1
  173. package/dist/server/proto-guard.js +114 -9
  174. package/dist/server/proto-guard.js.map +1 -1
  175. package/dist/server/query.d.ts +7 -0
  176. package/dist/server/query.d.ts.map +1 -1
  177. package/dist/server/query.js +47 -0
  178. package/dist/server/query.js.map +1 -1
  179. package/dist/server/registry.d.ts +13 -2
  180. package/dist/server/registry.d.ts.map +1 -1
  181. package/dist/server/request-context.d.ts +11 -0
  182. package/dist/server/request-context.d.ts.map +1 -1
  183. package/dist/server/request-context.js +33 -19
  184. package/dist/server/request-context.js.map +1 -1
  185. package/dist/server/request-lineage.d.ts +8 -0
  186. package/dist/server/request-lineage.d.ts.map +1 -0
  187. package/dist/server/request-lineage.js +14 -0
  188. package/dist/server/request-lineage.js.map +1 -0
  189. package/dist/server/request-target.d.ts +31 -0
  190. package/dist/server/request-target.d.ts.map +1 -0
  191. package/dist/server/request-target.js +44 -0
  192. package/dist/server/request-target.js.map +1 -0
  193. package/dist/server/respond.d.ts +1 -1
  194. package/dist/server/respond.d.ts.map +1 -1
  195. package/dist/server/respond.js +16 -4
  196. package/dist/server/respond.js.map +1 -1
  197. package/dist/server/response-contract-lane.d.ts +19 -1
  198. package/dist/server/response-contract-lane.d.ts.map +1 -1
  199. package/dist/server/response-contract-lane.js +59 -14
  200. package/dist/server/response-contract-lane.js.map +1 -1
  201. package/dist/server/response-observer-runtime.d.ts +9 -5
  202. package/dist/server/response-observer-runtime.d.ts.map +1 -1
  203. package/dist/server/response-observer-runtime.js +10 -6
  204. package/dist/server/response-observer-runtime.js.map +1 -1
  205. package/dist/server/runtime-core.d.ts +2 -0
  206. package/dist/server/runtime-core.d.ts.map +1 -1
  207. package/dist/server/runtime-core.js.map +1 -1
  208. package/dist/server/server-types.d.ts +53 -3
  209. package/dist/server/server-types.d.ts.map +1 -1
  210. package/dist/server/server.d.ts +191 -19
  211. package/dist/server/server.d.ts.map +1 -1
  212. package/dist/server/server.js +1206 -218
  213. package/dist/server/server.js.map +1 -1
  214. package/dist/server/sse.d.ts.map +1 -1
  215. package/dist/server/sse.js +10 -7
  216. package/dist/server/sse.js.map +1 -1
  217. package/dist/server/static-headers.d.ts +3 -0
  218. package/dist/server/static-headers.d.ts.map +1 -1
  219. package/dist/server/static-headers.js +19 -1
  220. package/dist/server/static-headers.js.map +1 -1
  221. package/dist/server/transport-lane.d.ts +8 -0
  222. package/dist/server/transport-lane.d.ts.map +1 -1
  223. package/dist/server/transport-lane.js +9 -8
  224. package/dist/server/transport-lane.js.map +1 -1
  225. package/dist/server/validation.d.ts.map +1 -1
  226. package/dist/server/validation.js +5 -1
  227. package/dist/server/validation.js.map +1 -1
  228. package/dist/server/websocket.d.ts +10 -3
  229. package/dist/server/websocket.d.ts.map +1 -1
  230. package/dist/server/websocket.js +7 -4
  231. package/dist/server/websocket.js.map +1 -1
  232. package/dist/server/ws-bun.d.ts +2 -0
  233. package/dist/server/ws-bun.d.ts.map +1 -1
  234. package/dist/server/ws-bun.js +6 -3
  235. package/dist/server/ws-bun.js.map +1 -1
  236. package/dist/server/ws-hook.d.ts +2 -1
  237. package/dist/server/ws-hook.d.ts.map +1 -1
  238. package/dist/server.d.ts +7 -4
  239. package/dist/server.d.ts.map +1 -1
  240. package/dist/server.js +4 -1
  241. package/dist/server.js.map +1 -1
  242. package/dist/single-copy-register.js +5 -0
  243. package/dist/single-copy-register.js.map +1 -1
  244. package/dist/single-copy.d.ts +46 -3
  245. package/dist/single-copy.d.ts.map +1 -1
  246. package/dist/single-copy.js +125 -12
  247. package/dist/single-copy.js.map +1 -1
  248. package/dist/tool-contract.d.ts +23 -0
  249. package/dist/tool-contract.d.ts.map +1 -1
  250. package/dist/tool-contract.js +37 -3
  251. package/dist/tool-contract.js.map +1 -1
  252. package/dist/transport-codec.d.ts.map +1 -1
  253. package/dist/transport-codec.js +2 -1
  254. package/dist/transport-codec.js.map +1 -1
  255. package/package.json +27 -2
  256. package/src/assurance.ts +52 -2
  257. package/src/capabilities.ts +81 -22
  258. package/src/diff.ts +31 -6
  259. package/src/durable-adapters.ts +20 -3
  260. package/src/durable-effect.ts +10 -3
  261. package/src/edge-kit.ts +7 -0
  262. package/src/effect-lifecycle.ts +5 -1
  263. package/src/effect-scope.ts +2 -1
  264. package/src/errors.ts +3 -0
  265. package/src/evidence.ts +237 -12
  266. package/src/idempotency.ts +72 -8
  267. package/src/index.ts +40 -3
  268. package/src/internal/code-unit-order.ts +8 -0
  269. package/src/internal/durable-saga-protocol.ts +2 -0
  270. package/src/internal/route-catalog.ts +19 -18
  271. package/src/internal/route-compiler.ts +62 -13
  272. package/src/internal/route-execution.ts +13 -3
  273. package/src/internal/route-lanes.ts +37 -28
  274. package/src/internal/route-program.ts +434 -20
  275. package/src/ledger.ts +11 -5
  276. package/src/manifest.ts +41 -36
  277. package/src/mount.ts +62 -0
  278. package/src/reflection.ts +84 -1
  279. package/src/response-observer.ts +1 -0
  280. package/src/router/overlap.ts +258 -0
  281. package/src/router/pattern.ts +349 -25
  282. package/src/router/router.ts +76 -51
  283. package/src/server/auth.ts +75 -0
  284. package/src/server/bare-error-lane.ts +12 -11
  285. package/src/server/body-lane.ts +46 -6
  286. package/src/server/body-parser.ts +241 -0
  287. package/src/server/body.ts +82 -9
  288. package/src/server/client-ip.ts +9 -0
  289. package/src/server/context.ts +219 -12
  290. package/src/server/contract.ts +54 -9
  291. package/src/server/core-copies.ts +21 -0
  292. package/src/server/edge.ts +2 -0
  293. package/src/server/http.ts +34 -0
  294. package/src/server/idempotency-lane.ts +147 -72
  295. package/src/server/install.ts +6 -0
  296. package/src/server/methods.ts +143 -0
  297. package/src/server/multipart.ts +424 -0
  298. package/src/server/node-outcome-hook.ts +40 -7
  299. package/src/server/not-found-answer.ts +109 -0
  300. package/src/server/not-found.ts +121 -0
  301. package/src/server/plugin.ts +45 -1
  302. package/src/server/proto-guard.ts +128 -9
  303. package/src/server/query.ts +49 -0
  304. package/src/server/registry.ts +17 -2
  305. package/src/server/request-context.ts +44 -16
  306. package/src/server/request-lineage.ts +15 -0
  307. package/src/server/request-target.ts +46 -0
  308. package/src/server/respond.ts +14 -3
  309. package/src/server/response-contract-lane.ts +97 -16
  310. package/src/server/response-observer-runtime.ts +37 -8
  311. package/src/server/runtime-core.ts +2 -0
  312. package/src/server/server-types.ts +59 -2
  313. package/src/server/server.ts +1613 -269
  314. package/src/server/sse.ts +10 -8
  315. package/src/server/static-headers.ts +22 -1
  316. package/src/server/transport-lane.ts +17 -9
  317. package/src/server/validation.ts +7 -1
  318. package/src/server/websocket.ts +23 -9
  319. package/src/server/ws-bun.ts +8 -3
  320. package/src/server/ws-hook.ts +2 -1
  321. package/src/server.ts +38 -2
  322. package/src/single-copy-register.ts +5 -0
  323. package/src/single-copy.ts +160 -12
  324. package/src/tool-contract.ts +66 -3
  325. package/src/transport-codec.ts +2 -1
@@ -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 &&
@@ -8,24 +8,27 @@
8
8
  */
9
9
 
10
10
  import type { StandardSchemaV1 } from "../schema/standard.ts"
11
+ import { pathnameOf } from "../server/http.ts"
11
12
  import type { Registry } from "../server/registry.ts"
12
13
  import {
13
14
  CONTEXT_SET,
14
15
  EMPTY_RESPONSE_CONTROLS,
15
16
  isResponseResult,
16
17
  type ResponseResult,
18
+ status,
17
19
  } from "../server/runtime-core.ts"
18
20
  import type { CtxSet, MaybePromise, RawContext, RequestSource, Server } from "../server/server.ts"
19
21
  import type {
20
22
  InternalHandler,
21
23
  RawAfterHandle,
24
+ RawAuthStage,
22
25
  RawBeforeHandle,
23
26
  RawDerive,
24
27
  RawErrorHandler,
25
28
  RouteEntry,
26
29
  } from "./route-execution.ts"
27
30
 
28
- export type ProgramValidationKind = "headers" | "params" | "body" | "query"
31
+ export type ProgramValidationKind = "headers" | "cookies" | "params" | "body" | "query"
29
32
 
30
33
  export type RouteProgramStage =
31
34
  | { readonly kind: ProgramValidationKind; readonly schema: StandardSchemaV1 }
@@ -37,6 +40,11 @@ export type RouteProgramStage =
37
40
  /** The complete registration-time lifecycle, in semantic execution order. */
38
41
  export interface RouteProgram {
39
42
  readonly stages: readonly RouteProgramStage[]
43
+ readonly validationOrder: "validate-before-auth" | "auth-before-validation"
44
+ readonly authBeforeValidation: boolean
45
+ readonly authStages: readonly RawAuthStage[]
46
+ /** Protected route fast lane: auth plus validation/handler, with no general lifecycle hooks. */
47
+ readonly authFastLane: boolean
40
48
  readonly validationStages: readonly Extract<RouteProgramStage, { kind: ProgramValidationKind }>[]
41
49
  readonly derives: readonly RawDerive[]
42
50
  readonly beforeHandle: readonly RawBeforeHandle[]
@@ -54,6 +62,7 @@ export interface RouteProgramInput {
54
62
  readonly schema: RouteEntry["schema"]
55
63
  readonly handler: InternalHandler
56
64
  readonly derives: readonly RawDerive[]
65
+ readonly authStages?: readonly RawAuthStage[]
57
66
  readonly beforeHandle: readonly RawBeforeHandle[]
58
67
  readonly afterHandle: readonly RawAfterHandle[]
59
68
  readonly onError: readonly RawErrorHandler[]
@@ -69,25 +78,18 @@ export function compileRouteProgram(input: RouteProgramInput): RouteProgram {
69
78
  const stages: RouteProgramStage[] = []
70
79
  const validationStages: Extract<RouteProgramStage, { kind: ProgramValidationKind }>[] = []
71
80
  const schema = input.schema
72
- if (schema?.headers !== undefined) {
73
- const stage = { kind: "headers", schema: schema.headers } as const
74
- stages.push(stage)
75
- validationStages.push(stage)
76
- }
77
- if (schema?.params !== undefined) {
78
- const stage = { kind: "params", schema: schema.params } as const
79
- stages.push(stage)
80
- validationStages.push(stage)
81
- }
82
- if (schema?.body !== undefined) {
83
- const stage = { kind: "body", schema: schema.body } as const
84
- stages.push(stage)
85
- validationStages.push(stage)
86
- }
87
- if (schema?.query !== undefined) {
88
- const stage = { kind: "query", schema: schema.query } as const
89
- stages.push(stage)
90
- validationStages.push(stage)
81
+ const authStages = input.authStages ?? []
82
+ const authBeforeValidation =
83
+ authStages.length > 0
84
+ ? schema?.validationOrder !== "validate-before-auth"
85
+ : schema?.validationOrder === "auth-before-validation"
86
+ for (const kind of ["headers", "cookies", "params", "body", "query"] as const) {
87
+ const validator = schema?.[kind]
88
+ if (validator !== undefined) {
89
+ const stage = { kind, schema: validator }
90
+ stages.push(stage)
91
+ validationStages.push(stage)
92
+ }
91
93
  }
92
94
  for (const derive of input.derives) stages.push({ kind: "derive", run: derive })
93
95
  for (const before of input.beforeHandle) stages.push({ kind: "before", run: before })
@@ -96,6 +98,14 @@ export function compileRouteProgram(input: RouteProgramInput): RouteProgram {
96
98
 
97
99
  return Object.freeze({
98
100
  stages: Object.freeze(stages),
101
+ validationOrder: authBeforeValidation ? "auth-before-validation" : "validate-before-auth",
102
+ authBeforeValidation,
103
+ authStages: Object.freeze([...authStages]),
104
+ authFastLane:
105
+ authStages.length > 0 &&
106
+ input.derives.length === 0 &&
107
+ input.beforeHandle.length === 0 &&
108
+ input.afterHandle.length === 0,
99
109
  validationStages: Object.freeze(validationStages),
100
110
  derives: Object.freeze([...input.derives]),
101
111
  beforeHandle: Object.freeze([...input.beforeHandle]),
@@ -146,8 +156,395 @@ function isEarly(value: unknown): value is Response | ResponseResult {
146
156
  return value instanceof Response || isResponseResult(value)
147
157
  }
148
158
 
159
+ function authHeadersOf(source: RequestSource): import("../server/auth.ts").AuthHeaders {
160
+ return Object.freeze({
161
+ get: (name: string) => source.headers.get(name),
162
+ has: (name: string) => source.headers.has(name),
163
+ forEach: (callback: (value: string, name: string) => void) => {
164
+ source.headers.forEach(callback)
165
+ },
166
+ })
167
+ }
168
+
169
+ function authenticationInputOf(
170
+ source: RequestSource,
171
+ ctx: RawContext,
172
+ ): import("../server/auth.ts").AuthenticationInput<unknown> {
173
+ return Object.freeze({
174
+ method: source.method,
175
+ pathname: pathnameOf(source.url),
176
+ headers: authHeadersOf(source),
177
+ params: Object.freeze({ ...ctx.params }),
178
+ env: ctx.env,
179
+ clientIp: ctx.clientIp,
180
+ signal: ctx.signal,
181
+ })
182
+ }
183
+
184
+ function defaultAuthenticationResponse(
185
+ reason: import("../server/auth.ts").AuthenticationFailureReason | undefined,
186
+ ): ResponseResult {
187
+ if (reason === "forbidden") return status(403, { ok: false, error: "forbidden" })
188
+ if (reason === "unavailable") return status(503, { ok: false, error: "auth_unavailable" })
189
+ return status(401, { ok: false, error: "unauthorized" })
190
+ }
191
+
192
+ function validAuthenticationFailureReason(
193
+ value: unknown,
194
+ ): value is import("../server/auth.ts").AuthenticationFailureReason | undefined {
195
+ return (
196
+ value === undefined ||
197
+ value === "unauthenticated" ||
198
+ value === "forbidden" ||
199
+ value === "unavailable"
200
+ )
201
+ }
202
+
203
+ function runAuthenticationStages<T>(
204
+ host: RouteProgramRuntime,
205
+ entry: RouteEntry,
206
+ program: RouteProgram,
207
+ source: RequestSource,
208
+ ctx: RawContext,
209
+ finalize: (result: unknown, set: CtxSet) => T,
210
+ wrapResponse: (response: Response | ResponseResult) => T,
211
+ start = 0,
212
+ validationComplete = false,
213
+ ): ProgramResult<T> {
214
+ if (start >= program.authStages.length) {
215
+ if (program.authFastLane) {
216
+ const validationIndex = validationComplete ? program.validationStages.length : 0
217
+ return runFastAuthenticatedProgram(
218
+ host,
219
+ entry,
220
+ program,
221
+ source,
222
+ ctx,
223
+ finalize,
224
+ wrapResponse,
225
+ validationIndex,
226
+ )
227
+ }
228
+ return validationComplete
229
+ ? runSync(
230
+ host,
231
+ entry,
232
+ program,
233
+ source,
234
+ ctx,
235
+ finalize,
236
+ wrapResponse,
237
+ program.validationStages.length,
238
+ undefined,
239
+ )
240
+ : runValidatedProgram(host, entry, program, source, ctx, finalize, wrapResponse)
241
+ }
242
+ const input = authenticationInputOf(source, ctx)
243
+ const stage = program.authStages[start]!
244
+ let outcome: MaybePromise<import("../server/auth.ts").AuthenticationResult<unknown>>
245
+ try {
246
+ outcome = stage.run(input)
247
+ } catch {
248
+ return finalize(defaultAuthenticationResponse("unavailable"), responseSet(ctx))
249
+ }
250
+ if (outcome instanceof Promise) {
251
+ return outcome.then(
252
+ (settled) =>
253
+ finishAuthentication(
254
+ host,
255
+ entry,
256
+ program,
257
+ source,
258
+ ctx,
259
+ finalize,
260
+ wrapResponse,
261
+ settled,
262
+ start + 1,
263
+ validationComplete,
264
+ ),
265
+ () => finalize(defaultAuthenticationResponse("unavailable"), responseSet(ctx)),
266
+ )
267
+ }
268
+ return finishAuthentication(
269
+ host,
270
+ entry,
271
+ program,
272
+ source,
273
+ ctx,
274
+ finalize,
275
+ wrapResponse,
276
+ outcome,
277
+ start + 1,
278
+ validationComplete,
279
+ )
280
+ }
281
+
282
+ function finishAuthentication<T>(
283
+ host: RouteProgramRuntime,
284
+ entry: RouteEntry,
285
+ program: RouteProgram,
286
+ source: RequestSource,
287
+ ctx: RawContext,
288
+ finalize: (result: unknown, set: CtxSet) => T,
289
+ wrapResponse: (response: Response | ResponseResult) => T,
290
+ result: import("../server/auth.ts").AuthenticationResult<unknown>,
291
+ next: number,
292
+ validationComplete = false,
293
+ ): ProgramResult<T> {
294
+ if (
295
+ result === null ||
296
+ typeof result !== "object" ||
297
+ (result.kind !== "authenticated" && result.kind !== "rejected")
298
+ ) {
299
+ return finalize(defaultAuthenticationResponse("unavailable"), responseSet(ctx))
300
+ }
301
+ if (result.kind === "rejected") {
302
+ if (!validAuthenticationFailureReason(result.reason)) {
303
+ return finalize(defaultAuthenticationResponse("unavailable"), responseSet(ctx))
304
+ }
305
+ if (
306
+ result.response !== undefined &&
307
+ !(result.response instanceof Response) &&
308
+ !isResponseResult(result.response)
309
+ ) {
310
+ return finalize(defaultAuthenticationResponse("unavailable"), responseSet(ctx))
311
+ }
312
+ const response = result.response ?? defaultAuthenticationResponse(result.reason)
313
+ return finalize(response, responseSet(ctx))
314
+ }
315
+ if (result.principal === null || result.principal === undefined) {
316
+ return finalize(defaultAuthenticationResponse("unavailable"), responseSet(ctx))
317
+ }
318
+ Object.defineProperty(ctx, "principal", {
319
+ value: result.principal,
320
+ enumerable: true,
321
+ configurable: true,
322
+ writable: false,
323
+ })
324
+ return runAuthenticationStages(
325
+ host,
326
+ entry,
327
+ program,
328
+ source,
329
+ ctx,
330
+ finalize,
331
+ wrapResponse,
332
+ next,
333
+ validationComplete,
334
+ )
335
+ }
336
+
337
+ /** Validate first only when a route explicitly opts out of the protected auth-first default. */
338
+ function runFastAuthenticatedProgram<T>(
339
+ host: RouteProgramRuntime,
340
+ entry: RouteEntry,
341
+ program: RouteProgram,
342
+ source: RequestSource,
343
+ ctx: RawContext,
344
+ finalize: (result: unknown, set: CtxSet) => T,
345
+ wrapResponse: (response: Response | ResponseResult) => T,
346
+ validationIndex: number,
347
+ ): ProgramResult<T> {
348
+ for (let i = validationIndex; i < program.validationStages.length; i++) {
349
+ const stage = program.validationStages[i]!
350
+ const outcome =
351
+ stage.kind === "body"
352
+ ? host.readProgramBody(entry, source, ctx)
353
+ : host.validateProgramStage(entry, stage, source, ctx)
354
+ if (outcome instanceof Promise) {
355
+ return continueFastAuthenticatedProgram(
356
+ host,
357
+ entry,
358
+ program,
359
+ source,
360
+ ctx,
361
+ finalize,
362
+ wrapResponse,
363
+ i + 1,
364
+ outcome,
365
+ )
366
+ }
367
+ if (outcome !== undefined) return wrapResponse(outcome)
368
+ }
369
+ return runFastAuthenticatedHandler(host, entry, program, ctx, finalize, wrapResponse)
370
+ }
371
+
372
+ async function continueFastAuthenticatedProgram<T>(
373
+ host: RouteProgramRuntime,
374
+ entry: RouteEntry,
375
+ program: RouteProgram,
376
+ source: RequestSource,
377
+ ctx: RawContext,
378
+ finalize: (result: unknown, set: CtxSet) => T,
379
+ wrapResponse: (response: Response | ResponseResult) => T,
380
+ validationIndex: number,
381
+ pending: Promise<unknown>,
382
+ ): Promise<T> {
383
+ try {
384
+ const settled = await pending
385
+ if (settled !== undefined) return wrapResponse(settled as Response | ResponseResult)
386
+ return await runFastAuthenticatedProgram(
387
+ host,
388
+ entry,
389
+ program,
390
+ source,
391
+ ctx,
392
+ finalize,
393
+ wrapResponse,
394
+ validationIndex,
395
+ )
396
+ } catch (error) {
397
+ return await host.handleProgramError(entry, error, ctx, finalize, wrapResponse)
398
+ }
399
+ }
400
+
401
+ function runFastAuthenticatedHandler<T>(
402
+ host: RouteProgramRuntime,
403
+ entry: RouteEntry,
404
+ program: RouteProgram,
405
+ ctx: RawContext,
406
+ finalize: (result: unknown, set: CtxSet) => T,
407
+ wrapResponse: (response: Response | ResponseResult) => T,
408
+ ): ProgramResult<T> {
409
+ let result: unknown
410
+ try {
411
+ result = program.handler(ctx)
412
+ } catch (error) {
413
+ return host.handleProgramError(entry, error, ctx, finalize, wrapResponse)
414
+ }
415
+ if (result instanceof Promise) {
416
+ return result.then(
417
+ (value) => host.finishProgramResult(entry, ctx, value, finalize, wrapResponse),
418
+ (error) => host.handleProgramError(entry, error, ctx, finalize, wrapResponse),
419
+ )
420
+ }
421
+ return host.finishProgramResult(entry, ctx, result, finalize, wrapResponse)
422
+ }
423
+
424
+ function runValidationBeforeAuthentication<T>(
425
+ host: RouteProgramRuntime,
426
+ entry: RouteEntry,
427
+ program: RouteProgram,
428
+ source: RequestSource,
429
+ ctx: RawContext,
430
+ finalize: (result: unknown, set: CtxSet) => T,
431
+ wrapResponse: (response: Response | ResponseResult) => T,
432
+ validationIndex = 0,
433
+ ): ProgramResult<T> {
434
+ for (let i = validationIndex; i < program.validationStages.length; i++) {
435
+ const stage = program.validationStages[i]!
436
+ const outcome =
437
+ stage.kind === "body"
438
+ ? host.readProgramBody(entry, source, ctx)
439
+ : host.validateProgramStage(entry, stage, source, ctx)
440
+ if (outcome instanceof Promise) {
441
+ return continueValidationBeforeAuthentication(
442
+ host,
443
+ entry,
444
+ program,
445
+ source,
446
+ ctx,
447
+ finalize,
448
+ wrapResponse,
449
+ i + 1,
450
+ outcome,
451
+ )
452
+ }
453
+ if (outcome !== undefined) return wrapResponse(outcome)
454
+ }
455
+ return runAuthenticationStages(host, entry, program, source, ctx, finalize, wrapResponse, 0, true)
456
+ }
457
+
458
+ async function continueValidationBeforeAuthentication<T>(
459
+ host: RouteProgramRuntime,
460
+ entry: RouteEntry,
461
+ program: RouteProgram,
462
+ source: RequestSource,
463
+ ctx: RawContext,
464
+ finalize: (result: unknown, set: CtxSet) => T,
465
+ wrapResponse: (response: Response | ResponseResult) => T,
466
+ validationIndex: number,
467
+ pending: Promise<unknown>,
468
+ ): Promise<T> {
469
+ try {
470
+ const settled = await pending
471
+ if (settled !== undefined) return wrapResponse(settled as Response | ResponseResult)
472
+ return await runValidationBeforeAuthentication(
473
+ host,
474
+ entry,
475
+ program,
476
+ source,
477
+ ctx,
478
+ finalize,
479
+ wrapResponse,
480
+ validationIndex,
481
+ )
482
+ } catch (error) {
483
+ return await host.handleProgramError(entry, error, ctx, finalize, wrapResponse)
484
+ }
485
+ }
486
+
487
+ function runValidatedProgram<T>(
488
+ host: RouteProgramRuntime,
489
+ entry: RouteEntry,
490
+ program: RouteProgram,
491
+ source: RequestSource,
492
+ ctx: RawContext,
493
+ finalize: (result: unknown, set: CtxSet) => T,
494
+ wrapResponse: (response: Response | ResponseResult) => T,
495
+ ): ProgramResult<T> {
496
+ if (program.hasDecorations) Object.assign(ctx, program.decorations)
497
+ return runSync(host, entry, program, source, ctx, finalize, wrapResponse, 0, undefined)
498
+ }
499
+
149
500
  type ProgramResult<T> = MaybePromise<T>
150
501
 
502
+ /** Opt-in sensitive-route lane: run authentication/policy hooks before parsing untrusted input. */
503
+ async function runAuthBeforeValidation<T>(
504
+ host: RouteProgramRuntime,
505
+ entry: RouteEntry,
506
+ program: RouteProgram,
507
+ source: RequestSource,
508
+ ctx: RawContext,
509
+ finalize: (result: unknown, set: CtxSet) => T,
510
+ wrapResponse: (response: Response | ResponseResult) => T,
511
+ ): Promise<T> {
512
+ try {
513
+ if (program.hasDecorations) Object.assign(ctx, program.decorations)
514
+
515
+ for (const derive of program.derives) {
516
+ const result = derive(ctx)
517
+ const settled = result instanceof Promise ? await result : result
518
+ if (isEarly(settled)) return finalize(settled, responseSet(ctx))
519
+ Object.assign(ctx, settled)
520
+ }
521
+
522
+ for (const before of program.beforeHandle) {
523
+ const result = before(ctx)
524
+ const settled = result instanceof Promise ? await result : result
525
+ if (settled !== undefined) return finalize(settled, responseSet(ctx))
526
+ }
527
+
528
+ for (const stage of program.validationStages) {
529
+ const result =
530
+ stage.kind === "body"
531
+ ? await host.readProgramBody(entry, source, ctx)
532
+ : host.validateProgramStage(entry, stage, source, ctx)
533
+ const settled = result instanceof Promise ? await result : result
534
+ if (settled !== undefined) return wrapResponse(settled)
535
+ }
536
+
537
+ let result = await program.handler(ctx)
538
+ for (const after of program.afterHandle) {
539
+ const transformed = after(result, ctx)
540
+ result = transformed instanceof Promise ? await transformed : transformed
541
+ }
542
+ return await host.finishProgramResult(entry, ctx, result, finalize, wrapResponse)
543
+ } catch (error) {
544
+ return await host.handleProgramError(entry, error, ctx, finalize, wrapResponse)
545
+ }
546
+ }
547
+
151
548
  type PendingPhase =
152
549
  | { readonly kind: "validation"; readonly next: number }
153
550
  | { readonly kind: "derive"; readonly next: number }
@@ -167,6 +564,23 @@ export function executeRouteProgram<T, R extends Registry, Ctx>(
167
564
  ): ProgramResult<T> {
168
565
  const host = runtime as unknown as RouteProgramRuntime
169
566
  try {
567
+ if (program.authStages.length > 0) {
568
+ if (program.hasDecorations) Object.assign(ctx, program.decorations)
569
+ return program.authBeforeValidation
570
+ ? runAuthenticationStages(host, entry, program, source, ctx, finalize, wrapResponse)
571
+ : runValidationBeforeAuthentication(
572
+ host,
573
+ entry,
574
+ program,
575
+ source,
576
+ ctx,
577
+ finalize,
578
+ wrapResponse,
579
+ )
580
+ }
581
+ if (program.validationOrder === "auth-before-validation") {
582
+ return runAuthBeforeValidation(host, entry, program, source, ctx, finalize, wrapResponse)
583
+ }
170
584
  if (program.hasDecorations) Object.assign(ctx, program.decorations)
171
585
  return runSync(host, entry, program, source, ctx, finalize, wrapResponse, 0, undefined)
172
586
  } catch (error) {