@nifrajs/cli 4.0.2 → 5.1.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 (364) hide show
  1. package/README.md +10 -2
  2. package/dist/agent-files.d.ts +107 -0
  3. package/dist/agent-files.d.ts.map +1 -0
  4. package/dist/agent-files.js +162 -0
  5. package/dist/agent-files.js.map +1 -0
  6. package/dist/app-files.d.ts +6 -0
  7. package/dist/app-files.d.ts.map +1 -1
  8. package/dist/app-files.js +27 -2
  9. package/dist/app-files.js.map +1 -1
  10. package/dist/assure-hydration.d.ts.map +1 -1
  11. package/dist/assure-hydration.js +9 -2
  12. package/dist/assure-hydration.js.map +1 -1
  13. package/dist/assure.d.ts +12 -2
  14. package/dist/assure.d.ts.map +1 -1
  15. package/dist/assure.js +59 -8
  16. package/dist/assure.js.map +1 -1
  17. package/dist/capabilities-tool.d.ts.map +1 -1
  18. package/dist/capabilities-tool.js +36 -9
  19. package/dist/capabilities-tool.js.map +1 -1
  20. package/dist/check-diagnostics.d.ts +22 -2
  21. package/dist/check-diagnostics.d.ts.map +1 -1
  22. package/dist/check-diagnostics.js +54 -7
  23. package/dist/check-diagnostics.js.map +1 -1
  24. package/dist/check-scan.d.ts +83 -24
  25. package/dist/check-scan.d.ts.map +1 -1
  26. package/dist/check-scan.js +1516 -318
  27. package/dist/check-scan.js.map +1 -1
  28. package/dist/check.d.ts +2 -2
  29. package/dist/check.d.ts.map +1 -1
  30. package/dist/check.js +203 -70
  31. package/dist/check.js.map +1 -1
  32. package/dist/cli.d.ts +9 -0
  33. package/dist/cli.d.ts.map +1 -1
  34. package/dist/cli.js +215 -50
  35. package/dist/cli.js.map +1 -1
  36. package/dist/client-module.d.ts +20 -0
  37. package/dist/client-module.d.ts.map +1 -0
  38. package/dist/client-module.js +73 -0
  39. package/dist/client-module.js.map +1 -0
  40. package/dist/command-catalog.d.ts +24 -5
  41. package/dist/command-catalog.d.ts.map +1 -1
  42. package/dist/command-catalog.js +194 -45
  43. package/dist/command-catalog.js.map +1 -1
  44. package/dist/component-check.d.ts +13 -0
  45. package/dist/component-check.d.ts.map +1 -0
  46. package/dist/component-check.js +59 -0
  47. package/dist/component-check.js.map +1 -0
  48. package/dist/contract-proof.d.ts +1 -1
  49. package/dist/contract-proof.d.ts.map +1 -1
  50. package/dist/contract-proof.js +2 -24
  51. package/dist/contract-proof.js.map +1 -1
  52. package/dist/contracts.d.ts +19 -0
  53. package/dist/contracts.d.ts.map +1 -1
  54. package/dist/contracts.js +50 -3
  55. package/dist/contracts.js.map +1 -1
  56. package/dist/db-tool.d.ts.map +1 -1
  57. package/dist/db-tool.js +4 -3
  58. package/dist/db-tool.js.map +1 -1
  59. package/dist/dev-bun-config.d.ts +5 -1
  60. package/dist/dev-bun-config.d.ts.map +1 -1
  61. package/dist/dev-bun-config.js +44 -11
  62. package/dist/dev-bun-config.js.map +1 -1
  63. package/dist/dev-feed-tool.d.ts +18 -1
  64. package/dist/dev-feed-tool.d.ts.map +1 -1
  65. package/dist/dev-feed-tool.js +93 -0
  66. package/dist/dev-feed-tool.js.map +1 -1
  67. package/dist/diagnostics.d.ts +3 -0
  68. package/dist/diagnostics.d.ts.map +1 -1
  69. package/dist/diagnostics.js.map +1 -1
  70. package/dist/diff-tool.d.ts +9 -8
  71. package/dist/diff-tool.d.ts.map +1 -1
  72. package/dist/diff-tool.js +28 -24
  73. package/dist/diff-tool.js.map +1 -1
  74. package/dist/doctor.d.ts +13 -0
  75. package/dist/doctor.d.ts.map +1 -1
  76. package/dist/doctor.js +23 -15
  77. package/dist/doctor.js.map +1 -1
  78. package/dist/env-file.d.ts +6 -0
  79. package/dist/env-file.d.ts.map +1 -1
  80. package/dist/env-file.js +6 -0
  81. package/dist/env-file.js.map +1 -1
  82. package/dist/fix-recipes.d.ts +2 -0
  83. package/dist/fix-recipes.d.ts.map +1 -1
  84. package/dist/fix-recipes.js +102 -2
  85. package/dist/fix-recipes.js.map +1 -1
  86. package/dist/hand-built-manifest.d.ts +27 -0
  87. package/dist/hand-built-manifest.d.ts.map +1 -0
  88. package/dist/hand-built-manifest.js +124 -0
  89. package/dist/hand-built-manifest.js.map +1 -0
  90. package/dist/hooks-report.d.ts +23 -0
  91. package/dist/hooks-report.d.ts.map +1 -0
  92. package/dist/hooks-report.js +104 -0
  93. package/dist/hooks-report.js.map +1 -0
  94. package/dist/init-agents.d.ts +18 -2
  95. package/dist/init-agents.d.ts.map +1 -1
  96. package/dist/init-agents.js +20 -13
  97. package/dist/init-agents.js.map +1 -1
  98. package/dist/internal/config-paths.d.ts +16 -0
  99. package/dist/internal/config-paths.d.ts.map +1 -0
  100. package/dist/internal/config-paths.js +124 -0
  101. package/dist/internal/config-paths.js.map +1 -0
  102. package/dist/internal/installed-package.d.ts +4 -0
  103. package/dist/internal/installed-package.d.ts.map +1 -0
  104. package/dist/internal/installed-package.js +17 -0
  105. package/dist/internal/installed-package.js.map +1 -0
  106. package/dist/internal/nifra-project.d.ts +41 -0
  107. package/dist/internal/nifra-project.d.ts.map +1 -0
  108. package/dist/internal/nifra-project.js +183 -0
  109. package/dist/internal/nifra-project.js.map +1 -0
  110. package/dist/internal/project-specifiers.d.ts +29 -0
  111. package/dist/internal/project-specifiers.d.ts.map +1 -0
  112. package/dist/internal/project-specifiers.js +435 -0
  113. package/dist/internal/project-specifiers.js.map +1 -0
  114. package/dist/internal/script-kind.d.ts.map +1 -1
  115. package/dist/internal/script-kind.js +2 -1
  116. package/dist/internal/script-kind.js.map +1 -1
  117. package/dist/internal/source-facts.d.ts +8 -1
  118. package/dist/internal/source-facts.d.ts.map +1 -1
  119. package/dist/internal/source-facts.js +22 -5
  120. package/dist/internal/source-facts.js.map +1 -1
  121. package/dist/internal/typescript-import.d.ts +1 -1
  122. package/dist/internal/typescript-import.d.ts.map +1 -1
  123. package/dist/internal/typescript-import.js +10 -6
  124. package/dist/internal/typescript-import.js.map +1 -1
  125. package/dist/introspect.d.ts +15 -6
  126. package/dist/introspect.d.ts.map +1 -1
  127. package/dist/introspect.js +87 -25
  128. package/dist/introspect.js.map +1 -1
  129. package/dist/levels-tool.d.ts.map +1 -1
  130. package/dist/levels-tool.js +98 -23
  131. package/dist/levels-tool.js.map +1 -1
  132. package/dist/load.d.ts +33 -10
  133. package/dist/load.d.ts.map +1 -1
  134. package/dist/load.js +60 -10
  135. package/dist/load.js.map +1 -1
  136. package/dist/mcp-context.d.ts +5 -5
  137. package/dist/mcp-context.d.ts.map +1 -1
  138. package/dist/mcp-context.js +8 -23
  139. package/dist/mcp-context.js.map +1 -1
  140. package/dist/mcp-delegate.d.ts +2 -2
  141. package/dist/mcp-delegate.d.ts.map +1 -1
  142. package/dist/mcp-delegate.js +19 -7
  143. package/dist/mcp-delegate.js.map +1 -1
  144. package/dist/mcp-exec.d.ts +13 -4
  145. package/dist/mcp-exec.d.ts.map +1 -1
  146. package/dist/mcp-exec.js +56 -14
  147. package/dist/mcp-exec.js.map +1 -1
  148. package/dist/mcp-http.js +1 -1
  149. package/dist/mcp-io.d.ts +30 -0
  150. package/dist/mcp-io.d.ts.map +1 -1
  151. package/dist/mcp-io.js +63 -0
  152. package/dist/mcp-io.js.map +1 -1
  153. package/dist/mcp-isolate.d.ts.map +1 -1
  154. package/dist/mcp-isolate.js +3 -2
  155. package/dist/mcp-isolate.js.map +1 -1
  156. package/dist/mcp-path.d.ts +2 -1
  157. package/dist/mcp-path.d.ts.map +1 -1
  158. package/dist/mcp-path.js +9 -24
  159. package/dist/mcp-path.js.map +1 -1
  160. package/dist/mcp-project-child.js +1 -1
  161. package/dist/mcp-project-child.js.map +1 -1
  162. package/dist/mcp-root.d.ts.map +1 -1
  163. package/dist/mcp-root.js +50 -10
  164. package/dist/mcp-root.js.map +1 -1
  165. package/dist/mcp-ws.d.ts.map +1 -1
  166. package/dist/mcp-ws.js +3 -1
  167. package/dist/mcp-ws.js.map +1 -1
  168. package/dist/mcp.d.ts.map +1 -1
  169. package/dist/mcp.js +48 -19
  170. package/dist/mcp.js.map +1 -1
  171. package/dist/migrate-layout.d.ts +47 -1
  172. package/dist/migrate-layout.d.ts.map +1 -1
  173. package/dist/migrate-layout.js +1972 -170
  174. package/dist/migrate-layout.js.map +1 -1
  175. package/dist/output-schemas.d.ts +128 -0
  176. package/dist/output-schemas.d.ts.map +1 -0
  177. package/dist/output-schemas.js +975 -0
  178. package/dist/output-schemas.js.map +1 -0
  179. package/dist/port.d.ts.map +1 -1
  180. package/dist/port.js +3 -2
  181. package/dist/port.js.map +1 -1
  182. package/dist/project-facts.d.ts +15 -4
  183. package/dist/project-facts.d.ts.map +1 -1
  184. package/dist/project-facts.js +1 -0
  185. package/dist/project-facts.js.map +1 -1
  186. package/dist/project-path.d.ts +6 -0
  187. package/dist/project-path.d.ts.map +1 -1
  188. package/dist/project-path.js +48 -14
  189. package/dist/project-path.js.map +1 -1
  190. package/dist/recipes/5.0.0.d.ts +11 -0
  191. package/dist/recipes/5.0.0.d.ts.map +1 -0
  192. package/dist/recipes/5.0.0.js +34 -0
  193. package/dist/recipes/5.0.0.js.map +1 -0
  194. package/dist/recipes/index.d.ts +7 -0
  195. package/dist/recipes/index.d.ts.map +1 -1
  196. package/dist/recipes/index.js +2 -0
  197. package/dist/recipes/index.js.map +1 -1
  198. package/dist/release-verification.d.ts +5 -0
  199. package/dist/release-verification.d.ts.map +1 -1
  200. package/dist/release-verification.js +14 -0
  201. package/dist/release-verification.js.map +1 -1
  202. package/dist/replay.d.ts.map +1 -1
  203. package/dist/replay.js +12 -3
  204. package/dist/replay.js.map +1 -1
  205. package/dist/review.d.ts +3 -1
  206. package/dist/review.d.ts.map +1 -1
  207. package/dist/review.js +43 -7
  208. package/dist/review.js.map +1 -1
  209. package/dist/rules/codes.d.ts +12 -0
  210. package/dist/rules/codes.d.ts.map +1 -1
  211. package/dist/rules/codes.js +12 -0
  212. package/dist/rules/codes.js.map +1 -1
  213. package/dist/rules/comment-markers.d.ts +16 -7
  214. package/dist/rules/comment-markers.d.ts.map +1 -1
  215. package/dist/rules/comment-markers.js +58 -19
  216. package/dist/rules/comment-markers.js.map +1 -1
  217. package/dist/rules/contract-visibility.d.ts +3 -0
  218. package/dist/rules/contract-visibility.d.ts.map +1 -0
  219. package/dist/rules/contract-visibility.js +337 -0
  220. package/dist/rules/contract-visibility.js.map +1 -0
  221. package/dist/rules/data-guard.d.ts +13 -0
  222. package/dist/rules/data-guard.d.ts.map +1 -1
  223. package/dist/rules/data-guard.js +356 -37
  224. package/dist/rules/data-guard.js.map +1 -1
  225. package/dist/rules/layout.d.ts +4 -0
  226. package/dist/rules/layout.d.ts.map +1 -0
  227. package/dist/rules/layout.js +19 -0
  228. package/dist/rules/layout.js.map +1 -0
  229. package/dist/rules/legacy.d.ts.map +1 -1
  230. package/dist/rules/legacy.js +178 -44
  231. package/dist/rules/legacy.js.map +1 -1
  232. package/dist/rules/pages.d.ts +50 -0
  233. package/dist/rules/pages.d.ts.map +1 -1
  234. package/dist/rules/pages.js +436 -4
  235. package/dist/rules/pages.js.map +1 -1
  236. package/dist/rules/routes.d.ts +6 -0
  237. package/dist/rules/routes.d.ts.map +1 -1
  238. package/dist/rules/routes.js +111 -16
  239. package/dist/rules/routes.js.map +1 -1
  240. package/dist/rules/secrets.js +2 -2
  241. package/dist/rules/secrets.js.map +1 -1
  242. package/dist/rules/security.d.ts +13 -1
  243. package/dist/rules/security.d.ts.map +1 -1
  244. package/dist/rules/security.js +313 -69
  245. package/dist/rules/security.js.map +1 -1
  246. package/dist/rules/zones.d.ts +13 -0
  247. package/dist/rules/zones.d.ts.map +1 -1
  248. package/dist/rules/zones.js +218 -44
  249. package/dist/rules/zones.js.map +1 -1
  250. package/dist/scaffold.d.ts.map +1 -1
  251. package/dist/scaffold.js +6 -4
  252. package/dist/scaffold.js.map +1 -1
  253. package/dist/sdk.d.ts.map +1 -1
  254. package/dist/sdk.js +14 -5
  255. package/dist/sdk.js.map +1 -1
  256. package/dist/stylex-migrate.d.ts.map +1 -1
  257. package/dist/stylex-migrate.js +290 -24
  258. package/dist/stylex-migrate.js.map +1 -1
  259. package/dist/test-tool.d.ts.map +1 -1
  260. package/dist/test-tool.js +6 -3
  261. package/dist/test-tool.js.map +1 -1
  262. package/dist/typecheck-coverage.d.ts +20 -0
  263. package/dist/typecheck-coverage.d.ts.map +1 -0
  264. package/dist/typecheck-coverage.js +129 -0
  265. package/dist/typecheck-coverage.js.map +1 -0
  266. package/dist/upgrade.d.ts +84 -0
  267. package/dist/upgrade.d.ts.map +1 -1
  268. package/dist/upgrade.js +498 -78
  269. package/dist/upgrade.js.map +1 -1
  270. package/dist/verification-plan.d.ts.map +1 -1
  271. package/dist/verification-plan.js +16 -2
  272. package/dist/verification-plan.js.map +1 -1
  273. package/dist/version-drift.d.ts +22 -0
  274. package/dist/version-drift.d.ts.map +1 -0
  275. package/dist/version-drift.js +94 -0
  276. package/dist/version-drift.js.map +1 -0
  277. package/dist/web-app-options.d.ts +1 -1
  278. package/dist/web-app-options.d.ts.map +1 -1
  279. package/dist/web-app-options.js +1 -1
  280. package/dist/web-app-options.js.map +1 -1
  281. package/dist/work-graph.d.ts.map +1 -1
  282. package/dist/work-graph.js +20 -8
  283. package/dist/work-graph.js.map +1 -1
  284. package/docs/examples.json +55 -7
  285. package/docs/llms-full.txt +2043 -222
  286. package/docs/types.json +1573 -246
  287. package/package.json +11 -12
  288. package/src/agent-files.ts +203 -0
  289. package/src/app-files.ts +34 -4
  290. package/src/assure-hydration.ts +18 -2
  291. package/src/assure.ts +88 -10
  292. package/src/capabilities-tool.ts +47 -9
  293. package/src/check-diagnostics.ts +75 -9
  294. package/src/check-scan.ts +1816 -377
  295. package/src/check.ts +258 -81
  296. package/src/cli.ts +242 -37
  297. package/src/client-module.ts +97 -0
  298. package/src/command-catalog.ts +237 -52
  299. package/src/component-check.ts +79 -0
  300. package/src/contract-proof.ts +3 -25
  301. package/src/contracts.ts +60 -3
  302. package/src/db-tool.ts +5 -2
  303. package/src/dev-bun-config.ts +51 -10
  304. package/src/dev-feed-tool.ts +112 -1
  305. package/src/diagnostics.ts +3 -0
  306. package/src/diff-tool.ts +33 -26
  307. package/src/doctor.ts +43 -21
  308. package/src/env-file.ts +7 -0
  309. package/src/fix-recipes.ts +116 -2
  310. package/src/hand-built-manifest.ts +144 -0
  311. package/src/hooks-report.ts +135 -0
  312. package/src/init-agents.ts +21 -14
  313. package/src/internal/config-paths.ts +154 -0
  314. package/src/internal/installed-package.ts +15 -0
  315. package/src/internal/nifra-project.ts +191 -0
  316. package/src/internal/project-specifiers.ts +477 -0
  317. package/src/internal/script-kind.ts +2 -1
  318. package/src/internal/source-facts.ts +48 -5
  319. package/src/internal/typescript-import.ts +15 -6
  320. package/src/introspect.ts +104 -28
  321. package/src/levels-tool.ts +124 -29
  322. package/src/load.ts +107 -17
  323. package/src/mcp-context.ts +12 -25
  324. package/src/mcp-delegate.ts +22 -10
  325. package/src/mcp-exec.ts +68 -18
  326. package/src/mcp-http.ts +1 -1
  327. package/src/mcp-io.ts +74 -0
  328. package/src/mcp-isolate.ts +9 -2
  329. package/src/mcp-path.ts +8 -22
  330. package/src/mcp-project-child.ts +1 -1
  331. package/src/mcp-root.ts +58 -8
  332. package/src/mcp-ws.ts +4 -2
  333. package/src/mcp.ts +56 -22
  334. package/src/migrate-layout.ts +2257 -176
  335. package/src/output-schemas.ts +1240 -0
  336. package/src/port.ts +3 -2
  337. package/src/project-facts.ts +17 -3
  338. package/src/project-path.ts +44 -13
  339. package/src/recipes/5.0.0.ts +35 -0
  340. package/src/recipes/index.ts +10 -0
  341. package/src/release-verification.ts +20 -0
  342. package/src/replay.ts +14 -3
  343. package/src/review.ts +44 -10
  344. package/src/rules/codes.ts +12 -0
  345. package/src/rules/comment-markers.ts +57 -21
  346. package/src/rules/contract-visibility.ts +383 -0
  347. package/src/rules/data-guard.ts +391 -42
  348. package/src/rules/layout.ts +22 -0
  349. package/src/rules/legacy.ts +206 -52
  350. package/src/rules/pages.ts +481 -4
  351. package/src/rules/routes.ts +124 -16
  352. package/src/rules/secrets.ts +2 -2
  353. package/src/rules/security.ts +391 -72
  354. package/src/rules/zones.ts +232 -42
  355. package/src/scaffold.ts +6 -4
  356. package/src/sdk.ts +14 -5
  357. package/src/stylex-migrate.ts +251 -24
  358. package/src/test-tool.ts +12 -3
  359. package/src/typecheck-coverage.ts +134 -0
  360. package/src/upgrade.ts +576 -72
  361. package/src/verification-plan.ts +33 -2
  362. package/src/version-drift.ts +118 -0
  363. package/src/web-app-options.ts +2 -2
  364. package/src/work-graph.ts +17 -12
@@ -12,6 +12,7 @@ nifra is a Bun-native, contract-first, framework-agnostic full-stack TypeScript
12
12
  - **Secure by default.** Body-size cap, `requestTimeoutMs` + `c.signal`, graceful shutdown, redacting logger, same-origin `redirect()`, constant-time secret comparison, fail-closed middleware.
13
13
  - **Money** in integer minor units; **time** parsed to absolute UTC at the boundary.
14
14
  - Throwing a `Response` anywhere in the lifecycle is control flow (returned as-is), not an error.
15
+ Optional server components (post-4.0): React, Vue, Svelte and Solid share owner-GET admission, closed input/public-props schemas and private/no-store output. React uses native RSC/Flight. Use *.component.server.ts(x), defineServerComponent, an explicit componentPipeline named config export, native outlets and bindComponentRouter. Await invalidateComponentScope on identity/tenant/permission changes. Component GETs cannot acquire domain write privileges; use validated serverFn/actions for mutations. Native use-server actions are unsupported. Pin the certified compiler tuple documented in /docs/server-components.
15
16
 
16
17
  ---
17
18
 
@@ -212,24 +213,19 @@ The data model (loaders, actions, `defer`, progressive enhancement, fetchers,
212
213
  revalidation) is closest to **Remix / React Router** - delivered across five UI libraries instead
213
214
  of one.
214
215
 
215
- ### Server code on every page - RSC is the one protocol Nifra trades away
216
-
217
- Nifra covers the full server side: typed **loaders and actions**, **streaming SSR** with
218
- out-of-order Suspense, **islands**, and typed
219
- [server functions](/docs/server-functions) - `serverFn` RPC whose body (DB
220
- handles, secrets, imports) is _never_ shipped to the browser, callable as a normal typed
221
- function from any component, on all five UI libraries. Data on the server, mutations on the
222
- server, minimal client JS: all there, by a framework-agnostic mechanism instead of a
223
- React-only one.
224
-
225
- What Nifra deliberately does not adopt is **React Server Components as a component
226
- protocol** - `"use server"` / `"use client"` directives and the
227
- server-only component tree. RSC is React-specific; a core that also serves Vue, Solid,
228
- Svelte, and Preact can't be built on it. (Nuxt, SvelteKit, and Remix don't ship RSC either -
229
- this is _Nifra vs. Next App Router specifically_.) So the call is simple: if your app is
230
- architected around RSC itself, **Next.js App Router is the right tool**. For everything RSC
231
- is usually reached for - server-side data, secrets off the client, less JS - loaders,
232
- `defer()`, islands, and server functions cover it here.
216
+ ### Server code and server-only views
217
+
218
+ Nifra provides typed loaders and actions, streaming SSR, islands and typed
219
+ [server functions](/docs/server-functions). Server functions keep their
220
+ implementation, secrets and database handles off the browser and use the same validated
221
+ mutation API across the supported UI libraries.
222
+
223
+ The optional [server-components pipeline](/docs/server-components) adds
224
+ server-only views and state-preserving refresh. React uses native RSC/Flight; Vue, Svelte
225
+ and Solid use their native islands in validated server-rendered ranges. The feature is
226
+ intended for the release after 4.0.0 and requires all four adapters to pass certification.
227
+ Server functions remain the mutation API; native React `"use server"` actions
228
+ are unsupported.
233
229
 
234
230
  ### Also a standalone backend (vs. Hono & Elysia)
235
231
 
@@ -347,10 +343,10 @@ version-checked examples, which no other framework ships.
347
343
  **To start lean and grow** - begin as a typed API, graduate to full-stack on the same core,
348
344
  no rewrite.
349
345
 
350
- The one deliberate trade: Nifra is streaming SSR + islands + typed
351
- [server functions](/docs/server-functions), **not** the RSC component protocol -
352
- framework-agnostic by design. Apps architected around RSC itself belong on Next App Router;
353
- everyone else gives up nothing server-side here.
346
+ Nifra provides streaming SSR, islands and typed
347
+ [server functions](/docs/server-functions). The optional
348
+ [server-components pipeline](/docs/server-components) adds native React
349
+ RSC/Flight and corresponding Vue, Svelte and Solid views under a shared execution contract.
354
350
 
355
351
  ## Nifra - Routing
356
352
 
@@ -396,7 +392,8 @@ reaches the browser - see [Project structure](/docs/structure).
396
392
 
397
393
  -
398
394
  `_404.tsx` renders unmatched paths, and whatever a loader answers with
399
- `notFound()`. At the routes root it renders on its own. In a directory below (
395
+ `notFound()`. At the routes root it renders inside the root layout, as below. In
396
+ a directory below (
400
397
  `admin/_404.tsx`) it answers for that part of the app: the unmatched URLs under
401
398
  `/admin` and `notFound()` from the routes beneath it, the nearest one
402
399
  winning, rendered inside the layouts at or above it with their loader data. Those layouts run
@@ -408,6 +405,21 @@ with a `_404` at one URL prefix are a boot error unless a directory containing
408
405
  both has one too. `gone()`, `statusPage()` and the
409
406
  `_410.tsx`-style pages stay at the root, as does the 404 of a static export.
410
407
 
408
+ -
409
+ A root status page (`_404.tsx`, `_503.tsx`, ...) or an
410
+ `_error.tsx` renders inside the layouts above it with their loader data, and the
411
+ server puts its `meta` in the head. For a request no route ran (an unmatched
412
+ URL) the root middleware and layout loaders run first; otherwise the page reuses the data
413
+ the request already loaded. When those layouts cannot render it - a loader throws, a
414
+ `gate` or the middleware refuses - a root status page renders alone and an
415
+ `_error` inside its layouts without their data, with the same status, so a
416
+ status page never fails into a second error. The response is
417
+ `private, no-store` once a layout loaded data, and a navigation that lands on
418
+ one loads the URL as a document. `export const inLayouts = false` always renders
419
+ the page that way and runs no layout loader for it. A root status page that wraps the root
420
+ layout itself would render it twice; `nifra check` reports one as
421
+ `NF-C034`.
422
+
411
423
  -
412
424
  `_loading.tsx` is what the page slot shows while a client navigation loads. A
413
425
  navigation still waiting on its data after about 120 ms swaps the page for the target
@@ -429,8 +441,9 @@ renders in its place, wrapped by the layouts at/above that segment, at status 50
429
441
  non-hydrated). On the **client** - a render error during navigation/interaction is caught by
430
442
  the nearest boundary, which renders `_error` in place (all five adapters). It
431
443
  receives the serialized error as `{ data: { name, message } }` (never the
432
- stack; on the server in production, a generic `Internal Server Error` rather
433
- than the error's own text); a thrown control-flow value (e.g. a guard
444
+ stack; on the server, a generic `Internal Server Error` rather than the error's
445
+ own text unless `NODE_ENV` is `development` or `test`); a
446
+ thrown control-flow value (e.g. a guard
434
447
  `redirect`) passes through.
435
448
 
436
449
  -
@@ -467,6 +480,13 @@ Each page default-exports a component; an optional `meta` export drives
467
480
  `loader` in its `.backend.ts` half - see
468
481
  [Loaders & actions](/docs/data).
469
482
 
483
+ A layout's `meta` receives its own loader's data as `data`, the value its
484
+ component renders, or `null` when the layout has no loader. The page's data is
485
+ `pageData`, for a layout title template. A layout that loads the locale can set
486
+ `lang` from its data, and a client navigation keeps it. `nifra check`
487
+ reports a layout with no loader whose `meta` reads `data` as
488
+ `NF-C040`.
489
+
470
490
  ```ts
471
491
  // routes/users/[id].tsx
472
492
  import type { Route } from "./+types/[id]"
@@ -855,7 +875,7 @@ answers `422`.
855
875
 
856
876
  Other param modifiers (`:id+`, `:id*`, `:id(\\d+)`) are not
857
877
  part of the path grammar and match as literal text. Where that text is intended, put
858
- `// nifra-expect param-modifier` above the registration.
878
+ `// nifra-expect NF-C026: ` above the registration.
859
879
 
860
880
  ### Param constraints
861
881
 
@@ -933,7 +953,7 @@ not look for another split.
933
953
  `/users/:id{[0-9]+}` serve no request in common, so `nifra check`
934
954
  (`NF-C024`) has nothing to report. A constrained route beside a bare
935
955
  `:param` route still overlaps and is still reported; mark the pair with
936
- `// nifra-expect route-overlap` where it is intended.
956
+ `// nifra-expect NF-C024: ` where it is intended.
937
957
 
938
958
  -
939
959
  **OpenAPI and the typed client use the bare name.** The path is
@@ -993,9 +1013,11 @@ in the same call keep both. Call a custom one with
993
1013
  `fetch(url, { method: "PURGE" })`.
994
1014
 
995
1015
  -
996
- **An assurance policy selects standard methods only.** A rule with `methods`
997
- never matches a custom-method route, so classify it with a path rule. Left unmatched, it is
998
- reported as `unclassified-route`.
1016
+ **An assurance rule names standard methods only.** A rule with `methods`
1017
+ never matches a custom-method route; one with `exceptMethods` does, unless the
1018
+ route is under a method it lists. `securityBaseline()` holds a custom method to
1019
+ its mutation rules, CSRF included, and `csrf()` publishes its evidence for one.
1020
+ Left unmatched, a route is reported as `unclassified-route`.
999
1021
 
1000
1022
  Whether a custom method reaches the app is up to the runtime's HTTP parser.
1001
1023
  `PROPFIND`, `REPORT`, `PURGE` and `QUERY` arrive on
@@ -1033,6 +1055,14 @@ return makes the typed client infer `data: never`, so you silently lose drift
1033
1055
  detection for that route (`nifra check` flags it). `c.set` keeps your
1034
1056
  plain-object return fully typed.
1035
1057
 
1058
+ A `Response` a handler or hook returns or throws still gets the
1059
+ `c.set.headers` it lacks and the queued cookies, so a guard's
1060
+ `cache-control: no-store` reaches it. Its own headers win, `content-type`, `content-length`,
1061
+ `content-encoding`, `content-range` and `transfer-encoding`
1062
+ come from it alone, and `c.set.status` does not change its status. A
1063
+ `Response` shared across requests is copied first, so one request's headers never
1064
+ reach another.
1065
+
1036
1066
  When you genuinely want a `Response` - an **error short-circuit** from a
1037
1067
  `derive` / `beforeHandle` (auth, rate limits) - `c.json(body, status?)`
1038
1068
  and `c.text(body, status?)` build one in a line:
@@ -1046,6 +1076,42 @@ The request is on `c.req`, also available as `c.request` - the same name a
1046
1076
  page loader/action receives (which in turn also accepts `ctx.req`), so one name works
1047
1077
  in both places.
1048
1078
 
1079
+ ### Typed error responses
1080
+
1081
+ Declare a route's error bodies in `errors`, keyed by status, and answer with
1082
+ `status(code, body, { headers })` from `@nifrajs/core/server`. It
1083
+ ends the request from a handler or any hook, returned or thrown, without building a
1084
+ `Response`: the body is serialized like a plain return, the headers ride with it,
1085
+ and queued cookies still apply. On the typed client, checking `res.status === 409`
1086
+ narrows `res.data` to the declared 409 body; a status the route does not declare
1087
+ falls into one arm whose `data` is `unknown`.
1088
+
1089
+ ```ts
1090
+ import { client } from "@nifrajs/client"
1091
+ import { server, status } from "@nifrajs/core/server"
1092
+ import { t } from "@nifrajs/schema"
1093
+
1094
+ const conflict = t.object({ code: t.literal("conflict"), retryAfter: t.number() })
1095
+ const locked = new Set(["7"])
1096
+
1097
+ export const app = server().post("/orders/:id/pay", { errors: { 409: conflict } }, (c) => {
1098
+ if (locked.has(c.params.id))
1099
+ // The body is the typed contract; the header serves proxies and plain HTTP clients.
1100
+ return status(409, { code: "conflict", retryAfter: 5 }, { headers: { "retry-after": "5" } })
1101
+ return { paid: true }
1102
+ })
1103
+
1104
+ const api = client<typeof app>("https://api.example.com")
1105
+ const res = await api.orders({ id: "7" }).pay.post()
1106
+ if (!res.ok && res.status === 409) {
1107
+ const wait: number = res.data.retryAfter // narrowed to the declared 409 body
1108
+ }
1109
+ ```
1110
+
1111
+ The client's `Result` carries the body, not the response headers. A value the
1112
+ caller acts on, such as how long to wait, belongs in the declared body; set the matching
1113
+ header too when proxies or plain HTTP clients read it.
1114
+
1049
1115
  ### Typed request cookies (schema.cookies)
1050
1116
 
1051
1117
  `c.cookies` is the request's `Cookie` header as a name-to-value record,
@@ -1227,7 +1293,7 @@ same call params use, accepting exactly the colliding segment names. The client
1227
1293
  the dot access at compile time with that guidance, and `nifra check` reports the
1228
1294
  collision (`NF-C018`, advisory). Prefer a verb-free segment
1229
1295
  (e.g. `/api/remove`) when you control the path; mark a route served only to
1230
- non-typed-client consumers with `// nifra-expect reserved-segment` above its
1296
+ non-typed-client consumers with `// nifra-expect NF-C018: ` above its
1231
1297
  registration.
1232
1298
 
1233
1299
  ### Results never throw
