@nifrajs/core 3.5.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 (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
@@ -15,6 +15,12 @@ export interface IdempotencyConfig {
15
15
  readonly scope: IdempotencyScope
16
16
  /** Retention for the stored response. Default 24h. */
17
17
  readonly ttlMs?: number
18
+ /**
19
+ * How long a key whose handler is still running stays reserved without a renewal. The server
20
+ * renews it every third of this while the handler runs, so only a dead process lets it lapse and a
21
+ * retry waits this long rather than `ttlMs`. Needs a store with `renew()`; default 60s with one.
22
+ */
23
+ readonly pendingTtlMs?: number
18
24
  /** Store override. Defaults to the server's shared in-memory store; inject a durable store here. */
19
25
  readonly store?: IdempotencyStore
20
26
  /** Header carrying the key. Default `idempotency-key`. */
@@ -42,26 +48,213 @@ export interface IdempotencyConfig {
42
48
  /** Flattens an intersection into a single object type for readable hovers. */
43
49
  export type Prettify<T> = { [K in keyof T]: T[K] } & {}
44
50
 
51
+ type ParamNameStart =
52
+ | "a"
53
+ | "b"
54
+ | "c"
55
+ | "d"
56
+ | "e"
57
+ | "f"
58
+ | "g"
59
+ | "h"
60
+ | "i"
61
+ | "j"
62
+ | "k"
63
+ | "l"
64
+ | "m"
65
+ | "n"
66
+ | "o"
67
+ | "p"
68
+ | "q"
69
+ | "r"
70
+ | "s"
71
+ | "t"
72
+ | "u"
73
+ | "v"
74
+ | "w"
75
+ | "x"
76
+ | "y"
77
+ | "z"
78
+ | "A"
79
+ | "B"
80
+ | "C"
81
+ | "D"
82
+ | "E"
83
+ | "F"
84
+ | "G"
85
+ | "H"
86
+ | "I"
87
+ | "J"
88
+ | "K"
89
+ | "L"
90
+ | "M"
91
+ | "N"
92
+ | "O"
93
+ | "P"
94
+ | "Q"
95
+ | "R"
96
+ | "S"
97
+ | "T"
98
+ | "U"
99
+ | "V"
100
+ | "W"
101
+ | "X"
102
+ | "Y"
103
+ | "Z"
104
+ | "_"
105
+ type ParamNameChar = ParamNameStart | "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
106
+
107
+ /** The longest parameter name at the start of `Text`, and the text after it. */
108
+ type TakeParamName<
109
+ Text extends string,
110
+ Name extends string = "",
111
+ > = Text extends `${infer Char}${infer Rest}`
112
+ ? Char extends (Name extends "" ? ParamNameStart : ParamNameChar)
113
+ ? TakeParamName<Rest, `${Name}${Char}`>
114
+ : [Name, Text]
115
+ : [Name, ""]
116
+
117
+ /**
118
+ * Whether a colon is literal text: one with no name after it, or one that follows a name character
119
+ * and whose name runs to the end of the segment (`/things:batchGet`).
120
+ */
121
+ type IsLiteralColon<
122
+ Before extends string,
123
+ Name extends string,
124
+ Rest extends string,
125
+ > = Name extends ""
126
+ ? true
127
+ : Before extends `${string}${ParamNameChar}`
128
+ ? Rest extends "" | `/${string}`
129
+ ? true
130
+ : false
131
+ : false
132
+
45
133
  /**
46
134
  * Extracts `:param` and trailing `*wildcard` names from a route-path literal into
47
135
  * a string→string record: `/users/:id/posts/:postId` → `{ id: string; postId:
48
136
  * string }`, `/files/*path` → `{ path: string }`, `/files/*` → `{ "*": string }`.
137
+ * A name ends where the router ends it, at the first character that cannot be part of one, so
138
+ * `/files/:name.json` → `{ name: string }` and `/v:major.:minor` → `{ major: string; minor: string }`.
139
+ * A constraint does not change the type: `/users/:id{[0-9]+}` → `{ id: string }`.
49
140
  * A non-literal `string` path widens to `Record<string, string>`.
50
141
  */
51
142
  type RawParams<Path extends string> = string extends Path
52
143
  ? Record<string, string>
53
- : Path extends `${infer _Start}:${infer Param}/${infer Rest}`
54
- ? Record<Param, string> & RawParams<`/${Rest}`>
55
- : Path extends `${infer _Start}:${infer Param}`
56
- ? Record<Param, string>
57
- : Path extends `${infer _Start}*${infer Wild}`
58
- ? Record<Wild extends "" ? "*" : Wild, string>
59
- : Record<never, string>
144
+ : Path extends `${infer Before}:${infer After}`
145
+ ? TakeParamName<After> extends [infer Name extends string, infer Rest extends string]
146
+ ? IsLiteralColon<Before, Name, Rest> extends true
147
+ ? RawParams<Rest>
148
+ : Record<Name, string> & RawParams<Rest>
149
+ : never
150
+ : Path extends `${infer _Start}*${infer Wild}`
151
+ ? Record<Wild extends "" ? "*" : Wild, string>
152
+ : Record<never, string>
153
+
154
+ /** `[constraint, rest]` for a leading `{...}`, else `["", Text]`. One nested level, for `{[0-9]{2}}`. */
155
+ type TakeConstraint<Text extends string> = Text extends `{${infer Inner}}${infer Rest}`
156
+ ? Inner extends `[${string}` | `\\${string}` | `${string}|${string}`
157
+ ? Inner extends `${string}{${string}`
158
+ ? Rest extends `}${infer After}`
159
+ ? [`{${Inner}}}`, After]
160
+ : ["", Text]
161
+ : [`{${Inner}}`, Rest]
162
+ : ["", Text]
163
+ : ["", Text]
164
+
165
+ /**
166
+ * Whether `Run` is nothing but whole-segment optional parameters, each with or without a constraint:
167
+ * `/:a?`, `/:a?/:b?`, `/:id{[0-9]+}?`.
168
+ */
169
+ type IsOptionalRun<Run extends string> = Run extends ""
170
+ ? true
171
+ : Run extends `/:${infer After}`
172
+ ? TakeParamName<After> extends [infer Name extends string, infer Tail extends string]
173
+ ? TakeConstraint<Tail> extends [string, `?${infer Rest}`]
174
+ ? Name extends ""
175
+ ? false
176
+ : IsOptionalRun<Rest>
177
+ : false
178
+ : false
179
+ : false
180
+
181
+ /** `[head, run]` for a path ending in `:name?` segments, else `false`; the same cut the router makes. */
182
+ type SplitOptionalRun<
183
+ Path extends string,
184
+ Head extends string = "",
185
+ > = Path extends `${infer Before}/:${infer After}`
186
+ ? IsOptionalRun<`/:${After}`> extends true
187
+ ? [`${Head}${Before}`, `/:${After}`]
188
+ : SplitOptionalRun<After, `${Head}${Before}/:`>
189
+ : false
190
+
191
+ /** `Head`, then `Head` with each longer prefix of `Run` appended - the `?` marks dropped. */
192
+ type OptionalForms<Head extends string, Run extends string, Done extends string = ""> =
193
+ | (`${Head}${Done}` extends "" ? "/" : `${Head}${Done}`)
194
+ | (Run extends `/:${infer After}`
195
+ ? TakeParamName<After> extends [infer Name extends string, infer Tail extends string]
196
+ ? TakeConstraint<Tail> extends [infer Constraint extends string, `?${infer Rest}`]
197
+ ? OptionalForms<Head, Rest, `${Done}/:${Name}${Constraint}`>
198
+ : never
199
+ : never
200
+ : never)
60
201
 
61
- export type Params<Path extends string> = Prettify<RawParams<Path>>
202
+ /** The registry keys of a route path: `/users/:id?` is `"/users" | "/users/:id"`, others are themselves. */
203
+ export type RoutePaths<Path extends string> = Path extends `${string}?`
204
+ ? SplitOptionalRun<Path> extends [infer Head extends string, infer Run extends string]
205
+ ? OptionalForms<Head, Run>
206
+ : Path
207
+ : Path
208
+
209
+ /** One concrete path's request text: each parameter replaced by `${string}`, literal text kept. */
210
+ type PathText<Path extends string> = Path extends `${infer Before}:${infer After}`
211
+ ? TakeParamName<After> extends [infer Name extends string, infer Rest extends string]
212
+ ? IsLiteralColon<Before, Name, Rest> extends true
213
+ ? `${Before}:${Name}${PathText<Rest>}`
214
+ : TakeConstraint<Rest> extends [string, infer Tail extends string]
215
+ ? `${Before}${string}${PathText<Tail>}`
216
+ : never
217
+ : never
218
+ : Path extends `${infer Start}/*${string}`
219
+ ? `${Start}/${string}`
220
+ : Path
221
+
222
+ /**
223
+ * The text of a request a route path serves, as a template: `/users/:id` → `` `/users/${string}` ``,
224
+ * `/files/:name.json` → `` `/files/${string}.json` ``, `/files/*path` → `` `/files/${string}` ``.
225
+ * Literal text is kept, so a value has to carry it. A constraint is not checked - `/users/:id{[0-9]+}`
226
+ * is `` `/users/${string}` `` - and a path ending in optional parameters is one template per form:
227
+ * `/users/:id?` → `` "/users" | `/users/${string}` ``. A non-literal `string` path stays `string`.
228
+ */
229
+ export type RequestPath<Path extends string> = string extends Path
230
+ ? string
231
+ : RoutePaths<Path> extends infer Form extends string
232
+ ? Form extends unknown
233
+ ? PathText<Form>
234
+ : never
235
+ : never
236
+
237
+ /**
238
+ * The params a handler for `Path` reads. A parameter in a trailing optional run is optional, since
239
+ * the one handler serves the path with and without it: `/users/:id?` → `{ id?: string }`.
240
+ */
241
+ export type Params<Path extends string> = Path extends `${string}?`
242
+ ? SplitOptionalRun<Path> extends [infer Head extends string, infer Run extends string]
243
+ ? Prettify<RawParams<Head> & Partial<RawParams<Run>>>
244
+ : Prettify<RawParams<Path>>
245
+ : Prettify<RawParams<Path>>
62
246
 
63
247
  /** Per-route input schemas. Each is any Standard Schema (zod/valibot/arktype/…). */
64
248
  export interface RouteSchema {
249
+ /** Explicit external-protocol mode. The handler owns raw `c.req` parsing/streaming; no JSON body
250
+ * parser or schema-driven 422 path is installed. A finite transport cap still applies by default. */
251
+ readonly wire?: "raw"
252
+ /**
253
+ * Choose trust-boundary order for sensitive routes. Public routes validate first by default;
254
+ * routes with `authenticate()` default to auth first. `validate-before-auth` is an explicit
255
+ * compatibility escape hatch, while `auth-before-validation` can reject without parsing the body.
256
+ */
257
+ readonly validationOrder?: "validate-before-auth" | "auth-before-validation"
65
258
  /** Transport body cap for this route. Defaults to the server's maxBodyBytes. Use a finite
66
259
  * smaller/larger limit for a specific endpoint. The 'unlimited' value is an explicit
67
260
  * streaming/upload exemption and requires a non-empty bodyLimitReason for auditability.
@@ -122,6 +315,12 @@ export interface RouteSchema {
122
315
  * output is exposed as `c.headers`; use lower-case keys because HTTP header names are
123
316
  * case-insensitive. */
124
317
  readonly headers?: StandardSchemaV1
318
+ /** Optional request-cookie schema. It validates the cookies parsed from the `Cookie` header -
319
+ * names as sent, values URL-decoded strings, the first of a repeated name - before the handler
320
+ * runs, and the validated output is `c.cookies`. A failure is a `422`, like `headers`. A browser
321
+ * sends every cookie the site has set, so declare the ones the route reads with an open schema
322
+ * (`t.cookies`, `t.looseObject`) rather than rejecting the rest. */
323
+ readonly cookies?: StandardSchemaV1
125
324
  readonly body?: StandardSchemaV1
126
325
  readonly query?: StandardSchemaV1
127
326
  /** Optional **response contract**. When declared: the handler's return is type-checked against it
@@ -146,7 +345,7 @@ export interface RouteSchema {
146
345
  */
147
346
  readonly sse?: StandardSchemaV1
148
347
  /**
149
- * Hook fired when the request fails `headers`/`params`/`body`/`query` validation, before the handler
348
+ * Hook fired when the request fails `headers`/`cookies`/`params`/`body`/`query` validation, before the handler
150
349
  * runs. `kind` says which input failed. Its return value selects one of three outcomes (may be async):
151
350
  * - a **`Response`** → returned as-is, short-circuiting the route (custom error envelope, redirect, …).
152
351
  * - **any other value** → treated as a repaired payload and **re-validated once** against the same
@@ -161,7 +360,7 @@ export interface RouteSchema {
161
360
  readonly onValidationError?: (
162
361
  issues: ReadonlyArray<StandardIssue>,
163
362
  ctx: Context,
164
- kind: "body" | "query" | "params" | "headers",
363
+ kind: "body" | "query" | "params" | "headers" | "cookies",
165
364
  ) => Response | unknown | Promise<Response | unknown>
166
365
  }
167
366
 
@@ -182,6 +381,13 @@ type HeadersOf<S extends RouteSchema> = S extends {
182
381
  ? InferOutput<H>
183
382
  : Readonly<Record<string, string>>
184
383
 
384
+ /** The validated cookie type when a cookie schema is declared, else the parsed name → value record. */
385
+ type CookiesOf<S extends RouteSchema> = S extends {
386
+ cookies: infer C extends StandardSchemaV1
387
+ }
388
+ ? InferOutput<C>
389
+ : Readonly<Record<string, string>>
390
+
185
391
  /** The validated params type when a params schema is declared, else the path-inferred `Params<Path>`. */
186
392
  type ParamsOf<S extends RouteSchema, Path extends string> = S extends {
187
393
  params: infer P extends StandardSchemaV1
@@ -244,8 +450,9 @@ export interface Context<Path extends string = string, S extends RouteSchema = R
244
450
  readonly query: QueryOf<S>
245
451
  readonly body: BodyOf<S>
246
452
  /** The request's cookies, parsed from the `Cookie` header (values URL-decoded). Parsed lazily on
247
- * first access + cached. Signed cookies arrive as `value.signature` - verify with `unsignValue`. */
248
- readonly cookies: Readonly<Record<string, string>>
453
+ * first access + cached; validated/coerced when `schema.cookies` is declared. Signed cookies arrive
454
+ * as `value.signature` - verify with `unsignValue`. */
455
+ readonly cookies: CookiesOf<S>
249
456
  readonly set: ResponseControls
250
457
  /**
251
458
  * Aborts when the server's `requestTimeoutMs` elapses (and never, when no timeout
@@ -1,10 +1,11 @@
1
1
  import type { DataClassification } from "../classification.ts"
2
2
  import { RouteConfigError } from "../errors.ts"
3
3
  import { normalizeRouteCapabilities } from "../internal/capability-runtime.ts"
4
+ import { expandOptionalParams } from "../router/pattern.ts"
4
5
  import { METHODS, type Method } from "../router/router.ts"
5
6
  import type { InferInput, InferOutput, StandardSchemaV1 } from "../schema/standard.ts"
6
7
  import { assertByteLimit } from "./body.ts"
7
- import type { Context, IdempotencyConfig, Params, RouteSchema } from "./context.ts"
8
+ import type { Context, IdempotencyConfig, Params, RoutePaths, RouteSchema } from "./context.ts"
8
9
  import type { EmptyRegistry, OutputOf, Registry, RouteInfoFor } from "./registry.ts"
9
10
  import { Server } from "./server.ts"
10
11
 
@@ -33,7 +34,13 @@ export interface OperationDef {
33
34
  readonly params?: StandardSchemaV1
34
35
  /** Optional request-header schema (header names are normalized to lower-case). */
35
36
  readonly headers?: StandardSchemaV1
37
+ /** Optional request-cookie schema (see {@link RouteSchema.cookies}). */
38
+ readonly cookies?: StandardSchemaV1
36
39
  readonly body?: StandardSchemaV1
40
+ /** Explicit external-protocol mode; raw bytes/text are read by the handler from `c.req`. */
41
+ readonly wire?: "raw"
42
+ /** Run derive/beforeHandle authentication before body/query validation on sensitive routes. */
43
+ readonly validationOrder?: "validate-before-auth" | "auth-before-validation"
37
44
  readonly query?: StandardSchemaV1
38
45
  readonly response?: StandardSchemaV1
39
46
  /** Declared effect tokens, carried into route reflection and capability assurance. */
@@ -153,8 +160,10 @@ type RouteInfoForOp<O extends OperationDef> = {
153
160
 
154
161
  /** Re-key the name-keyed ops into the `path → method → RouteInfo` registry. */
155
162
  export type RegistryFor<C extends ContractShape> = {
156
- [P in C[keyof C]["path"]]: {
157
- [K in keyof C as C[K]["path"] extends P ? C[K]["method"] : never]: RouteInfoForOp<C[K]>
163
+ [P in RoutePaths<C[keyof C]["path"]>]: {
164
+ [K in keyof C as P extends RoutePaths<C[K]["path"]> ? C[K]["method"] : never]: RouteInfoForOp<
165
+ C[K]
166
+ >
158
167
  }
159
168
  }
160
169
 
@@ -166,6 +175,9 @@ type SchemaForOp<O extends OperationDef> = (O extends { body: infer B extends St
166
175
  (O extends { headers: infer H extends StandardSchemaV1 }
167
176
  ? { headers: H }
168
177
  : Record<never, never>) &
178
+ (O extends { cookies: infer K extends StandardSchemaV1 }
179
+ ? { cookies: K }
180
+ : Record<never, never>) &
169
181
  (O extends { params: infer P extends StandardSchemaV1 } ? { params: P } : Record<never, never>)
170
182
 
171
183
  type OpResponseSchemas<O extends OperationDef> = O extends {
@@ -267,12 +279,37 @@ export function defineContract<const C extends ContractShape>(contract: C): C {
267
279
  )
268
280
  }
269
281
  }
270
- const key = `${method} ${op.path}`
271
- if (seen.has(key)) {
272
- throw new RouteConfigError("DUPLICATE_ROUTE", `duplicate operation route: ${key}`)
282
+ if (op.wire !== undefined && op.wire !== "raw") {
283
+ throw new RouteConfigError(
284
+ "INVALID_WIRE",
285
+ `operation "${name}": wire must be "raw" when specified`,
286
+ )
287
+ }
288
+ if (op.wire === "raw" && op.body !== undefined) {
289
+ throw new RouteConfigError(
290
+ "INVALID_WIRE",
291
+ `operation "${name}": wire: "raw" cannot be combined with a body schema`,
292
+ )
293
+ }
294
+ if (
295
+ op.validationOrder !== undefined &&
296
+ op.validationOrder !== "validate-before-auth" &&
297
+ op.validationOrder !== "auth-before-validation"
298
+ ) {
299
+ throw new RouteConfigError(
300
+ "INVALID_VALIDATION_ORDER",
301
+ `operation "${name}": validationOrder is invalid`,
302
+ )
303
+ }
304
+ // Compared per concrete path, so `/users` collides with the short form of `/users/:id?`.
305
+ for (const path of expandOptionalParams(op.path)) {
306
+ const key = `${method} ${path}`
307
+ if (seen.has(key)) {
308
+ throw new RouteConfigError("DUPLICATE_ROUTE", `duplicate operation route: ${key}`)
309
+ }
310
+ seen.add(key)
273
311
  }
274
312
  normalizeRouteCapabilities(op.capabilities)
275
- seen.add(key)
276
313
  }
277
314
  return contract
278
315
  }
@@ -291,8 +328,10 @@ export type RegistryFromImpl<
291
328
  Ctx = NonNullable<unknown>,
292
329
  HookOutput = never,
293
330
  > = {
294
- [P in C[keyof C]["path"]]: {
295
- [K in keyof C as C[K]["path"] extends P ? C[K]["method"] : never]: H[K] extends AnyFn
331
+ [P in RoutePaths<C[keyof C]["path"]>]: {
332
+ [K in keyof C as P extends RoutePaths<C[K]["path"]>
333
+ ? C[K]["method"]
334
+ : never]: H[K] extends AnyFn
296
335
  ? RouteInfoFor<C[K]["path"], RouteSchemaForOp<C[K]>, OutputOf<H[K]>, HookOutput>
297
336
  : RouteInfoFor<C[K]["path"], RouteSchemaForOp<C[K]>, unknown, HookOutput>
298
337
  }
@@ -349,7 +388,10 @@ export function implement<
349
388
  const schema: RouteSchema | undefined =
350
389
  op.params !== undefined ||
351
390
  op.headers !== undefined ||
391
+ op.cookies !== undefined ||
352
392
  op.body !== undefined ||
393
+ op.wire !== undefined ||
394
+ op.validationOrder !== undefined ||
353
395
  op.query !== undefined ||
354
396
  op.response !== undefined ||
355
397
  (errors !== undefined && Object.keys(errors).length > 0) ||
@@ -362,7 +404,10 @@ export function implement<
362
404
  ? {
363
405
  ...(op.params !== undefined ? { params: op.params } : {}),
364
406
  ...(op.headers !== undefined ? { headers: op.headers } : {}),
407
+ ...(op.cookies !== undefined ? { cookies: op.cookies } : {}),
365
408
  ...(op.body !== undefined ? { body: op.body } : {}),
409
+ ...(op.wire !== undefined ? { wire: op.wire } : {}),
410
+ ...(op.validationOrder !== undefined ? { validationOrder: op.validationOrder } : {}),
366
411
  ...(op.query !== undefined ? { query: op.query } : {}),
367
412
  ...(op.response !== undefined ? { response: op.response } : {}),
368
413
  ...(errors !== undefined && Object.keys(errors).length > 0 ? { errors } : {}),
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Two physical copies of `@nifrajs/core` in one process (a linked sibling checkout, a nested install)
3
+ * split every piece of module-scoped identity: a route from one copy's `server()` merged into the
4
+ * other answers 500 from an internal TypeError as soon as it sets a cookie. The install is the
5
+ * defect, so it is named the moment the second copy loads, not at the first failing request.
6
+ * `nifra check` is the gate that fails the build on it; this is the runtime notice.
7
+ *
8
+ * Each copy records its module URL in a process-wide list. The query is dropped, so a dev server that
9
+ * re-evaluates this same file (HMR, `--hot`) is one copy, not two.
10
+ */
11
+ const registry = globalThis as unknown as Record<symbol, string[] | undefined>
12
+ const COPIES = Symbol.for("nifra.core.copies")
13
+ registry[COPIES] ??= []
14
+ const copies = registry[COPIES]
15
+ const here = String(import.meta.url).split("?")[0] as string
16
+
17
+ if (!copies.includes(here) && copies.push(here) > 1) {
18
+ console.warn(
19
+ `[nifra] @nifrajs/core is loaded ${copies.length} times; \`nifra check\` names each importer:\n ${copies.join("\n ")}`,
20
+ )
21
+ }
@@ -140,6 +140,8 @@ export function toFetchHandler<Env = unknown>(
140
140
  outcome.attach(server, outcome.handler, outcome.data, {
141
141
  openNow: true,
142
142
  pubsub: outcome.pubsub,
143
+ // `wsMaxPayloadBytes`, as the Bun, Node, Deno and hub lanes enforce it.
144
+ maxPayloadBytes: outcome.maxPayloadBytes,
143
145
  })
144
146
  // `webSocket` is a Workers-only `ResponseInit` field (absent from the standard type), and a
145
147
  // 101 status is only valid on the Workers runtime - both gated by the `Pair` feature check.
@@ -33,6 +33,40 @@ export function isSameOriginPath(value: string): boolean {
33
33
  return true
34
34
  }
35
35
 
36
+ /**
37
+ * Whether `method` is a token a route can be registered under: uppercase ASCII letters, digits and
38
+ * hyphens, a letter first, 1 to 32 characters. That covers every method in the IANA registry.
39
+ *
40
+ * Every registered method has this shape and the router compares tokens exactly, so a request whose
41
+ * token does not have it can never reach a handler. The server uses this to keep such a request away
42
+ * from everything that runs around routing as well - `onRequest` hooks and mounted handlers - because
43
+ * those see the raw token, and code that tests `req.method === "POST"` would otherwise read a
44
+ * differently-cased token as a method it does not guard.
45
+ */
46
+ export function isRoutableMethod(method: string): boolean {
47
+ // The common verbs first: a string switch settles them without walking the token, which keeps the
48
+ // check close to free on the lanes that run it per request.
49
+ switch (method) {
50
+ case "GET":
51
+ case "POST":
52
+ case "PUT":
53
+ case "PATCH":
54
+ case "DELETE":
55
+ case "HEAD":
56
+ case "OPTIONS":
57
+ return true
58
+ }
59
+ const length = method.length
60
+ if (length === 0 || length > 32) return false
61
+ for (let i = 0; i < length; i++) {
62
+ const code = method.charCodeAt(i)
63
+ if (code >= 65 && code <= 90) continue
64
+ if (i > 0 && (code === 45 || (code >= 48 && code <= 57))) continue
65
+ return false
66
+ }
67
+ return true
68
+ }
69
+
36
70
  /** A uniform JSON error envelope: `{ ok: false, error }` at the given status. */
37
71
  export function jsonError(
38
72
  status: number,