@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
@@ -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) {
package/src/ledger.ts CHANGED
@@ -145,7 +145,7 @@ const ERROR_CODE = /^[a-z][a-z0-9_.-]{0,63}$/
145
145
  const DIGEST_HEX = /^[0-9a-f]{64}$/
146
146
 
147
147
  function printableToken(value: string, maxLength: number): boolean {
148
- if (value.length === 0 || value.length > maxLength) return false
148
+ if (typeof value !== "string" || value.length === 0 || value.length > maxLength) return false
149
149
  for (let index = 0; index < value.length; index++) {
150
150
  const code = value.charCodeAt(index)
151
151
  if (code < 33 || code > 126) return false
@@ -153,6 +153,9 @@ function printableToken(value: string, maxLength: number): boolean {
153
153
  return true
154
154
  }
155
155
 
156
+ // `RegExp.test` stringifies its argument, so a check on a token's shape must first check it is a string.
157
+ const validDigest = (value: string): boolean => typeof value === "string" && DIGEST_HEX.test(value)
158
+
156
159
  function validateCost(cost: EffectCost): EffectCost {
157
160
  if (Object(cost) !== cost || Array.isArray(cost)) {
158
161
  throw new TypeError("effect ledger: invalid cost")
@@ -181,7 +184,7 @@ export function normalizeEffectMetadata(input: EffectMetadata): EffectMetadata {
181
184
  if (input.target !== undefined && !printableToken(input.target, TARGET_MAX_LENGTH)) {
182
185
  throw new TypeError("effect ledger: invalid target")
183
186
  }
184
- if (input.digest !== undefined && !DIGEST_HEX.test(input.digest)) {
187
+ if (input.digest !== undefined && !validDigest(input.digest)) {
185
188
  throw new TypeError("effect ledger: invalid digest")
186
189
  }
187
190
  return Object.freeze({
@@ -236,7 +239,7 @@ class BoundedRequestLedger implements RequestLedger {
236
239
  append(input: EffectEntryInput): EffectEntry {
237
240
  if (this.sealResult !== undefined) throw new EffectLedgerSealedError()
238
241
  if (this.items.length >= this.maxEntries) throw new EffectLedgerOverflowError(this.maxEntries)
239
- if (!validCapabilityId(input.capability)) {
242
+ if (typeof input.capability !== "string" || !validCapabilityId(input.capability)) {
240
243
  throw new TypeError(`effect ledger: invalid capability ${JSON.stringify(input.capability)}`)
241
244
  }
242
245
  const phase = input.phase ?? "intent"
@@ -249,10 +252,13 @@ class BoundedRequestLedger implements RequestLedger {
249
252
  if (input.target !== undefined && !printableToken(input.target, TARGET_MAX_LENGTH)) {
250
253
  throw new TypeError("effect ledger: invalid target")
251
254
  }
252
- if (input.digest !== undefined && !DIGEST_HEX.test(input.digest)) {
255
+ if (input.digest !== undefined && !validDigest(input.digest)) {
253
256
  throw new TypeError("effect ledger: invalid digest")
254
257
  }
255
- if (input.error !== undefined && !ERROR_CODE.test(input.error.code)) {
258
+ if (
259
+ input.error !== undefined &&
260
+ (typeof input.error?.code !== "string" || !ERROR_CODE.test(input.error.code))
261
+ ) {
256
262
  throw new TypeError("effect ledger: error code must be a bounded lowercase token")
257
263
  }
258
264
  // Only the known token fields are copied - a stray `payload`-like property never survives append.