@@ -1253,9 +1319,11 @@ if (res.ok) {
1253
1319
  const { data, error } = await api.users.post({ name: "" }) // 422 if the schema rejects it
1254
1320
  ```
1255
1321
 
1256
- A call that gets no HTTP answer has `status: 0` and one of four codes:
1257
- `network_error`, `timeout`, `response_too_large`, or
1258
- `invalid_path`. `invalid_path` means a param value was `.`
1322
+ A call that gets no HTTP answer it can read has `status: 0` and one of five
1323
+ codes: `network_error`, `timeout`, `response_too_large`,
1324
+ `malformed_response`, or `invalid_path`.
1325
+ `malformed_response` means the body did not decode as its content type says - a
1326
+ proxy's HTML error page labelled JSON, say. `invalid_path` means a param value was `.`
1259
1327
  or `..`: a URL reads those as steps to another path, in every encoding, so the
1260
1328
  client sends nothing rather than reach a route the call does not name.
1261
1329
  `.subscribe()` reports it through `onError` and closes;
@@ -1279,8 +1347,11 @@ the client never throws, it returns `{ data, error }`.
1279
1347
  A route's `loader` runs on the server and calls your backend in-process during
1280
1348
  SSR - no network hop. Its `loaderOutput` schema is the contract with the page:
1281
1349
  the page receives that shape as `data`, and nothing else the loader returns
1282
- leaves the server. A `Date` travels as its ISO string, so declare a timestamp
1283
- column as `t.string()`. A loader without a schema is flagged by
1350
+ leaves the server. Declare a timestamp as `t.date()` and the page gets a
1351
+ `Date` in the server render and in the browser alike; under
1352
+ `t.string()` it travels as its ISO string. `t.json()` declares a
1353
+ field that is any JSON value, passed through whole - a tool-call payload or a settings blob
1354
+ the page does not pick apart. A loader without a schema is flagged by
1284
1355
  `nifra check`. See
1285
1356
  [Project structure](/docs/structure) for the route pair.
1286
1357
 
@@ -1294,7 +1365,8 @@ import type { Route } from "./+types/[id]"
1294
1365
  // The page receives exactly this shape. Anything else the loader returns stays on the server.
1295
1366
  export const loaderOutput = t.object({ id: t.string(), name: t.string() })
1296
1367
 
1297
- export async function loader({ params, api }: Route.LoaderArgs) {
1368
+ // Route.Loader checks what this returns against loaderOutput at compile time.
1369
+ export const loader: Route.Loader = async ({ params, api }) => {
1298
1370
  const res = await api.users({ id: params.id }).get()
1299
1371
  if (!res.ok) throw notFound()
1300
1372
  return res.data
@@ -1310,6 +1382,32 @@ export default function User({ data }: Route.ComponentProps) {
1310
1382
  }
1311
1383
  ```
1312
1384
 
1385
+ Annotating the loader with `Route.Loader` from the generated
1386
+ `./+types/<name>` checks what it returns against `loaderOutput`
1387
+ at compile time: a field of the wrong type, a `defer(...)` whose value does not
1388
+ match its `t.deferred(...)` field, or any data from a loader with no
1389
+ `loaderOutput` is a type error. A redirect, a `Response` and returning
1390
+ nothing stay allowed. `Route.Action` does the same against
1391
+ `actionOutput`, `revalidate(paths, data)` included. A field the schema
1392
+ does not declare is not a type error: the guard drops it before it ships. The annotation
1393
+ makes the loader's type the schema's, so `nifra schemas` cannot read what an
1394
+ annotated loader returns. It reports a generated schema over one as unchecked: annotate a
1395
+ loader whose schema you write, and leave a loader whose schema is generated unannotated.
1396
+
1397
+ `nifra schemas` drafts the schemas from your code for review: it reads each
1398
+ loader's and action's TypeScript return type, and with `--write` puts a schema
1399
+ above every one that has none. It leaves out a field named like a credential and anything
1400
+ that is not JSON, says so in a comment, and marks what it wrote with a
1401
+ `nifra:generated` line. While that line stays, a later
1402
+ `nifra schemas --write` keeps the schema in step with the return type, and
1403
+ `nifra check` fails (NF-C033) when the two have drifted apart, or when the schema
1404
+ under the line was edited (reformatting it is not an edit); delete the line to own the
1405
+ schema by hand. A hand-written schema is never rewritten: the report lists the
1406
+ returned fields it drops, by path. Nothing is derived at request time - the schema in the
1407
+ file is the one the guard enforces. `nifra fix --code NF-C030` writes the same
1408
+ draft for one flagged route, and `nifra migrate layout` drafts one for each
1409
+ route it splits whose loader or action returns data without a schema.
1410
+
1313
1411
  A field named like a credential (`password`, `token`,
1314
1412
  `apiKey`) fails the route when it loads unless its schema is wrapped in
1315
1413
  `t.declassified("why it may reach the browser", ...)`; a schema library that
@@ -1333,7 +1431,7 @@ import type { Route } from "./+types/new"
1333
1431
 
1334
1432
  export const actionOutput = t.object({ ok: t.boolean() })
1335
1433
 
1336
- export async function action({ api, request }: Route.ActionArgs) {
1434
+ export const action: Route.Action = async ({ api, request }) => {
1337
1435
  const form = await request.formData()
1338
1436
  await api.users.post({ name: String(form.get("name")) })
1339
1437
  return { ok: true } // the page's loaders revalidate automatically
@@ -1352,6 +1450,18 @@ export default function NewUser() {
1352
1450
  }
1353
1451
  ```
1354
1452
 
1453
+ A form can post to another site and the browser sends that site's cookies with it, so an action
1454
+ refuses a POST whose `Origin` is another site's with 403, before middleware, gates or
1455
+ the action run. A request without an `Origin`, such as one from a server, passes, and
1456
+ so does a form on the app's own page sent under `Referrer-Policy: no-referrer`, which
1457
+ arrives as `Origin: null` with `Sec-Fetch-Site: same-origin`.
1458
+ Same-origin is judged from the request URL's host, so a proxy in front must forward the
1459
+ `Host` header. A route that takes posts from elsewhere, such as an identity
1460
+ provider's `form_post` callback, exports `allowedOrigins` from its backend
1461
+ file: an array of exact origins, or a function that receives the origin (`null` when
1462
+ absent). It checks where the browser says the request comes from, not who sent it, so keep the
1463
+ action's own authentication.
1464
+
1355
1465
  ### Layout loaders on navigation
1356
1466
 
1357
1467
  A `_layout.backend.ts` can have a `loader` too; its output reaches the
@@ -1359,7 +1469,11 @@ layout component as `data`. On a client navigation that keeps the layout on scre
1359
1469
  browser already holds that data, so the loader runs again only when a param the layout owns
1360
1470
  changes (`org` for `orgs/[org]/_layout.tsx`) or the query changes. After
1361
1471
  an action, every loader runs. A layout that exports `gate = true` runs on every
1362
- request; a layout loader without it is not an authorization boundary. A
1472
+ request; a layout loader without it is not an authorization boundary: a client navigation can
1473
+ skip it, and an action below it runs without it. Put a check that refuses access in a gate or
1474
+ in middleware. In development, nifra warns the first time a layout loader without
1475
+ `gate` refuses a request with a redirect, a `Response`, or a 401 or 403,
1476
+ and `nifra check` reports one whose loader refuses in its own body (NF-C042). A
1363
1477
  `_layout.backend.ts` that exports `middleware` guards a directory, with
1364
1478
  or without a layout component - see [Route middleware](/docs/routing#middleware).
1365
1479
 
@@ -1465,8 +1579,9 @@ host - defence in depth behind the JSON requirement, at the cost of one comparis
1465
1579
  Serialising closed-over variables to the browser and back is a class of problem worth not
1466
1580
  having rather than encrypting.
1467
1581
 
1468
- Both content-type rules were measured rather than assumed. A body schema alone still accepts a
1469
- cross-origin urlencoded form - 200, attacker-controlled fields - and a bounded JSON read alone
1582
+ Both content-type rules were measured rather than assumed. A body schema alone still reads a
1583
+ urlencoded form that carries no `Origin` or one from a site in
1584
+ `formOrigins` - 200, attacker-controlled fields - and a bounded JSON read alone
1470
1585
  accepts the `text/plain` trick where a form's `name=value` is crafted to
1471
1586
  parse as valid JSON. Neither is sufficient by itself.
1472
1587
 
@@ -1608,6 +1723,10 @@ Nothing here touches the kernel or the request path. An app that mounts no serve
1608
1723
  pays exactly nothing for the feature - which is the reason it is built as a plugin over the
1609
1724
  public registration API rather than as a bespoke dispatcher.
1610
1725
 
1726
+ ## Nifra - Server components
1727
+
1728
+ > Server-only views with schema-guarded props, owner-route authorization and native React, Vue, Svelte, Solid and Preact hydration.
1729
+
1611
1730
  ## Nifra - Optimistic UI & fetchers
1612
1731
 
1613
1732
  > Optimistic updates and concurrent fetchers in Nifra: instant feedback, row-level mutations.
@@ -1838,7 +1957,9 @@ wouldn't propagate across instances. In production use a shared, durable store.
1838
1957
  `KVCacheStore` wraps a Workers KV namespace; any backend that fits the small
1839
1958
  `CacheStore` interface (Redis, the Cache API) works too. Every read is validated
1840
1959
  before it's trusted, so a corrupt or version-skewed entry is treated as a miss, not served
1841
- broken.
1960
+ broken. A store that also implements `fence()` keeps a purge that lands while a
1961
+ page renders from being undone when that render stores the page it built from the old data;
1962
+ both built-in stores do.
1842
1963
 
1843
1964
  ```ts
1844
1965
  // doc-check: skip - Workers entry: `Env`/`ExecutionContext` globals + your `app` from server.ts.
@@ -1858,6 +1979,13 @@ export default {
1858
1979
  }
1859
1980
  ```
1860
1981
 
1982
+ A store outage never fails a page. A read that throws, rejects, or outlives
1983
+ `storeTimeoutMs` is a miss and the page renders; a failed write still returns the
1984
+ rendered page; a failed background regeneration keeps the stale entry. Each failure goes to
1985
+ `onStoreError(error, &#123; op &#125;)`, which by default logs one line per operation
1986
+ per minute without the cache key. Set `storeTimeoutMs` for a network store, so a
1987
+ hung connection costs a miss rather than the request.
1988
+
1861
1989
  ### On-demand revalidation
1862
1990
 
1863
1991
  Purge a path the moment its data changes (a CMS webhook, an admin action) with
@@ -1872,6 +2000,21 @@ curl -X POST 'https://example.com/__nifra/revalidate?path=/posts/hello' \
1872
2000
  # → { "revalidated": "/posts/hello" } (the token is checked in constant time)
1873
2001
  ```
1874
2002
 
2003
+ A per-request CSP nonce makes a page uncacheable. For a strict policy under ISR, give
2004
+ `createWebApp` a `csp` from `createCspPolicy` instead of a
2005
+ `nonce`: constant scripts are allowed by hash, so only a page that needs a nonce
2006
+ carries one - see [A strict CSP that still caches](/docs/contract#strict-csp).
2007
+
2008
+ A page that must not be cached as a whole, because it is personalized or carries a nonce, can
2009
+ still cache the data its loader reads. Wrap the expensive call in `cache.wrap` from
2010
+ `@nifrajs/cache`, with the same tags the pages use. Concurrent loads of one key run
2011
+ once, and a store outage falls through to the loader. Pass that cache to
2012
+ `revalidateEndpoint` as `dataCaches: [cache]`. A tag purge then
2013
+ invalidates it first, then the page store, then the CDN, so no page is rebuilt from stale
2014
+ data. If a data cache fails, the endpoint answers `500` before it touches the page
2015
+ store, so a retry purges everything again. `createInvalidator` from
2016
+ `@nifrajs/web/cdn` takes the same `dataCaches` for purges from app code.
2017
+
1875
2018
  `revalidateTags` names what a purge by tag reaches. A list tags every page of the
1876
2019
  route alike; a function of the route's params and URL tags each page on its own. Tags travel to
1877
2020
  the CDN and to anyone reading the origin, so build them from params, never from loader data.
@@ -2570,6 +2713,54 @@ stripped from `event`/`id` to prevent frame injection); multi-line
2570
2713
  `data` is split into multiple `data:` lines per the spec; and
2571
2714
  `keepAlive` emits comment pings so idle proxies don't drop the connection.
2572
2715
 
2716
+ A client that stops reading is dropped once it has `maxBufferedBytes` (default 1
2717
+ MiB) of events unread: `stream.signal` aborts with `sse_client_too_slow`,
2718
+ the queued frames are released, and later sends do nothing, so one stalled reader cannot hold
2719
+ server memory without limit. Raise it for a burst larger than that sent before the client can
2720
+ read, or pass `Infinity` to turn the limit off.
2721
+
2722
+ ### Typed SSE routes
2723
+
2724
+ `app.sse(path, schema, run)` declares the event payload with `sse`, so
2725
+ `stream.send()` is type-checked and the typed client grows
2726
+ `.subscribe(onEvent)` with the same event type. A route whose schema declares a
2727
+ `body` is a POST stream: the body validates as on any POST, and
2728
+ `.subscribe(onEvent, &#123; body &#125;)` sends it. A GET stream reconnects after a
2729
+ drop and resumes with `Last-Event-ID`; a POST stream does not reconnect unless you
2730
+ pass `reconnect: true`, since a reconnect sends the body again. The client&apos;s
2731
+ `onRequest` hook runs before each connect, so a refreshed token reaches the stream.
2732
+
2733
+ ```ts
2734
+ import { client } from "@nifrajs/client"
2735
+ import { server } from "@nifrajs/core/server"
2736
+ import { streaming } from "@nifrajs/core/sse"
2737
+ import { t } from "@nifrajs/schema"
2738
+
2739
+ declare function generate(prompt: string): AsyncIterable<string>
2740
+
2741
+ export const app = server()
2742
+ .use(streaming())
2743
+ // A GET stream: the event payload is contracted by `sse`.
2744
+ .sse("/feed", { sse: t.object({ id: t.integer(), title: t.string() }) }, (_c, stream) => {
2745
+ stream.send({ id: 1, title: "hello" })
2746
+ stream.close()
2747
+ })
2748
+ // A body makes it a POST stream, validated like any POST: a chat that posts its prompt.
2749
+ .sse(
2750
+ "/chat",
2751
+ { body: t.object({ prompt: t.string() }), sse: t.object({ token: t.string() }) },
2752
+ async (c, stream) => {
2753
+ for await (const token of generate(c.body.prompt)) stream.send({ token })
2754
+ stream.close()
2755
+ },
2756
+ )
2757
+
2758
+ const api = client<typeof app>("https://example.com")
2759
+ const feed = api.feed.subscribe((post) => console.log(post.title))
2760
+ api.chat.subscribe((event) => console.log(event.token), { body: { prompt: "Hi" } })
2761
+ feed.close()
2762
+ ```
2763
+
2573
2764
  ## Nifra - Query cache
2574
2765
 
2575
2766
  > Nifra's keyed query cache: useQuery / createQuery, dedup, staleness, invalidation.
@@ -2657,7 +2848,7 @@ than small.
2657
2848
  ```ts
2658
2849
  import { html, vanillaAdapter } from "@nifrajs/web-vanilla"
2659
2850
 
2660
- // routes/hotels.ts - a route file, no .tsx needed.
2851
+ // routes/hotels.tsx - a page keeps a route extension even with no JSX in it.
2661
2852
  export default function Hotels({ data }: { data: { hotels: Array<{ name: string }> } }) {
2662
2853
  // Interpolated values are escaped; wrap trusted markup in raw() to opt out deliberately.
2663
2854
  return html`<ul>${data.hotels.map((h) => html`<li>${h.name}</li>`)}</ul>`
@@ -2780,6 +2971,20 @@ Svelte resolve deferred data on the **client**, so JS-off users see the fallback
2780
2971
  deferred boundary. Put critical page data in the route `loader`; use
2781
2972
  `defer()` for slower, non-critical data.
2782
2973
 
2974
+ ### Markup outside the page
2975
+
2976
+ Svelte&apos;s `<svelte:head>` renders into the document&apos;s
2977
+ `<head>`, after the tags from the route&apos;s `meta`. The document
2978
+ always has a `<title>` of its own and a browser uses the first one, so set the
2979
+ title in `meta`.
2980
+
2981
+ A Vue `<Teleport>` is server-rendered where Vue hydrates it from, the first
2982
+ children of its target. `to="body"` content comes first in
2983
+ `<body>`. An `#id` target the app renders empty receives it, and one
2984
+ the app does not render gets a container before the app root. Any other selector, a target that
2985
+ has children of its own, and every teleport on a page that uses `defer()` render on
2986
+ the client, with a warning in development.
2987
+
2783
2988
  ### Dev HMR support
2784
2989
 
2785
2990
  Dev HMR works for all five adapters through `createViteDevServer` and the
@@ -2825,6 +3030,47 @@ app
2825
3030
  app.use((a) => a.decorate("db", db).derive((c) => ({ now: Date.now() })))
2826
3031
  ```
2827
3032
 
3033
+ ### What a parent&rsquo;s use() covers
3034
+
3035
+ A `group()` scope inherits the parent&rsquo;s chain as it stands at the call. A
3036
+ `merge()`d server was compiled on its own, so its routes keep _their_ chain. Only
3037
+ what the parent installed **before** the `group()` or `merge()` counts.
3038
+
3039
+ Installed on the parent
3040
+
3041
+ `group()` route
3042
+
3043
+ `merge()`d route
3044
+
3045
+ `derive`, `decorate`, `authenticate`,
3046
+ `beforeHandle`, `afterHandle`, `onError`,
3047
+ `around`, route-scoped assurance
3048
+
3049
+ yes
3050
+ no - wire it on the merged server
3051
+
3052
+ `onRequest`, `onResponse`, `responseHeaders`
3053
+
3054
+ yes
3055
+ yes (app-wide)
3056
+
3057
+ `responseContract()`
3058
+
3059
+ yes
3060
+ yes, when the route declares a response and its own server installed no contract
3061
+
3062
+ `effectLedger()`
3063
+
3064
+ yes
3065
+ yes, when the route declares capabilities and has no ledger of its own
3066
+
3067
+ `idempotency()`
3068
+
3069
+ yes
3070
+
3071
+ the merged server&rsquo;s own - a route declaring `idempotency` without one is
3072
+ refused at registration
3073
+
2828
3074
  ### Route/hook plugins: keep types with defineRouterPlugin
2829
3075
 
2830
3076
  A plugin that registers routes or hooks but adds **no context type** - e.g. mounting an auth
@@ -2877,6 +3123,48 @@ const app = server()
2877
3123
  .use(rateLimit({ store: new MemoryStore(), max: 100, windowMs: 60_000 }))
2878
3124
  ```
2879
3125
 
3126
+ To refuse a request in `onRequest`, return the same `status()` value a
3127
+ route would: `return status(401, { error: "key required" })`. It takes the
3128
+ normal response path, so the body is serialized with its content-type, and declared
3129
+ `responseHeaders` and every `onResponse` hook apply to it. A hand-built
3130
+ `Response` still works.
3131
+
3132
+ #### Which hooks run where
3133
+
3134
+ Where a hook is registered decides which requests it sees. `nifra routes --hooks`
3135
+ prints, for every backend route and for each prefix's unmatched paths, the hooks that run
3136
+ and the site that registered them (`app`, `group /admin`,
3137
+ `merge #1`, `mount /m`), named by plugin where one registered them:
3138
+
3139
+ -
3140
+ A **group**'s hooks cover its whole prefix, including that prefix's 404 and 405
3141
+ responses.
3142
+
3143
+ -
3144
+ A **merged** server's `onRequest` runs only for its own routes; its
3145
+ response hooks and static headers become app-wide.
3146
+
3147
+ -
3148
+ A **mount** runs the child's full lifecycle inside the parent's request and response
3149
+ hooks.
3150
+
3151
+ -
3152
+ `around`, `derive`, and `beforeHandle` apply only to
3153
+ routes declared after them.
3154
+
3155
+ -
3156
+ A **WebSocket** route, listed as `WS /path`, runs the request hooks, then the
3157
+ authentication stages, `derive`s, `beforeHandle`s and
3158
+ `onError` declared before it. Its handshake runs no `around`,
3159
+ `afterHandle` or response hooks, but a refused handshake carries the
3160
+ app&rsquo;s `responseHeaders()`.
3161
+
3162
+ `nifra routes --hooks --json` writes the report; `--since <file>`
3163
+ prints what runs differently from a saved one. `nifra upgrade --write` saves the
3164
+ report before it edits anything and prints the scope diff, and `nifra check --json`
3165
+ includes it as `hooks` when `nifra.assurance.ts` names a
3166
+ `source`. Origins are recorded only outside production builds.
3167
+
2880
3168
  ### Header middleware: one portable hook, fast everywhere
2881
3169
 
2882
3170
  Most response middleware only reads or writes **headers** - security headers, CORS
@@ -3071,7 +3359,10 @@ const app = server()
3071
3359
  `healthcheck()` adds liveness (`/health`) and readiness (`/ready`)
3072
3360
  endpoints - apply it **before** any auth guard so they stay public. `openapi()`
3073
3361
  serves an OpenAPI 3.1 document at `/openapi.json`, generated from your routes - add
3074
- `ui: true` for a Scalar API-reference page at `/reference`. Paths,
3362
+ `ui: true` for a Scalar API-reference page at `/reference`. The page
3363
+ loads an exact Scalar version from jsDelivr under a Subresource Integrity hash; serve your own
3364
+ copy with `ui: { cdn, integrity }` to keep a third-party origin out of your
3365
+ Content-Security-Policy. Paths,
3075
3366
  methods, and params are introspected; `servers`, `tags`,
3076
3367
  `security`, and `securitySchemes` are document options, and
3077
3368
  `operations` (keyed by `"GET /users/:id"`) supplies per-route bodies/security
@@ -3116,7 +3407,10 @@ app.use(idempotency({ store: new MemoryIdempotencyStore() }))
3116
3407
  enforced by default, and claims (`iss`/`aud`/`nbf`) are checked.
3117
3408
  Read the typed claims off the returned plugin - `auth.requireClaims(c.req)` (throws
3118
3409
  `401`) or `auth.claims(c.req)` (nullable). For rotating keys, pass
3119
- `key: jwks({ url })` (HTTPS-only, cached, size/time-bounded). `basicAuth`
3410
+ `key: jwks({ url })` (HTTPS-only, cached, size/time-bounded). A fetch that fails
3411
+ or returns no keys is remembered for `failureCooldownMs` (default 5 s), so while the
3412
+ identity provider is down a request fails at once instead of waiting on a fetch of its own.
3413
+ `basicAuth`
3120
3414
  compares static credentials in **constant time** (or takes a `verify` callback).
3121
3415
 
3122
3416
  ```ts
@@ -3177,6 +3471,36 @@ const app = server()
3177
3471
  // poweredBy() is opt-in (Nifra emits no X-Powered-By by default). combine(a, b, c) bundles several into one plugin.
3178
3472
  ```
3179
3473
 
3474
+ ### Rate-limit tiers
3475
+
3476
+ A `rateLimit()` inside a `group()` still sits under an app-wide one, so a
3477
+ group can only be stricter than the app. `rateLimitTiers()` is one limiter that
3478
+ charges each request to exactly one bucket - the tier with the longest static
3479
+ `prefix` holding the path, else the `floor` - so a public API can allow
3480
+ more than the floor while a login path allows less. It runs before routing, so 404s, 405s and
3481
+ bodies that fail validation are charged too. Prefixes are checked when the limiter is built
3482
+ (static, no trailing slash, no duplicates), and each tier counts in its own keys, so one store
3483
+ serves them all. `skip`, on both limiters, leaves a request uncharged.
3484
+
3485
+ ```ts
3486
+ import { server } from "@nifrajs/core/server"
3487
+ import { MemoryStore, rateLimitTiers } from "@nifrajs/middleware"
3488
+
3489
+ // Each request is charged to ONE bucket: the longest matching prefix, else the floor.
3490
+ // A tier may sit above the floor - tiers never stack on it.
3491
+ const app = server().use(
3492
+ rateLimitTiers({
3493
+ store: new MemoryStore(),
3494
+ floor: { max: 120, windowMs: 60_000 },
3495
+ tiers: [
3496
+ { prefix: "/api/public", max: 600, windowMs: 60_000 },
3497
+ { prefix: "/auth/login", max: 10, windowMs: 60_000 },
3498
+ ],
3499
+ skip: (req) => new URL(req.url).pathname === "/health", // never charged
3500
+ }),
3501
+ )
3502
+ ```
3503
+
3180
3504
  ### Security middleware
3181
3505
 
3182
3506
  The security set - `csrf` (signed double-submit + Origin/Referer), `jwt`,
@@ -3229,7 +3553,7 @@ const PublicUser = t.object({ id: t.string(), name: t.string() })
3229
3553
 
3230
3554
  // "warn" logs and changes nothing; "enforce" makes the contract the upper bound too.
3231
3555
  // Not installing the plugin is "off" - and keeps the lane out of your bundle entirely.
3232
- export const app = server().use(responseContract("enforce")).get(
3556
+ export const app = server().use(responseContract("enforce", { extra: "strip" })).get(
3233
3557
  "/me",
3234
3558
  { response: PublicUser },
3235
3559
  async () => {
@@ -3239,8 +3563,10 @@ export const app = server().use(responseContract("enforce")).get(
3239
3563
  },
3240
3564
  )
3241
3565
 
3242
- // off -> {"id":"u1","name":"Ada","email":"a@b.c","passwordHash":"..."}
3243
- // enforce -> {"id":"u1","name":"Ada"}
3566
+ // off -> 200 {"id":"u1","name":"Ada","email":"a@b.c","passwordHash":"..."}
3567
+ // enforce -> 500 {"ok":false,"error":"internal_error"}
3568
+ // (t.object is closed; the log names email and passwordHash)
3569
+ // enforce, { extra: "strip" } -> 200 {"id":"u1","name":"Ada"}
3244
3570
  ```
3245
3571
 
3246
3572
  Not installing the plugin is &ldquo;off&rdquo;, which is exactly today&rsquo;s behaviour and
@@ -3249,16 +3575,25 @@ keeps the lane out of your bundle rather than shipping a disabled branch to ever
3249
3575
  serves the payload **unchanged** - so turning it on in staging can never be the
3250
3576
  thing that broke production. `"enforce"` serializes the validated value
3251
3577
  instead of the raw result. Both cover a returned value, a `status(...)` result and
3252
- a `c.json(...)` reply alike; only a hand-built `Response` (a redirect, a
3253
- stream) passes unchecked. Install it before the routes it should cover: like
3578
+ a `c.json(...)` reply alike, whether the handler returns or throws it, a hook
3579
+ returns it early, or `onError` answers with it; only a hand-built
3580
+ `Response` (a redirect, a stream) passes unchecked. Install it before the routes it
3581
+ should cover: like
3254
3582
  `idempotency()`, the decision is made per route at registration.
3255
3583
 
3256
- Enforcement follows your schema&rsquo;s own semantics, because Standard Schema exposes
3257
- `validate` and no way to enumerate declared keys. A _stripping_ schema (Zod,
3258
- Valibot) yields a cleaned value, so the extra fields are dropped. A _strict_ one
3584
+ By default, enforcement follows your schema&rsquo;s own semantics, because Standard Schema
3585
+ exposes `validate` and no way to enumerate declared keys. A _stripping_ schema
3586
+ (Zod, Valibot) yields a cleaned value, so the extra fields are dropped. A _strict_ one
3259
3587
  (`@nifrajs/schema`&rsquo;s `t.object`) reports them as issues, so the
3260
- response becomes a 500 and the detail goes to the logger, never to the caller. Both are what you
3261
- already declared about extra fields.
3588
+ response becomes a 500 and the detail goes to the logger, never to the caller.
3589
+ `{ extra: "strip" }` changes that for `t` schemas: they carry their
3590
+ JSON Schema, which does list the declared keys, so the payload is projected onto them - nested
3591
+ objects, arrays and records included - before it is validated. In `"warn"`
3592
+ mode the same option makes the log name the fields `"enforce"` would drop.
3593
+
3594
+ A `merge()`d server&rsquo;s routes are covered by a contract the parent installed
3595
+ before the merge, as long as the route declares a response and its own server installed none.
3596
+ See [what a parent&rsquo;s use() covers](/docs/plugins#parent-use).
3262
3597
 
3263
3598
  The check itself is essentially free: with a compiled validator it measures in the
3264
3599
  ~100ns-per-response range, and a realistic middleware-carrying route benchmarks within noise
@@ -3481,8 +3816,12 @@ one the bytes proved.
3481
3816
 
3482
3817
  -
3483
3818
  **A form post is a simple cross-origin request.** A browser sends
3484
- `multipart/form-data` with cookies and no preflight, so a cookie-authenticated upload
3485
- route needs `csrf()` like any other state change.
3819
+ `multipart/form-data`, urlencoded and `text/plain` with cookies and no
3820
+ preflight. A body-schema route refuses one whose `Origin` is another site&rsquo;s
3821
+ with `403` before reading it; list a site that must post forms to the app in the
3822
+ server&rsquo;s `formOrigins`. A request without an `Origin` passes, so
3823
+ a cookie-authenticated upload route still needs `csrf()` like any other state
3824
+ change.
3486
3825
 
3487
3826
  -
3488
3827
  **The form is held in memory.** Budget roughly twice `bodyLimit` per request in
@@ -3654,14 +3993,21 @@ Verification is **constant-time** - the provider's signature goes straight into
3654
3993
  `crypto.subtle.verify`, so a wrong signature can't be discovered byte-by-byte through
3655
3994
  timing. Presets cover **Stripe** (parses `t=…,v1=…` and enforces a 5-minute replay
3656
3995
  window on the signed timestamp) and **GitHub** (`sha256=…`); the `generic`
3657
- preset takes an explicit header, encoding, and prefix for anything else. Pass an array of secrets to
3996
+ preset takes an explicit header, encoding (`"hex"`, `"base64"` or
3997
+ `"base64url"`, padded or not), and prefix for anything else. A signature that does not
3998
+ decode is refused as `malformed_signature`, never thrown. Pass an array of secrets to
3658
3999
  accept either during a key rotation.
3659
4000
 
3660
4001
  ### Idempotency - `idempotency()` middleware
3661
4002
 
3662
4003
  A dropped connection or an impatient double-tap shouldn't double-charge a card. With an
3663
4004
  `Idempotency-Key` header, a retried unsafe request replays the first response instead of
3664
- re-running the side effect. It short-circuits in `onRequest`, _before_ the handler.
4005
+ re-running the side effect. The key is claimed in `onRequest`, so a concurrent retry gets
4006
+ a `409`; the stored response is replayed from `beforeHandle`, _after_ the
4007
+ route's authentication accepts the retry, so a credential revoked since the first request gets the
4008
+ route's current answer. Declare routes after `.use(idempotency())`:
4009
+ `beforeHandle` is order-scoped, and a route declared earlier replays from
4010
+ `onRequest`.
3665
4011
 
3666
4012
  ```ts
3667
4013
  // doc-check: skip - fragment: `app`, `chargeCard`, and `id` are your application's.
@@ -3786,6 +4132,13 @@ group can apply its own configuration over the app&apos;s - `admin.use(securityH
3786
4132
  inside `app.group("/admin", ...)` - and each header it sets replaces the app&apos;s
3787
4133
  value on the group&apos;s routes only.
3788
4134
 
4135
+ A fixed `Content-Security-Policy` here cannot allow nifra&apos;s inline scripts by
4136
+ nonce. For a strict `script-src` on web pages, give `createWebApp` a
4137
+ `csp` from `createCspPolicy`: it allows the framework&apos;s constant
4138
+ scripts by hash and adds a nonce only to a page that needs one, so every other page sends the
4139
+ same header and stays cacheable under `withISR` or a CDN. See
4140
+ [A strict CSP that still caches](/docs/contract#strict-csp).
4141
+
3789
4142
  ### Route assurance - prove every route is guarded
3790
4143
 
3791
4144
  Installing security middleware is not the same as proving it covers every route. Nifra&apos;s
@@ -3806,11 +4159,15 @@ export default defineAssuranceConfig({
3806
4159
  rules: [
3807
4160
  { name: "health", match: { paths: ["/health"] }, require: [],
3808
4161
  forbid: [NIFRA_ASSURANCE.AUTHENTICATED] },
3809
- { name: "mutation", match: { methods: ["POST", "PUT", "PATCH", "DELETE"] },
4162
+ // Every method but these, so a custom one such as PURGE is a mutation too.
4163
+ { name: "mutation", match: { exceptMethods: ["GET", "HEAD", "OPTIONS", "WS"] },
3810
4164
  require: [NIFRA_ASSURANCE.AUTHENTICATED, NIFRA_ASSURANCE.CSRF,
3811
4165
  NIFRA_ASSURANCE.BODY_BOUNDED] },
3812
4166
  { name: "read", match: { methods: ["GET", "HEAD"] },
3813
4167
  require: [NIFRA_ASSURANCE.AUTHENTICATED] },
4168
+ // WebSocket routes reflect under the pseudo-method "WS".
4169
+ { name: "socket", match: { methods: ["WS"] },
4170
+ require: [NIFRA_ASSURANCE.AUTHENTICATED] },
3814
4171
  ],
3815
4172
  },
3816
4173
  })
@@ -3824,6 +4181,14 @@ while authentication and other order-scoped hooks cover only routes registered a
3824
4181
  Method and path filters prevent a narrow guard from claiming broader coverage. The evaluation
3825
4182
  runs only through reflection or `nifra assure`, so requests pay no assurance cost.
3826
4183
 
4184
+ WebSocket routes are reflected under the pseudo-method `"WS"`, so a policy classifies
4185
+ every socket too. A handshake runs the auth guards registered before the socket and the
4186
+ app&apos;s request hooks, so a socket carries authentication and rate-limit evidence from them.
4187
+ It never claims evidence whose enforcement its message handlers bypass, such as a durable
4188
+ command journal. A socket declares its effects with
4189
+ `app.ws(path, { capabilities })`, and capability assurance checks them against
4190
+ the module that registers it.
4191
+
3827
4192
  ### Effect assurance - declared capability versus provenance
3828
4193
 
3829
4194
  Authentication does not reveal what a route can do. Capability assurance compares an exact
@@ -3887,7 +4252,11 @@ Approval resumes are HMAC-signed, expire, bind to the tenant, principal, capabil
3887
4252
  digest, and are consumed atomically once. The effect journal marks execution before the provider
3888
4253
  call, so a crash after an external commit remains `ambiguous` for reconciliation instead
3889
4254
  of being retried blindly. Typed sagas persist compensation arguments separately from the sealed
3890
- token-only ledger and compensate committed steps in reverse order with retry/backoff state.
4255
+ token-only ledger and compensate committed steps in reverse order with retry/backoff state. One
4256
+ `resume` or `compensate` call owns a saga at a time through a lease every
4257
+ transition renews (`leaseMs`, default 5 minutes): a second call meanwhile throws
4258
+ `SagaConcurrencyError` and changes nothing, and a call that died is taken over once its
4259
+ lease lapses.
3891
4260
 
3892
4261
  ```ts
3893
4262
  // doc-check: skip - durable store implementations are deployment-specific.
@@ -4109,6 +4478,15 @@ there and grants exactly `db.read` - it never reaches the driver underneath. Kee
4109
4478
  the two halves import-disjoint, at the seam and at the route modules, and every route's
4110
4479
  declaration equals its reach.
4111
4480
 
4481
+ The walk starts at the module that registers the route, which Nifra finds by reading the
4482
+ source without running it. It reads a path written as a string literal, or as a name bound to
4483
+ a module-level `const` string, with or without `as const`. The
4484
+ `const` can live in the same file or be exported from a project file it imports.
4485
+ The same holds for a WebSocket route's `app.ws(path, ...)`. A path built at runtime, such as a template with `${...}` in it, cannot be read
4486
+ this way. `nifra check` names that expression in an NF-C036 finding, because the
4487
+ route then has no module to start from. List the module in
4488
+ `provenance.routeModules` to give it one.
4489
+
4112
4490
  ### Runtime beacons
4113
4491
 
4114
4492
  Static provenance answers what a _module_ can reach, so its evidence is as broad as the
@@ -4220,6 +4598,33 @@ $ nifra check
4220
4598
  • known gap, not capability-analyzed: /api/auth/* - better-auth's own handler; its effects are its own
4221
4599
  ```
4222
4600
 
4601
+ A mount can also state the routes it serves and the capabilities each one exercises. Pass an
4602
+ object with the `reason` and a `routes` list. Each path is the full
4603
+ request path and must sit under the mount:
4604
+
4605
+ ```ts
4606
+ export const app = server().mount({
4607
+ path: "/api/auth",
4608
+ app: authHandler,
4609
+ opaque: {
4610
+ reason: "better-auth's own handler; its effects are its own",
4611
+ routes: [
4612
+ { method: "POST", path: "/api/auth/sign-in/email", capabilities: ["session.write"] },
4613
+ { method: "GET", path: "/api/auth/get-session", capabilities: ["session.read"] },
4614
+ ],
4615
+ },
4616
+ })
4617
+ ```
4618
+
4619
+ Assurance lists these routes with `analysis: "declared"`, apart from the routes it
4620
+ analyzed. The mount stays a known gap with its reason. A declared capability the policy does
4621
+ not define fails, as it does on a route, and so does a domain write declared on
4622
+ `GET`. Nothing else is checked, because there is no evidence to read. The routes
4623
+ also appear in the capability lockfile, so a changed declaration shows up in review. They
4624
+ appear in the `nifra context` route index too. An L4 run lists them as having no
4625
+ contract to test. A malformed entry, or a path outside the mount, stops the analysis with an
4626
+ error that names it.
4627
+
4223
4628
  ### The lockfile
4224
4629
 
4225
4630
  Once assurance passes, snapshot the result. The lockfile is a review artifact: a route that
@@ -4352,6 +4757,15 @@ a `SessionStore`; in **cookie mode** (no store) the data is signed into the cook
4352
4757
  Stores mirror the ISR discipline: `MemorySessionStore` is prod-guarded;
4353
4758
  `KVSessionStore` is the durable, shared production store.
4354
4759
 
4760
+ The signature covers the cookie name, so two managers that share a secret never read each
4761
+ other's cookies; still, give sessions a secret no other signed value uses. A cookie-mode
4762
+ session lives at most `maxAge`, whatever expiry its payload names. Upgrading from
4763
+ 4.x, whose cookies were signed without the name, signs everyone out unless you pass
4764
+ `acceptLegacyCookies: true`: keep it for one `maxAge` after the upgrade
4765
+ (each such session is re-signed on its next commit), then remove it, since while it is on any
4766
+ session-shaped value the app signs with the same secret through `signValue` reads
4767
+ as a session.
4768
+
4355
4769
  ```ts
4356
4770
  // backend/auth.ts - one session manager, created on first use so the secret is read at request time.
4357
4771
  // Store mode keeps data server-side; cookie mode (no store) is stateless.
@@ -4417,6 +4831,102 @@ export async function loader({ request }: Route.LoaderArgs) {
4417
4831
  }
4418
4832
  ```
4419
4833
 
4834
+ Without `redirectTo`, a guard denies with a 401 JSON
4835
+ `{ ok: false, error: "unauthorized" }`. `unauthenticated` sets that
4836
+ response's status (400-599), body and headers instead; the same option exists on
4837
+ `@nifrajs/better-auth`'s `requireSession`, `requirePrincipal`
4838
+ and `authed`, and on `@nifrajs/authjs`'s `requireAuthUser`. A
4839
+ guard given both `redirectTo` and `unauthenticated` throws.
4840
+
4841
+ ```ts
4842
+ // doc-check: skip - fragment: `session` is the request's session.
4843
+ requireUser(session, "userId", {
4844
+ unauthenticated: { status: 403, body: { error: "login_required" }, headers: { "www-authenticate": "Session" } },
4845
+ })
4846
+ ```
4847
+
4848
+ ### One request-scoped context from authentication to the handler
4849
+
4850
+ `around` wraps everything after routing for the routes declared after it:
4851
+ authentication, body validation, `derive` and the handler. An
4852
+ `AsyncLocalStorage` frame or a database session setting opened there stays open
4853
+ until the response is decided. `authenticate` runs inside that frame before
4854
+ validation, resolves the caller once and types it as `c.principal`, so it can write
4855
+ what it resolved into the frame instead of a second lookup in a `derive`. Both are
4856
+ order-scoped, so inside a `group` they cover only its routes.
4857
+
4858
+ ```ts
4859
+ // doc-check: skip - fragment: `app`, `readSession`, `Note`, and `db` are your application's.
4860
+ import { AsyncLocalStorage } from "node:async_hooks"
4861
+ import { authenticated, rejected } from "@nifrajs/core"
4862
+
4863
+ const frame = new AsyncLocalStorage<{ userId?: string }>()
4864
+
4865
+ app.group("/api", (api) =>
4866
+ api
4867
+ // Opens before authentication and closes after the response: validation, derive and the
4868
+ // handler all run inside it, and so does a thrown Response.
4869
+ .around((_c, next) => frame.run({}, next))
4870
+ // Resolves the caller once: typed as c.principal, and written into the open frame.
4871
+ .authenticate({
4872
+ id: "session",
4873
+ run: async (input) => {
4874
+ const session = await readSession(input.headers.get("cookie"))
4875
+ if (session === null) return rejected()
4876
+ const store = frame.getStore()
4877
+ if (store !== undefined) store.userId = session.userId
4878
+ return authenticated(session)
4879
+ },
4880
+ })
4881
+ .post("/notes", { body: Note }, (c) => db.notes.insert({ ...c.body, owner: c.principal.userId })),
4882
+ )
4883
+ ```
4884
+
4885
+ ### Authentication runs first
4886
+
4887
+ `authenticate()` refuses a request before anything reads it: the body is not read,
4888
+ no schema runs, and no `derive` or `beforeHandle` sees it. An
4889
+ unauthenticated caller gets the 401 (or the refusal `run` returns), never a 413 or a
4890
+ 422 that describes the route's input. `basicAuth`, `bearer`,
4891
+ `apiKey` and `jwt` from `@nifrajs/middleware` run there. With
4892
+ `optional: true` they never refuse, so validation answers as usual.
4893
+
4894
+ `run` receives the method, pathname, headers, path params, the raw
4895
+ `search` and the parsed `query` (frozen, read by the parser
4896
+ `c.query` uses; a repeated key is an array), the `request` (never read
4897
+ its body), `env`, `clientIp` and an abort `signal`. Every
4898
+ field is the caller's and unvalidated. Read the query only for a short-lived, single-use
4899
+ ticket, such as one for a WebSocket handshake a browser cannot set a header on: a URL lands in
4900
+ proxy and access logs. The dev feed records a query's keys, never its values.
4901
+
4902
+ ```ts
4903
+ // doc-check: skip - fragment: `app` and `tickets` (a single-use store) are your application's.
4904
+ import { authenticated, rejected } from "@nifrajs/core"
4905
+
4906
+ app
4907
+ .authenticate({
4908
+ id: "socket-ticket",
4909
+ run: async (input) => {
4910
+ // A browser cannot set a header on a WebSocket handshake, so the ticket rides in the URL.
4911
+ const ticket = input.query.ticket
4912
+ if (typeof ticket !== "string") return rejected()
4913
+ const user = await tickets.take(ticket) // single use, expires in seconds
4914
+ return user === null ? rejected() : authenticated(user)
4915
+ },
4916
+ })
4917
+ .ws("/live", { upgrade: (c) => ({ user: c.principal.id }) })
4918
+ ```
4919
+
4920
+ A guard written as a `beforeHandle` runs after validation, so an unauthenticated
4921
+ caller reaches the body parser. When `nifra check` loads
4922
+ `backend/app.ts` (it does when a `contracts.lock.json` exists), NF-S008
4923
+ names each route that claims authentication this way and declares an input schema. Move the
4924
+ check into `authenticate()`, or set
4925
+ `validationOrder: "auth-before-validation"` on the route to run its hooks first. A
4926
+ rate limiter written as a `beforeHandle` behind `authenticate()` does not
4927
+ see refused requests; `rateLimit()` runs in `onRequest`, before routing,
4928
+ and counts them.
4929
+
4420
4930
  ### Session code stays in the backend
4421
4931
 
4422
4932
  The session manager, an auth instance and their secrets live in `backend/`, and only
@@ -5215,15 +5725,16 @@ app.listen(3000) // Bun
5215
5725
  ### Lifecycle
5216
5726
 
5217
5727
  Every callback is optional - `{ message }` alone is a valid echo server.
5218
- `upgrade` is the only one that runs before the socket opens, so it’s the one place
5219
- to reject a connection.
5728
+ `upgrade` is the only callback that runs before the socket opens, so it’s where a
5729
+ route rejects a connection or seeds its data.
5220
5730
 
5221
5731
  ```ts
5222
5732
  // doc-check: skip - illustrative lifecycle (empty close/error bodies, cookie shape).
5223
5733
  app.ws<{ user: string }>("/chat", {
5224
- // upgrade(c) runs in the full HTTP request context, BEFORE the socket opens -
5225
- // authenticate, check origin, rate-limit. Return the per-connection data (→ ws.data,
5226
- // typed), or a Response to REJECT the upgrade. A thrown error rejects with 500.
5734
+ // upgrade(c) runs in the full HTTP request context, BEFORE the socket opens, after the
5735
+ // guards registered before this route. Return the per-connection data (→ ws.data, typed),
5736
+ // or a Response or status(...) to REJECT the upgrade. A thrown Response rejects with it;
5737
+ // any other throw goes to onError, then a 500.
5227
5738
  upgrade(c) {
5228
5739
  const user = c.cookies.session
5229
5740
  if (user === undefined) return new Response("unauthorized", { status: 401 })
@@ -5236,6 +5747,37 @@ app.ws<{ user: string }>("/chat", {
5236
5747
  })
5237
5748
  ```
5238
5749
 
5750
+ ### Guards
5751
+
5752
+ A handshake is a `GET`, and it runs the chain an HTTP `GET` registered
5753
+ at the same point runs: the `authenticate()` stages, `derive`s and
5754
+ `beforeHandle`s added before `app.ws()`, then `upgrade`. So
5755
+ auth middleware such as `basicAuth` or `jwt` guards the sockets
5756
+ registered after it, any guard can refuse the handshake with the response it would send an
5757
+ HTTP request, and `upgrade` reads what the guards derived. The origin check runs
5758
+ first. A socket registered before a guard stays public, like an HTTP route there. A browser
5759
+ cannot set a header on the handshake, so `authenticate()` can read a short-lived,
5760
+ single-use ticket from `input.query` - see
5761
+ [Authentication runs first](/docs/auth#auth-first).
5762
+
5763
+ ```ts
5764
+ // doc-check: skip - illustrative: an existing auth guard and session lookup.
5765
+ app
5766
+ .ws("/status", {}) // registered before the guard: public
5767
+ .authenticate(sessionAuth) // or .use(basicAuth(...)), .derive(...), .beforeHandle(...)
5768
+ .get("/me", (c) => c.principal)
5769
+ .ws("/chat", {
5770
+ // Reached only when sessionAuth accepted the handshake, exactly as GET /me is.
5771
+ upgrade: (c) => ({ user: c.principal.id }),
5772
+ })
5773
+ ```
5774
+
5775
+ A socket declares the effects its code may reach with `capabilities`, as an HTTP
5776
+ route does. `nifra capabilities check` compares them with what the registering
5777
+ module can reach, and the handshake context carries the runtime guard for
5778
+ `upgrade()`. Reflection lists every socket under the pseudo-method
5779
+ `"WS"`, so `nifra assure` classifies sockets too.
5780
+
5239
5781
  ### NifraWebSocket
5240
5782
 
5241
5783
  The `ws` handed to each callback is a portable wrapper over the runtime’s native
@@ -5335,7 +5877,9 @@ bridge an external fan-out (Redis pub/sub, NATS, a queue) to broadcast across al
5335
5877
  **Cloudflare Workers**, a stateless isolate can’t broadcast across connections,
5336
5878
  so Nifra ships a Durable Object hub: `createWebSocketHub(app)` holds the
5337
5879
  connections, and `toFetchHandler(app, { webSocketHub })` routes upgrades to it
5338
- - then `ws.subscribe` / `app.publish` behave exactly as on Bun.
5880
+ - then `ws.subscribe` / `app.publish` behave exactly as on Bun. An
5881
+ upgrade request for a path with no WebSocket route still gets that path's HTTP answer, as on
5882
+ every other runtime.
5339
5883
 
5340
5884
  ```ts
5341
5885
  // doc-check: skip - Workers entry: a Durable Object hub holds the connections.
@@ -5388,6 +5932,60 @@ it). Install it when you use `app.ws()`; without it a WS upgrade gets a clean
5388
5932
  npm i ws
5389
5933
  ```
5390
5934
 
5935
+ `server({ wsMaxPayloadBytes })` caps one inbound message (default
5936
+ `maxBodyBytes`, 1 MB): a socket that sends more is closed with `1009`
5937
+ and the message never reaches your handler. Bun and Node refuse it while it arrives. Deno's
5938
+ `Deno.upgradeWebSocket` takes no size limit, so Deno receives each message whole
5939
+ (up to its own limit, 64 MiB on Deno 2.9) before nifra can check it: the cap still keeps the
5940
+ message from your handler, but it does not bound the memory Deno spent receiving it. Where that
5941
+ matters, cap message size in a proxy in front of a Deno app.
5942
+
5943
+ ### In a composed app
5944
+
5945
+ `mount({ path, app })` carries WebSocket upgrades as well as HTTP: when the
5946
+ serving adapter sees an upgrade under the mount's path, it asks the child's
5947
+ `resolveWebSocketUpgrade` and upgrades its own socket with the outcome. Any nifra
5948
+ server with `websocket()` installed has one. A child that is not a nifra server can
5949
+ implement it too, building the outcome from `@nifrajs/core/ws`:
5950
+ `attachWebSocket` wires the socket to a handler and a `TopicRegistry`
5951
+ owns its pub/sub. On Bun, install `websocket()` on the parent or on a mounted nifra
5952
+ child, or `listen()` refuses to start with `INVALID_WS_RUNTIME`.
5953
+
5954
+ Each mounted nifra app keeps its own `wsMaxPayloadBytes`: a message over the
5955
+ child's cap closes that socket with `1009`, whether the cap is below or above the
5956
+ parent's. On Bun one listener serves them all, so its frame limit is the largest cap among the
5957
+ parent and the apps mounted on it, and a socket whose app declares less checks its own.
5958
+
5959
+ `mountFetch(path, handler)` and a plain fetch handler carry HTTP only. An upgrade
5960
+ request under such a path reaches the handler as an ordinary request and never becomes a
5961
+ socket. Serve that path's WebSockets from a mounted child, or keep that server beside nifra.
5962
+
5963
+ ```ts
5964
+ // server.ts - one listener; the chat child and a non-nifra child both take upgrades.
5965
+ import { server } from "@nifrajs/core/server"
5966
+ import { attachWebSocket, TopicRegistry, websocket, type WebSocketUpgradeOutcome } from "@nifrajs/core/ws"
5967
+
5968
+ const chat = server()
5969
+ .use(websocket())
5970
+ .ws("/room", { message: (ws, data) => ws.send(data) })
5971
+
5972
+ // Not a nifra server: it answers HTTP through fetch and builds its own upgrade outcome.
5973
+ const topics = new TopicRegistry()
5974
+ const legacy = {
5975
+ fetch: () => new Response("legacy"),
5976
+ resolveWebSocketUpgrade: (request: Request): WebSocketUpgradeOutcome =>
5977
+ new URL(request.url).pathname === "/feed"
5978
+ ? { kind: "upgrade", handler: { open: (ws) => ws.send("hello") }, data: undefined, pubsub: topics, attach: attachWebSocket }
5979
+ : { kind: "pass" },
5980
+ }
5981
+
5982
+ server()
5983
+ .use(websocket()) // Bun: the parent holds the socket table every mounted upgrade runs through
5984
+ .mount({ path: "/chat", app: chat, stripPrefix: true })
5985
+ .mount({ path: "/legacy", app: legacy, stripPrefix: true })
5986
+ .listen(3000)
5987
+ ```
5988
+
5391
5989
  ## Nifra - Deployment
5392
5990
 
5393
5991
  > One Nifra app, one command per target - Bun, Node (Docker), Deno Deploy, Cloudflare Pages, Vercel Edge.
@@ -5518,6 +6116,11 @@ hand one visitor a response produced for another:
5518
6116
  **Only content-hashed assets are precached.** A hashed URL names its bytes, so serving it
5519
6117
  from cache forever is correct by construction. Unhashed URLs are left to the network.
5520
6118
 
6119
+ -
6120
+ **Only the build's own URLs are answered, from the build's own cache.** Any other request,
6121
+ an API call included, goes to the network untouched, and a cache the page or another app
6122
+ stored on the origin is never read.
6123
+
5521
6124
  -
5522
6125
  **Documents are never cached.** Only navigations that FAIL are answered, and only with the
5523
6126
  offline page you nominate - which must be static and user-independent, since every visitor
@@ -5529,8 +6132,9 @@ for another.
5529
6132
  straight to the network.
5530
6133
 
5531
6134
  -
5532
- **The cache name carries the build id**, and activation deletes every older cache - so a
5533
- stale worker cannot pin an old build indefinitely.
6135
+ **The cache name carries the build id**, and activation deletes every older cache named
6136
+ `<cacheName>:<buildId>` - so a stale worker cannot pin an old build
6137
+ indefinitely, and the origin's other caches are left alone.
5534
6138
 
5535
6139
  Omit `offlineUrl` and a failed navigation simply fails, which is the honest default:
5536
6140
  an offline page you have not written is not better than a browser error.
@@ -5572,6 +6176,14 @@ content-typed, with an immutable cache - before the app runs, leaving the SSR fa
5572
6176
  untouched: `serve(app, {'{ port, static: { dir: new URL("./assets/", import.meta.url) } }'})`.
5573
6177
  On Cloudflare/Vercel the platform serves assets, so you don't need it there.
5574
6178
 
6179
+ Every file under the mount is cached as immutable by default. If the directory also holds
6180
+ files that keep their name across deploys, pass the build manifest's `assets` as
6181
+ `hashedFiles`: only those URL paths stay immutable, and every other file is sent
6182
+ with its validators so the browser revalidates it. The server that
6183
+ `nifra build --target bun` or `node` generates does this already. It
6184
+ caches only the client build's own content-hashed outputs as immutable. A file copied from
6185
+ `public/`, even under `public/assets/`, gets a one-day policy.
6186
+
5575
6187
  ### HTTPS without a proxy
5576
6188
 
5577
6189
  Most deploys end TLS in front of the app - a load balancer, a CDN, the platform - and the app
@@ -5766,6 +6378,43 @@ import { stylexVite } from "@nifrajs/web/plugins/stylex"
5766
6378
  export const vitePlugins = [react(), stylexVite()]
5767
6379
  ```
5768
6380
 
6381
+ ### Upgrading nifra
6382
+
6383
+ Every `@nifrajs/*` package, `nifra` and `create-nifra` ship
6384
+ together at one version. Move a workspace between releases with
6385
+ `nifra upgrade <version>` rather than by editing pins: it runs every release
6386
+ recipe between the oldest version your `package.json` files pin and the target,
6387
+ oldest first. A recipe moves a removed package to its replacement, rewrites imports that
6388
+ moved, lists comments a release stopped honoring, and pins each group dependency to the
6389
+ target, keeping its `^` or `~` unless you pass `--exact`.
6390
+ The pins in `overrides`, `resolutions` and
6391
+ `pnpm.overrides` move with them, since an override decides what installs.
6392
+
6393
+ ```ts
6394
+ nifra upgrade --list # the release targets this CLI knows
6395
+ nifra upgrade 5.0.0 # dry-run: every planned edit, nothing written
6396
+ nifra upgrade 5.0.0 --write # apply, then nifra check verifies (--no-verify skips it)
6397
+ nifra doctor # afterwards: no "mixed nifra versions" advisory left
6398
+ ```
6399
+
6400
+ It is a dry-run until `--write`, and the same run twice makes no further edits. An
6401
+ unknown version or a target below what is installed is refused (`--allow-downgrade`
6402
+ when the rollback is intended); a target newer than the running CLI prints the command for
6403
+ that release&apos;s CLI. It edits files under the workspace root only: a symlinked package
6404
+ directory or `package.json` is never followed, and a group package installed by a
6405
+ link from outside the root is listed under "Left alone" for you to upgrade in its
6406
+ own repository. The MCP launch pin in `.mcp.json`, `.cursor/mcp.json`,
6407
+ `CLAUDE.md` and `AGENTS.md` moves to the target, the edit
6408
+ `nifra init-agents --sync-mcp` makes. Another version pinned outside
6409
+ `package.json` is listed with its line: the rest of an agent file, an MCP config
6410
+ elsewhere, a Dockerfile or compose file, a CI workflow, and a lockfile other than the
6411
+ workspace&apos;s own (an image build&apos;s frozen lockfile, say), which you regenerate after
6412
+ installing. With `--write` it also
6413
+ prints how hook scope changed across the
6414
+ upgrade (`--no-hooks` skips that). `nifra doctor` and
6415
+ `nifra check` (NF-D002) point here when a workspace carries more than one version;
6416
+ see [mixed nifra versions](/docs/troubleshooting#mixed-versions).
6417
+
5769
6418
  ### Scope
5770
6419
 
5771
6420
  `nifra build` defaults to a self-hosted Bun server. Use `--target node`,
@@ -6310,8 +6959,10 @@ database connection is not the same as handing it a query tool, and this is the
6310
6959
  `@nifrajs/agent-protocol` is the small versioned session and event contract.
6311
6960
  `@nifrajs/coding-agent` provides the standalone `nifra-agent` host with
6312
6961
  bounded sessions, checkpoints, workflows, extensions, verification, and authenticated local
6313
- RPC. `@nifrajs/pi` is an optional Pi backend; other backends implement the same
6314
- protocol port.
6962
+ RPC. `@nifrajs/pi` is an optional Pi backend, and `@nifrajs/acp` drives
6963
+ any Agent Client Protocol agent (Claude Code, Codex, Gemini CLI) over stdio; other backends
6964
+ implement the same protocol port and can prove it with `checkAgentBackend` from
6965
+ `@nifrajs/agent-protocol/conformance`.
6315
6966
 
6316
6967
  ```ts
6317
6968
  bun add @nifrajs/coding-agent @nifrajs/pi
@@ -6343,7 +6994,10 @@ emit `approval.required` with an opaque approval id, action, capability, and bou
6343
6994
  coordinates. Resolve it through the host or RPC with `approval.resolve`; the backend
6344
6995
  emits `approval.resolved` and only then executes the tool. Denial, cancellation,
6345
6996
  timeout, and closed sessions all fail closed, and `approvalTimeoutMs` bounds how long
6346
- a pending decision can remain open.
6997
+ a pending decision can remain open: 5 minutes by default, at most 24 hours, or
6998
+ `"never"` to wait until it is answered, cancelled, its turn ends, or the session
6999
+ closes. The host's broker takes the same values as `approvals.timeoutMs` on
7000
+ `CodingAgentHost` and `CodingAgentRpcServer`.
6347
7001
 
6348
7002
  ### Expose the same agent to other clients
6349
7003
 
@@ -6440,6 +7094,15 @@ resume. `@nifrajs/agent-telemetry` converts the same constrained evidence into
6440
7094
  fail-open OpenTelemetry run and step spans. Providers, credentials, durable storage, retention,
6441
7095
  and tenant policy remain application-owned ports.
6442
7096
 
7097
+ A step that suspends for approval resumes with the decision the caller sends in
7098
+ `resume.approval`. On an AG-UI or `mountAgent` route that anyone can
7099
+ reach, that caller may be the party asking for the tool. Return
7100
+ `resumeApproval: "port"` from `ports` to keep the configured approval
7101
+ port authoritative: it is asked again on resume, receives the caller's decision as
7102
+ `callerDecision`, and its verdict stands. Without a port, such a resume suspends
7103
+ again. A2A does this by default, since the resuming peer is the agent that asked for the
7104
+ tool; return `resumeApproval: "caller"` there only when the peer is the approver.
7105
+
6443
7106
  ### Where your code and secrets run
6444
7107
 
6445
7108
  The server your client starts runs none of your project's code. Each tool that loads the app,
@@ -6466,9 +7129,11 @@ values (`nifra mcp --env-file .env.secrets`), reach all of them.
6466
7129
  -
6467
7130
  **No `.env` in the server.** When Bun loaded `.env`
6468
7131
  values into the server at startup, the server hands the session to a copy of itself that
6469
- loads none, keeping the values from the environment and `--env-file`. The process
6470
- the client started stays to forward signals and the exit code: it still holds the values
6471
- Bun loaded, but runs no project code and reads no messages.
7132
+ loads none, keeping the values from the environment and `--env-file`. A project
7133
+ that installs another release of `@nifrajs/cli` gets the session handed to that
7134
+ CLI the same way, with no `.env` loaded. The process the client started stays to
7135
+ forward signals and the exit code: it still holds the values Bun loaded, but runs no project
7136
+ code and reads no messages.
6472
7137
 
6473
7138
  -
6474
7139
  **A process per call has a cost.** A call that loads the app pays one process
@@ -6766,6 +7431,17 @@ with `export const dev = { indicator: false }` in `nifra.config.ts`,
6766
7431
  or with `indicator: false` on `createDevServer`; the errors still reach
6767
7432
  the feed.
6768
7433
 
7434
+ ### Host check
7435
+
7436
+ The Bun dev server listens on 127.0.0.1 and answers only loopback host names:
7437
+ `localhost`, any `*.localhost`, `127.0.0.1` and
7438
+ `[::1]`. Any other `Host` gets a 403, so a page on another site whose
7439
+ name a DNS rebind points at your machine cannot read your pages, loader data or error overlay.
7440
+ To reach the server under another name (a hosts-file entry, a tunnel), list it with
7441
+ `export const dev = { allowedHosts: ["app.test"] }` in
7442
+ `nifra.config.ts` or `allowedHosts` on `createDevServer`.
7443
+ The Vite pipeline applies Vite's own `server.allowedHosts`.
7444
+
6769
7445
  ### Containers & sandboxes
6770
7446
 
6771
7447
  In Docker, networked volumes, and some sandboxes, pass
@@ -7217,7 +7893,12 @@ classes with separate request state. The second copy prints
7217
7893
 
7218
7894
  **The build honours the declaration on its own** -
7219
7895
  `buildClient` and `buildServer` inject the resolver, so bundled output
7220
- already loads one copy. Unbundled phases do not: Bun's _runtime_ resolver never offers a
7896
+ already loads one copy, and so does the browser bundle `nifra dev` serves on Bun.
7897
+ Listing `singleCopyPlugin()` in `clientPlugins` as well is harmless: dev
7898
+ runs that instance in the resolver's place instead of a second one. On Vite, the dev server and
7899
+ both Vite builds add each declared package to `resolve.dedupe`, a
7900
+ `@scope/*` pattern as every package installed under that scope. Unbundled phases do
7901
+ not: Bun's _runtime_ resolver never offers a
7221
7902
  bare specifier to a plugin, so `bun test`, `bun run`, and preloaded
7222
7903
  scripts still load both copies unless you preload the registrar:
7223
7904
 
@@ -7420,6 +8101,23 @@ which the server logs. Read the body in a hook with `c.req.bytes()`,
7420
8101
  instead: they read it once, within the route's body limit, and every reader after the hook
7421
8102
  replays the same bytes.
7422
8103
 
8104
+ ### "mixed nifra versions in this workspace"
8105
+
8106
+ Every `@nifrajs/*` package, `nifra` and `create-nifra` ship
8107
+ together at one version. `nifra doctor` (and `nifra check`, as the
8108
+ `NF-D002` advisory) reads the versions your `package.json` files pin,
8109
+ the versions installed in `node_modules` - followed to their real location, so a
8110
+ linked repository reports its own - and the running CLI. When they disagree it names each
8111
+ version and where it came from. It only reads files, and it never fails the run.
8112
+
8113
+ The fix is [`nifra upgrade <highest>`](/docs/cli#upgrade), not a
8114
+ hand edit of the pins: it runs
8115
+ every release's migration steps between your oldest pin and the target, then re-pins. Run it
8116
+ without `--write` first to see each planned edit. It edits files under the
8117
+ workspace root only. A symlinked package directory or `package.json` is never
8118
+ followed, and a group package installed by a link from outside the root is listed under
8119
+ "Left alone" - upgrade that repository on its own.
8120
+
7423
8121
  ### Still stuck?
7424
8122
 
7425
8123
  Run `nifra check --json` as the done-gate - it surfaces the import-chain leaks,
@@ -7429,13 +8127,14 @@ package ships a tight `LLM.md` contract card.
7429
8127
 
7430
8128
  ## Nifra - Backends & API (dev and prod)
7431
8129
 
7432
- > How a Nifra backend reaches your app: inProcessClient feeds ctx.api to loaders, and createWebApp auto-mounts the backend over HTTP at /api/* - in `nifra dev` and production alike, no hand-dispatch.
8130
+ > How a Nifra backend reaches your app: inProcessClient feeds ctx.api to loaders, and createWebApp serves the backend over HTTP at the apiPrefix you set - in `nifra dev` and production alike, no hand-dispatch.
7433
8131
 
7434
8132
  Your `@nifrajs/core` backend reaches a Nifra app two ways from a single wiring.
7435
8133
  `inProcessClient(backend)` is fed to every loader and action as
7436
- `ctx.api` - and `createWebApp` now **auto-mounts** that
7437
- backend over HTTP at `/api/*`, so the browser can call the very same routes. No
7438
- hand-written `if (pathname.startsWith("/api/"))` branch in your server entry.
8134
+ `ctx.api` - and, given an `apiPrefix`, `createWebApp`
8135
+ **mounts** that backend over HTTP there, so the browser can call the very same
8136
+ routes. No hand-written `if (pathname.startsWith("/api/"))` branch in your server
8137
+ entry.
7439
8138
 
7440
8139
  ### One backend, two call paths
7441
8140
 
@@ -7475,20 +8174,19 @@ export async function loader({ api }: Route.LoaderArgs) {
7475
8174
  }
7476
8175
  ```
7477
8176
 
7478
- ### The auto-mounted /api/* (the new part)
8177
+ ### The mounted /api/*
7479
8178
 
7480
- Before, `inProcessClient` fed `ctx.api` but did _not_ serve the
7481
- backend over HTTP - so a browser `POST /api/sync` hit the page router and 404/405'd
7482
- until you hand-wrote a dispatch branch in `server-bun.ts`. Now
7483
- `createWebApp` mounts it for you: a request whose pathname is exactly
7484
- `apiPrefix` (default `/api`) or starts with
8179
+ Without an `apiPrefix`, `inProcessClient` feeds `ctx.api` but
8180
+ the backend is _not_ served over HTTP, so a browser `POST /api/sync` reaches
8181
+ the page router. Set `apiPrefix: "/api"` and `createWebApp` mounts it: a
8182
+ request whose pathname is exactly `apiPrefix` or starts with
7485
8183
  `apiPrefix + "/"` is dispatched through Nifra's platform-aware backend mount
7486
8184
  interface **before** page routing. The backend receives the same Workers
7487
8185
  `env` bindings and `waitUntil` lifetime as the web app, and its
7488
8186
  `Response` is returned untouched (the request body is passed through, never pre-read).
7489
8187
 
7490
8188
  ```ts
7491
- // backend/server.ts - a hand-written server: createWebApp serves pages AND auto-mounts the
8189
+ // backend/server.ts - a hand-written server: createWebApp serves pages AND mounts the
7492
8190
  // backend at /api/*. (`nifra build` emits this wiring for you; write it only for a custom host.)
7493
8191
  import { inProcessClient } from "@nifrajs/client"
7494
8192
  import { createWebApp } from "@nifrajs/web"
@@ -7500,8 +8198,8 @@ export const app = createWebApp({
7500
8198
  adapter: reactAdapter,
7501
8199
  manifest,
7502
8200
  clientEntry,
7503
- api: inProcessClient(backend), // → ctx.api in loaders/actions AND auto-mounted at /api/*
7504
- // apiPrefix: "/api", // the default; pass "" to disable the HTTP mount (pages only)
8201
+ api: inProcessClient(backend), // → ctx.api in loaders/actions
8202
+ apiPrefix: "/api", // AND served over HTTP at /api/*; without it the backend is loader-only
7505
8203
  })
7506
8204
 
7507
8205
  // Bun: Bun.serve({ fetch: app.fetch }). No `if (pathname.startsWith("/api/")) …` branch needed -
@@ -7513,8 +8211,23 @@ for **every method** - so `GET`/`POST`/`PUT`/… all
7513
8211
  reach the backend, and an unknown `/api/…` path returns the _backend's_ 404,
7514
8212
  not the page's. A sibling path that merely shares the prefix string (e.g.
7515
8213
  `/apidocs`) is **not** captured - only the `/api` boundary
7516
- is. Pass `apiPrefix: ""` to turn the mount off and keep `ctx.api` as a
7517
- loader-only client.
8214
+ is. Leave `apiPrefix` unset, or set it to `""`, to keep
8215
+ `ctx.api` as a loader-only client.
8216
+
8217
+ Loader-only is the default because an app that also serves the same backend behind its own
8218
+ server - with that server's rate limits and guards - would otherwise get a second path to it
8219
+ that skips them. `nifra check` reports `NF-C035` for a
8220
+ `backend/framework.ts` that sets no `apiPrefix`, so the choice is
8221
+ written down: `nifra fix --code NF-C035` adds
8222
+ `export const apiPrefix = "/api"`, which is what 4.x mounted by default, and
8223
+ `""` keeps the backend loader-only.
8224
+
8225
+ With both files, `nifra dev` and `nifra build` take
8226
+ `apiPrefix`, `apiStrip`, `mounts`, `csp`,
8227
+ `nonce`, `serverComponents` and `use` from
8228
+ `backend/framework.ts` when `nifra.config.ts` does not set them, so the
8229
+ server entry and the dev server read one value without a re-export. A hand-written
8230
+ `createWebApp` call passes `apiPrefix` itself.
7518
8231
 
7519
8232
  Because the backend answers first and its 404 is final, a page file under the prefix (
7520
8233
  `routes/api/report.tsx`) could never render. Nifra refuses it instead of serving a
@@ -7549,8 +8262,8 @@ export const use = (app: { get(path: string, handler: () => Response): unknown }
7549
8262
 
7550
8263
  [!NOTE] The mount lives in `createWebApp`, and `nifra dev` (the
7551
8264
  Vite-backed dev server) routes every request through that same app's `fetch`. So
7552
- the `/api/*` routes are served identically in development and production - there is
7553
- nothing extra to wire for the dev loop.
8265
+ the mounted routes are served identically in development and production - there is nothing
8266
+ extra to wire for the dev loop.
7554
8267
 
7555
8268
  ### Events that survive the transport
7556
8269
 
@@ -7650,6 +8363,7 @@ const agents = server().get("/api/agents", (c) => ({ ok: new URL(c.req.url).path
7650
8363
 
7651
8364
  // merge() folds both groups into ONE flat path space at their defined paths; a path+method clash
7652
8365
  // throws a RouteConfigError right here at merge time (fail-closed), never a silent shadow at runtime.
8366
+ // A group holding .ws() routes or mount()s is refused the same way: add those on the parent.
7653
8367
  export const backend = server().merge(listings).merge(agents)
7654
8368
  ```
7655
8369
 
@@ -7902,11 +8616,12 @@ than something we vouch for.
7902
8616
  {
7903
8617
  "schemaVersion": 1,
7904
8618
  "ok": true,
7905
- "profile": { "id": "cache-store", "version": 1 },
8619
+ "profile": { "id": "cache-store", "version": 2 },
7906
8620
  "adapterId": "redis-cache",
7907
8621
  "capabilities": [
7908
- { "capability": "read-write", "status": "passed", "checks": ["set-get", "delete"] },
7909
- { "capability": "tag-invalidation", "status": "passed", "checks": ["invalidate-tag"] }
8622
+ { "capability": "roundtrip", "status": "passed", "checks": ["set-get-delete"] },
8623
+ { "capability": "tag-invalidation", "status": "passed", "checks": ["invalidate-tag"] },
8624
+ { "capability": "clear", "status": "passed", "checks": ["clear"] }
7910
8625
  ],
7911
8626
  "evidenceHash": "9f2c..."
7912
8627
  }
@@ -7972,11 +8687,13 @@ A route is two files. The page (`x.tsx`) holds the view and its head:
7972
8687
  // own backend, so it can't compile standalone (see the typed examples elsewhere).
7973
8688
  import type { Route } from "./+types/[id]"
7974
8689
 
7975
- // Static OR a function of { data, params, origin }. Static meta is serialized once; function meta
7976
- // recomputes per request (its content can vary with loader data). `origin` is the request's
7977
- // scheme+host (server-resolved, matches the client's location.origin) - use it for ABSOLUTE
7978
- // canonical / og:url / og:image URLs without threading siteUrl through loader data.
7979
- export const meta = ({ data, params, origin }) => ({
8690
+ // Static OR a function of { data, pageData, params, origin }. Static meta is serialized once; function
8691
+ // meta recomputes per request (its content can vary with loader data). `data` is the exporting
8692
+ // module's own loader data: a layout's meta gets its layout's (null without a loader) and reads the
8693
+ // page's as `pageData`. `origin` is the request's scheme+host (server-resolved, matches the
8694
+ // client's location.origin) - use it for ABSOLUTE canonical / og:url / og:image URLs without threading
8695
+ // siteUrl through loader data.
8696
+ export const meta = ({ data, params, origin }: Route.MetaArgs) => ({
7980
8697
  title: `User ${data.name}`,
7981
8698
  meta: [{ name: "description", content: data.bio }],
7982
8699
  link: [{ rel: "canonical", href: `${origin}/users/${params.id}` }],
@@ -8228,7 +8945,9 @@ pass through, and every `<link>` attribute with a normal name -
8228
8945
 
8229
8946
  A route's `meta` emits the full social / structured-data head with three public
8230
8947
  helpers - `canonical()`, `openGraph()`, and `jsonLd()` - and
8231
- builds **absolute** URLs from the `origin` argument. Because
8948
+ builds **absolute** URLs from the `origin` argument. Import them from
8949
+ `@nifrajs/web/seo`: the entry imports nothing at runtime, so a page's browser bundle
8950
+ gets the three helpers and none of the server modules the package root also exports. Because
8232
8951
  `meta()` runs in **both** SSR and client navigation it has
8233
8952
  **no server env**: never read `process.env` or the request inside it.
8234
8953
  The framework injects `origin` (the site's scheme + host, e.g.
@@ -8240,9 +8959,11 @@ base, a CDN host) belongs in the `loader`, not `meta()`.
8240
8959
 
8241
8960
  ```ts
8242
8961
  // routes/articles/[slug].tsx - a complete SEO head: canonical + Open Graph + Twitter + JSON-LD,
8243
- // with ABSOLUTE URLs built from `origin`. The three helpers (canonical/openGraph/jsonLd) are public
8244
- // exports of @nifrajs/web; `origin` is the third MetaArgs field (after data + params).
8245
- import { canonical, jsonLd, openGraph, type MetaArgs } from "@nifrajs/web"
8962
+ // with ABSOLUTE URLs built from `origin`. The three helpers (canonical/openGraph/jsonLd) come from
8963
+ // @nifrajs/web/seo, which imports nothing at runtime, so the page's browser bundle takes only them;
8964
+ // `origin` is a MetaArgs field beside data, pageData and params.
8965
+ import type { MetaArgs } from "@nifrajs/web"
8966
+ import { canonical, jsonLd, openGraph } from "@nifrajs/web/seo"
8246
8967
 
8247
8968
  // CAVEAT: meta() runs in BOTH SSR and client navigation, so it has NO server env - never read
8248
8969
  // `process.env` or the request here. The framework injects `origin` (the site's scheme+host, e.g.
@@ -8329,6 +9050,8 @@ const page = renderPage({
8329
9050
  })
8330
9051
  ```
8331
9052
 
9053
+ #### A strict CSP that still caches
9054
+
8332
9055
  A nonce makes every document unique, so a nonce-bearing page is
8333
9056
  `private, no-store` and no shared cache - `withISR`, a CDN - may store
8334
9057
  it. Page data never needs one: it rides in an inert
@@ -8386,6 +9109,16 @@ file that reaches it.
8386
9109
  // same layout-chain head, so sitewide tags persist across navigation - no page-only flash.
8387
9110
  ```
8388
9111
 
9112
+ ### Optional server components
9113
+
9114
+ The post-4.0 [server-components pipeline](/docs/server-components) supports
9115
+ React, Vue, Svelte and Solid. Declare closed input/public-props schemas and authorization
9116
+ in `*.component.server.ts(x)`, register owner instances and export a named
9117
+ `componentPipeline` configuration. React uses native RSC/Flight; the other three
9118
+ hydrate native islands in validated server ranges. Refresh repeats owner GET admission;
9119
+ HTML/data/refresh are private/no-store. Writes use server functions or actions. Bind the
9120
+ router and await auth-scope invalidation on identity, tenant or permission changes.
9121
+
8389
9122
  ### The build-output contract
8390
9123
 
8391
9124
  The programmatic `buildClient()` API emits a content-hashed directory plus a
@@ -8554,82 +9287,475 @@ See [SSG & ISR](/docs/rendering) for prerendering, [Dev & HMR](/docs/dev) for th
8554
9287
  two dev loops and the `--port` flag, and [Loaders & actions](/docs/data) for the
8555
9288
  data layer in depth.
8556
9289
 
8557
- ## Nifra - Databases
9290
+ ## Nifra - Control APIs (one schema, every client)
8558
9291
 
8559
- > Nifra is database-agnostic: use SQLite, Postgres, or any ORM (Drizzle, Prisma, Kysely) from your handlers and loaders, on Bun, Node, Deno, and the edge.
9292
+ > Build an RPC-style control API on core routes: one schema per method gives request validation, a typed TypeScript client, and an OpenAPI 3.2 document whose SSE events let Swift, Kotlin, or Python clients be generated.
8560
9293
 
8561
- Nifra's core bundles **no database layer**. Like Hono or Elysia, it owns the HTTP boundary -
8562
- routing, validation, the typed client - and your handlers, loaders, and actions are just
8563
- functions. Import any database client or ORM and call it there. SQLite, Postgres, MySQL,
8564
- MongoDB, Drizzle, Prisma, Kysely - all work, because none of them are Nifra's concern.
9294
+ A desktop app, a CLI, or a mobile companion that drives a local service needs a control API:
9295
+ a set of methods, a stream of events, and clients in more than one language. Write each
9296
+ method once as a route with a schema. The schema validates requests, types the TypeScript
9297
+ client, and describes the method in an OpenAPI document that generators turn into clients for
9298
+ other languages.
8565
9299
 
8566
- ### Scaffold it (recommended)
9300
+ ### One file, three methods
8567
9301
 
8568
- You don't have to wire Drizzle by hand. `bun create Nifra` takes a
8569
- `--db` flag and generates a correct, production-grade data layer - schema, typed
8570
- client, `drizzle.config.ts`, the `db:generate`/`db:migrate`
8571
- scripts, and `.env.example` - plus a Database section in the app's
8572
- `AGENTS.md`, so a human _or a coding agent_ starts from the right setup instead
8573
- of inventing one.
9302
+ Two methods take a JSON body and answer JSON. The third is a typed event stream. This file
9303
+ typechecks against the current packages.
8574
9304
 
8575
9305
  ```ts
8576
- # Scaffold an app with the Drizzle layer already wired (schema, typed client, migrations, scripts, .env):
8577
- bun create nifra notes-api --db drizzle-libsql # SQLite everywhere incl. the edge (or Turso)
8578
- bun create nifra notes-api --db drizzle-postgres # Postgres (postgres.js) on Bun/Node/Deno
8579
- bun create nifra notes-api --db drizzle-sqlite # Bun's built-in bun:sqlite, local file
9306
+ import { timingSafeEqual } from "node:crypto"
9307
+ import { server } from "@nifrajs/core"
9308
+ import { streaming } from "@nifrajs/core/sse"
9309
+ import { t, toOpenAPI } from "@nifrajs/schema"
9310
+
9311
+ const token = process.env.CONTROL_TOKEN
9312
+ if (token === undefined || token.length < 32) throw new Error("CONTROL_TOKEN must be 32+ characters")
9313
+ const expected = Buffer.from(`Bearer ${token}`)
9314
+ const authorized = (request: Request): boolean => {
9315
+ const given = Buffer.from(request.headers.get("authorization") ?? "")
9316
+ return given.length === expected.length && timingSafeEqual(given, expected)
9317
+ }
9318
+
9319
+ const Session = t.object({
9320
+ id: t.string(),
9321
+ status: t.union([t.literal("idle"), t.literal("running")]),
9322
+ })
9323
+ const TurnEvent = t.object({
9324
+ seq: t.integer({ minimum: 0 }),
9325
+ type: t.string(),
9326
+ text: t.optional(t.string()),
9327
+ })
9328
+
9329
+ export const app = server()
9330
+ .use(streaming())
9331
+ .onRequest((request) =>
9332
+ authorized(request) ? undefined : Response.json({ error: "unauthorized" }, { status: 401 }),
9333
+ )
9334
+ .post(
9335
+ "/session/create",
9336
+ { body: t.object({ cwd: t.string({ minLength: 1, maxLength: 4096 }) }), response: Session },
9337
+ () => ({ id: crypto.randomUUID(), status: "idle" as const }),
9338
+ )
9339
+ .post(
9340
+ "/turn/send",
9341
+ {
9342
+ body: t.object({
9343
+ sessionId: t.string(),
9344
+ message: t.string({ minLength: 1, maxLength: 256 * 1024 }),
9345
+ }),
9346
+ response: t.object({ turnId: t.string() }),
9347
+ bodyLimit: 512 * 1024,
9348
+ },
9349
+ () => ({ turnId: crypto.randomUUID() }),
9350
+ )
9351
+ .sse(
9352
+ "/turn/events",
9353
+ { query: t.object({ turnId: t.string() }), sse: TurnEvent },
9354
+ (c, stream) => {
9355
+ const after = Number(c.req.headers.get("last-event-id") ?? -1)
9356
+ for (let seq = after + 1; seq < 3; seq++)
9357
+ stream.send({ seq, type: "assistant.delta", text: `part ${seq}` }, { id: String(seq) })
9358
+ stream.close()
9359
+ },
9360
+ )
9361
+
9362
+ export const spec = toOpenAPI(app, { openapiVersion: "3.2.0", title: "Control API", version: "1.0.0" })
8580
9363
  ```
8581
9364
 
8582
- `drizzle-libsql` is the default recommendation - it's the one SQLite client that runs
8583
- on every runtime, including the edge (a local file in dev, [Turso](https://turso.tech)
8584
- in prod).
9365
+ -
9366
+ **Methods are routes.** A method name like `session/create` is a path, so a
9367
+ call is `POST /session/create` and the typed client reads
9368
+ `api.session.create.post(body)`. Use `POST` for anything that changes
9369
+ state; one envelope endpoint that dispatches on a method field would hide every method
9370
+ from the schema, the client, and the document.
8585
9371
 
8586
- ### Inject the client once, read it as c.db
9372
+ -
9373
+ **The schema is the validator.** A body that does not match answers `422`
9374
+ before the handler runs. `maxLength` bounds each field and
9375
+ `bodyLimit` bounds the request as it arrives.
8587
9376
 
8588
- The whole integration is one seam: open the DB client once, `decorate` it onto the
8589
- server, and every handler reads it as a typed `c.db`. This is how the scaffold wires
8590
- it - and it keeps the client out of each handler's imports.
9377
+ -
9378
+ **Authorization runs first.** An `onRequest` hook that returns a response
9379
+ ends the request before routing, so no method is reachable without the token. The
9380
+ comparison is constant-time, and the server refuses to start without a long token.
9381
+
9382
+ -
9383
+ **Retries.** A method that must not run twice declares
9384
+ `idempotency` on its route, with `.use(idempotency())` from
9385
+ `@nifrajs/core/idempotency-plugin` and a namespace per caller.
9386
+
9387
+ ### Event streams that resume
9388
+
9389
+ `.sse(path, { sse: schema }, run)` declares the type of every event the stream
9390
+ sends. Give each event an `id` (here its sequence number). A browser
9391
+ `EventSource`, or the typed client's `subscribe`, sends the last id it
9392
+ saw as `Last-Event-ID` when it reconnects, so the handler replays only what the
9393
+ client missed. Keep the events a reconnect may need in memory or in your own store: core
9394
+ sends the header, and what to replay is your service's to decide.
9395
+
9396
+ ### The TypeScript client
8591
9397
 
8592
9398
  ```ts
8593
- // backend/app.ts
8594
- import { server } from "@nifrajs/core/server"
8595
- import { desc } from "drizzle-orm"
8596
- import { db, notes } from "./db" // your Drizzle client + schema (what the scaffold generates)
9399
+ // doc-check: skip - fragment: app is the control server above, imported from its module.
9400
+ import { client } from "@nifrajs/client"
8597
9401
 
8598
- // decorate() hangs the client on the context ONCE - every handler then reads it as `c.db`, fully typed.
8599
- export const backend = server()
8600
- .decorate("db", db)
8601
- .get("/notes", async (c) => c.db.select().from(notes).orderBy(desc(notes.createdAt)))
9402
+ const api = client<typeof app>("http://127.0.0.1:4100", {
9403
+ headers: { authorization: `Bearer ${token}` },
9404
+ })
8602
9405
 
8603
- export type Backend = typeof backend
9406
+ const session = await api.session.create.post({ cwd: "/work/repo" })
9407
+ if (!session.ok) throw new Error(`create failed: ${session.status}`)
9408
+
9409
+ const subscription = api.turn.events.subscribe((event) => render(event.seq, event.text ?? ""), {
9410
+ query: { turnId },
9411
+ // After a dropped connection the client reconnects and sends Last-Event-ID itself.
9412
+ })
9413
+ // subscription.close() ends it.
8604
9414
  ```
8605
9415
 
8606
- ### SQLite
9416
+ Every call is typed from the server's routes, with no generated code. A call returns a
9417
+ `Result`: check `ok`, then read `data`, or
9418
+ `status` and `error`.
8607
9419
 
8608
- `bun:sqlite` is built into Bun (zero install); use `better-sqlite3` on
8609
- Node, or `libSQL`/Turso anywhere. Open the connection once and query from your
8610
- routes:
9420
+ ### Clients in other languages
9421
+
9422
+ `toOpenAPI(app, { openapiVersion: "3.2.0" })` from `@nifrajs/schema`
9423
+ describes each method's request and response, and each typed stream as
9424
+ `text/event-stream` whose `itemSchema` is the SSE event: a
9425
+ `data` field holding JSON that matches the route's event schema, plus
9426
+ `event`, `id`, and `retry`. The event schema is also a named
9427
+ component (`TurnEventsEvent` here), so a generator emits a type for it. The
9428
+ `openapi()` plugin from `@nifrajs/middleware` takes the same option and
9429
+ serves the document.
8611
9430
 
8612
9431
  ```ts
8613
- import { server } from "@nifrajs/core/server"
8614
- import { Database } from "bun:sqlite" // Node: better-sqlite3 · edge: Turso/libSQL
9432
+ // doc-check: skip - shell commands, not TypeScript.
9433
+ // Write the document at build time, then hand it to any OpenAPI 3.2 generator.
9434
+ bun -e 'import { spec } from "./control.ts"; await Bun.write("openapi.json", JSON.stringify(spec, null, 2))'
9435
+ ```
8615
9436
 
8616
- // Open the connection ONCE at module scope (the driver pools internally).
8617
- const db = new Database("app.db")
8618
- const byId = db.query<{ id: number; name: string }, [number]>("SELECT * FROM users WHERE id = ?")
9437
+ OpenAPI 3.1 has no field for the items of a stream. Without `openapiVersion` the
9438
+ document stays 3.1, declares the stream as a string, and carries the same item schema as
9439
+ `x-itemSchema`; the named event component is there either way. A generator that
9440
+ reads only 3.1 still produces the event type, and your code decodes each
9441
+ `data` line with it.
8619
9442
 
8620
- export const app = server().get("/users/:id", (c) =>
8621
- // Parameterized (?) - never interpolate user input into SQL.
8622
- byId.get(Number(c.params.id)) ?? new Response("Not found", { status: 404 }),
8623
- )
8624
- ```
9443
+ ## Nifra - Custom server (createWebApp)
8625
9444
 
8626
- ### Postgres with Drizzle (Recommended)
9445
+ > Run a page app from your own server entry with createWebApp: static and hashed files, a strict CSP on nifra's nonce, middleware order, loader error logging, graceful shutdown, and nifra check settings.
8627
9446
 
8628
- Drizzle gives you a **fully typed schema**, auto-generated SQL migrations, and a composable query
8629
- builder. Define your schema once, run `bunx drizzle-kit generate` to create
8630
- migrations, and Nifra's loaders use fully-typed queries:
9447
+ The standard project layout gets its server entry from `nifra build`. An app that
9448
+ calls `createWebApp` from its own entry wires the same pieces by hand. Each one is an
9449
+ option or a single call.
8631
9450
 
8632
- ```ts
9451
+ ### A complete entry
9452
+
9453
+ Everything on this page in one file. The sections below explain each part. This file
9454
+ typechecks against the current packages.
9455
+
9456
+ ```ts
9457
+ import { fileURLToPath } from "node:url"
9458
+ import { createCspPolicy, createWebApp, servePublicDir } from "@nifrajs/web"
9459
+ import type { Manifest, RenderAdapter } from "@nifrajs/web"
9460
+
9461
+ // From your build: the framework adapter, the route manifest, and what buildClient() returned.
9462
+ declare const adapter: RenderAdapter
9463
+ declare const manifest: Manifest
9464
+ declare const build: {
9465
+ readonly entry: string
9466
+ readonly assets: readonly string[]
9467
+ readonly routes: Readonly<Record<string, readonly string[]>>
9468
+ readonly css?: readonly string[]
9469
+ }
9470
+ declare const errors: { capture(error: unknown, tags: Record<string, string>): void }
9471
+
9472
+ // buildClient({ outDir: "dist/assets" }) writes the chunks where their /assets/ URLs point.
9473
+ const bundle = servePublicDir({
9474
+ dir: fileURLToPath(new URL("./dist/", import.meta.url)),
9475
+ hashedFiles: new Set(build.assets),
9476
+ })
9477
+ const publicFiles = servePublicDir({ dir: fileURLToPath(new URL("./public/", import.meta.url)) })
9478
+
9479
+ const csp = createCspPolicy({
9480
+ header: ({ sources }) => `default-src 'self'; script-src 'self' ${sources}; object-src 'none'`,
9481
+ })
9482
+
9483
+ export const app = createWebApp({
9484
+ adapter,
9485
+ manifest,
9486
+ clientEntry: build.entry,
9487
+ routePreload: build.routes,
9488
+ styles: build.css ?? [],
9489
+ csp,
9490
+ server: { requestTimeoutMs: 15_000, gracefulSignals: { drainMs: 8_000 } },
9491
+ use: (app) => {
9492
+ app.onRequest(async (request) => (await bundle(request)) ?? (await publicFiles(request)))
9493
+ },
9494
+ onLoaderError: (error, { route }) => errors.capture(error, { route }),
9495
+ })
9496
+
9497
+ app.listen(3000)
9498
+ ```
9499
+
9500
+ ### Static and hashed files
9501
+
9502
+ `servePublicDir` from `@nifrajs/web` serves a directory on Bun, Node and
9503
+ Deno. It answers `GET` and `HEAD` only, refuses paths that leave the
9504
+ directory or name a hidden file, sets the content type and `nosniff`, handles range
9505
+ requests, and returns `undefined` on a miss so the request falls through to your
9506
+ routes.
9507
+
9508
+ -
9509
+ **Hashed build output.** Pass `buildClient()`'s `assets` as
9510
+ `hashedFiles`: those files get `immutable` for a year, and anything
9511
+ else in the directory gets one day. Write the build to `outDir: "dist/assets"` so
9512
+ a chunk's `/assets/...` URL is also its path under `dist/`.
9513
+
9514
+ -
9515
+ **Your own files.** A second handler on `public/` serves them at the root,
9516
+ with the one-day policy.
9517
+
9518
+ -
9519
+ **Where it runs.** Register the handlers with `app.onRequest` inside
9520
+ `use`, so a static hit never reaches page rendering.
9521
+
9522
+ On Node, `serve()` from `@nifrajs/node` can serve the bundle itself before
9523
+ the app runs, which keeps the direct request path for everything else:
9524
+
9525
+ ```ts
9526
+ // doc-check: skip - fragment: app and build are the createWebApp app and buildClient() output above.
9527
+ import { serve } from "@nifrajs/node"
9528
+
9529
+ await serve(app, {
9530
+ port: 3000,
9531
+ static: { dir: new URL("./dist/assets/", import.meta.url), hashedFiles: build.assets },
9532
+ signals: { drainMs: 8_000 },
9533
+ })
9534
+ ```
9535
+
9536
+ ### A strict CSP on nifra's nonce
9537
+
9538
+ Pass `createCspPolicy` as `csp` and nifra writes the
9539
+ `Content-Security-Policy` header for each document. You do not stash a nonce or set
9540
+ the header yourself. Your `header` function receives `sources`: a hash
9541
+ for each constant inline script nifra writes, plus a nonce only when the document carries a
9542
+ request-specific script (a `defer()`ed value, or `unsafeInlineScript` in
9543
+ `meta`).
9544
+
9545
+ A document without a nonce gets the same header on every request, so it can still be cached by
9546
+ `withISR` or a CDN. A document with a nonce is sent `private, no-store`.
9547
+ The older `nonce` option puts a nonce on every document, so nothing is cacheable.
9548
+ `createWebApp` refuses `csp` and `nonce` together.
9549
+
9550
+ ### Middleware goes in use
9551
+
9552
+ `createWebApp` declares every page route, and the `/*` catch-all, before
9553
+ it returns. `beforeHandle`, `afterHandle`, `around`,
9554
+ `derive`, `decorate` and `onError` are copied into each route
9555
+ when the route is declared. One of those hooks added to the returned app would therefore reach
9556
+ no page. `requestId()` is a `derive`: added late, no page would see
9557
+ `c.requestId`. Outside production, adding one throws a `FrameworkError`
9558
+ with code `HOOK_AFTER_PAGES` that names the call site. Hooks added inside
9559
+ `app.group(prefix, ...)` are still allowed, because a group is its own scope.
9560
+
9561
+ Apply middleware in the `use` option instead. It runs before any page is declared,
9562
+ and ahead of `mounts` and the `api` mount, so an `onRequest`
9563
+ guard also covers a mounted auth handler:
9564
+
9565
+ ```ts
9566
+ // doc-check: skip - fragment: the use option of the createWebApp call above; @nifrajs/middleware is not installed in the docs sandbox.
9567
+ import { requestId, securityHeaders } from "@nifrajs/middleware"
9568
+
9569
+ createWebApp({
9570
+ // ...
9571
+ use: (app) => {
9572
+ app.use(securityHeaders())
9573
+ app.use(requestId())
9574
+ },
9575
+ })
9576
+ ```
9577
+
9578
+ `onRequest` and `onResponse` are read per request, so those two still
9579
+ work when added late. Putting everything in `use` means you never need to remember
9580
+ which hooks those are.
9581
+
9582
+ A plugin applied in `use` works at runtime but cannot add to the returned app's type.
9583
+ To get `c.requestId` typed on routes you declare afterwards, call
9584
+ `app.use(requestId())` again on the returned app. Named plugins apply once.
9585
+
9586
+ ### Loader errors
9587
+
9588
+ When a loader or action throws and an `_error` boundary renders the page, the error
9589
+ is written to the server's `logger`, at the detail set by
9590
+ `server.errorLogDetail`. That is the same log line an unhandled 500 produces, and you
9591
+ do not need to configure anything for it. A soft-navigation data request that fails is logged
9592
+ the same way. A thrown `Response`, such as `redirect()`, is not an
9593
+ error and is not logged.
9594
+
9595
+ `onLoaderError` is for an error reporter. It sees every real throw with the request,
9596
+ params and route id before the boundary renders. If it throws, the error is ignored so rendering
9597
+ continues.
9598
+
9599
+ ### Graceful shutdown
9600
+
9601
+ On Bun, `server: { gracefulSignals: { drainMs } }` makes `listen()`
9602
+ handle `SIGTERM` and `SIGINT`. The server gives in-flight requests up
9603
+ to `drainMs` (default 10 000) to finish, then stops, closing any that are still
9604
+ running. On Bun, new connections can still arrive during that window, so the load balancer
9605
+ should stop routing to the instance first, as most platforms do before they send
9606
+ `SIGTERM`. Keep `drainMs` under your platform's kill grace period:
9607
+ `docker stop` waits 10 seconds. `true` uses the default. On Node and
9608
+ Deno, pass
9609
+ `signals: { drainMs }` to `serve()` instead, as above.
9610
+
9611
+ ### Letting check and assure read the app
9612
+
9613
+ `nifra check`, `assure`, `routes` and the other commands that
9614
+ read the app import it, so everything the app does at module scope runs. A command that only
9615
+ reads what the app declares sets `NIFRA_REFLECT` to its own name first:
9616
+ `check`, `routes`, `context`, `openapi`,
9617
+ `contracts`, `capabilities`, `manifest`, `doctor`
9618
+ , `diff`, `sync-routes`, `sync-manifest`, and
9619
+ `assure` without `--bundle`, `--strict`,
9620
+ `--hydration`, `--interact` or `--out`. Commands that send the
9621
+ app requests, such as `smoke`, `prove`, `levels` and the
9622
+ `assure` bundle, leave it unset. Check it to skip work a read never needs:
9623
+
9624
+ ```ts
9625
+ // doc-check: skip - fragment: an app module's Redis client; ioredis is not installed in the docs sandbox.
9626
+ import Redis from "ioredis"
9627
+
9628
+ // Unset when the app really runs; "check", "routes", "assure", ... while nifra only reads it.
9629
+ export const redis = process.env.NIFRA_REFLECT ? undefined : new Redis(process.env.REDIS_URL!)
9630
+ ```
9631
+
9632
+ -
9633
+ **Environment validation.** An app that validates its environment on import still needs
9634
+ values to pass it. Supply them with `--env-file`, for example
9635
+ `nifra check --env-file .env.example`. Variables already set in the shell win.
9636
+
9637
+ -
9638
+ **A process that will not exit.** If something the app opened is still keeping the process
9639
+ alive 2 seconds after the command has answered, the command exits with its own result and
9640
+ says that the app kept it open.
9641
+
9642
+ ### nifra check for a custom entry
9643
+
9644
+ -
9645
+ **Routes nifra cannot see.** A mounted handler such as better-auth answers paths that are
9646
+ not in your backend, and no typed client covers them. Declare the mount opaque,
9647
+ `
9648
+ mounts: [{ path: "/api/auth", app: auth, opaque: "better-auth" }]
9649
+ `
9650
+ , and when `nifra.assurance.ts` names this app as its `source`, the
9651
+ check skips a `fetch` into that prefix by itself. Without that, list the prefixes
9652
+ in `nifra.check.json` next to `package.json`:
9653
+
9654
+ ```ts
9655
+ {
9656
+ "externalMounts": ["/api/auth"]
9657
+ }
9658
+ ```
9659
+
9660
+ -
9661
+ **One call nifra cannot type.** For a single call rather than a whole prefix, such as a
9662
+ static file, put `// nifra-expect NF-C002: ` on the line or in the
9663
+ comment block above it. The finding stays in the report as info, with your reason. A comment
9664
+ with no reason silences nothing.
9665
+
9666
+ -
9667
+ **Two repositories, one runtime.** When a linked sibling repository brings its own install
9668
+ of nifra or React, `nifra check` reports the duplicates. A reinstall in this app
9669
+ cannot remove a copy that another project installed. Declare the packages single-copy and
9670
+ nifra loads this app's copy for every importer:
9671
+
9672
+ ```ts
9673
+ {
9674
+ "name": "my-app",
9675
+ "nifra": { "singleCopy": ["@nifrajs/*", "react", "react-dom"] }
9676
+ }
9677
+ ```
9678
+
9679
+ Packages duplicated the same way are reported as one finding with one combined
9680
+ `singleCopy` list. The preload this needs, and the version-skew case it cannot
9681
+ cover, are in [Troubleshooting](/docs/troubleshooting).
9682
+
9683
+ ## Nifra - Databases
9684
+
9685
+ > Nifra is database-agnostic: use SQLite, Postgres, or any ORM (Drizzle, Prisma, Kysely) from your handlers and loaders, on Bun, Node, Deno, and the edge.
9686
+
9687
+ Nifra's core bundles **no database layer**. Like Hono or Elysia, it owns the HTTP boundary -
9688
+ routing, validation, the typed client - and your handlers, loaders, and actions are just
9689
+ functions. Import any database client or ORM and call it there. SQLite, Postgres, MySQL,
9690
+ MongoDB, Drizzle, Prisma, Kysely - all work, because none of them are Nifra's concern.
9691
+
9692
+ ### Scaffold it (recommended)
9693
+
9694
+ You don't have to wire Drizzle by hand. `bun create Nifra` takes a
9695
+ `--db` flag and generates a correct, production-grade data layer - schema, typed
9696
+ client, `drizzle.config.ts`, the `db:generate`/`db:migrate`
9697
+ scripts, and `.env.example` - plus a Database section in the app's
9698
+ `AGENTS.md`, so a human _or a coding agent_ starts from the right setup instead
9699
+ of inventing one.
9700
+
9701
+ ```ts
9702
+ # Scaffold an app with the Drizzle layer already wired (schema, typed client, migrations, scripts, .env):
9703
+ bun create nifra notes-api --db drizzle-libsql # SQLite everywhere incl. the edge (or Turso)
9704
+ bun create nifra notes-api --db drizzle-postgres # Postgres (postgres.js) on Bun/Node/Deno
9705
+ bun create nifra notes-api --db drizzle-sqlite # Bun's built-in bun:sqlite, local file
9706
+ ```
9707
+
9708
+ `drizzle-libsql` is the default recommendation - it's the one SQLite client that runs
9709
+ on every runtime, including the edge (a local file in dev, [Turso](https://turso.tech)
9710
+ in prod).
9711
+
9712
+ ### Inject the client once, read it as c.db
9713
+
9714
+ The whole integration is one seam: open the DB client once, `decorate` it onto the
9715
+ server, and every handler reads it as a typed `c.db`. This is how the scaffold wires
9716
+ it - and it keeps the client out of each handler's imports.
9717
+
9718
+ ```ts
9719
+ // backend/app.ts
9720
+ import { server } from "@nifrajs/core/server"
9721
+ import { desc } from "drizzle-orm"
9722
+ import { db, notes } from "./db" // your Drizzle client + schema (what the scaffold generates)
9723
+
9724
+ // decorate() hangs the client on the context ONCE - every handler then reads it as `c.db`, fully typed.
9725
+ export const backend = server()
9726
+ .decorate("db", db)
9727
+ .get("/notes", async (c) => c.db.select().from(notes).orderBy(desc(notes.createdAt)))
9728
+
9729
+ export type Backend = typeof backend
9730
+ ```
9731
+
9732
+ ### SQLite
9733
+
9734
+ `bun:sqlite` is built into Bun (zero install); use `better-sqlite3` on
9735
+ Node, or `libSQL`/Turso anywhere. Open the connection once and query from your
9736
+ routes:
9737
+
9738
+ ```ts
9739
+ import { server } from "@nifrajs/core/server"
9740
+ import { Database } from "bun:sqlite" // Node: better-sqlite3 · edge: Turso/libSQL
9741
+
9742
+ // Open the connection ONCE at module scope (the driver pools internally).
9743
+ const db = new Database("app.db")
9744
+ const byId = db.query<{ id: number; name: string }, [number]>("SELECT * FROM users WHERE id = ?")
9745
+
9746
+ export const app = server().get("/users/:id", (c) =>
9747
+ // Parameterized (?) - never interpolate user input into SQL.
9748
+ byId.get(Number(c.params.id)) ?? new Response("Not found", { status: 404 }),
9749
+ )
9750
+ ```
9751
+
9752
+ ### Postgres with Drizzle (Recommended)
9753
+
9754
+ Drizzle gives you a **fully typed schema**, auto-generated SQL migrations, and a composable query
9755
+ builder. Define your schema once, run `bunx drizzle-kit generate` to create
9756
+ migrations, and Nifra's loaders use fully-typed queries:
9757
+
9758
+ ```ts
8633
9759
  // backend/db/schema.ts - define your schema (source of truth)
8634
9760
  import { pgTable, serial, text, timestamp } from "drizzle-orm/pg-core"
8635
9761
  export const users = pgTable("users", {
@@ -8929,7 +10055,11 @@ Route data failed the output guard that stands between loaders/actions and the b
8929
10055
 
8930
10056
  **Fix:** declare `loaderOutput`/`actionOutput` as a Standard
8931
10057
  Schema that describes exactly the fields the page needs. `nifra check` flags routes
8932
- without one (NF-C030).
10058
+ without one (NF-C030): an error when the loader's own body returns a literal (an object,
10059
+ array, string, number or boolean), since every request that reaches that return fails, and a
10060
+ warning when only running it shows what it returns. `nifra schemas --write` drafts
10061
+ a schema from the loader's return type for review, and
10062
+ `nifra fix --code NF-C030` writes that draft for each flagged route.
8933
10063
 
8934
10064
  ### Rendering and data
8935
10065
 
@@ -8992,6 +10122,26 @@ serving the old page until its freshness runs out. The revalidate endpoint answe
8992
10122
  permission for this zone, project or service; a 5xx or `network_error` is retried on
8993
10123
  its own.
8994
10124
 
10125
+ ### Silencing a nifra check finding
10126
+
10127
+ `nifra check` names each finding with an `NF-` code. Where a finding is
10128
+ reviewed and intended, a comment on the flagged line, or anywhere in the comment block directly
10129
+ above it, says so and why:
10130
+
10131
+ The comment answers only the code it names. A finding that fails the check, and every security
10132
+ finding, is silenced only with a reason after the colon; an advisory also accepts the comment
10133
+ without one. A blank line or a line of code ends the block, so a comment never reaches the next
10134
+ statement. A silenced security finding, interpolated SQL (NF-C006) included, stays in the report
10135
+ as information, with the comment and its reason as evidence. Some codes also answer to a name: `sql-dynamic` (NF-C006),
10136
+ `raw-response` (NF-C007), `reserved-segment` (NF-C018),
10137
+ `route-overlap` (NF-C024 and NF-C025) and `param-modifier` (NF-C026).
10138
+ `@nifra-gate-reviewed` silences nothing; a finding under it names the comment to
10139
+ write instead.
10140
+
10141
+ A fail-open gate (NF-S001) or a non-constant-time comparison (NF-S002) in code that only
10142
+ ever runs in a browser is a warning rather than an error: the server's own gate decides
10143
+ there.
10144
+
8995
10145
  ## Nifra - Failure laboratory
8996
10146
 
8997
10147
  > Replayable durable-failure schedules on virtual time: crash after commit, duplicate delivery, reordering, budget expiry, and checkpoint contention.
@@ -9511,6 +10661,146 @@ If the project has `nifra.assurance.ts`, run `nifra assure --json`.
9511
10661
  Exercise every redirect path - loader, action, and layout gate - and, if the app proxies,
9512
10662
  each deploy adapter it uses.
9513
10663
 
10664
+ ## Nifra 5.0 migration guide - upgrade from 3.x or 4.x
10665
+
10666
+ > Upgrade a Nifra 3.x or 4.x application to 5.0: the chained upgrade command, the frontend/backend layout move and its follow-ups, drafted output schemas, the backend's HTTP mount, the 5.0 defaults that changed, and the release gates.
10667
+
10668
+ Nifra 4.0 split every app into a frontend and a backend half, and 5.0 enforces the split and
10669
+ changes a handful of defaults. The upgrade command pins the packages and moves imports; the
10670
+ layout migration moves files; this guide covers what each leaves to you and how to tell a
10671
+ finished migration from one that needs a decision.
10672
+
10673
+ ### 1. Run the executable upgrade
10674
+
10675
+ ```ts
10676
+
10677
+ ```
10678
+
10679
+ Run the command from the release you are moving to. It chains every release recipe between
10680
+ the installed version and 5.0, so a 3.x app takes the 4.0 steps (the retired
10681
+ `@nifrajs/web/server-only` import moves to `@nifrajs/web/backend-only`)
10682
+ and the 5.0 ones in one pass, and prints each release's notes. It lists every
10683
+ `@nifra-gate-reviewed` comment, which silences nothing in 5.0: after review, write
10684
+ `// nifra-expect : ` with the code `nifra check`
10685
+ reports there. The upgrade never writes a reason for you.
10686
+
10687
+ ### 2. Move onto the frontend/backend layout (from 3.x)
10688
+
10689
+ ```ts
10690
+
10691
+ ```
10692
+
10693
+ Each route file splits into `x.tsx`, the page, and `x.backend.ts`, which
10694
+ holds `loader`, `action`, `middleware` and the other server
10695
+ exports. The root `backend.ts` and `framework.ts` move to
10696
+ `backend/app.ts` and `backend/framework.ts`, and every other module goes
10697
+ to `frontend/`, `backend/` or `shared/` by who imports it at
10698
+ runtime. A folder whose imported files all land in one zone moves whole, so a component nothing
10699
+ imports yet travels with its folder; a folder split between zones is reported instead.
10700
+
10701
+ ```ts
10702
+
10703
+ ```
10704
+
10705
+ To keep a folder where it is, declare it a zone before migrating. The migration leaves zoned
10706
+ folders in place, and builds, `nifra dev` and `nifra check` enforce them
10707
+ like the default folders.
10708
+
10709
+ ```ts
10710
+
10711
+ ```
10712
+
10713
+ `nifra migrate layout --zones` writes those entries for you: every module the
10714
+ layout would move stays where it is, and `package.json` lists the widest folder
10715
+ whose files all belong to one side, or the file itself where its folder holds both. Route
10716
+ files are still split and the root entries still move. A path nested in another's folder
10717
+ overrides it, so a backend file can sit in a frontend folder without moving.
10718
+
10719
+ The command exits `0` when nothing needs a decision. A step with a known command,
10720
+ such as drafting an output schema for a backend half it did not write, is listed under
10721
+ `next` and in `followUps` with `--json`, and does not fail it.
10722
+ An issue (an import it could not place, a frontend that still uses a moved name at runtime)
10723
+ exits `1`.
10724
+
10725
+ 5.0 refuses a route file that still exports a server name: `nifra build` and the
10726
+ server stop on it, and `nifra check` reports it as NF-C041.
10727
+ `nifra fix --code NF-C041` moves the export and the code only it uses into the
10728
+ route's backend half.
10729
+
10730
+ ### 3. Draft and review output schemas
10731
+
10732
+ A loader or action that returns data needs `loaderOutput` or
10733
+ `actionOutput`; the data is projected through it, so undeclared keys never reach
10734
+ the page. `nifra schemas --write` drafts each missing one from the function's return
10735
+ type, leaves out credential-like fields and marks it `nifra:generated` so later runs
10736
+ keep it current. A value typed `any` or `unknown` drafts as
10737
+ `t.json()`, which admits anything: `nifra schemas` lists those paths and
10738
+ `nifra check` warns on them (NF-C030). Give the value a type and draft again.
10739
+ `nifra schemas --check` fails on a missing, stale or open schema, for CI.
10740
+
10741
+ ### 4. Choose where the backend is served
10742
+
10743
+ 3.x and 4.x served `backend/app.ts` at `/api` by default. In 5.0
10744
+ `createWebApp` serves it over HTTP only at an `apiPrefix` the app sets;
10745
+ without one the backend is loader-only, reached in-process through `ctx.api`, and a
10746
+ browser call to `/api` reaches page routing. `nifra migrate layout` adds
10747
+ `apiPrefix = "/api"` when it moves a root `backend.ts`, and
10748
+ `nifra check` reports a framework config that sets none (NF-C035).
10749
+
10750
+ ```ts
10751
+
10752
+ ```
10753
+
10754
+ With both `nifra.config.ts` and `backend/framework.ts`, every command
10755
+ takes `apiPrefix`, `apiStrip`, `mounts`, `csp`,
10756
+ `nonce`, `serverComponents` and `use` from
10757
+ `backend/framework.ts` when `nifra.config.ts` leaves them unset. The
10758
+ backend sees each request's full path unless `apiStrip` is on, so its routes are
10759
+ registered under the prefix; `nifra routes` marks one outside it as in-process
10760
+ only, and `nifra dev` and `nifra build` warn about it.
10761
+
10762
+ ### 5. Review the 5.0 defaults that changed
10763
+
10764
+ -
10765
+ Root status pages (`_404.tsx`, `_<status>.tsx`) and
10766
+ `_error.tsx` render inside the layouts above them. One that imports the root
10767
+ layout to wrap itself renders it twice (NF-C034); remove the wrap, or export
10768
+ `inLayouts = false`.
10769
+
10770
+ -
10771
+ A layout's `meta` receives its own loader data as `data` and the page's
10772
+ as `pageData` (NF-C040).
10773
+
10774
+ -
10775
+ A WebSocket handshake runs the route chain an HTTP `GET` at the same point runs, so
10776
+ guards added before `app.ws()` apply to it.
10777
+
10778
+ -
10779
+ A `Response` returned to more than one request is no longer shared; build one per
10780
+ request.
10781
+
10782
+ -
10783
+ `nifra check` fails a secret comparison (NF-S002) outside browser code of a web
10784
+ app.
10785
+
10786
+ `nifra upgrade` prints the full list with the finding or the one-line change that
10787
+ keeps the 4.x behavior for each.
10788
+
10789
+ ### 6. Run the release gates
10790
+
10791
+ -
10792
+ Run `nifra check --json` and fix every error. `nifra doctor` lists any
10793
+ package still on another release.
10794
+
10795
+ -
10796
+ If the project has `nifra.assurance.ts`, run `nifra assure --json`.
10797
+
10798
+ - Run the application test suite and a production build.
10799
+
10800
+ -
10801
+ Exercise the pages that load or post data, and every browser call to the backend, under
10802
+ `nifra dev` and against the production build.
10803
+
9514
10804
  ## Nifra - Migrating from Express, Hono, Fastify & Elysia
9515
10805
 
9516
10806
  > Move a backend to Nifra: routing, middleware, validation, and body parsing map directly from Express, Hono, Fastify, and Elysia - and you gain a typed client, multi-runtime deploy, and optional SSR.
@@ -10431,6 +11721,80 @@ in its `package.json`:
10431
11721
  `{'"nifra": { "environment": "frontend" | "backend" | "shared" | "library" }'}`.
10432
11722
  A file in no zone is a build error, so nothing is ever classified by accident.
10433
11723
 
11724
+ A package whose entries run on different sides gives each export subpath its own:
11725
+
11726
+ ```ts
11727
+ {
11728
+ "name": "@shop/payments",
11729
+ "exports": {
11730
+ ".": "./src/index.ts",
11731
+ "./react": "./src/react.tsx",
11732
+ "./schema": "./src/schema.ts"
11733
+ },
11734
+ "nifra": {
11735
+ "environment": {
11736
+ ".": "backend",
11737
+ "./react": "frontend",
11738
+ "./schema": "library"
11739
+ }
11740
+ }
11741
+ }
11742
+ ```
11743
+
11744
+ An import is judged by the entry it names: the exact subpath first, then the most specific
11745
+ `*` pattern. Here a page may import `@shop/payments/react` but not
11746
+ `@shop/payments`, and backend code may not import the React entry, even when two
11747
+ entries are backed by one file. Importing an entry the map leaves out is an error. A file
11748
+ reached by path rather than by entry, through a relative or aliased deep import, takes the
11749
+ side of every mapped entry whose own imports reach it, and a file no mapped entry reaches is
11750
+ an error.
11751
+
11752
+ The app's own `package.json` can set sides too, so a folder that has to stay
11753
+ where it is, or a family of workspace packages, needs no edit of its own:
11754
+
11755
+ ```ts
11756
+ {
11757
+ "name": "my-app",
11758
+ "nifra": {
11759
+ "zones": {
11760
+ "packages": { "@shop/*": "library", "@shop/db": "backend" },
11761
+ "backend": ["migrations", "src/ui/mailer.ts"],
11762
+ "frontend": ["src/ui"]
11763
+ }
11764
+ }
11765
+ }
11766
+ ```
11767
+
11768
+ `packages` gives a side to a package that does not declare one, by exact name or
11769
+ by a prefix ending in `*`; a package's own declaration always wins.
11770
+ `backend`, `frontend` and `shared` list app folders, or
11771
+ single files, that count as that zone. A path inside another's folder overrides it, so the
11772
+ most specific entry decides: above, `src/ui/mailer.ts` is backend code in a
11773
+ frontend folder. One path listed under two sides is refused. Builds, dev servers,
11774
+ `nifra check` and the editor plugin all read this setting.
11775
+
11776
+ A project that serves no pages has no browser, so the zones do not apply to it. Declare that
11777
+ in its `package.json` with `{'"nifra": { "kind": "api" }'}`:
11778
+ `nifra check` then skips the zone checks and the server-only import check there,
11779
+ and treats every file as server code. It is read from the nearest `package.json`
11780
+ and only takes values `"api"` and `"web"`. A directory with web config
11781
+ (`nifra.config.ts` exporting `adapter`, or
11782
+ `backend/framework.ts`) keeps its zone checks under it, and
11783
+ `nifra check` reports the clash (NF-C037); builds and dev servers never read it.
11784
+
11785
+ A directory with `backend/app.ts` and no web config is read as an API-only app
11786
+ whether or not it declares the kind: `nifra routes`, `nifra context`,
11787
+ `nifra openapi` and `nifra mcp` load `backend/app.ts` alone,
11788
+ and the tools, resources and prompts its backend declares reach `nifra mcp`.
11789
+
11790
+ Those commands, the contract lock and the route lints see only what
11791
+ `backend/app.ts` exports. An API whose app is built by a factory such as
11792
+ `createApp(deps)`, with its real dependencies wired in another entry, exports the
11793
+ same app from `backend/app.ts` built with stand-in dependencies, so every operation
11794
+ is visible without a database or secrets; `nifra check` names an implemented
11795
+ operation it does not serve (NF-C039). See
11796
+ [the stand-in entry](/docs/types-first#stub-entry).
11797
+
10434
11798
  ### A route is two files
10435
11799
 
10436
11800
  The page renders; its backend half loads. A server-only export (`loader`,
@@ -10461,7 +11825,8 @@ import { notFound } from "@nifrajs/web"
10461
11825
  import type { Route } from "./+types/[id]"
10462
11826
 
10463
11827
  // What the page receives - and the ONLY fields that leave the server. A field the loader returns
10464
- // but the schema does not declare is dropped before rendering.
11828
+ // but the schema does not declare is dropped before rendering; in development the console names
11829
+ // each dropped field, so a missing declaration never shows up as a silently blank page.
10465
11830
  export const loaderOutput = t.object({ name: t.string(), avatarUrl: t.string() })
10466
11831
 
10467
11832
  export async function loader({ params, api }: Route.LoaderArgs) {
@@ -10522,16 +11887,48 @@ import fails the build with the chain that led to it:
10522
11887
  -
10523
11888
  **Browser code reads only public environment variables** - `NODE_ENV` and
10524
11889
  names with the `PUBLIC_` prefix. Any other `process.env` read in a
10525
- page, `frontend/` or `shared/` fails the build.
11890
+ page, `frontend/` or `shared/` fails the build, optional access (
11891
+ `process.env?.["SECRET"]`) included.
10526
11892
 
10527
11893
  -
10528
11894
  **The build scans what it emits.** A client bundle, a public file or a prerendered page
10529
- that contains what looks like a credential fails the build.
11895
+ that contains what looks like a credential fails the build. A label in a message catalog (
11896
+ `{'{ password: "Password" }'}`) and a documented placeholder (
11897
+ `your-api-key`) are text, not credentials.
10530
11898
 
10531
11899
  The same rules run in `nifra dev` (a refused module is answered with a 403 before
10532
- its source is sent), in both bundlers, for every framework, and in `nifra check`.
10533
- An app on the older layout moves with `nifra migrate layout`: a dry run that
10534
- reports every move, then `--write`.
11900
+ its source is sent), in both bundlers, for every framework, and in `nifra check`,
11901
+ which follows the imports the bundler follows: relative paths, tsconfig `paths`
11902
+ and `baseUrl`, `package.json` `imports`, and the
11903
+ `clientModule` the browser entry loads. An app on the older layout moves with
11904
+ `nifra migrate layout`: a dry run that reports every move, then
11905
+ `--write`. It follows the same imports, rewrites them and test-runner mocks (an
11906
+ alias through the same alias), and moves the config's `clientModule`, any module
11907
+ path the config builds from its own folder, and component pipeline entries with their
11908
+ modules. A test moves with the file beside it, snapshots included. Each
11909
+ `tsconfig*.json` keeps every file in, or out of, its program as before. It adds
11910
+ `.nifra/` to `.gitignore` and leaves data files and folders zoned in
11911
+ `package.json` where they are.
11912
+
11913
+ A route file that holds server code splits into the page and its
11914
+ `x.backend.ts`. Data both halves read moves to `x.shared.ts` beside
11915
+ them; a value that cannot move is copied into each half and reported. A loader or action
11916
+ that returns data without an output schema gets a draft one, typed from what it returns:
11917
+ review that the browser may see every field it declares. The migration reports what still
11918
+ needs a hand, such as an alias with no target that covers a moved file, a specifier that
11919
+ names nothing installed, or a sibling workspace package that names a moved file. It never
11920
+ edits files outside the app.
11921
+
11922
+ An API-only app, one that declares `{'"nifra": { "kind": "api" }'}` or has no web
11923
+ config, no `routes/` and a `backend.ts` that exports
11924
+ `backend`, migrates as one: only
11925
+ `backend.ts` moves, to `backend/app.ts`, with the imports from it and
11926
+ to it. Nothing is split or zoned. The migration adds `backend` to tsconfig
11927
+ `include`, `.nifra/` to `.gitignore`, and the declaration
11928
+ to `package.json` when it is missing. Web config without `routes/`
11929
+ could be either kind of app, so the migration stops and asks which. Until an app moves,
11930
+ `nifra check` fails with NF-C038 on its root `backend.ts` or
11931
+ `framework.ts`, since no contract gate reads them there.
10535
11932
 
10536
11933
  ## Nifra - Contract-derived adversarial testing
10537
11934
 
@@ -10617,6 +12014,14 @@ await assertAdversarialContract(app, {
10617
12014
  Missing, invalid, or unsynthesizable witnesses become explicit coverage gaps. The default is
10618
12015
  fail-closed; `requireCoverage: false` makes gaps advisory.
10619
12016
 
12017
+ Declared `params` and `headers` get witnesses too, validated before any
12018
+ case runs, so a body or query case is rejected by the input it mutates. The harness never
12019
+ invents credentials: `authorization`, `cookie`, `x-api-key`
12020
+ and names that look like a key, token, secret or signature come from the witness and
12021
+ `prepareRequest` only, and are never mutated. A route with a `cookies`
12022
+ schema needs `headers.cookie` in its witness, and a missing credential is a gap that
12023
+ names it.
12024
+
10620
12025
  ### Auth and tenant context
10621
12026
 
10622
12027
  `prepareRequest` runs for every case and runtime. Use it to attach a test session,
@@ -10693,6 +12098,41 @@ await assertAdversarialContract(app, {
10693
12098
  })
10694
12099
  ```
10695
12100
 
12101
+ ### A web app's routes in a test
12102
+
12103
+ Give the test the same route manifest the app serves. `discoverRoutes(dir)` from
12104
+ `@nifrajs/web/fs` scans a routes directory the way the build does, so every page
12105
+ arrives with its `.backend.ts` half: its loader, action, output schemas and
12106
+ middleware. A built app can import the generated `server-manifest` instead, which
12107
+ bakes the same pairs into static imports.
12108
+
12109
+ ```ts
12110
+ // test/routes.test.ts - render the real routes in-process, each page with its backend half.
12111
+ import { expect, test } from "bun:test"
12112
+ import { createWebApp } from "@nifrajs/web"
12113
+ import { discoverRoutes } from "@nifrajs/web/fs"
12114
+ import { reactAdapter } from "@nifrajs/web-react"
12115
+
12116
+ const app = createWebApp({
12117
+ adapter: reactAdapter,
12118
+ // Scans routes/ the way the build does: a page and its .backend.ts half always arrive together.
12119
+ manifest: discoverRoutes(`${import.meta.dir}/../routes`),
12120
+ clientEntry: "/client.js",
12121
+ })
12122
+
12123
+ test("the users page runs its loader", async () => {
12124
+ const res = await app.fetch(new Request("http://localhost/users/7"))
12125
+ expect(res.status).toBe(200)
12126
+ })
12127
+ ```
12128
+
12129
+ `buildManifest(files, load)` pairs a page with its backend half only when both are
12130
+ in `files`. It is pure, so it cannot know a half is missing, and a hand-written list
12131
+ that names the page alone serves it without its loader and guards: the test passes against a
12132
+ route production never runs. `nifra doctor` flags a `buildManifest(...)`
12133
+ call whose array literal or `import.meta.glob` patterns reach a page but not the
12134
+ backend half beside it.
12135
+
10696
12136
  ### Browser end-to-end
10697
12137
 
10698
12138
  API flows stay in-process through `testSession` - typed, cookie-aware, no
@@ -10826,6 +12266,23 @@ implementation into the client.
10826
12266
  See [Framework contract](/docs/contract) for the decoupled form and its scaling
10827
12267
  guidance.
10828
12268
 
12269
+ The tooling reads the backend `backend/app.ts` exports. An app that
12270
+ `implement`s a contract somewhere else, such as a `createApp(deps)`
12271
+ factory, is invisible to the contract lock and the route lints until that file serves it.
12272
+ `nifra check` names each implemented operation the backend does not serve
12273
+ (NF-C039), and counts them when there is no `backend/app.ts` at all. Export the
12274
+ same app built with stand-in dependencies:
12275
+
12276
+ ```ts
12277
+ // backend/app.ts - the backend nifra's tooling reads
12278
+ import { createApp } from "../src/app"
12279
+ import { stubDeps } from "../src/stub-deps"
12280
+
12281
+ // The real entry builds the app with real dependencies elsewhere. This one builds the same routes with
12282
+ // stand-ins, so the contract lock, the typed client checks and `nifra routes` see every operation.
12283
+ export const backend = createApp(stubDeps())
12284
+ ```
12285
+
10829
12286
  ### OpenAPI
10830
12287
 
10831
12288
  The `openapi()` middleware builds an OpenAPI 3.1 document from your registered
@@ -10965,6 +12422,84 @@ input, so Nifra will not guess which app to point them at; a level that fuzzed y
10965
12422
  server by default would be a footgun, and a skipped route is reported rather than passed over
10966
12423
  in silence.
10967
12424
 
12425
+ An app that needs a session, real row ids or signed tokens passes them to the runner instead of
12426
+ rewriting requests inside the executor: `witnesses`, `prepareRequest`,
12427
+ `isRejected`, `include` and `maxMutationsPerInput` on
12428
+ `invariants` reach every L4 run, from `nifra levels` and the
12429
+ `nifra_levels` MCP tool alike. `setup` provisions a throwaway target only
12430
+ when L4 runs: it is called once, before the first L4 request, and its `teardown`
12431
+ always runs after the run, a failing one included. The example keeps its target in memory; a
12432
+ database or container belongs in your own `setup`.
12433
+
12434
+ ```ts
12435
+ // nifra.assurance.ts
12436
+ // doc-check: skip - the backend and its seed data are app-specific
12437
+ import { defineAssuranceConfig } from "@nifrajs/core/assurance"
12438
+ import { createBackend } from "./backend"
12439
+
12440
+ export default defineAssuranceConfig({
12441
+ source: createBackend({ rows: new Map() }),
12442
+ policy: { rules: [/* ... */] },
12443
+ invariants: {
12444
+ // Called once, before the first L4 request - never when this file is imported, so
12445
+ // `nifra check` and `nifra assure` provision nothing.
12446
+ setup: async () => {
12447
+ const rows = new Map([["row_1", { id: "row_1", owner: "fixture" }]])
12448
+ const target = createBackend({ rows })
12449
+ return {
12450
+ executor: (request) => target.fetch(request),
12451
+ teardown: () => rows.clear(), // runs after a failing run too
12452
+ }
12453
+ },
12454
+ // Known-good inputs for schemas L4 cannot synthesize (an opaque validator, a real id).
12455
+ witnesses: { "PATCH /rows/:id": { params: { id: "row_1" }, body: { title: "ok" } } },
12456
+ // Attach what the app needs on every case: a session, a signed token.
12457
+ prepareRequest: (request) => {
12458
+ const headers = new Headers(request.headers)
12459
+ headers.set("cookie", "session=fixture")
12460
+ return new Request(request, { headers })
12461
+ },
12462
+ },
12463
+ })
12464
+ ```
12465
+
12466
+ ### Response contracts
12467
+
12468
+ Next to the ladder, `nifra levels` counts the routes that declare a
12469
+ `response` or `errors` schema and how each is held at runtime:
12470
+ _enforced_, _warn-only_ (checked, original payload still sent) or _typed-only_
12471
+ (no `responseContract()` covers it). A typed-only route is usually registered on a
12472
+ server other than the one the plugin is installed on, or before it; the report names the routes
12473
+ and says which.
12474
+
12475
+ The count does not fail a level on its own. To make L1 require enforcement, add a
12476
+ `baseline` rule selecting `hasResponse: true`: unlike
12477
+ `rules`, where the first match owns a route, a baseline rule is checked on every
12478
+ route it matches, so one entry covers every class. `nifra assure --strict` also fails
12479
+ while any declared route is typed-only or warn-only.
12480
+
12481
+ ```ts
12482
+ // nifra.assurance.ts
12483
+ // doc-check: skip - the backend is app-specific
12484
+ import { defineAssuranceConfig, NIFRA_ASSURANCE } from "@nifrajs/core/assurance"
12485
+ import { backend } from "./backend"
12486
+
12487
+ export default defineAssuranceConfig({
12488
+ source: backend,
12489
+ policy: {
12490
+ rules: [/* first match owns each route: classify by path, method, capability ... */],
12491
+ // Checked on every route it matches, whichever rule owns the route.
12492
+ baseline: [
12493
+ {
12494
+ name: "response-contract",
12495
+ match: { hasResponse: true },
12496
+ require: [NIFRA_ASSURANCE.RESPONSE_CONTRACT],
12497
+ },
12498
+ ],
12499
+ },
12500
+ })
12501
+ ```
12502
+
10968
12503
  ### For coding agents
10969
12504
 
10970
12505
  The `nifra_levels` MCP tool returns the same
@@ -11136,9 +12671,11 @@ A suspended task's status message carries the pending continuation in `metadata`
11136
12671
  }
11137
12672
  ```
11138
12673
 
12674
+ The resuming peer is usually the agent that asked for the tool, so it does not approve its own call: the run asks `ports.approval` again and hands it the peer's `approval` as `callerDecision`. With no approval port the task stays `TASK_STATE_INPUT_REQUIRED`. Return `resumeApproval: "caller"` from `ports` only when the peer is the approver, such as a human operator's client.
12675
+
11139
12676
  Cross-request registries (cancellation, subscription fan-out, push notifications) are deliberately out of scope for the stateless seam; those spec methods answer `UnsupportedOperationError` (-32004).
11140
12677
 
11141
- A reply larger than `maxOutputBytes` answers the request with a JSON-RPC internal error (-32603, `output_limit`), the same error a stream sends when it runs out of budget. A request whose ports or store fail answers with -32603 `internal_error`, keeping the request's `id`.
12678
+ A reply larger than `maxOutputBytes` answers the request with a JSON-RPC internal error (-32603, `output_limit`), the same error a stream sends when it runs out of budget. A stream that runs out also aborts the run's signal, as a client disconnect does, so the run stops calling the model. A request whose ports or store fail answers with -32603 `internal_error`, keeping the request's `id`.
11142
12679
 
11143
12680
  The seam performs no authentication or authorization - wrap it with your app's route guards and scope every port in `ports(c)` to the caller.
11144
12681
 
@@ -11149,6 +12686,62 @@ Part of the **[nifra](https://nifra.dev)** full-stack TypeScript framework - one
11149
12686
  For AI agents, see [`LLM.md`](./LLM.md) and the full corpus
11150
12687
  [`../../llms-full.txt`](../../llms-full.txt).
11151
12688
 
12689
+ ## @nifrajs/acp
12690
+
12691
+ An `AgentBackend` for agents that speak the [Agent Client Protocol](https://agentclientprotocol.com)
12692
+ (version 1) over stdio: Claude Code through `claude-code-acp`, Codex through `codex-acp`, Gemini CLI
12693
+ with `--experimental-acp`, or any other ACP agent. A Nifra host, RPC server, or app drives it like any
12694
+ other backend.
12695
+
12696
+ ```ts
12697
+ import { AcpBackend } from "@nifrajs/acp"
12698
+ import { CodingAgentHost } from "@nifrajs/coding-agent"
12699
+
12700
+ const backend = new AcpBackend({
12701
+ command: "claude-code-acp",
12702
+ env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY },
12703
+ })
12704
+ const host = new CodingAgentHost({ backend })
12705
+ ```
12706
+
12707
+ Each session is one agent process: `initialize` (protocol version 1, refused otherwise), an optional
12708
+ `authenticate` with `authMethod`, then `session/new` in the session's absolute `cwd`. A message is a
12709
+ `session/prompt`; its updates become protocol events:
12710
+
12711
+ | ACP update | Event |
12712
+ | --- | --- |
12713
+ | `agent_message_chunk` | `assistant.delta`, and the whole reply as `assistant.message` before a tool call and at the end |
12714
+ | `agent_thought_chunk` | `reasoning.delta` |
12715
+ | `tool_call`, `tool_call_update` | `tool.started`, then `tool.completed` once the status is `completed` or `failed` |
12716
+ | `plan` | `plan.updated` |
12717
+ | `usage_update` | `context.usage` (`used`, `size`) |
12718
+ | anything else | `x-acp.<sessionUpdate>` with a bounded copy of the update |
12719
+
12720
+ The stop reason ends the turn: `end_turn`, `max_tokens`, and `max_turn_requests` complete it (any but
12721
+ `end_turn` also reported as `x-acp.stop_reason`), `refusal` fails it, and `cancelled` stops it.
12722
+
12723
+ `session/request_permission` surfaces as `approval.required` (`capability` is `acp.<tool kind>`).
12724
+ `resolveApproval` answers with the agent's `allow_once` or `reject_once` option and never an "always"
12725
+ one, so the host's approval policy stays in charge; with no such option it answers `cancelled`. An
12726
+ approval the agent withdraws with `$/cancel_request` is answered and reported as `approval.resolved`
12727
+ with `approved: false`.
12728
+
12729
+ The client offers no filesystem or terminal methods: the agent uses its own tools, and those
12730
+ requests get JSON-RPC `-32601`. `cancel` answers open approvals `cancelled` and sends
12731
+ `session/cancel`; an agent that has not ended the turn after `cancelGraceMs` (default 2 seconds) is
12732
+ stopped, which ends its session.
12733
+
12734
+ The agent runs with `PATH`, `HOME`, locale, terminal, user, shell, and temp variables plus what `env`
12735
+ adds; nothing else from the parent environment passes, so give it its model credentials explicitly.
12736
+ Error text from the agent (a refused request, or the last error-looking stderr line when it exits)
12737
+ is cut to a bounded length with those values and common key shapes masked. stdout carries only
12738
+ JSON-RPC, one message per line, each at most `maxRecordBytes` (default 1 MiB).
12739
+
12740
+ The package passes `@nifrajs/agent-protocol/conformance` against a scripted ACP agent.
12741
+
12742
+ For AI agents, see [`LLM.md`](./LLM.md) and the full corpus
12743
+ [`../../llms-full.txt`](../../llms-full.txt).
12744
+
11152
12745
  ## @nifrajs/ag-ui
11153
12746
 
11154
12747
  Mount a nifra agent as an [AG-UI](https://docs.ag-ui.com) (Agent-User Interaction protocol) endpoint: one POST accepting `RunAgentInput`, a stream of AG-UI events out over SSE - run lifecycle, step and tool-call events projected from the runner's step evidence, text message events for the output, and a typed continuation for human-in-the-loop resume.
@@ -11269,6 +12862,11 @@ terminal events - the run is never re-executed. A disconnected run keeps going a
11269
12862
  real terminal events for that reconnect. Without an `evidenceLog` nothing can rejoin a dropped
11270
12863
  stream, so the disconnect aborts `ports.signal` and the turn suspends as `cancelled`.
11271
12864
 
12865
+ Exceeding `maxOutputBytes` detaches the viewer in the same way, including an oversized initial state
12866
+ snapshot. The response closes immediately, with an `output_limit` error frame when the remaining
12867
+ budget permits it. Without a log the turn is aborted; with a log it continues recording its real
12868
+ terminal result for reconnect rather than storing the viewer's output-limit error as the run result.
12869
+
11272
12870
  ```ts
11273
12871
  import { createMemoryAgentEvidenceLog } from "@nifrajs/agent/events"
11274
12872
 
@@ -11281,7 +12879,8 @@ adapter implementing the same `AgentEvidenceLog` interface.
11281
12879
  When more than one caller shares the route, also pass `evidenceOwner: (c) => <the caller's user or
11282
12880
  tenant id>`: turns are recorded and replayed under that owner, so another caller's reconnect or
11283
12881
  reused `runId` finds none of them. Without it a turn id (by default the client's `runId`) works as a
11284
- bearer capability. The owner function runs before any replay; throwing from it refuses the request.
12882
+ bearer capability. The owner function runs before any replay; throwing from it refuses the request with `403`, and a
12883
+ thrown `Response` answers the request as built, as one thrown from `ports` does.
11285
12884
 
11286
12885
  The seam performs no authentication or authorization - wrap it with your app's route guards and scope every port in `ports(c)` to the caller.
11287
12886
 
@@ -11367,10 +12966,17 @@ dropped stream, so the disconnect aborts `ports.signal` and the turn suspends as
11367
12966
  `createMemoryAgentEvidenceLog` (from `@nifrajs/agent/events`) is the single-process dev/test
11368
12967
  reference; a durable, multi-process log is an adapter implementing the same `AgentEvidenceLog`
11369
12968
  interface.
12969
+
12970
+ One SSE response emits at most `maxOutputBytes` (default 4 MiB), so a client that stops reading
12971
+ cannot make the server queue a whole run. Past the cap the response ends with an `error` frame
12972
+ (`output_limit`): with an `evidenceLog` the run keeps recording and a `Last-Event-ID` reconnect reads
12973
+ on from where the response stopped; without one the run is cancelled.
11370
12974
  When more than one caller shares the route, also pass `evidenceOwner: (c) => <the caller's user or
11371
12975
  tenant id>`: turns are recorded and replayed under that owner, so another caller's reconnect or
11372
12976
  reused `turnId` finds none of them. Without it a turn id works as a bearer capability. The owner
11373
- function runs before any replay; throwing from it refuses the request.
12977
+ function runs before any replay; throwing from it refuses the request with `403`, and a thrown
12978
+ `Response` answers the request as built, as one thrown from `ports` does. A JSON-mode run that
12979
+ throws answers `500 run_failed`, or `409 resume_unavailable` when the POST resumed a turn.
11374
12980
 
11375
12981
  Execution policies can be required by a tool contract. `createLocalProcessAdapter` applies cwd and
11376
12982
  environment filtering, timeouts, and cancellation to child processes. On POSIX each run leads its
@@ -11404,6 +13010,11 @@ counts.
11404
13010
  - **Ordered, deduplicated streaming.** A turn arrives as an ordered stream of `AgentEventView`s.
11405
13011
  Out-of-order and redelivered frames are reordered and de-duplicated by sequence; a bounded buffer
11406
13012
  skips an unfilled gap rather than stalling.
13013
+ - **Turns that survive a dropped connection.** When a turn's stream breaks mid-turn, `send` follows
13014
+ the turn again with `turn.attach` from the last seq it received (`resumeAttempts`, default 3;
13015
+ `resumeDelayMs`, default 250), so the views continue without a gap or a repeat. The host must keep
13016
+ turns (`resumableTurns` on `CodingAgentRpcServer`); one that cannot resume refuses, and `send`
13017
+ throws the original break.
11407
13018
  - **Cursor resume.** A persisted log can be replayed from a cursor. A cursor whose next record was
11408
13019
  evicted from the retained window asks the caller to resync.
11409
13020
  - **Approvals and handoffs.** Pending approvals list as identifiers; decisions and handoff outcomes go
@@ -11469,10 +13080,57 @@ const events = createAgentEventStream(256)
11469
13080
  events.push(/* a versioned AgentEvent */)
11470
13081
  ```
11471
13082
 
13083
+ `isAgentEvent` refuses an event past the protocol's bounds: 256 KiB characters of `text`, `prompt`, or
13084
+ error message, and 64 KiB per string inside `input`, `output`, or `details`. A backend keeps its
13085
+ events inside them with `splitAgentText` (a long reply as consecutive events that join back),
13086
+ `boundAgentText` (one field, cut with a marker), and `boundAgentValue` (a display copy of a tool's
13087
+ input or output; the tool itself still gets the whole value). `agentEventProblem` says why an event
13088
+ was refused, naming the field and the limit but never its content.
13089
+
13090
+ Three events carry what most coding agents report besides their reply: `reasoning.delta` (a piece
13091
+ of the model's reasoning, grouped by an optional `itemId`), `plan.updated` (the whole current plan,
13092
+ entries of `content`, `status`, and optional `priority`, each event replacing the last), and
13093
+ `context.usage` (`usedTokens` of the latest request, the model's `windowTokens` when known, and
13094
+ optional input, output, cached, and reasoning counts).
13095
+
13096
+ Any other frame a backend has of its own travels as an extension event:
13097
+ `type` is `x-<vendor>.<name>` (lowercase, at most 64 characters), with an optional `turnId` and a
13098
+ `data` value inside the same payload bounds. Hosts that do not know the type pass it through; a host
13099
+ that does can register a check for it.
13100
+
13101
+ ```ts
13102
+ yield { version: 1, sessionId, seq, at: Date.now(), type: "x-codex.plan", turnId, data: { steps } }
13103
+ ```
13104
+
11472
13105
  Use `@nifrajs/pi` for the Pi backend or implement `AgentBackend` for another
11473
13106
  runtime. The protocol is intentionally small so CLI, desktop, mobile, and CI
11474
13107
  clients can share it without pulling in the Nifra application framework.
11475
13108
 
13109
+ ## Conformance kit
13110
+
13111
+ `@nifrajs/agent-protocol/conformance` drives an `AgentBackend` through what a host relies on and
13112
+ returns a report, so an adapter for another agent can prove itself in any test runner:
13113
+
13114
+ ```ts
13115
+ import { checkAgentBackend } from "@nifrajs/agent-protocol/conformance"
13116
+
13117
+ const report = await checkAgentBackend({
13118
+ backend: () => new MyBackend(),
13119
+ cwd: process.cwd(),
13120
+ prompt: "say hi",
13121
+ slowPrompt: "wait until cancelled", // enables turn.busy and turn.cancel
13122
+ approvalPrompt: "write a file", // enables the approval checks
13123
+ })
13124
+ expect(report.checks.filter((check) => check.status === "failed")).toEqual([])
13125
+ ```
13126
+
13127
+ Each scenario gets a fresh backend and session. The checks: a valid session snapshot; a turn whose
13128
+ events are valid, ordered, of one turn, and end with exactly one terminal event; `session.completed`
13129
+ for a plain turn and an idle snapshot after it; a second message refused while a turn runs, without
13130
+ ending it; a cancel that ends with `session.stopped` and leaves the session usable; approvals allowed
13131
+ and denied; `undefined` from `resolveApproval` for an approval it never asked for or whose turn was
13132
+ cancelled; and no turn for a closed session.
13133
+
11476
13134
  ## Version 1 compatibility
11477
13135
 
11478
13136
  Version 1 is additive. Hosts advertise capabilities and clients use only the negotiated intersection;
@@ -11690,6 +13348,7 @@ How it holds the line:
11690
13348
 
11691
13349
  - **Post-decode body limits.** The body is decoded (base64 or UTF-8) and its real `byteLength` checked against `maxBodyBytes` (default 1,000,000 - match your app's setting if you changed it) *before* a `Request` is constructed. The event's own length claims are never trusted; over the limit answers a flat `413`.
11692
13350
  - **One header merge site.** Request headers come from the v2 `headers` map plus the separate `cookies` array in exactly one function; the array is canonical, so a `cookie` key smuggled into `headers` never reaches the app. Response `Set-Cookie` values travel in the result's `cookies` array, one entry each - never comma-joined.
13351
+ - **Body-less HEAD.** A `HEAD` answer keeps the GET route's status and headers but carries no body, on both handlers. A streaming body is cancelled rather than read, so its producer stops.
11693
13352
  - **Base64 honesty.** Response `isBase64Encoded` is decided by a strict UTF-8 decode of the actual bytes, never by content-type guessing. Binary round-trips exactly; text ships as text.
11694
13353
  - **Client IP.** `event.requestContext.http.sourceIp` feeds core's client-IP seam; `X-Forwarded-For` trust stays where it belongs, in the app's `clientIp` server option.
11695
13354
  - **Flat errors.** Uncaught failures collapse to the same `{"ok":false,"error":"internal_error"}` 500 as every other nifra runtime - the event is never echoed.
@@ -11846,7 +13505,12 @@ await cache.invalidateTag(`user:${id}`)
11846
13505
  **immediately** while a **background** refresh runs (deduped). Latency stays flat; data self-heals.
11847
13506
  - **Miss/expired**: awaits `loader`. Concurrent misses for the same key share **one** call (no stampede).
11848
13507
  - A throwing `loader` is **not** cached (and in the background path goes to `onError`, never rejects the caller).
11849
- - **`set` / `get` / `has` / `delete`**, **`invalidateTag(tag)`**, **`clear()`**.
13508
+ - **Store outage**: a store `get` that throws or rejects is a miss, so the loader answers; a `set` that
13509
+ fails still returns the loaded value, and a `fence` that fails leaves that load uncached. Each
13510
+ failure goes to `onStoreError(error, { op })`, which by default logs one line per operation per
13511
+ minute without the key. The explicit `get`, `set`, `delete`, `invalidateTag` and `clear` still
13512
+ reject, so a failed invalidation is never silent.
13513
+ - **`set` / `get` / `has` / `delete`**, **`invalidateTag(tag)`**, **`clear()`**. A `delete`, `invalidateTag`, `clear` or newer `set` also stops a `wrap` load or background refresh already running for that key or tag from storing its result, and the next `wrap` loads afresh. A `delete`, `invalidateTag` or `clear` runs after this cache's own async store writes it covers, so a store that applies requests out of order cannot land one after it.
11850
13514
  - TTL: `ttlMs` (→ stale) + optional `swrMs` (→ served-stale window) + optional `tags`.
11851
13515
 
11852
13516
  ## Stores
@@ -11860,6 +13524,14 @@ opt-in. For a cache shared across instances, implement `CacheStore`
11860
13524
  const cache = createCache({ store: new RedisCacheStore(redis) })
11861
13525
  ```
11862
13526
 
13527
+ A store several caches or processes share should also implement the optional `fence(key, tags)` and
13528
+ `setIfFence(key, entry, tags, token)`. A `wrap` load takes a fence before it runs and stores its result
13529
+ through `setIfFence`, which must refuse, in one atomic step, when a `set` or `delete` of the key, an
13530
+ `invalidateTag` of one of its tags, or a `clear` ran since that fence, from any cache. Without them, a
13531
+ load running in one process can store a value another process invalidated meanwhile. `MemoryCache`
13532
+ implements both, and `cacheStoreCertificationProfile({ fencing: true })` from
13533
+ `@nifrajs/testing/certification` checks a store's implementation.
13534
+
11863
13535
  On **Cloudflare Workers** the in-memory cache is per-isolate and short-lived - back it with **CF KV** (or
11864
13536
  the Cache API) via a `CacheStore` for anything that should survive across requests/instances.
11865
13537
 
@@ -11867,7 +13539,8 @@ the Cache API) via a `CacheStore` for anything that should survive across reques
11867
13539
 
11868
13540
  `observer` receives one event per operation after it settles: `{ op, outcome, startedAt, durationMs,
11869
13541
  key, tag, tagCount, context }`. `outcome` is `hit`, `stale` or `miss` for reads, `ok` for writes and a
11870
- finished background refresh (`op: "revalidate"`), `error` when the store or loader threw. An observer
13542
+ finished background refresh (`op: "revalidate"`), `error` when the store or loader threw. A `wrap`
13543
+ that loaded through a store outage reports `miss`. An observer
11871
13544
  that throws is swallowed; it never changes a result. Without an observer the cache reads no extra clock.
11872
13545
 
11873
13546
  `cache.for(c)` binds a request (or job) context, and every event carries it. That is how
@@ -11890,7 +13563,7 @@ whenever it is set.
11890
13563
 
11891
13564
  ## API
11892
13565
 
11893
- - `createCache(options?)` → `Cache` - `{ store?, defaultTtlMs?, now?, onError?, observer?, beacon?, capabilities? }`.
13566
+ - `createCache(options?)` → `Cache` - `{ store?, defaultTtlMs?, now?, onError?, onStoreError?, observer?, beacon?, capabilities? }`.
11894
13567
  - `cache.wrap(key, loader, { ttlMs?, swrMs?, tags? })` · `get` · `has` · `set` · `delete` · `invalidateTag` · `clear`.
11895
13568
  - `cache.for(context)` → the same surface bound to a request or job context.
11896
13569
  - `MemoryCache({ maxEntries?, now? })` - the default store; implement `CacheStore` for your own.
@@ -11927,6 +13600,9 @@ nifra sdk --lang <python|go> [--strict]
11927
13600
  nifra doctor [--json] [--auto-fix] Catch undeclared imports and duplicate Nifra/React installs.
11928
13601
  ```
11929
13602
 
13603
+ `nifra dev`, `build` and `start` take a flag's value as `--target vercel` or `--target=vercel`, and refuse
13604
+ a flag or argument they do not know instead of ignoring it.
13605
+
11930
13606
  ## `nifra doctor` - dependency health
11931
13607
 
11932
13608
  Doctor diffs source imports against declared dependencies, then resolves identity-sensitive
@@ -11959,12 +13635,16 @@ the frontend asset `dist/manifest.json`.
11959
13635
  ## `nifra verify` - verification tiers
11960
13636
 
11961
13637
  `nifra check` is the fast project done-gate: TypeScript plus static contract and boundary diagnostics.
13638
+ It reads source without running it, with one exception: when the project has a `contracts.lock.json`,
13639
+ `nifra check` (with `--lints-only` too) imports `backend/app.ts`, running its top-level code, to compare
13640
+ the routes it serves against the lock.
11962
13641
  It is not a release claim. `nifra verify` runs the ordered local verification plan; its default mode
11963
13642
  is the feedback plan and reports which release gates it omits. CI and release automation should run
11964
13643
  `nifra verify --release` (or `bun run check:release`), which includes build, coverage, corpus,
11965
13644
  consumer, publish, and cross-runtime gates. Each gate runs a `package.json` script (`bun run lint`,
11966
13645
  `bun run check:docs`, ...); a gate whose script the project does not declare is reported as
11967
- `undeclared` and does not fail the run. Run `nifra port --ci --target <target>` for an app's
13646
+ `undeclared` and does not fail the run. The nifra repository's own `bun run verify` and
13647
+ `bun run check:release` fail that gate instead, so a renamed script cannot drop it. Run `nifra port --ci --target <target>` for an app's
11968
13648
  resolved deployment target; CI never guesses a target when none is configured.
11969
13649
 
11970
13650
  ## `nifra check` - the drift gate (run as "done")
@@ -11975,7 +13655,8 @@ remember them. It does three things and exits non-zero if any fails:
11975
13655
  1. **Typecheck** (`tsc --noEmit`, using the project's own TypeScript) - the frontend↔backend contract is
11976
13656
  compiler-enforced. `client<typeof app>` derives request inputs **and** `res.data` from your routes, so
11977
13657
  a screen that expects the wrong shape is a type error. Running the typecheck is what turns that latent
11978
- guarantee into an enforced one.
13658
+ guarantee into an enforced one. TypeScript sources with no `tsconfig.json` beside them fail this step,
13659
+ as a missing `typescript` install does: nothing typechecked them. A JavaScript-only project skips it.
11979
13660
  2. **Typed-client lint** - flags any hand-rolled `fetch()` to your own API (a relative URL like
11980
13661
  `fetch("/users")`), which bypasses the typed client so the compiler can't see the drift. External
11981
13662
  absolute-URL fetches are not flagged.
@@ -12141,8 +13822,11 @@ const { ok, status, data, error } = await api.users({ id: "42" }).get()
12141
13822
  `api.files("report.json").get()`.
12142
13823
  - **Result, never exceptions.** Every call resolves to `{ ok, status, data, error }`,
12143
13824
  so the failure path is in the types. Bodies are positional (`api.users.post({ name })`);
12144
- pass `{ query, headers, signal }` as call options. A call that gets no HTTP answer has
12145
- `status: 0` and the code `network_error`, `timeout`, `response_too_large` or `invalid_path`.
13825
+ pass `{ query, headers, signal }` as call options. A DELETE route with a body schema takes
13826
+ its body as an option: `api.items.delete({ body: { ids } })`. A call that gets no HTTP answer it
13827
+ can read has `status: 0` and the code `network_error`, `timeout`, `response_too_large`,
13828
+ `malformed_response` (a body its content type does not decode, like a proxy's HTML error page
13829
+ labelled JSON) or `invalid_path`.
12146
13830
  - **A param value is never a path step.** `.` and `..` are resolved by URL parsing in every
12147
13831
  encoding, so `api.users({ id: ".." }).delete()` sends nothing and returns `invalid_path`
12148
13832
  instead of reaching another route. `.subscribe()` reports it through `onError`; `.ws()` throws.
@@ -12198,6 +13882,22 @@ loopback debugger, set `exposeErrorStacks: true` (or pass
12198
13882
  `--expose-error-stacks` to `nifra-agent --rpc`) to include bounded exception
12199
13883
  stacks. This option is rejected for remote binding.
12200
13884
 
13885
+ Each `turn.send` SSE frame carries `id: <seq>`. By default a client that disconnects cancels its
13886
+ turn. With `resumableTurns: true` the turn keeps running: a client that comes back calls
13887
+ `turn.attach` with `{ afterSeq }` (or a `Last-Event-ID` header) to read the frames it missed and
13888
+ follow the rest. A cursor older than the held frames (`replayEvents`, default 1024; `replayBytes`,
13889
+ default 8 MiB) answers `409 resync_required`, so read `session.events` instead; a second
13890
+ `turn.send` while one runs answers `409 turn_running`; and a turn no client has followed for
13891
+ `detachedTimeoutMs` (default 2 minutes) is cancelled.
13892
+
13893
+ An approval, or a coding tool's question to the person, is denied once it has waited 5 minutes.
13894
+ Pass `approvals: { timeoutMs }` to `CodingAgentHost` or `CodingAgentRpcServer` for up to 24 h, or
13895
+ `timeoutMs: "never"` to keep it until it is answered, cancelled, its turn ends, or the session
13896
+ closes; `maxPending` (default 32) still bounds how many can wait. `NifraBackend` has its own
13897
+ `approvalTimeoutMs`, taking the same values, and the host denies in the backend whatever its broker
13898
+ denies, so the shorter of the two applies. A never-expiring approval reports `expiresAt` as
13899
+ `APPROVAL_NEVER_EXPIRES_AT`, which survives JSON and stays later than any `Date.now()`.
13900
+
12201
13901
  `nifra-agent` loads `.nifra/extensions/**` from `--cwd` only when started with
12202
13902
  `--extensions`. An extension module's top-level code runs as soon as it is
12203
13903
  imported, before its declared `capabilities` are checked, so pass the flag only
@@ -12211,7 +13911,10 @@ loading code you do not trust.
12211
13911
  an injected model implementation and bounded tools without importing a provider
12212
13912
  SDK. The conversation it sends the model is capped by `maxHistoryChars` (default
12213
13913
  1 MiB): past it, whole earlier turns are dropped, oldest first, and the current
12214
- turn is always sent in full. `ReplayBackend` and `readReplayEvents` provide deterministic protocol
13914
+ turn is always sent in full. `maxMessageChars` (default 64 KiB) caps the message `send` accepts and
13915
+ `maxReplyChars` (default 1 MiB) the model's reply; a reply longer than one event's text arrives as
13916
+ consecutive `assistant.message` events, and a tool's event shows a bounded copy of its input while
13917
+ the tool runs with all of it. `ReplayBackend` and `readReplayEvents` provide deterministic protocol
12215
13918
  replays for CI and Workbench regression tests.
12216
13919
 
12217
13920
  ## At-least-once run dispatch
@@ -12233,7 +13936,8 @@ Legacy `FileSessionStore` files remain supported for explicit local compatibilit
12233
13936
  `nifra-agent --migrate-session <id> --migrate-from <legacy-dir> --migrate-to <evidence-dir>` to
12234
13937
  create a separately validated, evidence-only target. The source is left untouched and the command
12235
13938
  does not switch an active configuration pointer: after reviewing its report, point the host at the
12236
- new directory yourself. To roll back, point it at the legacy directory again and remove the new
13939
+ new directory yourself. `FileSessionStore` reads the migrated rows as content-free entries whose
13940
+ type is the stable event code, and new appends continue the sequence. To roll back, point it at the legacy directory again and remove the new
12237
13941
  target; a failed or interrupted migration leaves the active choice unchanged.
12238
13942
 
12239
13943
  For AI agents, see [`LLM.md`](./LLM.md) and the full corpus
@@ -12413,7 +14117,9 @@ route. Split groups before the compiler reports `TS2589`, rather than weakening
12413
14117
  client's `transport` option with the same registry. `@nifrajs/core/transport-codec` negotiates
12414
14118
  bounded HTTP representations and supplies the same frame/loader adapters for WebSockets and
12415
14119
  deferred data. Import `richWireCodec()` from `@nifrajs/core/transport-codec-rich`; the separate
12416
- subpath keeps rich-wire code out of plain JSON bundles.
14120
+ subpath keeps rich-wire code out of plain JSON bundles. The plugin decodes a codec body before
14121
+ routing, so a route's `bodyLimit` and its authentication run after the decode; its own `maxBytes`
14122
+ is the bound on that work.
12417
14123
  - **Typed data seam (opt in).** `@nifrajs/core/data` defines token-only operation contracts,
12418
14124
  `db.read`/`db.write` capability names, an opaque request-local `RlsScope`, typed adapter requests,
12419
14125
  drift snapshots, and `createDataPort(contract, adapter, { beacon: useCapability })`, which emits the
@@ -12451,7 +14157,9 @@ contract-first registry.
12451
14157
 
12452
14158
  `response` and `errors` are typed by default but are not runtime enforcement by themselves. Install
12453
14159
  `responseContract("enforce")` before the routes it should cover to validate JSON/status payloads and
12454
- prevent extra fields from reaching the wire. `warn` preserves the original payload during migration.
14160
+ prevent extra fields from reaching the wire. That covers a `status(...)` or `c.json(...)` reply a
14161
+ handler returns or throws, a hook returns, or `onError` answers with. `warn` preserves the original
14162
+ payload during migration.
12455
14163
  Raw `Response`, streams, redirects, and `mountFetch` remain explicit transport escape hatches and
12456
14164
  must be governed by assurance/static review.
12457
14165
 
@@ -12498,8 +14206,11 @@ cron.stop() // graceful - in-flight handlers finish
12498
14206
 
12499
14207
  Macros: `@hourly` `@daily` (`@midnight`) `@weekly` `@monthly` `@yearly` (`@annually`).
12500
14208
 
14209
+ Day-of-week runs 0-6 from Sunday, and 7 is Sunday too.
14210
+
12501
14211
  Day-of-month + day-of-week follow the **standard OR rule**: when both are restricted, a match on
12502
- either fires the job.
14212
+ either fires the job. A field that starts with `*` is not restricted, a step like `*/2` included,
14213
+ so `0 0 */2 * 1` fires on an odd day that is also a Monday, as in Vixie cron.
12503
14214
 
12504
14215
  ## Guarantees
12505
14216
 
@@ -12559,6 +14270,7 @@ a Bun-`listen()`-style graceful `stop()` and opt-in signals.
12559
14270
 
12560
14271
  ```ts
12561
14272
  await serve(app, { port: 3000, signals: true }) // SIGTERM/SIGINT → graceful stop()
14273
+ await serve(app, { port: 3000, signals: { drainMs: 8_000 } }) // the same, with an 8 s drain window
12562
14274
  ```
12563
14275
 
12564
14276
  ## What you get for free
@@ -12568,6 +14280,14 @@ cap live inside `app.fetch`, so they apply through this adapter automatically.
12568
14280
 
12569
14281
  Works with any `{ fetch(req): Promise<Response> }` handler, not just nifra. ESM/TS. MIT.
12570
14282
 
14283
+ ## WebSocket message size
14284
+
14285
+ `app.ws()` routes upgrade through `Deno.upgradeWebSocket`, which takes no size limit: Deno receives
14286
+ each inbound message whole (up to its own limit, 64 MiB on Deno 2.9) before the app sees it.
14287
+ `server({ wsMaxPayloadBytes })` then closes the socket with `1009` and keeps the message from your
14288
+ handler, but it cannot bound the memory Deno spent receiving it. Where that matters, cap message
14289
+ size in a proxy in front of the app. On Bun and Node the cap is enforced while the message arrives.
14290
+
12571
14291
  ## For AI agents
12572
14292
 
12573
14293
  Start with [`LLM.md`](./LLM.md) - this package's contract card (the exports you call + its footguns),
@@ -12594,6 +14314,13 @@ The plugin is enabled by default only when `NODE_ENV=development`. It records a
12594
14314
  and serves the overlay stream at `/_nifra/devtools`; remote hosts and cross-origin access stay denied
12595
14315
  unless explicitly allowed. Leave it unregistered in production for zero request-path overhead.
12596
14316
 
14317
+ Each SSE connection also has a finite pending-byte budget (`maxPendingBytes`, default 64 KiB),
14318
+ including keep-alive frames. A slow reader that fills that budget is closed and releases its
14319
+ connection slot. Already queued frames drain before EOF; the overflow frame is dropped. Browser
14320
+ `EventSource` reconnects automatically. Initial delivery includes only the newest history suffix
14321
+ that fits the byte budget. This is a recent trace view, so reconnects can have event-id gaps when
14322
+ older events no longer fit the history or connection budget; it does not guarantee lossless replay.
14323
+
12597
14324
  ## For AI agents
12598
14325
 
12599
14326
  Start with [`LLM.md`](./LLM.md) - this package's contract card (the exports you call + its footguns),
@@ -12780,7 +14507,11 @@ pass the request's bindings: `defineEnv(shape, { source: env })`.
12780
14507
  ## Secret safety
12781
14508
 
12782
14509
  Error messages name the offending variable and the reason - **never its value** (it may be a
12783
- secret). `defineEnv` returns a frozen object, so a validated config can't be mutated downstream.
14510
+ secret). A BYO validator such as valibot or zod may quote the value in its message, escaped or
14511
+ cut short, so when it refuses a set value `defineEnv` reports the variable as invalid without the
14512
+ validator's message. For an unset variable the message is shown, since there is no value to quote.
14513
+ Use the `env.*` helpers when you want the reason as well. `defineEnv` returns a frozen object, so a
14514
+ validated config can't be mutated downstream.
12784
14515
 
12785
14516
  ## For AI agents
12786
14517
 
@@ -12875,6 +14606,15 @@ Resolvers read it as their third argument.
12875
14606
 
12876
14607
  Subscriptions run over the same route via `graphql-transport-ws`. Bring a subscription source - the bundled in-memory `createPubSub` is the reference implementation; swap in a durable bus (Redis, a Durable Object, NATS) that exposes the same `{ publish, subscribe }` shape for multi-instance deploys.
12877
14608
 
14609
+ Each reference subscriber queues at most 1024 events by default. If publication outruns consumption,
14610
+ the overloaded iterator detaches, clears its queued values, and its next pull rejects with
14611
+ `GraphqlPubSubOverflowError` (`code: "graphql_pubsub_overflow"`). Configure
14612
+ `createPubSub({ maxQueuedEvents: 256, overflow: "error" })` to change the finite limit. Explicit
14613
+ `"drop-oldest"` and `"drop-newest"` policies support consumers that accept missing events. Returning or
14614
+ throwing into an iterator clears its queue and terminates delivery. This is an event-count bound;
14615
+ publishers remain responsible for payload sizes. Synchronous `publish` does not provide producer
14616
+ backpressure or cross-instance delivery.
14617
+
12878
14618
  ```ts
12879
14619
  import { server } from "@nifrajs/core"
12880
14620
  import { websocket } from "@nifrajs/core/ws"
@@ -12897,6 +14637,8 @@ pubsub.publish("count", 1)
12897
14637
 
12898
14638
  The route must negotiate the `graphql-transport-ws` subprotocol at the WebSocket layer for a browser `graphql-ws` client to connect.
12899
14639
 
14640
+ With `subscriptions: true`, a socket is admitted only when `authorize` accepts its upgrade request, and operations over it get the same query limits as HTTP. A browser cannot set headers on a WebSocket upgrade, so cookie auth carries over and bearer-header auth does not; for a token sent in `connection_init`, pass WS options with `onConnect` instead. When `authorize` is set, WS options must have their own `authorize` or `onConnect` (`onConnect: () => true` admits every socket on purpose), or `mountGraphql` throws.
14641
+
12900
14642
  ## Options
12901
14643
 
12902
14644
  | Option | Default | Purpose |
@@ -12909,12 +14651,13 @@ The route must negotiate the `graphql-transport-ws` subprotocol at the WebSocket
12909
14651
  | `allowedOrigins` | reflect `*` | CORS origin allowlist. |
12910
14652
  | `authorize` | - | Per-request guard; return `false` for 401. |
12911
14653
  | `enableGet` | `true` | Serve `GET /graphql` (queries only). |
12912
- | `subscriptions` | `false` | `true` (reuse schema/context) or explicit WS options. |
14654
+ | `introspection` | `true` | `false` refuses `__schema` and `__type` as a validation error (400 over HTTP, an `error` frame over WS); `__typename` still resolves. Subscriptions inherit it unless their own options set it. |
14655
+ | `subscriptions` | `false` | `true` (reuse schema, context, `authorize` and limits) or explicit WS options. |
12913
14656
  | `legacyJsonResponse` | `false` | Old always-200 `application/json` shape. |
12914
14657
 
12915
14658
  ## Responses
12916
14659
 
12917
- By default responses use the `application/graphql-response+json` media type with spec status codes: `200` for an executed result (including field errors in `errors`), `400` for a parse or validation failure, `401` for a rejected `authorize`, `405` for a mutation attempted over GET. Set `legacyJsonResponse: true` for the older always-`200` `application/json` behavior some clients expect.
14660
+ By default responses use the `application/graphql-response+json` media type with spec status codes: `200` for an executed result (including field errors in `errors`), `400` for a parse or validation failure, `401` for a rejected `authorize`, `405` for a mutation attempted over GET, `415` for a POST whose body is not `application/json` (or another `+json` type). The last keeps a cross-site HTML form, which can send only urlencoded, multipart or plain-text bodies, from posting a mutation with the visitor's cookies. Set `legacyJsonResponse: true` for the older always-`200` `application/json` behavior some clients expect.
12918
14661
 
12919
14662
  ## License
12920
14663
 
@@ -13110,7 +14853,7 @@ framework runtime. The client companion to `@nifrajs/web-vanilla`: the server re
13110
14853
  (zero framework JS); islands attach behavior to it in place. No VDOM, no hydration re-render -
13111
14854
  the markup the server sent IS the initial state.
13112
14855
 
13113
- The full island bundle is just under 2 KB gzipped, and a size test keeps it there. Use
14856
+ The full island bundle is about 2 KB gzipped (2,080 B at most), and a size test keeps it there. Use
13114
14857
  it for small interactive widgets where a full framework runtime would be overkill.
13115
14858
 
13116
14859
  ## Server side (any adapter - `@nifrajs/web-vanilla` shown)
@@ -13176,7 +14919,10 @@ bind it, for example to show or hide the nested island.
13176
14919
 
13177
14920
  `signal(initial)` / `computed(fn)` / `effect(fn) → dispose` / `batch(fn)` - auto-tracking with
13178
14921
  per-run re-tracking, `Object.is` skips, synchronous updates; `batch` coalesces multiple writes
13179
- into one flush. Island-scale by design: if a widget outgrows this (lists, ownership trees,
14922
+ into one flush. A `computed` runs on first read and stops by itself once nothing reads it: the
14923
+ next change to its inputs stops it instead of recomputing, and reading it again computes it afresh.
14924
+ Every effect an island's setup creates stops when `mountIslands`' disposer unmounts it; one created
14925
+ outside an island runs until its disposer is called. Island-scale by design: if a widget outgrows this (lists, ownership trees,
13180
14926
  async orchestration), that's the signal to use the Solid adapter for that page - both tiers are
13181
14927
  first-class.
13182
14928
 
@@ -13225,6 +14971,8 @@ fails at the call site, not three retries later.
13225
14971
 
13226
14972
  Per-job: `retries: number` (attempts) or `{ attempts, backoff }`. A handler that throws is routed to
13227
14973
  `onError`, then retried with the backoff delay; after the last attempt it's **dead-lettered**, not lost.
14974
+ An attempt count (and `createQueue({ defaultAttempts })`) must be a whole number; `NaN`, `Infinity` or
14975
+ a fraction throws a `RangeError`, and a count below 1 means one attempt.
13228
14976
 
13229
14977
  ```ts
13230
14978
  import { exponentialBackoff, fixedBackoff } from "@nifrajs/jobs"
@@ -13246,6 +14994,23 @@ const q = createQueue({ store: new RedisJobStore(redis) })
13246
14994
  Leasing is at-least-once: a leased job is hidden for `leaseMs`; a worker that dies mid-job releases it
13247
14995
  back automatically. Make handlers **idempotent**.
13248
14996
 
14997
+ While a handler runs, the worker renews its lease about three times per `leaseMs` through the store's
14998
+ optional `renew(id, until, lease)`, so a job that runs longer than `leaseMs` is not handed to another
14999
+ worker. A store without `renew` cannot do that: such a job may then run twice, on two workers, though
15000
+ never twice on the same queue. Each `lease` may also return a `lease` token on the job; the queue
15001
+ passes it back to `complete` / `retry` / `deadLetter` / `renew`, so a store can ignore a worker whose
15002
+ lease another worker has since taken. `MemoryJobStore` does both.
15003
+
15004
+ A job's `timeoutMs` bounds each attempt: one still running after that many ms fails with a
15005
+ `JobTimeoutError` (then retries or dead-letters), frees its worker slot, and aborts `ctx.signal`. Pass the
15006
+ signal on to `fetch` or your driver, since a handler that ignores it keeps running unobserved.
15007
+ `timeoutMs` must be below the worker's `leaseMs`: `start()` (and a later `define()`) throws a
15008
+ `RangeError` otherwise.
15009
+
15010
+ ```ts
15011
+ q.define("render", { timeoutMs: 10_000, async handler(id, ctx) { await fetch(url(id), { signal: ctx.signal }) } })
15012
+ ```
15013
+
13249
15014
  `enqueue` may pass an optional `traceparent` (the producer's W3C trace context); a store should persist
13250
15015
  it and hand it back on every `lease`. A store that drops it still runs every job - a traced run only
13251
15016
  loses its link to the producer. `jobStoreCertificationProfile({ traceparent: true })` from
@@ -13309,10 +15074,10 @@ the run stays in the producer's trace.
13309
15074
  ## API
13310
15075
 
13311
15076
  - `createQueue(options?)` → `Queue` - `{ store?, onError?, now?, defaultAttempts?, backoff?, beacon?, capabilities?, instrument? }`.
13312
- - `queue.define(name, { handler, input?, retries? })` → typed `JobHandle` with `.enqueue(payload, { delayMs? | runAt?, traceparent? })` and `.for(context)`.
15077
+ - `queue.define(name, { handler, input?, retries?, timeoutMs? })` → typed `JobHandle` with `.enqueue(payload, { delayMs? | runAt?, traceparent? })` and `.for(context)`.
13313
15078
  - `queue.enqueue(name, payload, options?)` - enqueue by name.
13314
- - `queue.start({ concurrency?, pollIntervalMs?, leaseMs? })` → `Worker` (`.stop()` drains gracefully).
13315
- - `queue.process()` - run one poll round (for Workers / custom drivers). `queue.drain()` - process until empty.
15079
+ - `queue.start({ concurrency?, pollIntervalMs?, leaseMs? })` → `Worker` (`.stop()` drains gracefully). Each poll fills the slots free at that moment, so one slow job holds only its own slot.
15080
+ - `queue.process()` - run one poll round (for Workers / custom drivers). `queue.drain()` - process until nothing is due or in flight.
13316
15081
  - `queue.counts()` → `{ pending, active, dead }`. `queue.store` - the underlying store.
13317
15082
  - Stores: `MemoryJobStore`. Backoff: `exponentialBackoff`, `fixedBackoff`, `noBackoff`.
13318
15083
 
@@ -13477,8 +15242,8 @@ Each layer refuses on its own, in this order:
13477
15242
 
13478
15243
  `explainPostgres` returns the plan (with `analyze`, executed inside the same transaction),
13479
15244
  `readPostgresSchema` reads tables, columns, keys and indexes from `pg_catalog`, and `postgresRoleSql`
13480
- writes the SQL for a read-only role (`pg_read_all_data` on 14+, or `GRANT SELECT` per schema with a
13481
- `REVOKE` for each excluded table and its partitions) without running it.
15245
+ writes the SQL for a read-only role (`GRANT SELECT` on each allowed schema, with a `REVOKE` for each
15246
+ excluded table and its partitions) without running it.
13482
15247
 
13483
15248
  The live tests run against a throwaway server: set `NIFRA_TEST_POSTGRES_URL` to its superuser URL.
13484
15249
 
@@ -13533,6 +15298,9 @@ const app = server()
13533
15298
  - **`basicAuth(options)`** - Basic Auth plugin with constant-time static credential comparison or
13534
15299
  a custom verifier.
13535
15300
  - **`bearer(options)` / `apiKey(options)`** - token auth plugins with typed principals.
15301
+ - Required `basicAuth`, `bearer`, `apiKey` and `jwt` refuse in the core `authenticate()` stage:
15302
+ before the body is read or validated and before any `derive` or `beforeHandle`. A rate limiter
15303
+ that must count refused requests belongs in `onRequest`, where `rateLimit()` runs.
13536
15304
  - **`csrf({ secret })`** - signed double-submit CSRF protection plus Origin/Referer checking. The
13537
15305
  token travels in the `x-csrf-token` header; set `field: "_csrf"` to also accept it from a plain
13538
15306
  HTML form's hidden input (urlencoded or multipart, bodies up to `fieldMaxBytes`, default 64 KiB).
@@ -13636,9 +15404,10 @@ The mock server matches reflected routes and synthesizes values from supported J
13636
15404
  Unsupported or unsatisfiable schemas fail explicitly instead of returning known-invalid data. Use a
13637
15405
  fixed seed when responses must be replayable across local development and CI.
13638
15406
 
13639
- Routes whose response validator exposes no JSON Schema (zod, valibot, arktype) mock as `{}` by default.
13640
- Pass `reflectJsonSchema` to derive a schema from such a validator so those routes mock real data; for zod,
13641
- use the bridge shipped as `@nifrajs/testing/zod`:
15407
+ A validator that implements Standard JSON Schema (`~standard.jsonSchema`, zod 4.2+ among them) mocks
15408
+ real data with no wiring, and an older zod 4 is converted with `z.toJSONSchema`. Routes whose response
15409
+ validator exposes neither mock as `{}` by default. Pass `reflectJsonSchema` to derive a schema from such
15410
+ a validator; for zod, the bridge ships as `@nifrajs/testing/zod`:
13642
15411
 
13643
15412
  ```ts
13644
15413
  import { zodJsonSchema } from "@nifrajs/testing/zod"
@@ -13709,10 +15478,12 @@ The optional `static` root is served before `app.fetch` for GET and HEAD request
13709
15478
  public asset directory and must contain nothing private: no secrets, source maps, build
13710
15479
  metadata, or user uploads. The directory and its ancestor path must be read-only and owned by
13711
15480
  the service (or otherwise protected from untrusted local writers). Nifra resolves and confines
13712
- the file before opening it and refuses symlinked final files, but Node does not expose a native
13713
- per-component `openat` primitive; a process that can mutate the tree concurrently can still
13714
- create a local TOCTOU race. Mutable or attacker-writable static trees are therefore outside the
13715
- adapter's guarantee. Store user-controlled files outside the static root and serve them through
15481
+ the file before opening it, and refuses it when its final file or any directory on its path became
15482
+ a symlink in the meantime: on macOS the open itself refuses such a path, and on Linux the opened
15483
+ descriptor's path is checked. On Windows and other platforms no file API can open without following a
15484
+ middle link, so a process that swaps a directory concurrently can still win a local TOCTOU race;
15485
+ the check there only makes it need repeated swaps. Mutable or attacker-writable static trees are
15486
+ therefore outside the adapter's guarantee on those platforms. Store user-controlled files outside the static root and serve them through
13716
15487
  an application or object-storage path with its own validation policy.
13717
15488
 
13718
15489
  Every file carries a strong `ETag` (from mtime and size), `Last-Modified`, and
@@ -13730,6 +15501,13 @@ process signals should be a deliberate choice):
13730
15501
  await serve(app, { port: 3000, signals: true })
13731
15502
  ```
13732
15503
 
15504
+ In-flight requests get 10 s to finish before the stop forces them closed. Set the window to fit
15505
+ the platform's kill grace period:
15506
+
15507
+ ```ts
15508
+ await serve(app, { port: 3000, signals: { drainMs: 8_000 } })
15509
+ ```
15510
+
13733
15511
  ## What you get for free
13734
15512
 
13735
15513
  The request timeout (`server({ requestTimeoutMs })` → `503 request_timeout`) and body-size
@@ -13737,6 +15515,10 @@ cap live **inside `app.fetch`**, not Bun's `listen()` - so they apply through th
13737
15515
  with no extra wiring. Slow-client / slow-loris protection is Node's built-in
13738
15516
  `requestTimeout` (300s) and `headersTimeout` (60s) defaults.
13739
15517
 
15518
+ A hook that throws past `app.fetch`, or a response Node refuses to write (a header value carrying
15519
+ CR/LF, say), answers `500` and is logged through the app's own `logger`, as Bun's `listen()` logs it. The `req` a hook sees is a lazy stand-in for Node's request: copy it
15520
+ with `req.clone()`, not `new Request(req)`, which Node's `Request` constructor refuses.
15521
+
13740
15522
  Works with any `{ fetch(req): Promise<Response> }` handler, not just nifra. ESM-only. MIT.
13741
15523
 
13742
15524
  ## For AI agents
@@ -13773,7 +15555,9 @@ const app = server()
13773
15555
  the inbound span as the parent. No inbound header → starts a fresh trace.
13774
15556
  - **Opens a span** with HTTP semantic-convention attributes (`http.request.method`, `url.path`,
13775
15557
  `http.response.status_code`, optional `service.name`), ended on response with duration + status
13776
- (`error` for 5xx, `ok` otherwise).
15558
+ (`error` for 5xx, `ok` otherwise). A request that matched a route reports the route template as
15559
+ `url.path`, so an id or a token in the path stays out of the trace backend; `rawUrlPath: true`
15560
+ reports the raw path instead.
13777
15561
  - **Exposes `c.trace`** (`{ traceId, spanId, parentSpanId?, sampled, traceparent }`) - spread
13778
15562
  `traceHeaders(c.trace)` into any downstream `fetch`/`ctx.api` call to continue the trace.
13779
15563
  - **Exposes `c.causality`** - a bounded, payload-free request node that survives durable command,
@@ -14026,7 +15810,7 @@ const upstream = createProxy({ upstream: "http://127.0.0.1:8081" })
14026
15810
  const app = server().mountFetch("/api", upstream, { stripPrefix: true })
14027
15811
  ```
14028
15812
 
14029
- The upstream is a **bare origin** fixed at construction - the forwarded URL is built by mutating a clone of that origin, never by resolving request-derived strings, so no request input can change which host is dialed. Hop-by-hop and `Connection`-nominated headers are stripped in both directions. Caller IP/protocol metadata is dropped unless `forwardClientIp: true`; the inbound Host is never trusted, and `X-Forwarded-Host` is emitted only from a fixed `forwardedHost`. Upstream redirects are never followed, and TLS verification has no off switch. Unreachable upstreams answer a flat `502`; the deadline (`timeoutMs`, default 30s) answers `504`.
15813
+ The upstream is a **bare origin** fixed at construction - the forwarded URL is built by mutating a clone of that origin, never by resolving request-derived strings, so no request input can change which host is dialed. Hop-by-hop and `Connection`-nominated headers are stripped in both directions. Every inbound `Forwarded` and `X-Forwarded-*` header is dropped; caller IP/protocol metadata is re-added only under `forwardClientIp: true`, and any other forwarding header only as a fixed `headers` entry. The inbound Host is never trusted, and `X-Forwarded-Host` is emitted only from a fixed `forwardedHost`. Upstream redirects are never followed, and TLS verification has no off switch. Unreachable upstreams answer a flat `502`; the deadline (`timeoutMs`, default 30s) answers `504`.
14030
15814
 
14031
15815
  ## Docs
14032
15816
 
@@ -14296,7 +16080,7 @@ const keys = await storage.list({ prefix: "avatars/" })
14296
16080
  | Adapter | Use | Notes |
14297
16081
  |---|---|---|
14298
16082
  | `MemoryStorage` | dev / tests | not durable, not shared across instances |
14299
- | `FileStorage(root)` | a long-running server (Bun/Node/Deno) | persists content type + custom metadata in an adjacent sidecar tree; legacy objects still infer MIME from extension |
16083
+ | `FileStorage(root)` | a long-running server (Bun/Node/Deno) | writes each object whole (staged in `<root>.nifra-state`, then renamed into place); persists content type + custom metadata in a sidecar tree there; reads older `<root>.nifra-metadata` sidecars; objects with none infer MIME from extension |
14300
16084
  | `R2Storage(env.BUCKET)` | the edge (Cloudflare R2) | round-trips `contentType` + metadata; binding typed structurally (no `@cloudflare/workers-types`) |
14301
16085
 
14302
16086
  Implement `StorageAdapter` (`put` / `get` / `delete` / `exists` / `list`) for **S3 / GCS / anything else** -
@@ -14394,6 +16178,16 @@ The validator still decides which generated mutations are truly invalid; the sup
14394
16178
  weaken validation. A missing/invalid witness is a coverage gap, not a silent pass. Set
14395
16179
  `requireCoverage: false` only when gaps are intentionally advisory.
14396
16180
 
16181
+ Declared `params` and `headers` get witnesses too, validated before any case runs, so a body or query
16182
+ case is rejected by the input it mutates rather than by a placeholder param or a missing header. A `t`
16183
+ schema synthesizes them, the witness's `params` and `headers` override, and a route param the schema
16184
+ leaves out keeps a placeholder its pattern accepts. The harness never invents credentials:
16185
+ `authorization`, `proxy-authorization`, `cookie`, `x-api-key`, and names ending in `-key` or containing
16186
+ `token`, `secret` or `signature` come from the witness (and `prepareRequest`) only, and are never
16187
+ mutated; a schema that requires one the witness lacks is a `NO_WITNESS` gap that names it. Cookies are
16188
+ never synthesized either: a route with a `cookies` schema needs `headers.cookie` in its witness. A
16189
+ params or header field typed as bare text accepts any text, so it is not a `NO_REJECTED_MUTATION` gap.
16190
+
14397
16191
  To exercise adapter parity, pass runtime fetch targets. The reflected `app` remains the one source of
14398
16192
  the contract:
14399
16193
 
@@ -14415,10 +16209,13 @@ laboratory at production.
14415
16209
  ### Opaque validators (zod / valibot / arktype)
14416
16210
 
14417
16211
  A `t` schema exposes JSON Schema, so witnesses and constraint-driven mutations are synthesized
14418
- automatically. A Standard Schema validator that carries no JSON Schema (zod, valibot, arktype) validates
14419
- requests fine, but the laboratory can't inspect it - those routes report `NO_WITNESS` and get only
14420
- type-confusion mutations. Pass `reflectJsonSchema` to convert such a validator; for zod, use the shipped
14421
- bridge:
16212
+ automatically. So does any validator that implements Standard JSON Schema (`~standard.jsonSchema`,
16213
+ zod 4.2+ among them): the laboratory calls the schema's own converter, so nothing is resolved from disk
16214
+ and a monorepo run from its root reads an app's zod schemas too. An older zod 4 is converted with
16215
+ `z.toJSONSchema`, resolved from the project directory first, then from this package. A validator with
16216
+ neither validates requests fine, but the laboratory can't inspect it - those routes report `NO_WITNESS`
16217
+ and get only type-confusion mutations, and the gap says why when a conversion was tried and failed.
16218
+ Pass `reflectJsonSchema` to convert any other validator, or to choose the zod bridge explicitly:
14422
16219
 
14423
16220
  ```ts
14424
16221
  import { zodJsonSchema } from "@nifrajs/testing/zod"
@@ -14654,10 +16451,29 @@ done-gate, or `nifra mcp` to give the agent live project tools.
14654
16451
 
14655
16452
  ## @nifrajs/web
14656
16453
 
16454
+ Optional server components are implemented for React, Vue, Svelte and Solid in the post-4.0
16455
+ feature branch. React uses native RSC/Flight; the other adapters use native islands in validated
16456
+ server ranges. Definitions require closed input/public-props schemas and authorization. Refresh
16457
+ executes the actual owner GET, preserving admission, read capabilities, budgets and cancellation;
16458
+ output is private/no-store. Mutate through validated server functions or actions.
16459
+
16460
+ Use the `/server-components`, `/server-components/client`, `/server-components/islands` and
16461
+ `/server-components/build` exports with an explicit `componentPipeline` named config export.
16462
+ See [the server-components guide](https://nifra.dev/docs/server-components) and the four runnable
16463
+ repository examples for complete entries, native outlets, exact compiler pins and rollback.
16464
+
14657
16465
  Framework-agnostic SSR core for nifra - the render seam + HTML document orchestration.
14658
16466
 
14659
16467
  Part of the **[nifra](https://nifra.dev)** full-stack TypeScript framework - one core, five UI libraries, every runtime. Scaffold a new app with `bun create nifra`.
14660
16468
 
16469
+ ## Guarding pages
16470
+
16471
+ A `_layout.backend.ts` loader is data, not a guard: a client navigation can skip it while the browser
16472
+ keeps the layout's data, and an action below it runs without it. Refuse access in that file's
16473
+ `middleware`, or export `gate = true` so its loader runs on every request and before every action,
16474
+ ahead of the page loader. In development, nifra warns when a layout loader without `gate` refuses a
16475
+ request, and `nifra check` reports one whose refusal it can read (NF-C042). See [Layout loaders on navigation](https://nifra.dev/docs/data).
16476
+
14661
16477
  ## Install
14662
16478
 
14663
16479
  ```sh
@@ -14885,7 +16701,8 @@ const registration = await registerWebMcpTools([addItem])
14885
16701
 
14886
16702
  The returned WebMCP `execute` function runs Nifra's existing input validation, capability, approval,
14887
16703
  idempotency, budget, cancellation, output validation, and evidence path. By default it returns a bounded
14888
- receipt envelope so an agent can verify whether the operation committed. The normal human UI remains the
16704
+ receipt envelope so an agent can verify whether the operation committed. A `signal` the host passes as
16705
+ `execute`'s second argument reaches the executor as `ctx.signal`. The normal human UI remains the
14889
16706
  source of page rendering and works when WebMCP is absent.
14890
16707
 
14891
16708
  Receipt output is retained only while its JSON encoding stays within the bounded `maxReceiptBytes`
@@ -15094,6 +16911,10 @@ done-gate, or `nifra mcp` to give the agent live project tools.
15094
16911
 
15095
16912
  `A2AAgentCard`, `A2AAgentSkill`, `A2ACardInfo`, `A2AMountableApp`, `A2ARouteContext`, `A2ATaskState`, `A2A_ERROR_CODES`, `A2A_PROTOCOL_VERSION`, `AGENT_CARD_WELL_KNOWN_PATH`, `MountA2AOptions`, `agentCard`, `mountA2A`
15096
16913
 
16914
+ ## @nifrajs/acp
16915
+
16916
+ `AcpBackend`, `AcpBackendOptions`, `AcpClientInfo`
16917
+
15097
16918
  ## @nifrajs/ag-ui
15098
16919
 
15099
16920
  `AgUIMountableApp`, `AgUIRouteContext`, `AgUIRunContext`, `AgUIRunUsage`, `MountAgUIOptions`, `mountAgUI`
@@ -15104,11 +16925,11 @@ done-gate, or `nifra mcp` to give the agent live project tools.
15104
16925
 
15105
16926
  ## @nifrajs/agent-app
15106
16927
 
15107
- `AGENT_APP_FEATURES`, `AgentAppClient`, `AgentAppClientOptions`, `AgentAppError`, `AgentEventView`, `AgentTransport`, `AgentTransportError`, `AgentTransportRequest`, `ApprovalRequiredView`, `ApprovalResolvedView`, `AssistantChunkView`, `AuthProvider`, `BoundaryDecisionResult`, `BoundaryItemView`, `BoundaryStateView`, `CommandOutcome`, `CreateSessionInput`, `EvalComparisonView`, `EvidenceTimelineView`, `ExtensionReloadedView`, `FaultInjectionView`, `HandoffView`, `HttpAgentTransport`, `HttpAgentTransportOptions`, `MemoryCompactedView`, `OrderedEventBuffer`, `PendingApprovalView`, `RegistryCapabilityView`, `RepairRequiredView`, `ReplayEntryView`, `ReplayResult`, `ResolveHandoffInput`, `ResumeInput`, `ReviewView`, `ReviewViewCheck`, `ReviewViewEvidence`, `ReviewViewFinding`, `ReviewViewStatus`, `RunStudioNodeState`, `RunStudioNodeView`, `RunStudioView`, `RunView`, `SessionFailedView`, `SessionLifecycleView`, `SessionStoppedView`, `SessionView`, `ToolCompletedView`, `ToolDeltaView`, `ToolStartedView`, `TurnStartedView`, `VerificationCompletedView`, `boundaryCommands`, `boundaryIsStale`, `parseEventStream`, `toEvalComparisonView`, `toEventView`, `toEvidenceTimelineView`, `toFaultInjectionViews`, `toHandoffView`, `toRegistryCapabilityView`, `toReviewView`, `toRunStudioView`, `toRunView`, `toSessionView`, `virtualizeEvidenceRows`
16928
+ `AGENT_APP_FEATURES`, `AgentAppClient`, `AgentAppClientOptions`, `AgentAppError`, `AgentEventView`, `AgentTransport`, `AgentTransportError`, `AgentTransportRequest`, `ApprovalRequiredView`, `ApprovalResolvedView`, `AssistantChunkView`, `AuthProvider`, `BoundaryDecisionResult`, `BoundaryItemView`, `BoundaryStateView`, `CommandOutcome`, `ContextUsageView`, `CreateSessionInput`, `EvalComparisonView`, `EvidenceTimelineView`, `ExtensionEventView`, `ExtensionReloadedView`, `FaultInjectionView`, `HandoffView`, `HttpAgentTransport`, `HttpAgentTransportOptions`, `MemoryCompactedView`, `OrderedEventBuffer`, `PendingApprovalView`, `PlanUpdatedView`, `ReasoningDeltaView`, `RegistryCapabilityView`, `RepairRequiredView`, `ReplayEntryView`, `ReplayResult`, `ResolveHandoffInput`, `ResumeInput`, `ReviewView`, `ReviewViewCheck`, `ReviewViewEvidence`, `ReviewViewFinding`, `ReviewViewStatus`, `RunStudioNodeState`, `RunStudioNodeView`, `RunStudioView`, `RunView`, `SessionFailedView`, `SessionLifecycleView`, `SessionStoppedView`, `SessionView`, `ToolCompletedView`, `ToolDeltaView`, `ToolStartedView`, `TurnStartedView`, `VerificationCompletedView`, `boundaryCommands`, `boundaryIsStale`, `parseEventStream`, `toEvalComparisonView`, `toEventView`, `toEvidenceTimelineView`, `toFaultInjectionViews`, `toHandoffView`, `toRegistryCapabilityView`, `toReviewView`, `toRunStudioView`, `toRunView`, `toSessionView`, `virtualizeEvidenceRows`
15108
16929
 
15109
16930
  ## @nifrajs/agent-protocol
15110
16931
 
15111
- `AGENT_PROTOCOL_VERSION`, `AgentApprovalRequiredEvent`, `AgentApprovalResolvedEvent`, `AgentAssistantDeltaEvent`, `AgentAssistantMessageEvent`, `AgentBackend`, `AgentBackendInfo`, `AgentError`, `AgentEvent`, `AgentEventPayload`, `AgentEventSink`, `AgentEventStream`, `AgentEventType`, `AgentExtensionReloadedEvent`, `AgentMemoryCompactedEvent`, `AgentRepairRequiredEvent`, `AgentRepairTask`, `AgentSessionCheckpoint`, `AgentSessionCompletedEvent`, `AgentSessionEventBase`, `AgentSessionFailedEvent`, `AgentSessionSnapshot`, `AgentSessionStartedEvent`, `AgentSessionStatus`, `AgentSessionStoppedEvent`, `AgentSessionUpdatedEvent`, `AgentToolCompletedEvent`, `AgentToolDeltaEvent`, `AgentToolStartedEvent`, `AgentTurnStartedEvent`, `AgentVerificationCompletedEvent`, `CreateSessionInput`, `ForkSessionInput`, `ForkSessionResult`, `ReloadResult`, `SendMessageInput`, `agentError`, `createAgentEventStream`, `isAgentEvent`
16932
+ `AGENT_MAX_EVENT_TEXT`, `AGENT_MAX_PAYLOAD_STRING`, `AGENT_PROTOCOL_VERSION`, `AgentApprovalRequiredEvent`, `AgentApprovalResolvedEvent`, `AgentAssistantDeltaEvent`, `AgentAssistantMessageEvent`, `AgentBackend`, `AgentBackendInfo`, `AgentContextUsageEvent`, `AgentError`, `AgentEvent`, `AgentEventPayload`, `AgentEventSink`, `AgentEventStream`, `AgentEventType`, `AgentExtensionEvent`, `AgentExtensionEventType`, `AgentExtensionReloadedEvent`, `AgentMemoryCompactedEvent`, `AgentPlanEntry`, `AgentPlanEntryStatus`, `AgentPlanUpdatedEvent`, `AgentReasoningDeltaEvent`, `AgentRepairRequiredEvent`, `AgentRepairTask`, `AgentSessionCheckpoint`, `AgentSessionCompletedEvent`, `AgentSessionEventBase`, `AgentSessionFailedEvent`, `AgentSessionSnapshot`, `AgentSessionStartedEvent`, `AgentSessionStatus`, `AgentSessionStoppedEvent`, `AgentSessionUpdatedEvent`, `AgentToolCompletedEvent`, `AgentToolDeltaEvent`, `AgentToolStartedEvent`, `AgentTurnStartedEvent`, `AgentVerificationCompletedEvent`, `CreateSessionInput`, `ForkSessionInput`, `ForkSessionResult`, `ReloadResult`, `SendMessageInput`, `agentError`, `agentEventProblem`, `boundAgentText`, `boundAgentValue`, `createAgentEventStream`, `isAgentEvent`, `isAgentExtensionEventType`, `splitAgentText`
15112
16933
 
15113
16934
  ## @nifrajs/agent-review
15114
16935
 
@@ -15120,11 +16941,11 @@ done-gate, or `nifra mcp` to give the agent live project tools.
15120
16941
 
15121
16942
  ## @nifrajs/auth
15122
16943
 
15123
- `AuthorizationRequest`, `Authorizer`, `CsrfOptions`, `GuardOptions`, `KVNamespaceLike`, `KVSessionStore`, `MemorySessionStore`, `MemorySessionStoreOptions`, `Session`, `SessionContext`, `SessionCookieOptions`, `SessionManager`, `SessionOptions`, `SessionRecord`, `SessionStore`, `createSessions`, `csrf`, `isAuthorized`, `requireAuthorization`, `requireSession`, `requireUser`
16944
+ `AuthorizationRequest`, `Authorizer`, `CsrfOptions`, `GuardOptions`, `KVNamespaceLike`, `KVSessionStore`, `MemorySessionStore`, `MemorySessionStoreOptions`, `Session`, `SessionContext`, `SessionCookieOptions`, `SessionManager`, `SessionOptions`, `SessionRecord`, `SessionStore`, `UnauthenticatedResponse`, `createSessions`, `csrf`, `isAuthorized`, `requireAuthorization`, `requireSession`, `requireUser`
15124
16945
 
15125
16946
  ## @nifrajs/authjs
15126
16947
 
15127
- `AuthGuardOptions`, `AuthJSConfig`, `AuthJSOptions`, `Session`, `app`, `authjs`, `getSession`, `requireAuthUser`
16948
+ `AuthGuardOptions`, `AuthJSConfig`, `AuthJSOptions`, `Session`, `UnauthenticatedResponse`, `app`, `authjs`, `getSession`, `requireAuthUser`
15128
16949
 
15129
16950
  ## @nifrajs/aws-lambda
15130
16951
 
@@ -15132,19 +16953,19 @@ done-gate, or `nifra mcp` to give the agent live project tools.
15132
16953
 
15133
16954
  ## @nifrajs/better-auth
15134
16955
 
15135
- `AuthedOptions`, `BetterAuthLike`, `BetterAuthOptions`, `Principal`, `PrincipalFor`, `RequireSessionOptions`, `SessionOf`, `SessionUserOf`, `WithPrincipal`, `authed`, `betterAuth`, `getSession`, `requirePrincipal`, `requireSession`
16956
+ `AuthedOptions`, `BetterAuthLike`, `BetterAuthOptions`, `Principal`, `PrincipalFor`, `RequireSessionOptions`, `SessionOf`, `SessionUserOf`, `UnauthenticatedResponse`, `WithPrincipal`, `authed`, `betterAuth`, `getSession`, `requirePrincipal`, `requireSession`
15136
16957
 
15137
16958
  ## @nifrajs/cache
15138
16959
 
15139
- `Cache`, `CacheCapabilities`, `CacheEvent`, `CacheObserver`, `CacheOperation`, `CacheOptions`, `CacheOutcome`, `CacheStore`, `CapabilityBeacon`, `MemoryCache`, `MemoryCacheOptions`, `SetOptions`, `StoredEntry`, `WrapOptions`, `createCache`
16960
+ `Cache`, `CacheCapabilities`, `CacheEvent`, `CacheObserver`, `CacheOperation`, `CacheOptions`, `CacheOutcome`, `CacheStore`, `CacheStoreOp`, `CapabilityBeacon`, `MemoryCache`, `MemoryCacheOptions`, `SetOptions`, `StoredEntry`, `WrapOptions`, `createCache`
15140
16961
 
15141
16962
  ## @nifrajs/client
15142
16963
 
15143
- `ActionArgs`, `ActionData`, `ApiError`, `ApiProxy`, `ClientOptions`, `ClientRetryOptions`, `FetchFn`, `InProcessClient`, `InProcessClientOptions`, `Jsonify`, `LoaderArgs`, `LoaderData`, `LoaderResponseControls`, `OutputOf`, `RESERVED_EXACT_KEYS`, `RESERVED_KEY_READOUT`, `RESERVED_VERB_KEYS`, `Register`, `RegisteredBackend`, `RegisteredEnv`, `RegistryOf`, `ResponseContractViolation`, `Result`, `RouteLoaderArgs`, `SearchSchemaOf`, `SubscribeOptions`, `Subscription`, `TestClientOptions`, `Treaty`, `TreatyFromRegistry`, `WsCallOptions`, `WsHandle`, `client`, `inProcessClient`, `reservedKeyFor`, `testClient`
16964
+ `ActionArgs`, `ActionData`, `ApiError`, `ApiProxy`, `ClientOptions`, `ClientRetryOptions`, `FetchFn`, `InProcessClient`, `InProcessClientOptions`, `InputOf`, `Jsonify`, `LoaderArgs`, `LoaderData`, `LoaderResponseControls`, `OutputOf`, `RESERVED_EXACT_KEYS`, `RESERVED_KEY_READOUT`, `RESERVED_VERB_KEYS`, `Register`, `RegisteredBackend`, `RegisteredEnv`, `RegistryOf`, `ResponseContractViolation`, `Result`, `RouteActionReturn`, `RouteLoaderArgs`, `RouteLoaderReturn`, `SearchSchemaOf`, `SubscribeOptions`, `Subscription`, `TestClientOptions`, `Treaty`, `TreatyFromRegistry`, `WsCallOptions`, `WsHandle`, `client`, `inProcessClient`, `reservedKeyFor`, `testClient`
15144
16965
 
15145
16966
  ## @nifrajs/coding-agent
15146
16967
 
15147
- `AGENT_CAPABILITY_MANIFEST_VERSION`, `AGENT_PRESETS`, `AgentCapability`, `AgentCapabilityManifest`, `AgentPlan`, `AgentPreset`, `AgentPresetName`, `ApprovalDecision`, `ApprovalManager`, `ApprovalManagerOptions`, `ApprovalMatchRejection`, `ApprovalMatchResult`, `ApprovalRequest`, `BoundedSubagentRunner`, `BoundedText`, `CapabilityDescriptor`, `CiDeploymentAdapter`, `CodingAgentExtension`, `CodingAgentHost`, `CodingAgentHostOptions`, `CodingAgentRpcServer`, `CodingAgentRpcServerHandle`, `CodingAgentRpcServerOptions`, `CompactionReport`, `ContextRecord`, `ContextWindow`, `ContextWindowOptions`, `DEPLOYMENT_REFERENCE_PROFILES`, `ExtensionCommand`, `ExtensionContext`, `ExtensionDescriptorOptions`, `ExtensionDescriptorSource`, `ExtensionEventHandler`, `ExtensionHost`, `ExtensionHostOptions`, `ExtensionProvider`, `ExtensionReloadResult`, `ExtensionSubagent`, `ExtensionTool`, `FileSessionStore`, `FileSessionStoreOptions`, `HandoffCoordinator`, `HandoffCoordinatorOptions`, `HandoffDecision`, `HandoffError`, `HandoffRejection`, `HandoffView`, `HealingEvent`, `HealingOptions`, `HealingResult`, `IsolatedExtensionSnapshot`, `IsolatedExtensionTool`, `IsolatedExtensionWorker`, `IsolatedExtensionWorkerOptions`, `LocalProcessDeploymentAdapter`, `MigrateLegacySessionOptions`, `NIFRA_AGENT_INSTRUCTIONS`, `NativeApprovalPort`, `NativeGatewayModelPortOptions`, `NativeMessage`, `NativeModelChunk`, `NativeModelPort`, `NativeModelRequest`, `NativeModelResponse`, `NativeTool`, `NifraAgentTool`, `NifraBackend`, `NifraBackendOptions`, `NifraContextOptions`, `NifraContextResult`, `OpenHandoffInput`, `PiBackend`, `PiBackendOptions`, `PlanEvent`, `PlanPhase`, `PlanResult`, `PlanRunner`, `PlanRunnerOptions`, `ProjectDiffOptions`, `ProjectDiffResult`, `RepairProposal`, `ReplayBackend`, `ReplayBackendOptions`, `ReplayDeploymentAdapter`, `ReviewExecutionErrorCode`, `ReviewExecutionResult`, `ReviewOptions`, `SESSION_EVIDENCE_VERSION`, `SelfHealingController`, `SessionEvidenceRecord`, `SessionLogEntry`, `SessionMigrationError`, `SessionMigrationReport`, `SessionStore`, `SubagentAbandonment`, `SubagentAbandonmentLedger`, `SubagentExecutor`, `SubagentResult`, `SubagentRunnerOptions`, `SubagentSpec`, `UiExtensionHost`, `UiExtensionHostOptions`, `UiExtensionManifest`, `UiExtensionSlot`, `UiReloadResult`, `UiStatusWidget`, `UiThemeDescriptor`, `VerificationOptions`, `VerificationRepairTask`, `VerificationResult`, `WorkflowContext`, `WorkflowEvent`, `WorkflowResult`, `WorkflowRunner`, `WorkflowRunnerOptions`, `WorkflowStep`, `approvalRequestFromEvent`, `approvalResolvedEvent`, `createCapabilityManifest`, `createCiDeploymentAdapter`, `createLocalProcessDeploymentAdapter`, `createNativeGatewayModelPort`, `createNifraTools`, `createPresetSpec`, `createReplayDeploymentAdapter`, `createVerificationRepairTask`, `deniedCapabilities`, `discoverExtensions`, `extensionDescriptor`, `extensionDescriptors`, `getAgentPreset`, `isSafeReviewDiff`, `migrateLegacySession`, `parseCapabilityManifest`, `parseSessionEvidenceRecord`, `readBoundedText`, `readProjectDiff`, `readReplayEvents`, `runNifraContext`, `runNifraReview`, `runNifraVerification`, `stableSessionEventCode`, `validateExtensionModule`
16968
+ `AGENT_CAPABILITY_MANIFEST_VERSION`, `AGENT_PRESETS`, `APPROVAL_NEVER_EXPIRES_AT`, `AgentCapability`, `AgentCapabilityManifest`, `AgentPlan`, `AgentPreset`, `AgentPresetName`, `ApprovalDecision`, `ApprovalManager`, `ApprovalManagerOptions`, `ApprovalMatchRejection`, `ApprovalMatchResult`, `ApprovalRequest`, `ApprovalTimeout`, `BoundedSubagentRunner`, `BoundedText`, `CapabilityDescriptor`, `CiDeploymentAdapter`, `CodingAgentExtension`, `CodingAgentHost`, `CodingAgentHostOptions`, `CodingAgentRpcServer`, `CodingAgentRpcServerHandle`, `CodingAgentRpcServerOptions`, `CompactionReport`, `ContextRecord`, `ContextWindow`, `ContextWindowOptions`, `DEPLOYMENT_REFERENCE_PROFILES`, `ExtensionCommand`, `ExtensionContext`, `ExtensionDescriptorOptions`, `ExtensionDescriptorSource`, `ExtensionEventHandler`, `ExtensionHost`, `ExtensionHostOptions`, `ExtensionProvider`, `ExtensionReloadResult`, `ExtensionSubagent`, `ExtensionTool`, `FileSessionStore`, `FileSessionStoreOptions`, `HandoffCoordinator`, `HandoffCoordinatorOptions`, `HandoffDecision`, `HandoffError`, `HandoffRejection`, `HandoffView`, `HealingEvent`, `HealingOptions`, `HealingResult`, `IsolatedExtensionSnapshot`, `IsolatedExtensionTool`, `IsolatedExtensionWorker`, `IsolatedExtensionWorkerOptions`, `LocalProcessDeploymentAdapter`, `MigrateLegacySessionOptions`, `NIFRA_AGENT_INSTRUCTIONS`, `NativeApprovalPort`, `NativeGatewayModelPortOptions`, `NativeMessage`, `NativeModelChunk`, `NativeModelPort`, `NativeModelRequest`, `NativeModelResponse`, `NativeTool`, `NifraAgentTool`, `NifraBackend`, `NifraBackendOptions`, `NifraContextOptions`, `NifraContextResult`, `OpenHandoffInput`, `PiBackend`, `PiBackendOptions`, `PlanEvent`, `PlanPhase`, `PlanResult`, `PlanRunner`, `PlanRunnerOptions`, `ProjectDiffOptions`, `ProjectDiffResult`, `RepairProposal`, `ReplayBackend`, `ReplayBackendOptions`, `ReplayDeploymentAdapter`, `ReviewExecutionErrorCode`, `ReviewExecutionResult`, `ReviewOptions`, `SESSION_EVIDENCE_VERSION`, `SelfHealingController`, `SessionEvidenceRecord`, `SessionLogEntry`, `SessionMigrationError`, `SessionMigrationReport`, `SessionStore`, `SubagentAbandonment`, `SubagentAbandonmentLedger`, `SubagentExecutor`, `SubagentResult`, `SubagentRunnerOptions`, `SubagentSpec`, `UiExtensionHost`, `UiExtensionHostOptions`, `UiExtensionManifest`, `UiExtensionSlot`, `UiReloadResult`, `UiStatusWidget`, `UiThemeDescriptor`, `VerificationOptions`, `VerificationRepairTask`, `VerificationResult`, `WorkflowContext`, `WorkflowEvent`, `WorkflowResult`, `WorkflowRunner`, `WorkflowRunnerOptions`, `WorkflowStep`, `approvalRequestFromEvent`, `approvalResolvedEvent`, `createCapabilityManifest`, `createCiDeploymentAdapter`, `createLocalProcessDeploymentAdapter`, `createNativeGatewayModelPort`, `createNifraTools`, `createPresetSpec`, `createReplayDeploymentAdapter`, `createVerificationRepairTask`, `deniedCapabilities`, `discoverExtensions`, `extensionDescriptor`, `extensionDescriptors`, `getAgentPreset`, `isSafeReviewDiff`, `isVerificationAvailable`, `migrateLegacySession`, `parseCapabilityManifest`, `parseSessionEvidenceRecord`, `readBoundedText`, `readProjectDiff`, `readReplayEvents`, `runNifraContext`, `runNifraReview`, `runNifraVerification`, `stableSessionEventCode`, `validateExtensionModule`
15148
16969
 
15149
16970
  ## @nifrajs/content
15150
16971
 
@@ -15152,7 +16973,7 @@ done-gate, or `nifra mcp` to give the agent live project tools.
15152
16973
 
15153
16974
  ## @nifrajs/core
15154
16975
 
15155
- `AdmissionController`, `AdmissionDecision`, `AnyServer`, `AuthHeaders`, `AuthMaybePromise`, `AuthenticationFailure`, `AuthenticationFailureReason`, `AuthenticationInput`, `AuthenticationResult`, `AuthenticationStage`, `AuthenticationSuccess`, `Context`, `ContextPlugin`, `CookieOptions`, `CookieSecret`, `DefinePluginResult`, `DurableObjectNamespaceLike`, `ExecutionContext`, `FRAMEWORK_NAME`, `FetchApp`, `FrameworkError`, `FrameworkName`, `Handler`, `IdentityPlugin`, `InferInput`, `InferOutput`, `JoinRoutePath`, `LambdaEvent`, `LambdaHandler`, `LambdaResponse`, `LambdaV1Event`, `LambdaV2Event`, `ListenTlsOptions`, `LogFields`, `Logger`, `METHODS`, `McpPromptDescriptor`, `McpResourceDescriptor`, `Method`, `Middleware`, `MountFetchOptions`, `MountOptions`, `MountableApp`, `NetlifyEvent`, `NetlifyHandler`, `NifraFeatureVersion`, `NifraPlugin`, `NifraWebSocket`, `NodeRequestContext`, `NodeRequestHook`, `NodeResponseBodyHook`, `NodeResponseContext`, `NodeResponseHook`, `NodeServeOutcome`, `OnRequestResult`, `Params`, `PlainRender`, `Platform`, `PlatformResponse`, `PluginTypeCollapsed`, `PrefixRegistry`, `Prettify`, `PromptArgument`, `PromptMessage`, `ROUTE_PATTERN_OVERLAP_MAX_STATES`, `RedactOptions`, `Registry`, `RequestPath`, `ResponseBodyHook`, `ResponseBodyReplacement`, `ResponseControls`, `ResponseFinalization`, `ResponseHeadersHook`, `ResponseHeadersView`, `ResponseMapFor`, `ResponseResult`, `RouteConfigError`, `RouteConfigErrorCode`, `RouteDescriptor`, `RouteInfo`, `RouteInfoFor`, `RouteMethod`, `RoutePatternOverlapLimitError`, `RouteSchema`, `Router`, `RouterMatch`, `RunningServer`, `SSEContext`, `SSEInit`, `SSEMessage`, `SSEStream`, `ScheduledController`, `ScheduledHandler`, `Server`, `ServerOptions`, `StandardIssue`, `StandardResult`, `StandardSchemaV1`, `StandardTypes`, `StandardWebSocket`, `StatusResponse`, `ToolAnnotations`, `TypedSSEStream`, `UrlParts`, `VERSION`, `ValidationOutcome`, `VercelHandler`, `Version`, `WebSocketContext`, `WebSocketData`, `WebSocketHandler`, `WebSocketUpgradeOutcome`, `authenticated`, `commonSecretPatterns`, `cookieNamePrefix`, `defineContextPlugin`, `defineIdentityPlugin`, `definePlugin`, `defineRouterPlugin`, `hasDuplicateCookie`, `isSameOriginPath`, `isSameOriginRequest`, `jsonLogger`, `parseCookies`, `pathnameOf`, `redactLogFields`, `rejected`, `replacedRequestOf`, `routePatternOverlap`, `serializeCookie`, `server`, `signValue`, `silentLogger`, `status`, `toFetchHandler`, `toLambdaHandler`, `toNetlifyHandler`, `toVercelHandler`, `unsignValue`, `urlPartsOf`
16976
+ `AdmissionController`, `AdmissionDecision`, `AnyServer`, `AuthHeaders`, `AuthMaybePromise`, `AuthenticationFailure`, `AuthenticationFailureReason`, `AuthenticationInput`, `AuthenticationResult`, `AuthenticationStage`, `AuthenticationSuccess`, `Context`, `ContextPlugin`, `CookieOptions`, `CookieSecret`, `DeclaredMountRoute`, `DefinePluginResult`, `DurableObjectNamespaceLike`, `ExecutionContext`, `FRAMEWORK_NAME`, `FetchApp`, `FrameworkError`, `FrameworkName`, `Handler`, `IdentityPlugin`, `InferInput`, `InferOutput`, `JoinRoutePath`, `LambdaEvent`, `LambdaHandler`, `LambdaResponse`, `LambdaV1Event`, `LambdaV2Event`, `ListenTlsOptions`, `LogFields`, `Logger`, `METHODS`, `McpPromptDescriptor`, `McpResourceDescriptor`, `Method`, `Middleware`, `MountFetchOptions`, `MountOptions`, `MountableApp`, `NetlifyEvent`, `NetlifyHandler`, `NifraFeatureVersion`, `NifraPlugin`, `NifraWebSocket`, `NodeRequestContext`, `NodeRequestHook`, `NodeResponseBodyHook`, `NodeResponseContext`, `NodeResponseHook`, `NodeServeOutcome`, `OnRequestResult`, `OpaqueMountDeclaration`, `Params`, `PlainRender`, `Platform`, `PlatformResponse`, `PluginTypeCollapsed`, `PrefixRegistry`, `Prettify`, `PromptArgument`, `PromptMessage`, `ROUTE_PATTERN_OVERLAP_MAX_STATES`, `RedactOptions`, `Registry`, `RequestPath`, `ResponseBodyHook`, `ResponseBodyReplacement`, `ResponseControls`, `ResponseFinalization`, `ResponseHeadersHook`, `ResponseHeadersView`, `ResponseMapFor`, `ResponseResult`, `RouteConfigError`, `RouteConfigErrorCode`, `RouteDescriptor`, `RouteInfo`, `RouteInfoFor`, `RouteMethod`, `RoutePatternOverlapLimitError`, `RouteSchema`, `Router`, `RouterMatch`, `RunningServer`, `SSEContext`, `SSEInit`, `SSEMessage`, `SSEStream`, `ScheduledController`, `ScheduledHandler`, `Server`, `ServerOptions`, `StandardIssue`, `StandardResult`, `StandardSchemaV1`, `StandardTypes`, `StandardWebSocket`, `StatusResponse`, `ToolAnnotations`, `TypedSSEStream`, `UrlParts`, `VERSION`, `ValidationOutcome`, `VercelHandler`, `Version`, `WebSocketContext`, `WebSocketData`, `WebSocketHandler`, `WebSocketUpgradeOutcome`, `authenticated`, `commonSecretPatterns`, `cookieNamePrefix`, `defineContextPlugin`, `defineIdentityPlugin`, `definePlugin`, `defineRouterPlugin`, `hasDuplicateCookie`, `isAuthenticatedRequest`, `isSameOriginForm`, `isSameOriginPath`, `isSameOriginRequest`, `jsonLogger`, `markAuthenticatedRequest`, `parseCookies`, `pathnameOf`, `redactLogFields`, `rejected`, `replacedRequestOf`, `routePatternOverlap`, `serializeCookie`, `server`, `signValue`, `silentLogger`, `status`, `toFetchHandler`, `toLambdaHandler`, `toNetlifyHandler`, `toVercelHandler`, `unsignValue`, `urlPartsOf`, `writableResponse`
15156
16977
 
15157
16978
  ## @nifrajs/cron
15158
16979
 
@@ -15180,7 +17001,7 @@ done-gate, or `nifra mcp` to give the agent live project tools.
15180
17001
 
15181
17002
  ## @nifrajs/graphql
15182
17003
 
15183
- `GraphqlContextBuilder`, `GraphqlContextInput`, `GraphqlHttpOptions`, `GraphqlPubSub`, `GraphqlWsOptions`, `MountGraphqlOptions`, `NifraContextLike`, `buildContext`, `createPubSub`, `graphqlWebSocket`, `mountGraphql`, `respondGraphql`
17004
+ `GraphqlContextBuilder`, `GraphqlContextInput`, `GraphqlHttpOptions`, `GraphqlPubSub`, `GraphqlPubSubOptions`, `GraphqlPubSubOverflowError`, `GraphqlWsOptions`, `MountGraphqlOptions`, `NifraContextLike`, `buildContext`, `createPubSub`, `graphqlWebSocket`, `mountGraphql`, `respondGraphql`
15184
17005
 
15185
17006
  ## @nifrajs/i18n
15186
17007
 
@@ -15196,11 +17017,11 @@ done-gate, or `nifra mcp` to give the agent live project tools.
15196
17017
 
15197
17018
  ## @nifrajs/islets
15198
17019
 
15199
- `BindableElement`, `BindableRoot`, `IslandContext`, `IslandHost`, `IslandScope`, `IslandSetup`, `IslandStrategy`, `MountIslandOptions`, `Signal`, `batch`, `bindScope`, `computed`, `effect`, `island`, `islandState`, `mountIslands`, `signal`
17020
+ `BindableElement`, `BindableRoot`, `Computed`, `IslandContext`, `IslandHost`, `IslandScope`, `IslandSetup`, `IslandStrategy`, `MountIslandOptions`, `Signal`, `batch`, `bindScope`, `computed`, `effect`, `island`, `islandState`, `mountIslands`, `signal`
15200
17021
 
15201
17022
  ## @nifrajs/jobs
15202
17023
 
15203
- `Backoff`, `CapabilityBeacon`, `DeadLetterRecord`, `DeadLetterView`, `EnqueueOptions`, `ExponentialOptions`, `JobContext`, `JobCounts`, `JobDefinition`, `JobEnqueueInfo`, `JobError`, `JobHandle`, `JobHandler`, `JobRunInfo`, `JobRunOutcome`, `JobStore`, `JobTraceContext`, `JobValidationError`, `MemoryJobStore`, `Queue`, `QueueHealth`, `QueueInstrument`, `QueueOptions`, `RetryPolicy`, `StandardResult`, `StandardSchemaV1`, `StoredJob`, `Worker`, `WorkerOptions`, `createQueue`, `exponentialBackoff`, `fixedBackoff`, `noBackoff`, `toDeadLetterView`, `toQueueHealth`
17024
+ `Backoff`, `CapabilityBeacon`, `DeadLetterRecord`, `DeadLetterView`, `EnqueueOptions`, `ExponentialOptions`, `JobContext`, `JobCounts`, `JobDefinition`, `JobEnqueueInfo`, `JobError`, `JobHandle`, `JobHandler`, `JobRunInfo`, `JobRunOutcome`, `JobStore`, `JobTimeoutError`, `JobTraceContext`, `JobValidationError`, `MemoryJobStore`, `Queue`, `QueueHealth`, `QueueInstrument`, `QueueOptions`, `RetryPolicy`, `StandardResult`, `StandardSchemaV1`, `StoredJob`, `Worker`, `WorkerOptions`, `createQueue`, `exponentialBackoff`, `fixedBackoff`, `noBackoff`, `toDeadLetterView`, `toQueueHealth`
15204
17025
 
15205
17026
  ## @nifrajs/mcp
15206
17027
 
@@ -15212,11 +17033,11 @@ done-gate, or `nifra mcp` to give the agent live project tools.
15212
17033
 
15213
17034
  ## @nifrajs/middleware
15214
17035
 
15215
- `AdmissionControllerHandle`, `AdmissionEvidence`, `AdmissionOptions`, `AdmissionPolicy`, `AdmissionSnapshot`, `ApiKeyStaticOptions`, `ApiKeyVerifyOptions`, `AuthPlugin`, `BasicAuthPlugin`, `BasicAuthStaticOptions`, `BasicAuthVerifyOptions`, `BearerOptions`, `BodyLimitOptions`, `ByteRange`, `ByteRangeResult`, `CacheControlOptions`, `CacheOptions`, `CachedResponse`, `Composable`, `CompressionOptions`, `ConditionalResponseOptions`, `ContentPreference`, `CorsOptions`, `CsrfOptions`, `DurableCommandOptions`, `ETagOptions`, `HealthcheckOptions`, `IdempotencyClaim`, `IdempotencyOptions`, `IdempotencyRecord`, `IdempotencyStore`, `IpMatcher`, `IpRestrictionOptions`, `JwkKey`, `JwksOptions`, `JwtAlgorithm`, `JwtClaims`, `JwtHeader`, `JwtKeyResolver`, `JwtOptions`, `JwtPlugin`, `JwtVerificationKey`, `LanguageMatch`, `LanguageOptions`, `LoggerOptions`, `LoopDelayHistogram`, `LoopDelayMonitor`, `MemoryIdempotencyStore`, `MemoryIdempotencyStoreOptions`, `MemoryResponseCache`, `MemoryResponseCacheOptions`, `MemoryStore`, `MemoryStoreOptions`, `MethodOverrideOptions`, `MultipartBody`, `MultipartHeadersInit`, `MultipartPart`, `MultipartResponseOptions`, `OpenApiInfo`, `OpenApiOptions`, `OpenApiServer`, `OpenApiTag`, `OpenApiUiOptions`, `PoweredByOptions`, `PrettyJsonOptions`, `ProblemDetailsOptions`, `RangeResponseOptions`, `RateLimitOptions`, `RateLimitResult`, `RateLimitStore`, `RequestIdOptions`, `RequestLogFields`, `ResponseCacheStore`, `RouteLike`, `SecurityHeadersOptions`, `SecurityRequirement`, `ShedReason`, `TimingControls`, `TimingMetric`, `TimingOptions`, `TrailingSlashOptions`, `VerifiedJwt`, `VerifyJwtOptions`, `VerifyJwtResult`, `apiKey`, `appendTrailingSlash`, `basicAuth`, `bearer`, `bodyLimit`, `buildOpenApiDocument`, `cache`, `cacheControl`, `combine`, `compression`, `conditionalResponse`, `cors`, `createAdmissionController`, `createCsrfToken`, `createEventLoopLagSampler`, `csrf`, `durableCommand`, `etag`, `healthcheck`, `idempotency`, `ipRestriction`, `jwk`, `jwks`, `jwt`, `language`, `logger`, `methodOverride`, `multipartResponse`, `namedCombine`, `negotiateContentType`, `openapi`, `parseAcceptHeader`, `parseByteRange`, `pickLanguage`, `poweredBy`, `prettyJson`, `problemDetails`, `rangeResponse`, `rateLimit`, `requestId`, `responseCache`, `securityHeaders`, `timing`, `trimTrailingSlash`, `tryVerifyJwt`, `verifyCsrfToken`, `verifyJwt`
17036
+ `AdmissionControllerHandle`, `AdmissionEvidence`, `AdmissionOptions`, `AdmissionPolicy`, `AdmissionSnapshot`, `ApiKeyStaticOptions`, `ApiKeyVerifyOptions`, `AuthPlugin`, `BasicAuthPlugin`, `BasicAuthStaticOptions`, `BasicAuthVerifyOptions`, `BearerOptions`, `BodyLimitOptions`, `ByteRange`, `ByteRangeResult`, `CacheControlOptions`, `CacheOptions`, `CachedResponse`, `Composable`, `CompressionOptions`, `ConditionalResponseOptions`, `ContentPreference`, `CorsOptions`, `CsrfOptions`, `DurableCommandOptions`, `ETagOptions`, `HealthcheckOptions`, `IdempotencyClaim`, `IdempotencyOptions`, `IdempotencyRecord`, `IdempotencyStore`, `IpMatcher`, `IpRestrictionOptions`, `JwkKey`, `JwksOptions`, `JwtAlgorithm`, `JwtClaims`, `JwtHeader`, `JwtKeyResolver`, `JwtOptions`, `JwtPlugin`, `JwtVerificationKey`, `LanguageMatch`, `LanguageOptions`, `LoggerOptions`, `LoopDelayHistogram`, `LoopDelayMonitor`, `MemoryIdempotencyStore`, `MemoryIdempotencyStoreOptions`, `MemoryResponseCache`, `MemoryResponseCacheOptions`, `MemoryStore`, `MemoryStoreOptions`, `MethodOverrideOptions`, `MultipartBody`, `MultipartHeadersInit`, `MultipartPart`, `MultipartResponseOptions`, `OpenApiInfo`, `OpenApiOptions`, `OpenApiServer`, `OpenApiTag`, `OpenApiUiOptions`, `PoweredByOptions`, `PrettyJsonOptions`, `ProblemDetailsOptions`, `RangeResponseOptions`, `RateLimitOptions`, `RateLimitResult`, `RateLimitStore`, `RateLimitTier`, `RateLimitTiersOptions`, `RequestIdOptions`, `RequestLogFields`, `ResponseCacheStore`, `RouteLike`, `SecurityHeadersOptions`, `SecurityRequirement`, `ShedReason`, `TimingControls`, `TimingMetric`, `TimingOptions`, `TrailingSlashOptions`, `VerifiedJwt`, `VerifyJwtOptions`, `VerifyJwtResult`, `apiKey`, `appendTrailingSlash`, `basicAuth`, `bearer`, `bodyLimit`, `buildOpenApiDocument`, `cache`, `cacheControl`, `combine`, `compression`, `conditionalResponse`, `cors`, `createAdmissionController`, `createCsrfToken`, `createEventLoopLagSampler`, `csrf`, `durableCommand`, `etag`, `healthcheck`, `idempotency`, `ipRestriction`, `jwk`, `jwks`, `jwt`, `language`, `logger`, `methodOverride`, `multipartResponse`, `namedCombine`, `negotiateContentType`, `openapi`, `parseAcceptHeader`, `parseByteRange`, `pickLanguage`, `poweredBy`, `prettyJson`, `problemDetails`, `rangeResponse`, `rateLimit`, `rateLimitTiers`, `requestId`, `responseCache`, `securityHeaders`, `timing`, `trimTrailingSlash`, `tryVerifyJwt`, `verifyCsrfToken`, `verifyJwt`
15216
17037
 
15217
17038
  ## @nifrajs/mock
15218
17039
 
15219
- `MockServer`, `MockServerOptions`, `MockableApp`, `MockableRoute`, `UnsupportedMockSchemaError`, `autoReflectJsonSchema`, `createMockServer`, `generateMockValue`
17040
+ `MockServer`, `MockServerOptions`, `MockableApp`, `MockableRoute`, `UnsupportedMockSchemaError`, `autoReflectJsonSchema`, `createMockServer`, `explainAutoReflectJsonSchema`, `generateMockValue`
15220
17041
 
15221
17042
  ## @nifrajs/node
15222
17043
 
@@ -15244,7 +17065,7 @@ done-gate, or `nifra mcp` to give the agent live project tools.
15244
17065
 
15245
17066
  ## @nifrajs/schema
15246
17067
 
15247
- `DeferredValue`, `ImportOpenAPIOptions`, `ImportedApiInventory`, `ImportedRoute`, `ImportedRouteQuery`, `NifraSchema`, `OpenAPIDocument`, `OpenAPIImportError`, `OpenAPIInfo`, `OpenApiInventoryDrift`, `Page`, `decodeCursor`, `diffOpenApiInventory`, `encodeCursor`, `fromTypeBox`, `importOpenAPI`, `paginate`, `registerFormat`, `t`, `toOpenAPI`, `toOpenAPIFromEvidence`
17068
+ `DeferredValue`, `ImportOpenAPIOptions`, `ImportedApiInventory`, `ImportedRoute`, `ImportedRouteQuery`, `JsonValue`, `NifraSchema`, `OpenAPIDocument`, `OpenAPIImportError`, `OpenAPIInfo`, `OpenAPIVersion`, `OpenApiInventoryDrift`, `Page`, `decodeCursor`, `diffOpenApiInventory`, `encodeCursor`, `fromTypeBox`, `importOpenAPI`, `paginate`, `registerFormat`, `t`, `toOpenAPI`, `toOpenAPIFromEvidence`
15248
17069
 
15249
17070
  ## @nifrajs/storage
15250
17071
 
@@ -15252,7 +17073,7 @@ done-gate, or `nifra mcp` to give the agent live project tools.
15252
17073
 
15253
17074
  ## @nifrajs/testing
15254
17075
 
15255
- `AdapterCertificationError`, `AdapterCertificationProfile`, `AdapterCertificationReport`, `AdversarialContractError`, `AdversarialContractOptions`, `AdversarialContractReport`, `AdversarialContractResult`, `AgentEvalCase`, `AgentEvalComponentEvidence`, `AgentEvalCompositionOptions`, `AgentEvalCompositionReport`, `AgentEvalRegressionError`, `AgentEvalReport`, `AgentEvalSuite`, `AgentEvalSuiteSpec`, `AgentFailureMatrixCase`, `AgentFailureMatrixError`, `AgentFailureMatrixKind`, `AgentFailureMatrixReport`, `AppLike`, `BaselineComparison`, `BaselineOptions`, `CaptureIncidentOptions`, `CapturedRequest`, `CapturedRequestInput`, `CaseComparison`, `CaseResult`, `CertifiableCacheEntry`, `CertifiableCacheStore`, `CertifiableDeploymentAdapter`, `CertifiableDescriptorAdapter`, `CertifiableDomainEvent`, `CertifiableEventDeliveryAdapter`, `CertifiableEventRecord`, `CertifiableJobStore`, `CertifiableModelGateway`, `CertifiableRuntimeAdapter`, `CertifiableRuntimeServer`, `CertifiableStorageAdapter`, `CertificationCapabilityEvidence`, `CertificationCheck`, `CertificationCheckEvidence`, `CertificationTarget`, `ComparisonCode`, `ContractAuthContext`, `ContractCaseContext`, `ContractCaseKind`, `ContractCoverageGap`, `ContractCoverageGapCode`, `ContractExpectedResponse`, `ContractLabHandler`, `ContractLabRuntimeAdapter`, `ContractLabServer`, `ContractLabWitness`, `ContractReplay`, `ContractRuntime`, `ContractTarget`, `ContractTestApp`, `ContractWitness`, `CookieJar`, `CreateTrajectoryTranscriptOptions`, `DataAdapterConformanceError`, `DataAdapterConformanceReport`, `DataAdapterConformanceTarget`, `DataConformanceCheck`, `DataConformanceFailure`, `DataConformancePage`, `DataConformanceScope`, `DataConformanceTransaction`, `EffectLedger`, `FailureDirective`, `FailureEvidence`, `FailureInjectedError`, `FailureKind`, `FailureLab`, `FailureLabOptions`, `FailureReplay`, `FailureScenario`, `FailureScenarioReport`, `FaultProfile`, `FaultProfileFault`, `FaultProfileFaultKind`, `FaultProfileReport`, `FaultProfileScenario`, `FaultProfileScenarioReport`, `GenerateRegressionTestOptions`, `IdempotencyDivergence`, `IdempotencyProof`, `IncidentCapsule`, `IncidentReplayError`, `IncidentReplayResult`, `PREDICTION_LAB_SEED`, `PredictionLabContext`, `PredictionLabOptions`, `PredictionLabReplay`, `PredictionLabReport`, `PredictionLabResult`, `PredictionLabTarget`, `ReplayIncidentOptions`, `ReplayTrajectoryOptions`, `RubricShape`, `RubricSpec`, `RubricVerdict`, `RunFaultProfileOptions`, `ScoreTolerance`, `TestSession`, `TestSessionOptions`, `TrajectoryInvariantId`, `TrajectoryInvariantOptions`, `TrajectoryInvariantResult`, `TrajectoryReplayResult`, `TrajectoryRun`, `TrajectoryTranscript`, `assertAdapterCertification`, `assertAdversarialContract`, `assertAgentEval`, `assertAgentEvalBaseline`, `assertAgentFailureMatrix`, `assertDataAdapterConformance`, `assertIncidentReplays`, `assertPredictionLab`, `assertTrajectoryInvariants`, `cacheStoreCertificationProfile`, `captureIncident`, `certifyAdapter`, `checkTrajectoryInvariants`, `compareAgentEvalBaseline`, `contractLabWitnesses`, `cookieJar`, `createFailureLab`, `createReferenceContractLabHandler`, `createTrajectoryTranscript`, `defineAgentEvalSuite`, `defineCertificationProfile`, `defineFaultProfile`, `deploymentAdapterCertificationProfile`, `deploymentCertificationProfile`, `eventDeliveryCertificationProfile`, `gatewayCertificationProfile`, `generateRegressionTest`, `httpToolAdapter`, `inProcessToolAdapter`, `jobStoreCertificationProfile`, `mcpToolAdapter`, `modelGatewayCertificationProfile`, `parseRubricVerdict`, `predictionLabCaseIds`, `proveIdempotency`, `recordTrajectory`, `redactForEmission`, `referenceFaultProfile`, `registryCertificationProfile`, `replayIncident`, `replayTrajectory`, `runAdversarialContract`, `runAgentEvalComposition`, `runAgentFailureMatrix`, `runContractLab`, `runContractLabOverHttp`, `runContractLabThroughAdapter`, `runDataAdapterConformance`, `runFailureScenario`, `runFaultProfile`, `runPredictionLab`, `runTrajectory`, `runtimeAdapterCertificationProfile`, `shapeOf`, `storageAdapterCertificationProfile`, `testSession`, `testToolAdapter`, `toPredictionLabContext`, `trajectoryRegressionId`, `verifyAdapterCertification`
17076
+ `AdapterCertificationError`, `AdapterCertificationProfile`, `AdapterCertificationReport`, `AdversarialContractError`, `AdversarialContractOptions`, `AdversarialContractReport`, `AdversarialContractResult`, `AgentEvalCase`, `AgentEvalComponentEvidence`, `AgentEvalCompositionOptions`, `AgentEvalCompositionReport`, `AgentEvalRegressionError`, `AgentEvalReport`, `AgentEvalSuite`, `AgentEvalSuiteSpec`, `AgentFailureMatrixCase`, `AgentFailureMatrixError`, `AgentFailureMatrixKind`, `AgentFailureMatrixOptions`, `AgentFailureMatrixReport`, `AppLike`, `BaselineComparison`, `BaselineOptions`, `CaptureIncidentOptions`, `CapturedRequest`, `CapturedRequestInput`, `CaseComparison`, `CaseResult`, `CertifiableCacheEntry`, `CertifiableCacheStore`, `CertifiableDeploymentAdapter`, `CertifiableDescriptorAdapter`, `CertifiableDomainEvent`, `CertifiableEventDeliveryAdapter`, `CertifiableEventRecord`, `CertifiableJobStore`, `CertifiableModelGateway`, `CertifiableRuntimeAdapter`, `CertifiableRuntimeServer`, `CertifiableStorageAdapter`, `CertificationCapabilityEvidence`, `CertificationCheck`, `CertificationCheckEvidence`, `CertificationTarget`, `ComparisonCode`, `ContractAuthContext`, `ContractCaseContext`, `ContractCaseKind`, `ContractCoverageGap`, `ContractCoverageGapCode`, `ContractExpectedResponse`, `ContractLabHandler`, `ContractLabRuntimeAdapter`, `ContractLabServer`, `ContractLabWitness`, `ContractReplay`, `ContractRuntime`, `ContractTarget`, `ContractTestApp`, `ContractWitness`, `CookieJar`, `CreateTrajectoryTranscriptOptions`, `DataAdapterConformanceError`, `DataAdapterConformanceReport`, `DataAdapterConformanceTarget`, `DataConformanceCheck`, `DataConformanceFailure`, `DataConformancePage`, `DataConformanceScope`, `DataConformanceTransaction`, `EffectLedger`, `FailureDirective`, `FailureEvidence`, `FailureInjectedError`, `FailureKind`, `FailureLab`, `FailureLabOptions`, `FailureReplay`, `FailureScenario`, `FailureScenarioReport`, `FaultProfile`, `FaultProfileFault`, `FaultProfileFaultKind`, `FaultProfileReport`, `FaultProfileScenario`, `FaultProfileScenarioReport`, `GenerateRegressionTestOptions`, `IdempotencyDivergence`, `IdempotencyProof`, `IncidentCapsule`, `IncidentReplayError`, `IncidentReplayResult`, `PREDICTION_LAB_SEED`, `PredictionLabContext`, `PredictionLabOptions`, `PredictionLabReplay`, `PredictionLabReport`, `PredictionLabResult`, `PredictionLabTarget`, `ReplayIncidentOptions`, `ReplayTrajectoryOptions`, `RubricShape`, `RubricSpec`, `RubricVerdict`, `RunFaultProfileOptions`, `ScoreTolerance`, `TestSession`, `TestSessionOptions`, `TrajectoryInvariantId`, `TrajectoryInvariantOptions`, `TrajectoryInvariantResult`, `TrajectoryReplayResult`, `TrajectoryRun`, `TrajectoryTranscript`, `assertAdapterCertification`, `assertAdversarialContract`, `assertAgentEval`, `assertAgentEvalBaseline`, `assertAgentFailureMatrix`, `assertDataAdapterConformance`, `assertIncidentReplays`, `assertPredictionLab`, `assertTrajectoryInvariants`, `cacheStoreCertificationProfile`, `captureIncident`, `certifyAdapter`, `checkTrajectoryInvariants`, `compareAgentEvalBaseline`, `contractLabWitnesses`, `cookieJar`, `createFailureLab`, `createReferenceContractLabHandler`, `createTrajectoryTranscript`, `defineAgentEvalSuite`, `defineCertificationProfile`, `defineFaultProfile`, `deploymentAdapterCertificationProfile`, `deploymentCertificationProfile`, `eventDeliveryCertificationProfile`, `gatewayCertificationProfile`, `generateRegressionTest`, `httpToolAdapter`, `inProcessToolAdapter`, `jobStoreCertificationProfile`, `mcpToolAdapter`, `modelGatewayCertificationProfile`, `parseRubricVerdict`, `predictionLabCaseIds`, `proveIdempotency`, `recordTrajectory`, `redactForEmission`, `referenceFaultProfile`, `registryCertificationProfile`, `replayIncident`, `replayTrajectory`, `runAdversarialContract`, `runAgentEvalComposition`, `runAgentFailureMatrix`, `runContractLab`, `runContractLabOverHttp`, `runContractLabThroughAdapter`, `runDataAdapterConformance`, `runFailureScenario`, `runFaultProfile`, `runPredictionLab`, `runTrajectory`, `runtimeAdapterCertificationProfile`, `shapeOf`, `storageAdapterCertificationProfile`, `testSession`, `testToolAdapter`, `toPredictionLabContext`, `trajectoryRegressionId`, `verifyAdapterCertification`
15256
17077
 
15257
17078
  ## @nifrajs/ts-plugin
15258
17079
 
@@ -15264,7 +17085,7 @@ done-gate, or `nifra mcp` to give the agent live project tools.
15264
17085
 
15265
17086
  ## @nifrajs/web
15266
17087
 
15267
- `ACTION_GLOBAL`, `Action`, `BOUNDARY_GLOBAL`, `BackendOnly`, `Blocker`, `BlockerController`, `BlockerFunction`, `BlockerLocation`, `BlockerState`, `Boundary`, `BoundaryDescriptor`, `BoundaryError`, `BoundaryMode`, `BoundaryRegistration`, `BoundaryRequestCtx`, `BoundaryState`, `BoundaryStates`, `BoundaryStatus`, `BrowserNavigate`, `CacheStore`, `CachedResponse`, `CdnPurgeOutcome`, `CdnPurgeTarget`, `ClientAction`, `ClientActionArgs`, `ClientActionResult`, `ClientLoader`, `ClientLoaderArgs`, `ClientRequestBody`, `ClientRouteHooks`, `ClientRouter`, `ClientRouterOptions`, `CreateCspPolicyOptions`, `CreateNonceResolverOptions`, `CreateWebAppOptions`, `CspHeaderContext`, `CspPolicy`, `CssLoadingMode`, `DATA_GLOBAL`, `DATA_HEADER`, `DEFAULT_CSS_LOADING`, `DEFAULT_DEV_PORT`, `DRAFT_COOKIE`, `Deferred`, `DehydratedState`, `DraftCookieControls`, `DynamicBoundary`, `DynamicBoundaryBatch`, `EnableDraftOptions`, `FetchRouteData`, `Fetcher`, `FetcherState`, `FontDisplay`, `FontFace`, `FontPreloadInput`, `FontSource`, `GenerateClientEntryOptions`, `GenerateRouteSearchTypesOptions`, `GenerateServerManifestOptions`, `GetStaticPaths`, `HANDOVER_ID`, `HtmlSanitizer`, `IDLE_BLOCKER`, `ISRApp`, `ISROptions`, `ISRPlatform`, `ISRQuery`, `ISR_REVALIDATE_HEADER`, `ISR_REVALIDATE_TAGS_HEADER`, `ISR_STATUS_HEADER`, `InertScriptType`, `InfiniteData`, `InfiniteQueryHandle`, `InfiniteQueryOptions`, `InterceptBoundary`, `KVCacheStore`, `KVCacheStoreOptions`, `KVNamespaceLike`, `LAYOUT_DATA_GLOBAL`, `LayoutEntry`, `LinkDescriptor`, `Loader`, `LoaderContext`, `LoaderResponseControls`, `LoadingEntry`, `Manifest`, `MatchChain`, `MemoryCacheStore`, `MemoryCacheStoreOptions`, `MemoryStaticBoundaryCache`, `Meta`, `MetaArgs`, `MetaDescriptor`, `MetaInput`, `MiddlewareOutcome`, `MountRouterOptions`, `MutationCallbacks`, `MutationHandle`, `MutationState`, `MutationStatus`, `NAV_FROM_HEADER`, `NavigateFunction`, `NavigateOptions`, `NavigateSearchOf`, `NavigateTargetInput`, `NonceContext`, `NonceGenerator`, `NonceHeader`, `NonceHeaderContext`, `NonceResolver`, `NotFoundEntry`, `NotFoundScope`, `OpenGraphInput`, `PRE_HYDRATION_GUARD`, `PendingBoundary`, `PrefetchMode`, `PreviewEndpointOptions`, `PublicDirCache`, `QueryClient`, `QueryClientOptions`, `QueryHandle`, `QueryOptions`, `QueryState`, `QueryStatus`, `REDIRECT_HEADER`, `RETAIN_HEADER`, `REVALIDATE_HEADER`, `ROOT_ATTRIBUTE`, `ROUTE_GLOBAL`, `RedirectOptions`, `RenderAdapter`, `RenderAdapterConformanceError`, `RenderAdapterConformanceFixture`, `RenderAssemblyCache`, `RenderPageInput`, `RenderPageOptions`, `RenderProps`, `RenderStreamOptions`, `RenderedPage`, `RevalidateEndpointOptions`, `RevalidateResult`, `RevalidateTags`, `RevalidateTagsInput`, `RouteEntry`, `RouteMatch`, `RouteMiddleware`, `RouteModule`, `RoutePattern`, `RouteSearch`, `RouterState`, `SERVER_FN_MODULE`, `STATUS_HEADER`, `SanitizedHtml`, `ScriptDescriptor`, `SearchOf`, `ServePublicDirOptions`, `ShouldRevalidate`, `ShouldRevalidateArgs`, `SsrModuleLoader`, `StaticBoundary`, `StaticBoundaryCache`, `StaticBoundaryImportEdge`, `StaticBoundaryRoot`, `StaticCtx`, `StaticPath`, `StaticPaths`, `StaticRoutes`, `StatusPageOptions`, `Submission`, `SubmitOptions`, `TrustedHtml`, `UIMatch`, `UnsafeScriptDescriptor`, `assertCssLoadingCompatible`, `assertRenderAdapterConformance`, `assertStaticBoundaryImports`, `boundaryDescriptors`, `boundaryModeKey`, `buildManifest`, `canonical`, `createClientRouter`, `createCspPolicy`, `createMatcher`, `createMutation`, `createNonceResolver`, `createQueryClient`, `createWebApp`, `defer`, `disableDraft`, `enableDraft`, `enumerateStaticRoutes`, `filePathToPattern`, `filePathToPatterns`, `fontFace`, `fontPreload`, `generateClientEntry`, `generateRouteSearchTypes`, `generateServerManifest`, `getBrowserNavigate`, `gone`, `hashQueryKey`, `isCacheablePage`, `isDraftEnabled`, `jsonLd`, `mergeHeads`, `nifraScriptHashes`, `normalizeCssLoading`, `notFound`, `openGraph`, `previewEndpoint`, `redirect`, `registerBlocker`, `renderPage`, `renderPageResult`, `resolveDynamicBoundaries`, `resolveMeta`, `resolveNavigate`, `resolvePublicPath`, `resolveStaticBoundaries`, `revalidate`, `revalidateEndpoint`, `sanitizeHtml`, `sanitizedHtml`, `searchOf`, `searchOfChain`, `serializeData`, `serializeSearch`, `servePublicDir`, `setBlockerController`, `setBrowserNavigate`, `setSsrModuleLoader`, `ssrModuleLoader`, `startDynamicBoundaries`, `statusPage`, `trustHtml`, `unsafeInlineScript`, `webProjectEvidence`, `withISR`
17088
+ `ACTION_GLOBAL`, `Action`, `BOUNDARY_GLOBAL`, `BackendOnly`, `Blocker`, `BlockerController`, `BlockerFunction`, `BlockerLocation`, `BlockerState`, `Boundary`, `BoundaryDescriptor`, `BoundaryError`, `BoundaryMode`, `BoundaryRegistration`, `BoundaryRequestCtx`, `BoundaryState`, `BoundaryStates`, `BoundaryStatus`, `BrowserNavigate`, `CacheStore`, `CachedResponse`, `CdnPurgeOutcome`, `CdnPurgeTarget`, `ClientAction`, `ClientActionArgs`, `ClientActionResult`, `ClientLoader`, `ClientLoaderArgs`, `ClientRequestBody`, `ClientRouteHooks`, `ClientRouter`, `ClientRouterOptions`, `CreateCspPolicyOptions`, `CreateNonceResolverOptions`, `CreateWebAppOptions`, `CspHeaderContext`, `CspPolicy`, `CssLoadingMode`, `DATA_GLOBAL`, `DATA_HEADER`, `DEFAULT_CSS_LOADING`, `DEFAULT_DEV_PORT`, `DRAFT_COOKIE`, `Deferred`, `DehydratedState`, `DocumentParts`, `DraftCookieControls`, `DynamicBoundary`, `DynamicBoundaryBatch`, `EnableDraftOptions`, `FetchRouteData`, `Fetcher`, `FetcherState`, `FontDisplay`, `FontFace`, `FontPreloadInput`, `FontSource`, `GenerateClientEntryOptions`, `GenerateRouteSearchTypesOptions`, `GenerateServerManifestOptions`, `GetStaticPaths`, `HANDOVER_ID`, `HtmlSanitizer`, `IDLE_BLOCKER`, `ISRApp`, `ISROptions`, `ISRPlatform`, `ISRQuery`, `ISRStoreOp`, `ISR_REVALIDATE_HEADER`, `ISR_REVALIDATE_TAGS_HEADER`, `ISR_STATUS_HEADER`, `InertScriptType`, `InfiniteData`, `InfiniteQueryHandle`, `InfiniteQueryOptions`, `InterceptBoundary`, `KVCacheStore`, `KVCacheStoreOptions`, `KVNamespaceLike`, `LAYOUT_DATA_GLOBAL`, `LayoutEntry`, `LinkDescriptor`, `Loader`, `LoaderContext`, `LoaderResponseControls`, `LoadingEntry`, `Manifest`, `MatchChain`, `MemoryCacheStore`, `MemoryCacheStoreOptions`, `MemoryStaticBoundaryCache`, `Meta`, `MetaArgs`, `MetaDescriptor`, `MetaInput`, `MiddlewareOutcome`, `MountRouterOptions`, `MutationCallbacks`, `MutationHandle`, `MutationState`, `MutationStatus`, `NAV_FROM_HEADER`, `NavigateFunction`, `NavigateOptions`, `NavigateSearchOf`, `NavigateTargetInput`, `NonceContext`, `NonceGenerator`, `NonceHeader`, `NonceHeaderContext`, `NonceResolver`, `NotFoundEntry`, `NotFoundScope`, `OpenGraphInput`, `PRE_HYDRATION_GUARD`, `PendingBoundary`, `PrefetchMode`, `PreviewEndpointOptions`, `PublicDirCache`, `QueryClient`, `QueryClientOptions`, `QueryHandle`, `QueryOptions`, `QueryState`, `QueryStatus`, `REDIRECT_HEADER`, `RETAIN_HEADER`, `REVALIDATE_HEADER`, `ROOT_ATTRIBUTE`, `ROUTE_GLOBAL`, `RedirectOptions`, `RenderAdapter`, `RenderAdapterConformanceError`, `RenderAdapterConformanceFixture`, `RenderAssemblyCache`, `RenderPageInput`, `RenderPageOptions`, `RenderProps`, `RenderStreamOptions`, `RenderedPage`, `RevalidateEndpointOptions`, `RevalidateResult`, `RevalidateTags`, `RevalidateTagsInput`, `RouteEntry`, `RouteMatch`, `RouteMiddleware`, `RouteModule`, `RoutePattern`, `RouteSearch`, `RouterState`, `SERVER_FN_MODULE`, `STATUS_HEADER`, `SanitizedHtml`, `ScriptDescriptor`, `SearchOf`, `ServePublicDirOptions`, `ShouldRevalidate`, `ShouldRevalidateArgs`, `SsrModuleLoader`, `StaticBoundary`, `StaticBoundaryCache`, `StaticBoundaryImportEdge`, `StaticBoundaryRoot`, `StaticCtx`, `StaticPath`, `StaticPaths`, `StaticRoutes`, `StatusPageOptions`, `Submission`, `SubmitOptions`, `TrustedHtml`, `UIMatch`, `UnsafeScriptDescriptor`, `assertCssLoadingCompatible`, `assertRenderAdapterConformance`, `assertStaticBoundaryImports`, `boundaryDescriptors`, `boundaryModeKey`, `buildManifest`, `canonical`, `createClientRouter`, `createCspPolicy`, `createMatcher`, `createMutation`, `createNonceResolver`, `createQueryClient`, `createWebApp`, `defer`, `disableDraft`, `enableDraft`, `enumerateStaticRoutes`, `filePathToPattern`, `filePathToPatterns`, `fontFace`, `fontPreload`, `generateClientEntry`, `generateRouteSearchTypes`, `generateServerManifest`, `getBrowserNavigate`, `gone`, `hashQueryKey`, `isCacheablePage`, `isDraftEnabled`, `jsonLd`, `mergeHeads`, `nifraScriptHashes`, `normalizeCssLoading`, `notFound`, `openGraph`, `previewEndpoint`, `redirect`, `registerBlocker`, `renderPage`, `renderPageResult`, `resolveChainMeta`, `resolveDynamicBoundaries`, `resolveMeta`, `resolveNavigate`, `resolvePublicPath`, `resolveStaticBoundaries`, `revalidate`, `revalidateEndpoint`, `sanitizeHtml`, `sanitizedHtml`, `searchOf`, `searchOfChain`, `serializeData`, `serializeSearch`, `servePublicDir`, `setBlockerController`, `setBrowserNavigate`, `setSsrModuleLoader`, `ssrModuleLoader`, `startDynamicBoundaries`, `statusPage`, `trustHtml`, `unsafeInlineScript`, `webProjectEvidence`, `withISR`
15268
17089
 
15269
17090
  ## @nifrajs/web-preact
15270
17091
 
@@ -15292,7 +17113,7 @@ done-gate, or `nifra mcp` to give the agent live project tools.
15292
17113
 
15293
17114
  ## @nifrajs/webmcp
15294
17115
 
15295
- `AgentCapability`, `AgentCapabilityReference`, `AgentSurfaceAuthoritativeState`, `AgentSurfaceConformanceCase`, `AgentSurfaceConformanceOptions`, `AgentSurfaceConformanceResult`, `AgentSurfaceDescriptor`, `AgentSurfacePrediction`, `AgentSurfacePredictionContext`, `AgentSurfacePredictor`, `AgentSurfaceReconcileContext`, `AgentSurfaceReconciler`, `AgentSurfaceScope`, `AgentSurfaceSnapshot`, `DefineAgentCapabilityOptions`, `ExecutePredictedOptions`, `PredictedExecutionResult`, `PredictionCommitOptions`, `PredictionPatchOperation`, `PredictionStore`, `PredictionStoreEvent`, `PredictionStoreResult`, `PredictionStoreSnapshot`, `ReconciliationMode`, `ToolApproval`, `ToolCallOptions`, `ToolCallResult`, `ToolContract`, `ToolContractOptions`, `ToolError`, `ToolEvidence`, `WebMcpExecutionOptions`, `WebMcpModelContext`, `WebMcpReceipt`, `WebMcpRegistrationFailure`, `WebMcpRegistrationReport`, `WebMcpTarget`, `WebMcpToolDefinition`, `createPredictionStore`, `defineAgentCapability`, `executeAgentCapability`, `executePredicted`, `registerWebMcpTools`, `runAgentSurfaceConformance`, `toWebMcpTool`
17116
+ `AgentCapability`, `AgentCapabilityReference`, `AgentSurfaceAuthoritativeState`, `AgentSurfaceConformanceCase`, `AgentSurfaceConformanceOptions`, `AgentSurfaceConformanceResult`, `AgentSurfaceDescriptor`, `AgentSurfacePrediction`, `AgentSurfacePredictionContext`, `AgentSurfacePredictor`, `AgentSurfaceReconcileContext`, `AgentSurfaceReconciler`, `AgentSurfaceScope`, `AgentSurfaceSnapshot`, `DefineAgentCapabilityOptions`, `ExecutePredictedOptions`, `PredictedExecutionResult`, `PredictionCommitOptions`, `PredictionPatchOperation`, `PredictionStore`, `PredictionStoreEvent`, `PredictionStoreResult`, `PredictionStoreSnapshot`, `ReconciliationMode`, `ToolApproval`, `ToolCallOptions`, `ToolCallResult`, `ToolContract`, `ToolContractOptions`, `ToolError`, `ToolEvidence`, `WebMcpExecuteContext`, `WebMcpExecutionOptions`, `WebMcpModelContext`, `WebMcpReceipt`, `WebMcpRegistrationFailure`, `WebMcpRegistrationReport`, `WebMcpTarget`, `WebMcpToolDefinition`, `createPredictionStore`, `defineAgentCapability`, `executeAgentCapability`, `executePredicted`, `registerWebMcpTools`, `runAgentSurfaceConformance`, `toWebMcpTool`
15296
17117
 
15297
17118
  ## @nifrajs/workers
15298
17119