@nifrajs/cli 4.0.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (332) hide show
  1. package/README.md +3 -0
  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 +7 -2
  12. package/dist/assure-hydration.js.map +1 -1
  13. package/dist/capabilities-tool.d.ts.map +1 -1
  14. package/dist/capabilities-tool.js +13 -3
  15. package/dist/capabilities-tool.js.map +1 -1
  16. package/dist/check-diagnostics.d.ts +20 -2
  17. package/dist/check-diagnostics.d.ts.map +1 -1
  18. package/dist/check-diagnostics.js +54 -7
  19. package/dist/check-diagnostics.js.map +1 -1
  20. package/dist/check-scan.d.ts +75 -22
  21. package/dist/check-scan.d.ts.map +1 -1
  22. package/dist/check-scan.js +1475 -310
  23. package/dist/check-scan.js.map +1 -1
  24. package/dist/check.d.ts +2 -2
  25. package/dist/check.d.ts.map +1 -1
  26. package/dist/check.js +192 -65
  27. package/dist/check.js.map +1 -1
  28. package/dist/cli.d.ts +7 -0
  29. package/dist/cli.d.ts.map +1 -1
  30. package/dist/cli.js +112 -5
  31. package/dist/cli.js.map +1 -1
  32. package/dist/client-module.d.ts +18 -0
  33. package/dist/client-module.d.ts.map +1 -0
  34. package/dist/client-module.js +49 -0
  35. package/dist/client-module.js.map +1 -0
  36. package/dist/command-catalog.d.ts +21 -5
  37. package/dist/command-catalog.d.ts.map +1 -1
  38. package/dist/command-catalog.js +138 -38
  39. package/dist/command-catalog.js.map +1 -1
  40. package/dist/component-check.d.ts +13 -0
  41. package/dist/component-check.d.ts.map +1 -0
  42. package/dist/component-check.js +59 -0
  43. package/dist/component-check.js.map +1 -0
  44. package/dist/contract-proof.d.ts +1 -1
  45. package/dist/contract-proof.d.ts.map +1 -1
  46. package/dist/contract-proof.js +2 -24
  47. package/dist/contract-proof.js.map +1 -1
  48. package/dist/contracts.d.ts +19 -0
  49. package/dist/contracts.d.ts.map +1 -1
  50. package/dist/contracts.js +50 -3
  51. package/dist/contracts.js.map +1 -1
  52. package/dist/db-tool.d.ts.map +1 -1
  53. package/dist/db-tool.js +4 -3
  54. package/dist/db-tool.js.map +1 -1
  55. package/dist/dev-bun-config.d.ts +5 -1
  56. package/dist/dev-bun-config.d.ts.map +1 -1
  57. package/dist/dev-bun-config.js +44 -11
  58. package/dist/dev-bun-config.js.map +1 -1
  59. package/dist/diagnostics.d.ts +3 -0
  60. package/dist/diagnostics.d.ts.map +1 -1
  61. package/dist/diagnostics.js.map +1 -1
  62. package/dist/diff-tool.d.ts +9 -8
  63. package/dist/diff-tool.d.ts.map +1 -1
  64. package/dist/diff-tool.js +28 -24
  65. package/dist/diff-tool.js.map +1 -1
  66. package/dist/doctor.d.ts +13 -0
  67. package/dist/doctor.d.ts.map +1 -1
  68. package/dist/doctor.js +23 -15
  69. package/dist/doctor.js.map +1 -1
  70. package/dist/env-file.d.ts +6 -0
  71. package/dist/env-file.d.ts.map +1 -1
  72. package/dist/env-file.js +6 -0
  73. package/dist/env-file.js.map +1 -1
  74. package/dist/fix-recipes.d.ts.map +1 -1
  75. package/dist/fix-recipes.js +28 -3
  76. package/dist/fix-recipes.js.map +1 -1
  77. package/dist/hand-built-manifest.d.ts +27 -0
  78. package/dist/hand-built-manifest.d.ts.map +1 -0
  79. package/dist/hand-built-manifest.js +124 -0
  80. package/dist/hand-built-manifest.js.map +1 -0
  81. package/dist/hooks-report.d.ts +23 -0
  82. package/dist/hooks-report.d.ts.map +1 -0
  83. package/dist/hooks-report.js +104 -0
  84. package/dist/hooks-report.js.map +1 -0
  85. package/dist/init-agents.d.ts +2 -2
  86. package/dist/init-agents.d.ts.map +1 -1
  87. package/dist/init-agents.js +17 -11
  88. package/dist/init-agents.js.map +1 -1
  89. package/dist/internal/nifra-project.d.ts +41 -0
  90. package/dist/internal/nifra-project.d.ts.map +1 -0
  91. package/dist/internal/nifra-project.js +183 -0
  92. package/dist/internal/nifra-project.js.map +1 -0
  93. package/dist/internal/project-specifiers.d.ts +16 -0
  94. package/dist/internal/project-specifiers.d.ts.map +1 -0
  95. package/dist/internal/project-specifiers.js +396 -0
  96. package/dist/internal/project-specifiers.js.map +1 -0
  97. package/dist/internal/script-kind.d.ts.map +1 -1
  98. package/dist/internal/script-kind.js +2 -1
  99. package/dist/internal/script-kind.js.map +1 -1
  100. package/dist/internal/source-facts.d.ts +2 -0
  101. package/dist/internal/source-facts.d.ts.map +1 -1
  102. package/dist/internal/source-facts.js +11 -0
  103. package/dist/internal/source-facts.js.map +1 -1
  104. package/dist/internal/typescript-import.d.ts +1 -1
  105. package/dist/internal/typescript-import.d.ts.map +1 -1
  106. package/dist/internal/typescript-import.js +5 -3
  107. package/dist/internal/typescript-import.js.map +1 -1
  108. package/dist/introspect.d.ts +6 -6
  109. package/dist/introspect.d.ts.map +1 -1
  110. package/dist/introspect.js +55 -24
  111. package/dist/introspect.js.map +1 -1
  112. package/dist/levels-tool.d.ts.map +1 -1
  113. package/dist/levels-tool.js +93 -15
  114. package/dist/levels-tool.js.map +1 -1
  115. package/dist/load.d.ts +31 -8
  116. package/dist/load.d.ts.map +1 -1
  117. package/dist/load.js +37 -7
  118. package/dist/load.js.map +1 -1
  119. package/dist/mcp-context.d.ts +5 -5
  120. package/dist/mcp-context.d.ts.map +1 -1
  121. package/dist/mcp-context.js +2 -2
  122. package/dist/mcp-context.js.map +1 -1
  123. package/dist/mcp-exec.d.ts +12 -4
  124. package/dist/mcp-exec.d.ts.map +1 -1
  125. package/dist/mcp-exec.js +50 -13
  126. package/dist/mcp-exec.js.map +1 -1
  127. package/dist/mcp-http.js +1 -1
  128. package/dist/mcp-io.d.ts +17 -0
  129. package/dist/mcp-io.d.ts.map +1 -1
  130. package/dist/mcp-io.js +40 -0
  131. package/dist/mcp-io.js.map +1 -1
  132. package/dist/mcp-isolate.d.ts.map +1 -1
  133. package/dist/mcp-isolate.js +3 -2
  134. package/dist/mcp-isolate.js.map +1 -1
  135. package/dist/mcp-project-child.js +1 -1
  136. package/dist/mcp-project-child.js.map +1 -1
  137. package/dist/mcp-root.d.ts.map +1 -1
  138. package/dist/mcp-root.js +50 -10
  139. package/dist/mcp-root.js.map +1 -1
  140. package/dist/mcp-ws.d.ts.map +1 -1
  141. package/dist/mcp-ws.js +3 -1
  142. package/dist/mcp-ws.js.map +1 -1
  143. package/dist/mcp.d.ts.map +1 -1
  144. package/dist/mcp.js +8 -2
  145. package/dist/mcp.js.map +1 -1
  146. package/dist/migrate-layout.d.ts +7 -0
  147. package/dist/migrate-layout.d.ts.map +1 -1
  148. package/dist/migrate-layout.js +958 -71
  149. package/dist/migrate-layout.js.map +1 -1
  150. package/dist/output-schemas.d.ts +94 -0
  151. package/dist/output-schemas.d.ts.map +1 -0
  152. package/dist/output-schemas.js +864 -0
  153. package/dist/output-schemas.js.map +1 -0
  154. package/dist/port.d.ts.map +1 -1
  155. package/dist/port.js +3 -2
  156. package/dist/port.js.map +1 -1
  157. package/dist/project-facts.d.ts +15 -4
  158. package/dist/project-facts.d.ts.map +1 -1
  159. package/dist/project-facts.js +1 -0
  160. package/dist/project-facts.js.map +1 -1
  161. package/dist/recipes/4.0.0.d.ts.map +1 -1
  162. package/dist/recipes/4.0.0.js +2 -0
  163. package/dist/recipes/4.0.0.js.map +1 -1
  164. package/dist/recipes/5.0.0.d.ts +11 -0
  165. package/dist/recipes/5.0.0.d.ts.map +1 -0
  166. package/dist/recipes/5.0.0.js +34 -0
  167. package/dist/recipes/5.0.0.js.map +1 -0
  168. package/dist/recipes/index.d.ts +7 -0
  169. package/dist/recipes/index.d.ts.map +1 -1
  170. package/dist/recipes/index.js +2 -0
  171. package/dist/recipes/index.js.map +1 -1
  172. package/dist/replay.d.ts.map +1 -1
  173. package/dist/replay.js +12 -3
  174. package/dist/replay.js.map +1 -1
  175. package/dist/review.d.ts +3 -1
  176. package/dist/review.d.ts.map +1 -1
  177. package/dist/review.js +38 -7
  178. package/dist/review.js.map +1 -1
  179. package/dist/rules/codes.d.ts +10 -0
  180. package/dist/rules/codes.d.ts.map +1 -1
  181. package/dist/rules/codes.js +10 -0
  182. package/dist/rules/codes.js.map +1 -1
  183. package/dist/rules/comment-markers.d.ts +16 -7
  184. package/dist/rules/comment-markers.d.ts.map +1 -1
  185. package/dist/rules/comment-markers.js +58 -19
  186. package/dist/rules/comment-markers.js.map +1 -1
  187. package/dist/rules/contract-visibility.d.ts +3 -0
  188. package/dist/rules/contract-visibility.d.ts.map +1 -0
  189. package/dist/rules/contract-visibility.js +337 -0
  190. package/dist/rules/contract-visibility.js.map +1 -0
  191. package/dist/rules/data-guard.d.ts +7 -0
  192. package/dist/rules/data-guard.d.ts.map +1 -1
  193. package/dist/rules/data-guard.js +319 -37
  194. package/dist/rules/data-guard.js.map +1 -1
  195. package/dist/rules/layout.d.ts +4 -0
  196. package/dist/rules/layout.d.ts.map +1 -0
  197. package/dist/rules/layout.js +19 -0
  198. package/dist/rules/layout.js.map +1 -0
  199. package/dist/rules/legacy.d.ts.map +1 -1
  200. package/dist/rules/legacy.js +158 -43
  201. package/dist/rules/legacy.js.map +1 -1
  202. package/dist/rules/pages.d.ts +24 -0
  203. package/dist/rules/pages.d.ts.map +1 -1
  204. package/dist/rules/pages.js +221 -3
  205. package/dist/rules/pages.js.map +1 -1
  206. package/dist/rules/routes.d.ts +6 -0
  207. package/dist/rules/routes.d.ts.map +1 -1
  208. package/dist/rules/routes.js +111 -16
  209. package/dist/rules/routes.js.map +1 -1
  210. package/dist/rules/secrets.js +2 -2
  211. package/dist/rules/secrets.js.map +1 -1
  212. package/dist/rules/security.d.ts +12 -1
  213. package/dist/rules/security.d.ts.map +1 -1
  214. package/dist/rules/security.js +281 -68
  215. package/dist/rules/security.js.map +1 -1
  216. package/dist/rules/zones.d.ts +13 -0
  217. package/dist/rules/zones.d.ts.map +1 -1
  218. package/dist/rules/zones.js +192 -40
  219. package/dist/rules/zones.js.map +1 -1
  220. package/dist/scaffold.d.ts.map +1 -1
  221. package/dist/scaffold.js +6 -4
  222. package/dist/scaffold.js.map +1 -1
  223. package/dist/sdk.d.ts.map +1 -1
  224. package/dist/sdk.js +14 -5
  225. package/dist/sdk.js.map +1 -1
  226. package/dist/stylex-migrate.d.ts.map +1 -1
  227. package/dist/stylex-migrate.js +163 -7
  228. package/dist/stylex-migrate.js.map +1 -1
  229. package/dist/sync-manifest.d.ts +10 -2
  230. package/dist/sync-manifest.d.ts.map +1 -1
  231. package/dist/sync-manifest.js +42 -18
  232. package/dist/sync-manifest.js.map +1 -1
  233. package/dist/test-tool.d.ts.map +1 -1
  234. package/dist/test-tool.js +12 -3
  235. package/dist/test-tool.js.map +1 -1
  236. package/dist/typecheck-coverage.d.ts +20 -0
  237. package/dist/typecheck-coverage.d.ts.map +1 -0
  238. package/dist/typecheck-coverage.js +129 -0
  239. package/dist/typecheck-coverage.js.map +1 -0
  240. package/dist/upgrade.d.ts +69 -0
  241. package/dist/upgrade.d.ts.map +1 -1
  242. package/dist/upgrade.js +420 -77
  243. package/dist/upgrade.js.map +1 -1
  244. package/dist/verification-plan.d.ts.map +1 -1
  245. package/dist/verification-plan.js +11 -0
  246. package/dist/verification-plan.js.map +1 -1
  247. package/dist/version-drift.d.ts +22 -0
  248. package/dist/version-drift.d.ts.map +1 -0
  249. package/dist/version-drift.js +42 -0
  250. package/dist/version-drift.js.map +1 -0
  251. package/dist/web-app-options.d.ts +1 -1
  252. package/dist/web-app-options.d.ts.map +1 -1
  253. package/dist/web-app-options.js +1 -1
  254. package/dist/web-app-options.js.map +1 -1
  255. package/dist/work-graph.d.ts.map +1 -1
  256. package/dist/work-graph.js +20 -8
  257. package/dist/work-graph.js.map +1 -1
  258. package/docs/examples.json +54 -6
  259. package/docs/llms-full.txt +1539 -138
  260. package/docs/types.json +1456 -206
  261. package/package.json +11 -12
  262. package/src/agent-files.ts +203 -0
  263. package/src/app-files.ts +34 -4
  264. package/src/assure-hydration.ts +15 -2
  265. package/src/capabilities-tool.ts +15 -3
  266. package/src/check-diagnostics.ts +73 -9
  267. package/src/check-scan.ts +1773 -381
  268. package/src/check.ts +237 -76
  269. package/src/cli.ts +141 -5
  270. package/src/client-module.ts +61 -0
  271. package/src/command-catalog.ts +181 -47
  272. package/src/component-check.ts +79 -0
  273. package/src/contract-proof.ts +3 -25
  274. package/src/contracts.ts +60 -3
  275. package/src/db-tool.ts +5 -2
  276. package/src/dev-bun-config.ts +51 -10
  277. package/src/diagnostics.ts +3 -0
  278. package/src/diff-tool.ts +33 -26
  279. package/src/doctor.ts +43 -21
  280. package/src/env-file.ts +7 -0
  281. package/src/fix-recipes.ts +34 -3
  282. package/src/hand-built-manifest.ts +144 -0
  283. package/src/hooks-report.ts +135 -0
  284. package/src/init-agents.ts +17 -11
  285. package/src/internal/nifra-project.ts +191 -0
  286. package/src/internal/project-specifiers.ts +424 -0
  287. package/src/internal/script-kind.ts +2 -1
  288. package/src/internal/source-facts.ts +25 -0
  289. package/src/internal/typescript-import.ts +10 -3
  290. package/src/introspect.ts +65 -27
  291. package/src/levels-tool.ts +117 -20
  292. package/src/load.ts +78 -14
  293. package/src/mcp-context.ts +7 -7
  294. package/src/mcp-exec.ts +60 -17
  295. package/src/mcp-http.ts +1 -1
  296. package/src/mcp-io.ts +41 -0
  297. package/src/mcp-isolate.ts +9 -2
  298. package/src/mcp-project-child.ts +1 -1
  299. package/src/mcp-root.ts +58 -8
  300. package/src/mcp-ws.ts +4 -2
  301. package/src/mcp.ts +8 -3
  302. package/src/migrate-layout.ts +1109 -70
  303. package/src/output-schemas.ts +1084 -0
  304. package/src/port.ts +3 -2
  305. package/src/project-facts.ts +17 -3
  306. package/src/recipes/4.0.0.ts +2 -0
  307. package/src/recipes/5.0.0.ts +35 -0
  308. package/src/recipes/index.ts +10 -0
  309. package/src/replay.ts +14 -3
  310. package/src/review.ts +39 -10
  311. package/src/rules/codes.ts +10 -0
  312. package/src/rules/comment-markers.ts +57 -21
  313. package/src/rules/contract-visibility.ts +383 -0
  314. package/src/rules/data-guard.ts +351 -42
  315. package/src/rules/layout.ts +22 -0
  316. package/src/rules/legacy.ts +184 -51
  317. package/src/rules/pages.ts +238 -3
  318. package/src/rules/routes.ts +124 -16
  319. package/src/rules/secrets.ts +2 -2
  320. package/src/rules/security.ts +352 -71
  321. package/src/rules/zones.ts +205 -38
  322. package/src/scaffold.ts +6 -4
  323. package/src/sdk.ts +14 -5
  324. package/src/stylex-migrate.ts +134 -7
  325. package/src/sync-manifest.ts +48 -18
  326. package/src/test-tool.ts +18 -3
  327. package/src/typecheck-coverage.ts +134 -0
  328. package/src/upgrade.ts +487 -71
  329. package/src/verification-plan.ts +24 -0
  330. package/src/version-drift.ts +67 -0
  331. package/src/web-app-options.ts +2 -2
  332. 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,30 @@ 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.
1408
+
1313
1409
  A field named like a credential (`password`, `token`,
1314
1410
  `apiKey`) fails the route when it loads unless its schema is wrapped in
1315
1411
  `t.declassified("why it may reach the browser", ...)`; a schema library that
@@ -1333,7 +1429,7 @@ import type { Route } from "./+types/new"
1333
1429
 
1334
1430
  export const actionOutput = t.object({ ok: t.boolean() })
1335
1431
 
1336
- export async function action({ api, request }: Route.ActionArgs) {
1432
+ export const action: Route.Action = async ({ api, request }) => {
1337
1433
  const form = await request.formData()
1338
1434
  await api.users.post({ name: String(form.get("name")) })
1339
1435
  return { ok: true } // the page's loaders revalidate automatically
@@ -1608,6 +1704,10 @@ Nothing here touches the kernel or the request path. An app that mounts no serve
1608
1704
  pays exactly nothing for the feature - which is the reason it is built as a plugin over the
1609
1705
  public registration API rather than as a bespoke dispatcher.
1610
1706
 
1707
+ ## Nifra - Server components
1708
+
1709
+ > Server-only views with schema-guarded props, owner-route authorization and native React, Vue, Svelte, Solid and Preact hydration.
1710
+
1611
1711
  ## Nifra - Optimistic UI & fetchers
1612
1712
 
1613
1713
  > Optimistic updates and concurrent fetchers in Nifra: instant feedback, row-level mutations.
@@ -1838,7 +1938,9 @@ wouldn't propagate across instances. In production use a shared, durable store.
1838
1938
  `KVCacheStore` wraps a Workers KV namespace; any backend that fits the small
1839
1939
  `CacheStore` interface (Redis, the Cache API) works too. Every read is validated
1840
1940
  before it's trusted, so a corrupt or version-skewed entry is treated as a miss, not served
1841
- broken.
1941
+ broken. A store that also implements `fence()` keeps a purge that lands while a
1942
+ page renders from being undone when that render stores the page it built from the old data;
1943
+ both built-in stores do.
1842
1944
 
1843
1945
  ```ts
1844
1946
  // doc-check: skip - Workers entry: `Env`/`ExecutionContext` globals + your `app` from server.ts.
@@ -1858,6 +1960,13 @@ export default {
1858
1960
  }
1859
1961
  ```
1860
1962
 
1963
+ A store outage never fails a page. A read that throws, rejects, or outlives
1964
+ `storeTimeoutMs` is a miss and the page renders; a failed write still returns the
1965
+ rendered page; a failed background regeneration keeps the stale entry. Each failure goes to
1966
+ `onStoreError(error, &#123; op &#125;)`, which by default logs one line per operation
1967
+ per minute without the cache key. Set `storeTimeoutMs` for a network store, so a
1968
+ hung connection costs a miss rather than the request.
1969
+
1861
1970
  ### On-demand revalidation
1862
1971
 
1863
1972
  Purge a path the moment its data changes (a CMS webhook, an admin action) with
@@ -1872,6 +1981,21 @@ curl -X POST 'https://example.com/__nifra/revalidate?path=/posts/hello' \
1872
1981
  # → { "revalidated": "/posts/hello" } (the token is checked in constant time)
1873
1982
  ```
1874
1983
 
1984
+ A per-request CSP nonce makes a page uncacheable. For a strict policy under ISR, give
1985
+ `createWebApp` a `csp` from `createCspPolicy` instead of a
1986
+ `nonce`: constant scripts are allowed by hash, so only a page that needs a nonce
1987
+ carries one - see [A strict CSP that still caches](/docs/contract#strict-csp).
1988
+
1989
+ A page that must not be cached as a whole, because it is personalized or carries a nonce, can
1990
+ still cache the data its loader reads. Wrap the expensive call in `cache.wrap` from
1991
+ `@nifrajs/cache`, with the same tags the pages use. Concurrent loads of one key run
1992
+ once, and a store outage falls through to the loader. Pass that cache to
1993
+ `revalidateEndpoint` as `dataCaches: [cache]`. A tag purge then
1994
+ invalidates it first, then the page store, then the CDN, so no page is rebuilt from stale
1995
+ data. If a data cache fails, the endpoint answers `500` before it touches the page
1996
+ store, so a retry purges everything again. `createInvalidator` from
1997
+ `@nifrajs/web/cdn` takes the same `dataCaches` for purges from app code.
1998
+
1875
1999
  `revalidateTags` names what a purge by tag reaches. A list tags every page of the
1876
2000
  route alike; a function of the route's params and URL tags each page on its own. Tags travel to
1877
2001
  the CDN and to anyone reading the origin, so build them from params, never from loader data.
@@ -2570,6 +2694,54 @@ stripped from `event`/`id` to prevent frame injection); multi-line
2570
2694
  `data` is split into multiple `data:` lines per the spec; and
2571
2695
  `keepAlive` emits comment pings so idle proxies don't drop the connection.
2572
2696
 
2697
+ A client that stops reading is dropped once it has `maxBufferedBytes` (default 1
2698
+ MiB) of events unread: `stream.signal` aborts with `sse_client_too_slow`,
2699
+ the queued frames are released, and later sends do nothing, so one stalled reader cannot hold
2700
+ server memory without limit. Raise it for a burst larger than that sent before the client can
2701
+ read, or pass `Infinity` to turn the limit off.
2702
+
2703
+ ### Typed SSE routes
2704
+
2705
+ `app.sse(path, schema, run)` declares the event payload with `sse`, so
2706
+ `stream.send()` is type-checked and the typed client grows
2707
+ `.subscribe(onEvent)` with the same event type. A route whose schema declares a
2708
+ `body` is a POST stream: the body validates as on any POST, and
2709
+ `.subscribe(onEvent, &#123; body &#125;)` sends it. A GET stream reconnects after a
2710
+ drop and resumes with `Last-Event-ID`; a POST stream does not reconnect unless you
2711
+ pass `reconnect: true`, since a reconnect sends the body again. The client&apos;s
2712
+ `onRequest` hook runs before each connect, so a refreshed token reaches the stream.
2713
+
2714
+ ```ts
2715
+ import { client } from "@nifrajs/client"
2716
+ import { server } from "@nifrajs/core/server"
2717
+ import { streaming } from "@nifrajs/core/sse"
2718
+ import { t } from "@nifrajs/schema"
2719
+
2720
+ declare function generate(prompt: string): AsyncIterable<string>
2721
+
2722
+ export const app = server()
2723
+ .use(streaming())
2724
+ // A GET stream: the event payload is contracted by `sse`.
2725
+ .sse("/feed", { sse: t.object({ id: t.integer(), title: t.string() }) }, (_c, stream) => {
2726
+ stream.send({ id: 1, title: "hello" })
2727
+ stream.close()
2728
+ })
2729
+ // A body makes it a POST stream, validated like any POST: a chat that posts its prompt.
2730
+ .sse(
2731
+ "/chat",
2732
+ { body: t.object({ prompt: t.string() }), sse: t.object({ token: t.string() }) },
2733
+ async (c, stream) => {
2734
+ for await (const token of generate(c.body.prompt)) stream.send({ token })
2735
+ stream.close()
2736
+ },
2737
+ )
2738
+
2739
+ const api = client<typeof app>("https://example.com")
2740
+ const feed = api.feed.subscribe((post) => console.log(post.title))
2741
+ api.chat.subscribe((event) => console.log(event.token), { body: { prompt: "Hi" } })
2742
+ feed.close()
2743
+ ```
2744
+
2573
2745
  ## Nifra - Query cache
2574
2746
 
2575
2747
  > Nifra's keyed query cache: useQuery / createQuery, dedup, staleness, invalidation.
@@ -2657,7 +2829,7 @@ than small.
2657
2829
  ```ts
2658
2830
  import { html, vanillaAdapter } from "@nifrajs/web-vanilla"
2659
2831
 
2660
- // routes/hotels.ts - a route file, no .tsx needed.
2832
+ // routes/hotels.tsx - a page keeps a route extension even with no JSX in it.
2661
2833
  export default function Hotels({ data }: { data: { hotels: Array<{ name: string }> } }) {
2662
2834
  // Interpolated values are escaped; wrap trusted markup in raw() to opt out deliberately.
2663
2835
  return html`<ul>${data.hotels.map((h) => html`<li>${h.name}</li>`)}</ul>`
@@ -2877,6 +3049,48 @@ const app = server()
2877
3049
  .use(rateLimit({ store: new MemoryStore(), max: 100, windowMs: 60_000 }))
2878
3050
  ```
2879
3051
 
3052
+ To refuse a request in `onRequest`, return the same `status()` value a
3053
+ route would: `return status(401, { error: "key required" })`. It takes the
3054
+ normal response path, so the body is serialized with its content-type, and declared
3055
+ `responseHeaders` and every `onResponse` hook apply to it. A hand-built
3056
+ `Response` still works.
3057
+
3058
+ #### Which hooks run where
3059
+
3060
+ Where a hook is registered decides which requests it sees. `nifra routes --hooks`
3061
+ prints, for every backend route and for each prefix's unmatched paths, the hooks that run
3062
+ and the site that registered them (`app`, `group /admin`,
3063
+ `merge #1`, `mount /m`), named by plugin where one registered them:
3064
+
3065
+ -
3066
+ A **group**'s hooks cover its whole prefix, including that prefix's 404 and 405
3067
+ responses.
3068
+
3069
+ -
3070
+ A **merged** server's `onRequest` runs only for its own routes; its
3071
+ response hooks and static headers become app-wide.
3072
+
3073
+ -
3074
+ A **mount** runs the child's full lifecycle inside the parent's request and response
3075
+ hooks.
3076
+
3077
+ -
3078
+ `around`, `derive`, and `beforeHandle` apply only to
3079
+ routes declared after them.
3080
+
3081
+ -
3082
+ A **WebSocket** route, listed as `WS /path`, runs the request hooks, then the
3083
+ authentication stages, `derive`s, `beforeHandle`s and
3084
+ `onError` declared before it. Its handshake runs no `around`,
3085
+ `afterHandle` or response hooks, but a refused handshake carries the
3086
+ app&rsquo;s `responseHeaders()`.
3087
+
3088
+ `nifra routes --hooks --json` writes the report; `--since <file>`
3089
+ prints what runs differently from a saved one. `nifra upgrade --write` saves the
3090
+ report before it edits anything and prints the scope diff, and `nifra check --json`
3091
+ includes it as `hooks` when `nifra.assurance.ts` names a
3092
+ `source`. Origins are recorded only outside production builds.
3093
+
2880
3094
  ### Header middleware: one portable hook, fast everywhere
2881
3095
 
2882
3096
  Most response middleware only reads or writes **headers** - security headers, CORS
@@ -3177,6 +3391,36 @@ const app = server()
3177
3391
  // poweredBy() is opt-in (Nifra emits no X-Powered-By by default). combine(a, b, c) bundles several into one plugin.
3178
3392
  ```
3179
3393
 
3394
+ ### Rate-limit tiers
3395
+
3396
+ A `rateLimit()` inside a `group()` still sits under an app-wide one, so a
3397
+ group can only be stricter than the app. `rateLimitTiers()` is one limiter that
3398
+ charges each request to exactly one bucket - the tier with the longest static
3399
+ `prefix` holding the path, else the `floor` - so a public API can allow
3400
+ more than the floor while a login path allows less. It runs before routing, so 404s, 405s and
3401
+ bodies that fail validation are charged too. Prefixes are checked when the limiter is built
3402
+ (static, no trailing slash, no duplicates), and each tier counts in its own keys, so one store
3403
+ serves them all. `skip`, on both limiters, leaves a request uncharged.
3404
+
3405
+ ```ts
3406
+ import { server } from "@nifrajs/core/server"
3407
+ import { MemoryStore, rateLimitTiers } from "@nifrajs/middleware"
3408
+
3409
+ // Each request is charged to ONE bucket: the longest matching prefix, else the floor.
3410
+ // A tier may sit above the floor - tiers never stack on it.
3411
+ const app = server().use(
3412
+ rateLimitTiers({
3413
+ store: new MemoryStore(),
3414
+ floor: { max: 120, windowMs: 60_000 },
3415
+ tiers: [
3416
+ { prefix: "/api/public", max: 600, windowMs: 60_000 },
3417
+ { prefix: "/auth/login", max: 10, windowMs: 60_000 },
3418
+ ],
3419
+ skip: (req) => new URL(req.url).pathname === "/health", // never charged
3420
+ }),
3421
+ )
3422
+ ```
3423
+
3180
3424
  ### Security middleware
3181
3425
 
3182
3426
  The security set - `csrf` (signed double-submit + Origin/Referer), `jwt`,
@@ -3249,8 +3493,10 @@ keeps the lane out of your bundle rather than shipping a disabled branch to ever
3249
3493
  serves the payload **unchanged** - so turning it on in staging can never be the
3250
3494
  thing that broke production. `"enforce"` serializes the validated value
3251
3495
  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
3496
+ a `c.json(...)` reply alike, whether the handler returns or throws it, a hook
3497
+ returns it early, or `onError` answers with it; only a hand-built
3498
+ `Response` (a redirect, a stream) passes unchecked. Install it before the routes it
3499
+ should cover: like
3254
3500
  `idempotency()`, the decision is made per route at registration.
3255
3501
 
3256
3502
  Enforcement follows your schema&rsquo;s own semantics, because Standard Schema exposes
@@ -3654,7 +3900,9 @@ Verification is **constant-time** - the provider's signature goes straight into
3654
3900
  `crypto.subtle.verify`, so a wrong signature can't be discovered byte-by-byte through
3655
3901
  timing. Presets cover **Stripe** (parses `t=…,v1=…` and enforces a 5-minute replay
3656
3902
  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
3903
+ preset takes an explicit header, encoding (`"hex"`, `"base64"` or
3904
+ `"base64url"`, padded or not), and prefix for anything else. A signature that does not
3905
+ decode is refused as `malformed_signature`, never thrown. Pass an array of secrets to
3658
3906
  accept either during a key rotation.
3659
3907
 
3660
3908
  ### Idempotency - `idempotency()` middleware
@@ -3786,6 +4034,13 @@ group can apply its own configuration over the app&apos;s - `admin.use(securityH
3786
4034
  inside `app.group("/admin", ...)` - and each header it sets replaces the app&apos;s
3787
4035
  value on the group&apos;s routes only.
3788
4036
 
4037
+ A fixed `Content-Security-Policy` here cannot allow nifra&apos;s inline scripts by
4038
+ nonce. For a strict `script-src` on web pages, give `createWebApp` a
4039
+ `csp` from `createCspPolicy`: it allows the framework&apos;s constant
4040
+ scripts by hash and adds a nonce only to a page that needs one, so every other page sends the
4041
+ same header and stays cacheable under `withISR` or a CDN. See
4042
+ [A strict CSP that still caches](/docs/contract#strict-csp).
4043
+
3789
4044
  ### Route assurance - prove every route is guarded
3790
4045
 
3791
4046
  Installing security middleware is not the same as proving it covers every route. Nifra&apos;s
@@ -3806,11 +4061,15 @@ export default defineAssuranceConfig({
3806
4061
  rules: [
3807
4062
  { name: "health", match: { paths: ["/health"] }, require: [],
3808
4063
  forbid: [NIFRA_ASSURANCE.AUTHENTICATED] },
3809
- { name: "mutation", match: { methods: ["POST", "PUT", "PATCH", "DELETE"] },
4064
+ // Every method but these, so a custom one such as PURGE is a mutation too.
4065
+ { name: "mutation", match: { exceptMethods: ["GET", "HEAD", "OPTIONS", "WS"] },
3810
4066
  require: [NIFRA_ASSURANCE.AUTHENTICATED, NIFRA_ASSURANCE.CSRF,
3811
4067
  NIFRA_ASSURANCE.BODY_BOUNDED] },
3812
4068
  { name: "read", match: { methods: ["GET", "HEAD"] },
3813
4069
  require: [NIFRA_ASSURANCE.AUTHENTICATED] },
4070
+ // WebSocket routes reflect under the pseudo-method "WS".
4071
+ { name: "socket", match: { methods: ["WS"] },
4072
+ require: [NIFRA_ASSURANCE.AUTHENTICATED] },
3814
4073
  ],
3815
4074
  },
3816
4075
  })
@@ -3824,6 +4083,14 @@ while authentication and other order-scoped hooks cover only routes registered a
3824
4083
  Method and path filters prevent a narrow guard from claiming broader coverage. The evaluation
3825
4084
  runs only through reflection or `nifra assure`, so requests pay no assurance cost.
3826
4085
 
4086
+ WebSocket routes are reflected under the pseudo-method `"WS"`, so a policy classifies
4087
+ every socket too. A handshake runs the auth guards registered before the socket and the
4088
+ app&apos;s request hooks, so a socket carries authentication and rate-limit evidence from them.
4089
+ It never claims evidence whose enforcement its message handlers bypass, such as a durable
4090
+ command journal. A socket declares its effects with
4091
+ `app.ws(path, { capabilities })`, and capability assurance checks them against
4092
+ the module that registers it.
4093
+
3827
4094
  ### Effect assurance - declared capability versus provenance
3828
4095
 
3829
4096
  Authentication does not reveal what a route can do. Capability assurance compares an exact
@@ -3887,7 +4154,11 @@ Approval resumes are HMAC-signed, expire, bind to the tenant, principal, capabil
3887
4154
  digest, and are consumed atomically once. The effect journal marks execution before the provider
3888
4155
  call, so a crash after an external commit remains `ambiguous` for reconciliation instead
3889
4156
  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.
4157
+ token-only ledger and compensate committed steps in reverse order with retry/backoff state. One
4158
+ `resume` or `compensate` call owns a saga at a time through a lease every
4159
+ transition renews (`leaseMs`, default 5 minutes): a second call meanwhile throws
4160
+ `SagaConcurrencyError` and changes nothing, and a call that died is taken over once its
4161
+ lease lapses.
3891
4162
 
3892
4163
  ```ts
3893
4164
  // doc-check: skip - durable store implementations are deployment-specific.
@@ -4109,6 +4380,15 @@ there and grants exactly `db.read` - it never reaches the driver underneath. Kee
4109
4380
  the two halves import-disjoint, at the seam and at the route modules, and every route's
4110
4381
  declaration equals its reach.
4111
4382
 
4383
+ The walk starts at the module that registers the route, which Nifra finds by reading the
4384
+ source without running it. It reads a path written as a string literal, or as a name bound to
4385
+ a module-level `const` string, with or without `as const`. The
4386
+ `const` can live in the same file or be exported from a project file it imports.
4387
+ 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
4388
+ this way. `nifra check` names that expression in an NF-C036 finding, because the
4389
+ route then has no module to start from. List the module in
4390
+ `provenance.routeModules` to give it one.
4391
+
4112
4392
  ### Runtime beacons
4113
4393
 
4114
4394
  Static provenance answers what a _module_ can reach, so its evidence is as broad as the
@@ -4220,6 +4500,33 @@ $ nifra check
4220
4500
  • known gap, not capability-analyzed: /api/auth/* - better-auth's own handler; its effects are its own
4221
4501
  ```
4222
4502
 
4503
+ A mount can also state the routes it serves and the capabilities each one exercises. Pass an
4504
+ object with the `reason` and a `routes` list. Each path is the full
4505
+ request path and must sit under the mount:
4506
+
4507
+ ```ts
4508
+ export const app = server().mount({
4509
+ path: "/api/auth",
4510
+ app: authHandler,
4511
+ opaque: {
4512
+ reason: "better-auth's own handler; its effects are its own",
4513
+ routes: [
4514
+ { method: "POST", path: "/api/auth/sign-in/email", capabilities: ["session.write"] },
4515
+ { method: "GET", path: "/api/auth/get-session", capabilities: ["session.read"] },
4516
+ ],
4517
+ },
4518
+ })
4519
+ ```
4520
+
4521
+ Assurance lists these routes with `analysis: "declared"`, apart from the routes it
4522
+ analyzed. The mount stays a known gap with its reason. A declared capability the policy does
4523
+ not define fails, as it does on a route, and so does a domain write declared on
4524
+ `GET`. Nothing else is checked, because there is no evidence to read. The routes
4525
+ also appear in the capability lockfile, so a changed declaration shows up in review. They
4526
+ appear in the `nifra context` route index too. An L4 run lists them as having no
4527
+ contract to test. A malformed entry, or a path outside the mount, stops the analysis with an
4528
+ error that names it.
4529
+
4223
4530
  ### The lockfile
4224
4531
 
4225
4532
  Once assurance passes, snapshot the result. The lockfile is a review artifact: a route that
@@ -4417,6 +4724,102 @@ export async function loader({ request }: Route.LoaderArgs) {
4417
4724
  }
4418
4725
  ```
4419
4726
 
4727
+ Without `redirectTo`, a guard denies with a 401 JSON
4728
+ `{ ok: false, error: "unauthorized" }`. `unauthenticated` sets that
4729
+ response's status (400-599), body and headers instead; the same option exists on
4730
+ `@nifrajs/better-auth`'s `requireSession`, `requirePrincipal`
4731
+ and `authed`, and on `@nifrajs/authjs`'s `requireAuthUser`. A
4732
+ guard given both `redirectTo` and `unauthenticated` throws.
4733
+
4734
+ ```ts
4735
+ // doc-check: skip - fragment: `session` is the request's session.
4736
+ requireUser(session, "userId", {
4737
+ unauthenticated: { status: 403, body: { error: "login_required" }, headers: { "www-authenticate": "Session" } },
4738
+ })
4739
+ ```
4740
+
4741
+ ### One request-scoped context from authentication to the handler
4742
+
4743
+ `around` wraps everything after routing for the routes declared after it:
4744
+ authentication, body validation, `derive` and the handler. An
4745
+ `AsyncLocalStorage` frame or a database session setting opened there stays open
4746
+ until the response is decided. `authenticate` runs inside that frame before
4747
+ validation, resolves the caller once and types it as `c.principal`, so it can write
4748
+ what it resolved into the frame instead of a second lookup in a `derive`. Both are
4749
+ order-scoped, so inside a `group` they cover only its routes.
4750
+
4751
+ ```ts
4752
+ // doc-check: skip - fragment: `app`, `readSession`, `Note`, and `db` are your application's.
4753
+ import { AsyncLocalStorage } from "node:async_hooks"
4754
+ import { authenticated, rejected } from "@nifrajs/core"
4755
+
4756
+ const frame = new AsyncLocalStorage<{ userId?: string }>()
4757
+
4758
+ app.group("/api", (api) =>
4759
+ api
4760
+ // Opens before authentication and closes after the response: validation, derive and the
4761
+ // handler all run inside it, and so does a thrown Response.
4762
+ .around((_c, next) => frame.run({}, next))
4763
+ // Resolves the caller once: typed as c.principal, and written into the open frame.
4764
+ .authenticate({
4765
+ id: "session",
4766
+ run: async (input) => {
4767
+ const session = await readSession(input.headers.get("cookie"))
4768
+ if (session === null) return rejected()
4769
+ const store = frame.getStore()
4770
+ if (store !== undefined) store.userId = session.userId
4771
+ return authenticated(session)
4772
+ },
4773
+ })
4774
+ .post("/notes", { body: Note }, (c) => db.notes.insert({ ...c.body, owner: c.principal.userId })),
4775
+ )
4776
+ ```
4777
+
4778
+ ### Authentication runs first
4779
+
4780
+ `authenticate()` refuses a request before anything reads it: the body is not read,
4781
+ no schema runs, and no `derive` or `beforeHandle` sees it. An
4782
+ unauthenticated caller gets the 401 (or the refusal `run` returns), never a 413 or a
4783
+ 422 that describes the route's input. `basicAuth`, `bearer`,
4784
+ `apiKey` and `jwt` from `@nifrajs/middleware` run there. With
4785
+ `optional: true` they never refuse, so validation answers as usual.
4786
+
4787
+ `run` receives the method, pathname, headers, path params, the raw
4788
+ `search` and the parsed `query` (frozen, read by the parser
4789
+ `c.query` uses; a repeated key is an array), the `request` (never read
4790
+ its body), `env`, `clientIp` and an abort `signal`. Every
4791
+ field is the caller's and unvalidated. Read the query only for a short-lived, single-use
4792
+ ticket, such as one for a WebSocket handshake a browser cannot set a header on: a URL lands in
4793
+ proxy and access logs. The dev feed records a query's keys, never its values.
4794
+
4795
+ ```ts
4796
+ // doc-check: skip - fragment: `app` and `tickets` (a single-use store) are your application's.
4797
+ import { authenticated, rejected } from "@nifrajs/core"
4798
+
4799
+ app
4800
+ .authenticate({
4801
+ id: "socket-ticket",
4802
+ run: async (input) => {
4803
+ // A browser cannot set a header on a WebSocket handshake, so the ticket rides in the URL.
4804
+ const ticket = input.query.ticket
4805
+ if (typeof ticket !== "string") return rejected()
4806
+ const user = await tickets.take(ticket) // single use, expires in seconds
4807
+ return user === null ? rejected() : authenticated(user)
4808
+ },
4809
+ })
4810
+ .ws("/live", { upgrade: (c) => ({ user: c.principal.id }) })
4811
+ ```
4812
+
4813
+ A guard written as a `beforeHandle` runs after validation, so an unauthenticated
4814
+ caller reaches the body parser. When `nifra check` loads
4815
+ `backend/app.ts` (it does when a `contracts.lock.json` exists), NF-S008
4816
+ names each route that claims authentication this way and declares an input schema. Move the
4817
+ check into `authenticate()`, or set
4818
+ `validationOrder: "auth-before-validation"` on the route to run its hooks first. A
4819
+ rate limiter written as a `beforeHandle` behind `authenticate()` does not
4820
+ see refused requests; `rateLimit()` runs in `onRequest`, before routing,
4821
+ and counts them.
4822
+
4420
4823
  ### Session code stays in the backend
4421
4824
 
4422
4825
  The session manager, an auth instance and their secrets live in `backend/`, and only
@@ -5215,15 +5618,16 @@ app.listen(3000) // Bun
5215
5618
  ### Lifecycle
5216
5619
 
5217
5620
  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.
5621
+ `upgrade` is the only callback that runs before the socket opens, so it’s where a
5622
+ route rejects a connection or seeds its data.
5220
5623
 
5221
5624
  ```ts
5222
5625
  // doc-check: skip - illustrative lifecycle (empty close/error bodies, cookie shape).
5223
5626
  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.
5627
+ // upgrade(c) runs in the full HTTP request context, BEFORE the socket opens, after the
5628
+ // guards registered before this route. Return the per-connection data (→ ws.data, typed),
5629
+ // or a Response or status(...) to REJECT the upgrade. A thrown Response rejects with it;
5630
+ // any other throw goes to onError, then a 500.
5227
5631
  upgrade(c) {
5228
5632
  const user = c.cookies.session
5229
5633
  if (user === undefined) return new Response("unauthorized", { status: 401 })
@@ -5236,6 +5640,37 @@ app.ws<{ user: string }>("/chat", {
5236
5640
  })
5237
5641
  ```
5238
5642
 
5643
+ ### Guards
5644
+
5645
+ A handshake is a `GET`, and it runs the chain an HTTP `GET` registered
5646
+ at the same point runs: the `authenticate()` stages, `derive`s and
5647
+ `beforeHandle`s added before `app.ws()`, then `upgrade`. So
5648
+ auth middleware such as `basicAuth` or `jwt` guards the sockets
5649
+ registered after it, any guard can refuse the handshake with the response it would send an
5650
+ HTTP request, and `upgrade` reads what the guards derived. The origin check runs
5651
+ first. A socket registered before a guard stays public, like an HTTP route there. A browser
5652
+ cannot set a header on the handshake, so `authenticate()` can read a short-lived,
5653
+ single-use ticket from `input.query` - see
5654
+ [Authentication runs first](/docs/auth#auth-first).
5655
+
5656
+ ```ts
5657
+ // doc-check: skip - illustrative: an existing auth guard and session lookup.
5658
+ app
5659
+ .ws("/status", {}) // registered before the guard: public
5660
+ .authenticate(sessionAuth) // or .use(basicAuth(...)), .derive(...), .beforeHandle(...)
5661
+ .get("/me", (c) => c.principal)
5662
+ .ws("/chat", {
5663
+ // Reached only when sessionAuth accepted the handshake, exactly as GET /me is.
5664
+ upgrade: (c) => ({ user: c.principal.id }),
5665
+ })
5666
+ ```
5667
+
5668
+ A socket declares the effects its code may reach with `capabilities`, as an HTTP
5669
+ route does. `nifra capabilities check` compares them with what the registering
5670
+ module can reach, and the handshake context carries the runtime guard for
5671
+ `upgrade()`. Reflection lists every socket under the pseudo-method
5672
+ `"WS"`, so `nifra assure` classifies sockets too.
5673
+
5239
5674
  ### NifraWebSocket
5240
5675
 
5241
5676
  The `ws` handed to each callback is a portable wrapper over the runtime’s native
@@ -5388,6 +5823,47 @@ it). Install it when you use `app.ws()`; without it a WS upgrade gets a clean
5388
5823
  npm i ws
5389
5824
  ```
5390
5825
 
5826
+ ### In a composed app
5827
+
5828
+ `mount({ path, app })` carries WebSocket upgrades as well as HTTP: when the
5829
+ serving adapter sees an upgrade under the mount's path, it asks the child's
5830
+ `resolveWebSocketUpgrade` and upgrades its own socket with the outcome. Any nifra
5831
+ server with `websocket()` installed has one. A child that is not a nifra server can
5832
+ implement it too, building the outcome from `@nifrajs/core/ws`:
5833
+ `attachWebSocket` wires the socket to a handler and a `TopicRegistry`
5834
+ owns its pub/sub. On Bun, install `websocket()` on the parent or on a mounted nifra
5835
+ child, or `listen()` refuses to start with `INVALID_WS_RUNTIME`.
5836
+
5837
+ `mountFetch(path, handler)` and a plain fetch handler carry HTTP only. An upgrade
5838
+ request under such a path reaches the handler as an ordinary request and never becomes a
5839
+ socket. Serve that path's WebSockets from a mounted child, or keep that server beside nifra.
5840
+
5841
+ ```ts
5842
+ // server.ts - one listener; the chat child and a non-nifra child both take upgrades.
5843
+ import { server } from "@nifrajs/core/server"
5844
+ import { attachWebSocket, TopicRegistry, websocket, type WebSocketUpgradeOutcome } from "@nifrajs/core/ws"
5845
+
5846
+ const chat = server()
5847
+ .use(websocket())
5848
+ .ws("/room", { message: (ws, data) => ws.send(data) })
5849
+
5850
+ // Not a nifra server: it answers HTTP through fetch and builds its own upgrade outcome.
5851
+ const topics = new TopicRegistry()
5852
+ const legacy = {
5853
+ fetch: () => new Response("legacy"),
5854
+ resolveWebSocketUpgrade: (request: Request): WebSocketUpgradeOutcome =>
5855
+ new URL(request.url).pathname === "/feed"
5856
+ ? { kind: "upgrade", handler: { open: (ws) => ws.send("hello") }, data: undefined, pubsub: topics, attach: attachWebSocket }
5857
+ : { kind: "pass" },
5858
+ }
5859
+
5860
+ server()
5861
+ .use(websocket()) // Bun: the parent holds the socket table every mounted upgrade runs through
5862
+ .mount({ path: "/chat", app: chat, stripPrefix: true })
5863
+ .mount({ path: "/legacy", app: legacy, stripPrefix: true })
5864
+ .listen(3000)
5865
+ ```
5866
+
5391
5867
  ## Nifra - Deployment
5392
5868
 
5393
5869
  > One Nifra app, one command per target - Bun, Node (Docker), Deno Deploy, Cloudflare Pages, Vercel Edge.
@@ -5518,6 +5994,11 @@ hand one visitor a response produced for another:
5518
5994
  **Only content-hashed assets are precached.** A hashed URL names its bytes, so serving it
5519
5995
  from cache forever is correct by construction. Unhashed URLs are left to the network.
5520
5996
 
5997
+ -
5998
+ **Only the build's own URLs are answered, from the build's own cache.** Any other request,
5999
+ an API call included, goes to the network untouched, and a cache the page or another app
6000
+ stored on the origin is never read.
6001
+
5521
6002
  -
5522
6003
  **Documents are never cached.** Only navigations that FAIL are answered, and only with the
5523
6004
  offline page you nominate - which must be static and user-independent, since every visitor
@@ -5529,8 +6010,9 @@ for another.
5529
6010
  straight to the network.
5530
6011
 
5531
6012
  -
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.
6013
+ **The cache name carries the build id**, and activation deletes every older cache named
6014
+ `<cacheName>:<buildId>` - so a stale worker cannot pin an old build
6015
+ indefinitely, and the origin's other caches are left alone.
5534
6016
 
5535
6017
  Omit `offlineUrl` and a failed navigation simply fails, which is the honest default:
5536
6018
  an offline page you have not written is not better than a browser error.
@@ -5572,6 +6054,14 @@ content-typed, with an immutable cache - before the app runs, leaving the SSR fa
5572
6054
  untouched: `serve(app, {'{ port, static: { dir: new URL("./assets/", import.meta.url) } }'})`.
5573
6055
  On Cloudflare/Vercel the platform serves assets, so you don't need it there.
5574
6056
 
6057
+ Every file under the mount is cached as immutable by default. If the directory also holds
6058
+ files that keep their name across deploys, pass the build manifest's `assets` as
6059
+ `hashedFiles`: only those URL paths stay immutable, and every other file is sent
6060
+ with its validators so the browser revalidates it. The server that
6061
+ `nifra build --target bun` or `node` generates does this already. It
6062
+ caches only the client build's own content-hashed outputs as immutable. A file copied from
6063
+ `public/`, even under `public/assets/`, gets a one-day policy.
6064
+
5575
6065
  ### HTTPS without a proxy
5576
6066
 
5577
6067
  Most deploys end TLS in front of the app - a load balancer, a CDN, the platform - and the app
@@ -5766,6 +6256,40 @@ import { stylexVite } from "@nifrajs/web/plugins/stylex"
5766
6256
  export const vitePlugins = [react(), stylexVite()]
5767
6257
  ```
5768
6258
 
6259
+ ### Upgrading nifra
6260
+
6261
+ Every `@nifrajs/*` package, `nifra` and `create-nifra` ship
6262
+ together at one version. Move a workspace between releases with
6263
+ `nifra upgrade <version>` rather than by editing pins: it runs every release
6264
+ recipe between the oldest version your `package.json` files pin and the target,
6265
+ oldest first. A recipe moves a removed package to its replacement, rewrites imports that
6266
+ moved, lists comments a release stopped honoring, and pins each group dependency to the
6267
+ target, keeping its `^` or `~` unless you pass `--exact`.
6268
+ The pins in `overrides`, `resolutions` and
6269
+ `pnpm.overrides` move with them, since an override decides what installs.
6270
+
6271
+ ```ts
6272
+ nifra upgrade --list # the release targets this CLI knows
6273
+ nifra upgrade 5.0.0 # dry-run: every planned edit, nothing written
6274
+ nifra upgrade 5.0.0 --write # apply, then nifra check verifies (--no-verify skips it)
6275
+ nifra doctor # afterwards: no "mixed nifra versions" advisory left
6276
+ ```
6277
+
6278
+ It is a dry-run until `--write`, and the same run twice makes no further edits. An
6279
+ unknown version or a target below what is installed is refused (`--allow-downgrade`
6280
+ when the rollback is intended); a target newer than the running CLI prints the command for
6281
+ that release&apos;s CLI. It edits files under the workspace root only: a symlinked package
6282
+ directory or `package.json` is never followed, and a group package installed by a
6283
+ link from outside the root is listed under "Left alone" for you to upgrade in its
6284
+ own repository. So is another version pinned outside `package.json`, with its
6285
+ line: an MCP config such as `.mcp.json`, a Dockerfile or compose file, a CI
6286
+ workflow, and a lockfile other than the workspace&apos;s own (an image build&apos;s frozen
6287
+ lockfile, say), which you regenerate after installing. With `--write` it also
6288
+ prints how hook scope changed across the
6289
+ upgrade (`--no-hooks` skips that). `nifra doctor` and
6290
+ `nifra check` (NF-D002) point here when a workspace carries more than one version;
6291
+ see [mixed nifra versions](/docs/troubleshooting#mixed-versions).
6292
+
5769
6293
  ### Scope
5770
6294
 
5771
6295
  `nifra build` defaults to a self-hosted Bun server. Use `--target node`,
@@ -6310,8 +6834,10 @@ database connection is not the same as handing it a query tool, and this is the
6310
6834
  `@nifrajs/agent-protocol` is the small versioned session and event contract.
6311
6835
  `@nifrajs/coding-agent` provides the standalone `nifra-agent` host with
6312
6836
  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.
6837
+ RPC. `@nifrajs/pi` is an optional Pi backend, and `@nifrajs/acp` drives
6838
+ any Agent Client Protocol agent (Claude Code, Codex, Gemini CLI) over stdio; other backends
6839
+ implement the same protocol port and can prove it with `checkAgentBackend` from
6840
+ `@nifrajs/agent-protocol/conformance`.
6315
6841
 
6316
6842
  ```ts
6317
6843
  bun add @nifrajs/coding-agent @nifrajs/pi
@@ -6440,6 +6966,14 @@ resume. `@nifrajs/agent-telemetry` converts the same constrained evidence into
6440
6966
  fail-open OpenTelemetry run and step spans. Providers, credentials, durable storage, retention,
6441
6967
  and tenant policy remain application-owned ports.
6442
6968
 
6969
+ A step that suspends for approval resumes with the decision the caller sends in
6970
+ `resume.approval`. Over A2A, or on an AG-UI or `mountAgent` route that
6971
+ anyone can reach, that caller may be the party asking for the tool. Return
6972
+ `resumeApproval: "port"` from `ports` to keep the configured approval
6973
+ port authoritative: it is asked again on resume, receives the caller's decision as
6974
+ `callerDecision`, and its verdict stands. Without a port, such a resume suspends
6975
+ again.
6976
+
6443
6977
  ### Where your code and secrets run
6444
6978
 
6445
6979
  The server your client starts runs none of your project's code. Each tool that loads the app,
@@ -6766,6 +7300,17 @@ with `export const dev = { indicator: false }` in `nifra.config.ts`,
6766
7300
  or with `indicator: false` on `createDevServer`; the errors still reach
6767
7301
  the feed.
6768
7302
 
7303
+ ### Host check
7304
+
7305
+ The Bun dev server listens on 127.0.0.1 and answers only loopback host names:
7306
+ `localhost`, any `*.localhost`, `127.0.0.1` and
7307
+ `[::1]`. Any other `Host` gets a 403, so a page on another site whose
7308
+ name a DNS rebind points at your machine cannot read your pages, loader data or error overlay.
7309
+ To reach the server under another name (a hosts-file entry, a tunnel), list it with
7310
+ `export const dev = { allowedHosts: ["app.test"] }` in
7311
+ `nifra.config.ts` or `allowedHosts` on `createDevServer`.
7312
+ The Vite pipeline applies Vite's own `server.allowedHosts`.
7313
+
6769
7314
  ### Containers & sandboxes
6770
7315
 
6771
7316
  In Docker, networked volumes, and some sandboxes, pass
@@ -7217,7 +7762,12 @@ classes with separate request state. The second copy prints
7217
7762
 
7218
7763
  **The build honours the declaration on its own** -
7219
7764
  `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
7765
+ already loads one copy, and so does the browser bundle `nifra dev` serves on Bun.
7766
+ Listing `singleCopyPlugin()` in `clientPlugins` as well is harmless: dev
7767
+ runs that instance in the resolver's place instead of a second one. On Vite, the dev server and
7768
+ both Vite builds add each declared package to `resolve.dedupe`, a
7769
+ `@scope/*` pattern as every package installed under that scope. Unbundled phases do
7770
+ not: Bun's _runtime_ resolver never offers a
7221
7771
  bare specifier to a plugin, so `bun test`, `bun run`, and preloaded
7222
7772
  scripts still load both copies unless you preload the registrar:
7223
7773
 
@@ -7420,6 +7970,23 @@ which the server logs. Read the body in a hook with `c.req.bytes()`,
7420
7970
  instead: they read it once, within the route's body limit, and every reader after the hook
7421
7971
  replays the same bytes.
7422
7972
 
7973
+ ### "mixed nifra versions in this workspace"
7974
+
7975
+ Every `@nifrajs/*` package, `nifra` and `create-nifra` ship
7976
+ together at one version. `nifra doctor` (and `nifra check`, as the
7977
+ `NF-D002` advisory) reads the versions your `package.json` files pin,
7978
+ the versions installed in `node_modules` - followed to their real location, so a
7979
+ linked repository reports its own - and the running CLI. When they disagree it names each
7980
+ version and where it came from. It only reads files, and it never fails the run.
7981
+
7982
+ The fix is [`nifra upgrade <highest>`](/docs/cli#upgrade), not a
7983
+ hand edit of the pins: it runs
7984
+ every release's migration steps between your oldest pin and the target, then re-pins. Run it
7985
+ without `--write` first to see each planned edit. It edits files under the
7986
+ workspace root only. A symlinked package directory or `package.json` is never
7987
+ followed, and a group package installed by a link from outside the root is listed under
7988
+ "Left alone" - upgrade that repository on its own.
7989
+
7423
7990
  ### Still stuck?
7424
7991
 
7425
7992
  Run `nifra check --json` as the done-gate - it surfaces the import-chain leaks,
@@ -7429,13 +7996,14 @@ package ships a tight `LLM.md` contract card.
7429
7996
 
7430
7997
  ## Nifra - Backends & API (dev and prod)
7431
7998
 
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.
7999
+ > 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
8000
 
7434
8001
  Your `@nifrajs/core` backend reaches a Nifra app two ways from a single wiring.
7435
8002
  `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.
8003
+ `ctx.api` - and, given an `apiPrefix`, `createWebApp`
8004
+ **mounts** that backend over HTTP there, so the browser can call the very same
8005
+ routes. No hand-written `if (pathname.startsWith("/api/"))` branch in your server
8006
+ entry.
7439
8007
 
7440
8008
  ### One backend, two call paths
7441
8009
 
@@ -7475,20 +8043,19 @@ export async function loader({ api }: Route.LoaderArgs) {
7475
8043
  }
7476
8044
  ```
7477
8045
 
7478
- ### The auto-mounted /api/* (the new part)
8046
+ ### The mounted /api/*
7479
8047
 
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
8048
+ Without an `apiPrefix`, `inProcessClient` feeds `ctx.api` but
8049
+ the backend is _not_ served over HTTP, so a browser `POST /api/sync` reaches
8050
+ the page router. Set `apiPrefix: "/api"` and `createWebApp` mounts it: a
8051
+ request whose pathname is exactly `apiPrefix` or starts with
7485
8052
  `apiPrefix + "/"` is dispatched through Nifra's platform-aware backend mount
7486
8053
  interface **before** page routing. The backend receives the same Workers
7487
8054
  `env` bindings and `waitUntil` lifetime as the web app, and its
7488
8055
  `Response` is returned untouched (the request body is passed through, never pre-read).
7489
8056
 
7490
8057
  ```ts
7491
- // backend/server.ts - a hand-written server: createWebApp serves pages AND auto-mounts the
8058
+ // backend/server.ts - a hand-written server: createWebApp serves pages AND mounts the
7492
8059
  // backend at /api/*. (`nifra build` emits this wiring for you; write it only for a custom host.)
7493
8060
  import { inProcessClient } from "@nifrajs/client"
7494
8061
  import { createWebApp } from "@nifrajs/web"
@@ -7500,8 +8067,8 @@ export const app = createWebApp({
7500
8067
  adapter: reactAdapter,
7501
8068
  manifest,
7502
8069
  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)
8070
+ api: inProcessClient(backend), // → ctx.api in loaders/actions
8071
+ apiPrefix: "/api", // AND served over HTTP at /api/*; without it the backend is loader-only
7505
8072
  })
7506
8073
 
7507
8074
  // Bun: Bun.serve({ fetch: app.fetch }). No `if (pathname.startsWith("/api/")) …` branch needed -
@@ -7513,8 +8080,16 @@ for **every method** - so `GET`/`POST`/`PUT`/… all
7513
8080
  reach the backend, and an unknown `/api/…` path returns the _backend's_ 404,
7514
8081
  not the page's. A sibling path that merely shares the prefix string (e.g.
7515
8082
  `/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.
8083
+ is. Leave `apiPrefix` unset, or set it to `""`, to keep
8084
+ `ctx.api` as a loader-only client.
8085
+
8086
+ Loader-only is the default because an app that also serves the same backend behind its own
8087
+ server - with that server's rate limits and guards - would otherwise get a second path to it
8088
+ that skips them. `nifra check` reports `NF-C035` for a
8089
+ `backend/framework.ts` that sets no `apiPrefix`, so the choice is
8090
+ written down: `nifra fix --code NF-C035` adds
8091
+ `export const apiPrefix = "/api"`, which is what 4.x mounted by default, and
8092
+ `""` keeps the backend loader-only.
7518
8093
 
7519
8094
  Because the backend answers first and its 404 is final, a page file under the prefix (
7520
8095
  `routes/api/report.tsx`) could never render. Nifra refuses it instead of serving a
@@ -7549,8 +8124,8 @@ export const use = (app: { get(path: string, handler: () => Response): unknown }
7549
8124
 
7550
8125
  [!NOTE] The mount lives in `createWebApp`, and `nifra dev` (the
7551
8126
  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.
8127
+ the mounted routes are served identically in development and production - there is nothing
8128
+ extra to wire for the dev loop.
7554
8129
 
7555
8130
  ### Events that survive the transport
7556
8131
 
@@ -7650,6 +8225,7 @@ const agents = server().get("/api/agents", (c) => ({ ok: new URL(c.req.url).path
7650
8225
 
7651
8226
  // merge() folds both groups into ONE flat path space at their defined paths; a path+method clash
7652
8227
  // throws a RouteConfigError right here at merge time (fail-closed), never a silent shadow at runtime.
8228
+ // A group holding .ws() routes or mount()s is refused the same way: add those on the parent.
7653
8229
  export const backend = server().merge(listings).merge(agents)
7654
8230
  ```
7655
8231
 
@@ -7902,11 +8478,12 @@ than something we vouch for.
7902
8478
  {
7903
8479
  "schemaVersion": 1,
7904
8480
  "ok": true,
7905
- "profile": { "id": "cache-store", "version": 1 },
8481
+ "profile": { "id": "cache-store", "version": 2 },
7906
8482
  "adapterId": "redis-cache",
7907
8483
  "capabilities": [
7908
- { "capability": "read-write", "status": "passed", "checks": ["set-get", "delete"] },
7909
- { "capability": "tag-invalidation", "status": "passed", "checks": ["invalidate-tag"] }
8484
+ { "capability": "roundtrip", "status": "passed", "checks": ["set-get-delete"] },
8485
+ { "capability": "tag-invalidation", "status": "passed", "checks": ["invalidate-tag"] },
8486
+ { "capability": "clear", "status": "passed", "checks": ["clear"] }
7910
8487
  ],
7911
8488
  "evidenceHash": "9f2c..."
7912
8489
  }
@@ -7972,11 +8549,13 @@ A route is two files. The page (`x.tsx`) holds the view and its head:
7972
8549
  // own backend, so it can't compile standalone (see the typed examples elsewhere).
7973
8550
  import type { Route } from "./+types/[id]"
7974
8551
 
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 }) => ({
8552
+ // Static OR a function of { data, pageData, params, origin }. Static meta is serialized once; function
8553
+ // meta recomputes per request (its content can vary with loader data). `data` is the exporting
8554
+ // module's own loader data: a layout's meta gets its layout's (null without a loader) and reads the
8555
+ // page's as `pageData`. `origin` is the request's scheme+host (server-resolved, matches the
8556
+ // client's location.origin) - use it for ABSOLUTE canonical / og:url / og:image URLs without threading
8557
+ // siteUrl through loader data.
8558
+ export const meta = ({ data, params, origin }: Route.MetaArgs) => ({
7980
8559
  title: `User ${data.name}`,
7981
8560
  meta: [{ name: "description", content: data.bio }],
7982
8561
  link: [{ rel: "canonical", href: `${origin}/users/${params.id}` }],
@@ -8241,7 +8820,7 @@ base, a CDN host) belongs in the `loader`, not `meta()`.
8241
8820
  ```ts
8242
8821
  // routes/articles/[slug].tsx - a complete SEO head: canonical + Open Graph + Twitter + JSON-LD,
8243
8822
  // 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).
8823
+ // exports of @nifrajs/web; `origin` is a MetaArgs field beside data, pageData and params.
8245
8824
  import { canonical, jsonLd, openGraph, type MetaArgs } from "@nifrajs/web"
8246
8825
 
8247
8826
  // CAVEAT: meta() runs in BOTH SSR and client navigation, so it has NO server env - never read
@@ -8329,6 +8908,8 @@ const page = renderPage({
8329
8908
  })
8330
8909
  ```
8331
8910
 
8911
+ #### A strict CSP that still caches
8912
+
8332
8913
  A nonce makes every document unique, so a nonce-bearing page is
8333
8914
  `private, no-store` and no shared cache - `withISR`, a CDN - may store
8334
8915
  it. Page data never needs one: it rides in an inert
@@ -8386,6 +8967,16 @@ file that reaches it.
8386
8967
  // same layout-chain head, so sitewide tags persist across navigation - no page-only flash.
8387
8968
  ```
8388
8969
 
8970
+ ### Optional server components
8971
+
8972
+ The post-4.0 [server-components pipeline](/docs/server-components) supports
8973
+ React, Vue, Svelte and Solid. Declare closed input/public-props schemas and authorization
8974
+ in `*.component.server.ts(x)`, register owner instances and export a named
8975
+ `componentPipeline` configuration. React uses native RSC/Flight; the other three
8976
+ hydrate native islands in validated server ranges. Refresh repeats owner GET admission;
8977
+ HTML/data/refresh are private/no-store. Writes use server functions or actions. Bind the
8978
+ router and await auth-scope invalidation on identity, tenant or permission changes.
8979
+
8389
8980
  ### The build-output contract
8390
8981
 
8391
8982
  The programmatic `buildClient()` API emits a content-hashed directory plus a
@@ -8554,6 +9145,399 @@ See [SSG & ISR](/docs/rendering) for prerendering, [Dev & HMR](/docs/dev) for th
8554
9145
  two dev loops and the `--port` flag, and [Loaders & actions](/docs/data) for the
8555
9146
  data layer in depth.
8556
9147
 
9148
+ ## Nifra - Control APIs (one schema, every client)
9149
+
9150
+ > 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.
9151
+
9152
+ A desktop app, a CLI, or a mobile companion that drives a local service needs a control API:
9153
+ a set of methods, a stream of events, and clients in more than one language. Write each
9154
+ method once as a route with a schema. The schema validates requests, types the TypeScript
9155
+ client, and describes the method in an OpenAPI document that generators turn into clients for
9156
+ other languages.
9157
+
9158
+ ### One file, three methods
9159
+
9160
+ Two methods take a JSON body and answer JSON. The third is a typed event stream. This file
9161
+ typechecks against the current packages.
9162
+
9163
+ ```ts
9164
+ import { timingSafeEqual } from "node:crypto"
9165
+ import { server } from "@nifrajs/core"
9166
+ import { streaming } from "@nifrajs/core/sse"
9167
+ import { t, toOpenAPI } from "@nifrajs/schema"
9168
+
9169
+ const token = process.env.CONTROL_TOKEN
9170
+ if (token === undefined || token.length < 32) throw new Error("CONTROL_TOKEN must be 32+ characters")
9171
+ const expected = Buffer.from(`Bearer ${token}`)
9172
+ const authorized = (request: Request): boolean => {
9173
+ const given = Buffer.from(request.headers.get("authorization") ?? "")
9174
+ return given.length === expected.length && timingSafeEqual(given, expected)
9175
+ }
9176
+
9177
+ const Session = t.object({
9178
+ id: t.string(),
9179
+ status: t.union([t.literal("idle"), t.literal("running")]),
9180
+ })
9181
+ const TurnEvent = t.object({
9182
+ seq: t.integer({ minimum: 0 }),
9183
+ type: t.string(),
9184
+ text: t.optional(t.string()),
9185
+ })
9186
+
9187
+ export const app = server()
9188
+ .use(streaming())
9189
+ .onRequest((request) =>
9190
+ authorized(request) ? undefined : Response.json({ error: "unauthorized" }, { status: 401 }),
9191
+ )
9192
+ .post(
9193
+ "/session/create",
9194
+ { body: t.object({ cwd: t.string({ minLength: 1, maxLength: 4096 }) }), response: Session },
9195
+ () => ({ id: crypto.randomUUID(), status: "idle" as const }),
9196
+ )
9197
+ .post(
9198
+ "/turn/send",
9199
+ {
9200
+ body: t.object({
9201
+ sessionId: t.string(),
9202
+ message: t.string({ minLength: 1, maxLength: 256 * 1024 }),
9203
+ }),
9204
+ response: t.object({ turnId: t.string() }),
9205
+ bodyLimit: 512 * 1024,
9206
+ },
9207
+ () => ({ turnId: crypto.randomUUID() }),
9208
+ )
9209
+ .sse(
9210
+ "/turn/events",
9211
+ { query: t.object({ turnId: t.string() }), sse: TurnEvent },
9212
+ (c, stream) => {
9213
+ const after = Number(c.req.headers.get("last-event-id") ?? -1)
9214
+ for (let seq = after + 1; seq < 3; seq++)
9215
+ stream.send({ seq, type: "assistant.delta", text: `part ${seq}` }, { id: String(seq) })
9216
+ stream.close()
9217
+ },
9218
+ )
9219
+
9220
+ export const spec = toOpenAPI(app, { openapiVersion: "3.2.0", title: "Control API", version: "1.0.0" })
9221
+ ```
9222
+
9223
+ -
9224
+ **Methods are routes.** A method name like `session/create` is a path, so a
9225
+ call is `POST /session/create` and the typed client reads
9226
+ `api.session.create.post(body)`. Use `POST` for anything that changes
9227
+ state; one envelope endpoint that dispatches on a method field would hide every method
9228
+ from the schema, the client, and the document.
9229
+
9230
+ -
9231
+ **The schema is the validator.** A body that does not match answers `422`
9232
+ before the handler runs. `maxLength` bounds each field and
9233
+ `bodyLimit` bounds the request as it arrives.
9234
+
9235
+ -
9236
+ **Authorization runs first.** An `onRequest` hook that returns a response
9237
+ ends the request before routing, so no method is reachable without the token. The
9238
+ comparison is constant-time, and the server refuses to start without a long token.
9239
+
9240
+ -
9241
+ **Retries.** A method that must not run twice declares
9242
+ `idempotency` on its route, with `.use(idempotency())` from
9243
+ `@nifrajs/core/idempotency-plugin` and a namespace per caller.
9244
+
9245
+ ### Event streams that resume
9246
+
9247
+ `.sse(path, { sse: schema }, run)` declares the type of every event the stream
9248
+ sends. Give each event an `id` (here its sequence number). A browser
9249
+ `EventSource`, or the typed client's `subscribe`, sends the last id it
9250
+ saw as `Last-Event-ID` when it reconnects, so the handler replays only what the
9251
+ client missed. Keep the events a reconnect may need in memory or in your own store: core
9252
+ sends the header, and what to replay is your service's to decide.
9253
+
9254
+ ### The TypeScript client
9255
+
9256
+ ```ts
9257
+ // doc-check: skip - fragment: app is the control server above, imported from its module.
9258
+ import { client } from "@nifrajs/client"
9259
+
9260
+ const api = client<typeof app>("http://127.0.0.1:4100", {
9261
+ headers: { authorization: `Bearer ${token}` },
9262
+ })
9263
+
9264
+ const session = await api.session.create.post({ cwd: "/work/repo" })
9265
+ if (!session.ok) throw new Error(`create failed: ${session.status}`)
9266
+
9267
+ const subscription = api.turn.events.subscribe((event) => render(event.seq, event.text ?? ""), {
9268
+ query: { turnId },
9269
+ // After a dropped connection the client reconnects and sends Last-Event-ID itself.
9270
+ })
9271
+ // subscription.close() ends it.
9272
+ ```
9273
+
9274
+ Every call is typed from the server's routes, with no generated code. A call returns a
9275
+ `Result`: check `ok`, then read `data`, or
9276
+ `status` and `error`.
9277
+
9278
+ ### Clients in other languages
9279
+
9280
+ `toOpenAPI(app, { openapiVersion: "3.2.0" })` from `@nifrajs/schema`
9281
+ describes each method's request and response, and each typed stream as
9282
+ `text/event-stream` whose `itemSchema` is the SSE event: a
9283
+ `data` field holding JSON that matches the route's event schema, plus
9284
+ `event`, `id`, and `retry`. The event schema is also a named
9285
+ component (`TurnEventsEvent` here), so a generator emits a type for it. The
9286
+ `openapi()` plugin from `@nifrajs/middleware` takes the same option and
9287
+ serves the document.
9288
+
9289
+ ```ts
9290
+ // doc-check: skip - shell commands, not TypeScript.
9291
+ // Write the document at build time, then hand it to any OpenAPI 3.2 generator.
9292
+ bun -e 'import { spec } from "./control.ts"; await Bun.write("openapi.json", JSON.stringify(spec, null, 2))'
9293
+ ```
9294
+
9295
+ OpenAPI 3.1 has no field for the items of a stream. Without `openapiVersion` the
9296
+ document stays 3.1, declares the stream as a string, and carries the same item schema as
9297
+ `x-itemSchema`; the named event component is there either way. A generator that
9298
+ reads only 3.1 still produces the event type, and your code decodes each
9299
+ `data` line with it.
9300
+
9301
+ ## Nifra - Custom server (createWebApp)
9302
+
9303
+ > 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.
9304
+
9305
+ The standard project layout gets its server entry from `nifra build`. An app that
9306
+ calls `createWebApp` from its own entry wires the same pieces by hand. Each one is an
9307
+ option or a single call.
9308
+
9309
+ ### A complete entry
9310
+
9311
+ Everything on this page in one file. The sections below explain each part. This file
9312
+ typechecks against the current packages.
9313
+
9314
+ ```ts
9315
+ import { fileURLToPath } from "node:url"
9316
+ import { createCspPolicy, createWebApp, servePublicDir } from "@nifrajs/web"
9317
+ import type { Manifest, RenderAdapter } from "@nifrajs/web"
9318
+
9319
+ // From your build: the framework adapter, the route manifest, and what buildClient() returned.
9320
+ declare const adapter: RenderAdapter
9321
+ declare const manifest: Manifest
9322
+ declare const build: {
9323
+ readonly entry: string
9324
+ readonly assets: readonly string[]
9325
+ readonly routes: Readonly<Record<string, readonly string[]>>
9326
+ readonly css?: readonly string[]
9327
+ }
9328
+ declare const errors: { capture(error: unknown, tags: Record<string, string>): void }
9329
+
9330
+ // buildClient({ outDir: "dist/assets" }) writes the chunks where their /assets/ URLs point.
9331
+ const bundle = servePublicDir({
9332
+ dir: fileURLToPath(new URL("./dist/", import.meta.url)),
9333
+ hashedFiles: new Set(build.assets),
9334
+ })
9335
+ const publicFiles = servePublicDir({ dir: fileURLToPath(new URL("./public/", import.meta.url)) })
9336
+
9337
+ const csp = createCspPolicy({
9338
+ header: ({ sources }) => `default-src 'self'; script-src 'self' ${sources}; object-src 'none'`,
9339
+ })
9340
+
9341
+ export const app = createWebApp({
9342
+ adapter,
9343
+ manifest,
9344
+ clientEntry: build.entry,
9345
+ routePreload: build.routes,
9346
+ styles: build.css ?? [],
9347
+ csp,
9348
+ server: { requestTimeoutMs: 15_000, gracefulSignals: { drainMs: 8_000 } },
9349
+ use: (app) => {
9350
+ app.onRequest(async (request) => (await bundle(request)) ?? (await publicFiles(request)))
9351
+ },
9352
+ onLoaderError: (error, { route }) => errors.capture(error, { route }),
9353
+ })
9354
+
9355
+ app.listen(3000)
9356
+ ```
9357
+
9358
+ ### Static and hashed files
9359
+
9360
+ `servePublicDir` from `@nifrajs/web` serves a directory on Bun, Node and
9361
+ Deno. It answers `GET` and `HEAD` only, refuses paths that leave the
9362
+ directory or name a hidden file, sets the content type and `nosniff`, handles range
9363
+ requests, and returns `undefined` on a miss so the request falls through to your
9364
+ routes.
9365
+
9366
+ -
9367
+ **Hashed build output.** Pass `buildClient()`'s `assets` as
9368
+ `hashedFiles`: those files get `immutable` for a year, and anything
9369
+ else in the directory gets one day. Write the build to `outDir: "dist/assets"` so
9370
+ a chunk's `/assets/...` URL is also its path under `dist/`.
9371
+
9372
+ -
9373
+ **Your own files.** A second handler on `public/` serves them at the root,
9374
+ with the one-day policy.
9375
+
9376
+ -
9377
+ **Where it runs.** Register the handlers with `app.onRequest` inside
9378
+ `use`, so a static hit never reaches page rendering.
9379
+
9380
+ On Node, `serve()` from `@nifrajs/node` can serve the bundle itself before
9381
+ the app runs, which keeps the direct request path for everything else:
9382
+
9383
+ ```ts
9384
+ // doc-check: skip - fragment: app and build are the createWebApp app and buildClient() output above.
9385
+ import { serve } from "@nifrajs/node"
9386
+
9387
+ await serve(app, {
9388
+ port: 3000,
9389
+ static: { dir: new URL("./dist/assets/", import.meta.url), hashedFiles: build.assets },
9390
+ signals: { drainMs: 8_000 },
9391
+ })
9392
+ ```
9393
+
9394
+ ### A strict CSP on nifra's nonce
9395
+
9396
+ Pass `createCspPolicy` as `csp` and nifra writes the
9397
+ `Content-Security-Policy` header for each document. You do not stash a nonce or set
9398
+ the header yourself. Your `header` function receives `sources`: a hash
9399
+ for each constant inline script nifra writes, plus a nonce only when the document carries a
9400
+ request-specific script (a `defer()`ed value, or `unsafeInlineScript` in
9401
+ `meta`).
9402
+
9403
+ A document without a nonce gets the same header on every request, so it can still be cached by
9404
+ `withISR` or a CDN. A document with a nonce is sent `private, no-store`.
9405
+ The older `nonce` option puts a nonce on every document, so nothing is cacheable.
9406
+ `createWebApp` refuses `csp` and `nonce` together.
9407
+
9408
+ ### Middleware goes in use
9409
+
9410
+ `createWebApp` declares every page route, and the `/*` catch-all, before
9411
+ it returns. `beforeHandle`, `afterHandle`, `around`,
9412
+ `derive`, `decorate` and `onError` are copied into each route
9413
+ when the route is declared. One of those hooks added to the returned app would therefore reach
9414
+ no page. `requestId()` is a `derive`: added late, no page would see
9415
+ `c.requestId`. Outside production, adding one throws a `FrameworkError`
9416
+ with code `HOOK_AFTER_PAGES` that names the call site. Hooks added inside
9417
+ `app.group(prefix, ...)` are still allowed, because a group is its own scope.
9418
+
9419
+ Apply middleware in the `use` option instead. It runs before any page is declared,
9420
+ and ahead of `mounts` and the `api` mount, so an `onRequest`
9421
+ guard also covers a mounted auth handler:
9422
+
9423
+ ```ts
9424
+ // doc-check: skip - fragment: the use option of the createWebApp call above; @nifrajs/middleware is not installed in the docs sandbox.
9425
+ import { requestId, securityHeaders } from "@nifrajs/middleware"
9426
+
9427
+ createWebApp({
9428
+ // ...
9429
+ use: (app) => {
9430
+ app.use(securityHeaders())
9431
+ app.use(requestId())
9432
+ },
9433
+ })
9434
+ ```
9435
+
9436
+ `onRequest` and `onResponse` are read per request, so those two still
9437
+ work when added late. Putting everything in `use` means you never need to remember
9438
+ which hooks those are.
9439
+
9440
+ A plugin applied in `use` works at runtime but cannot add to the returned app's type.
9441
+ To get `c.requestId` typed on routes you declare afterwards, call
9442
+ `app.use(requestId())` again on the returned app. Named plugins apply once.
9443
+
9444
+ ### Loader errors
9445
+
9446
+ When a loader or action throws and an `_error` boundary renders the page, the error
9447
+ is written to the server's `logger`, at the detail set by
9448
+ `server.errorLogDetail`. That is the same log line an unhandled 500 produces, and you
9449
+ do not need to configure anything for it. A soft-navigation data request that fails is logged
9450
+ the same way. A thrown `Response`, such as `redirect()`, is not an
9451
+ error and is not logged.
9452
+
9453
+ `onLoaderError` is for an error reporter. It sees every real throw with the request,
9454
+ params and route id before the boundary renders. If it throws, the error is ignored so rendering
9455
+ continues.
9456
+
9457
+ ### Graceful shutdown
9458
+
9459
+ On Bun, `server: { gracefulSignals: { drainMs } }` makes `listen()`
9460
+ handle `SIGTERM` and `SIGINT`. The server gives in-flight requests up
9461
+ to `drainMs` (default 10 000) to finish, then stops, closing any that are still
9462
+ running. On Bun, new connections can still arrive during that window, so the load balancer
9463
+ should stop routing to the instance first, as most platforms do before they send
9464
+ `SIGTERM`. Keep `drainMs` under your platform's kill grace period:
9465
+ `docker stop` waits 10 seconds. `true` uses the default. On Node and
9466
+ Deno, pass
9467
+ `signals: { drainMs }` to `serve()` instead, as above.
9468
+
9469
+ ### Letting check and assure read the app
9470
+
9471
+ `nifra check`, `assure`, `routes` and the other commands that
9472
+ read the app import it, so everything the app does at module scope runs. A command that only
9473
+ reads what the app declares sets `NIFRA_REFLECT` to its own name first:
9474
+ `check`, `routes`, `context`, `openapi`,
9475
+ `contracts`, `capabilities`, `manifest`, `doctor`
9476
+ , `diff`, `sync-routes`, `sync-manifest`, and
9477
+ `assure` without `--bundle`, `--strict`,
9478
+ `--hydration`, `--interact` or `--out`. Commands that send the
9479
+ app requests, such as `smoke`, `prove`, `levels` and the
9480
+ `assure` bundle, leave it unset. Check it to skip work a read never needs:
9481
+
9482
+ ```ts
9483
+ // doc-check: skip - fragment: an app module's Redis client; ioredis is not installed in the docs sandbox.
9484
+ import Redis from "ioredis"
9485
+
9486
+ // Unset when the app really runs; "check", "routes", "assure", ... while nifra only reads it.
9487
+ export const redis = process.env.NIFRA_REFLECT ? undefined : new Redis(process.env.REDIS_URL!)
9488
+ ```
9489
+
9490
+ -
9491
+ **Environment validation.** An app that validates its environment on import still needs
9492
+ values to pass it. Supply them with `--env-file`, for example
9493
+ `nifra check --env-file .env.example`. Variables already set in the shell win.
9494
+
9495
+ -
9496
+ **A process that will not exit.** If something the app opened is still keeping the process
9497
+ alive 2 seconds after the command has answered, the command exits with its own result and
9498
+ says that the app kept it open.
9499
+
9500
+ ### nifra check for a custom entry
9501
+
9502
+ -
9503
+ **Routes nifra cannot see.** A mounted handler such as better-auth answers paths that are
9504
+ not in your backend, and no typed client covers them. Declare the mount opaque,
9505
+ `
9506
+ mounts: [{ path: "/api/auth", app: auth, opaque: "better-auth" }]
9507
+ `
9508
+ , and when `nifra.assurance.ts` names this app as its `source`, the
9509
+ check skips a `fetch` into that prefix by itself. Without that, list the prefixes
9510
+ in `nifra.check.json` next to `package.json`:
9511
+
9512
+ ```ts
9513
+ {
9514
+ "externalMounts": ["/api/auth"]
9515
+ }
9516
+ ```
9517
+
9518
+ -
9519
+ **One call nifra cannot type.** For a single call rather than a whole prefix, such as a
9520
+ static file, put `// nifra-expect NF-C002: ` on the line or in the
9521
+ comment block above it. The finding stays in the report as info, with your reason. A comment
9522
+ with no reason silences nothing.
9523
+
9524
+ -
9525
+ **Two repositories, one runtime.** When a linked sibling repository brings its own install
9526
+ of nifra or React, `nifra check` reports the duplicates. A reinstall in this app
9527
+ cannot remove a copy that another project installed. Declare the packages single-copy and
9528
+ nifra loads this app's copy for every importer:
9529
+
9530
+ ```ts
9531
+ {
9532
+ "name": "my-app",
9533
+ "nifra": { "singleCopy": ["@nifrajs/*", "react", "react-dom"] }
9534
+ }
9535
+ ```
9536
+
9537
+ Packages duplicated the same way are reported as one finding with one combined
9538
+ `singleCopy` list. The preload this needs, and the version-skew case it cannot
9539
+ cover, are in [Troubleshooting](/docs/troubleshooting).
9540
+
8557
9541
  ## Nifra - Databases
8558
9542
 
8559
9543
  > 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.
@@ -8929,7 +9913,10 @@ Route data failed the output guard that stands between loaders/actions and the b
8929
9913
 
8930
9914
  **Fix:** declare `loaderOutput`/`actionOutput` as a Standard
8931
9915
  Schema that describes exactly the fields the page needs. `nifra check` flags routes
8932
- without one (NF-C030).
9916
+ without one (NF-C030): an error when the loader's own body returns a literal (an object,
9917
+ array, string, number or boolean), since every request that reaches that return fails, and a
9918
+ warning when only running it shows what it returns. `nifra schemas --write` drafts
9919
+ a schema from the loader's return type for review.
8933
9920
 
8934
9921
  ### Rendering and data
8935
9922
 
@@ -8992,6 +9979,22 @@ serving the old page until its freshness runs out. The revalidate endpoint answe
8992
9979
  permission for this zone, project or service; a 5xx or `network_error` is retried on
8993
9980
  its own.
8994
9981
 
9982
+ ### Silencing a nifra check finding
9983
+
9984
+ `nifra check` names each finding with an `NF-` code. Where a finding is
9985
+ reviewed and intended, a comment on the flagged line, or anywhere in the comment block directly
9986
+ above it, says so and why:
9987
+
9988
+ The comment answers only the code it names. A finding that fails the check, and every security
9989
+ finding, is silenced only with a reason after the colon; an advisory also accepts the comment
9990
+ without one. A blank line or a line of code ends the block, so a comment never reaches the next
9991
+ statement. A silenced security finding, interpolated SQL (NF-C006) included, stays in the report
9992
+ as information, with the comment and its reason as evidence. Some codes also answer to a name: `sql-dynamic` (NF-C006),
9993
+ `raw-response` (NF-C007), `reserved-segment` (NF-C018),
9994
+ `route-overlap` (NF-C024 and NF-C025) and `param-modifier` (NF-C026).
9995
+ `@nifra-gate-reviewed` silences nothing; a finding under it names the comment to
9996
+ write instead.
9997
+
8995
9998
  ## Nifra - Failure laboratory
8996
9999
 
8997
10000
  > Replayable durable-failure schedules on virtual time: crash after commit, duplicate delivery, reordering, budget expiry, and checkpoint contention.
@@ -10431,6 +11434,49 @@ in its `package.json`:
10431
11434
  `{'"nifra": { "environment": "frontend" | "backend" | "shared" | "library" }'}`.
10432
11435
  A file in no zone is a build error, so nothing is ever classified by accident.
10433
11436
 
11437
+ The app's own `package.json` can set sides too, so a folder that has to stay
11438
+ where it is, or a family of workspace packages, needs no edit of its own:
11439
+
11440
+ ```ts
11441
+ {
11442
+ "name": "my-app",
11443
+ "nifra": {
11444
+ "zones": {
11445
+ "packages": { "@shop/*": "library", "@shop/db": "backend" },
11446
+ "backend": ["migrations"]
11447
+ }
11448
+ }
11449
+ }
11450
+ ```
11451
+
11452
+ `packages` gives a side to a package that does not declare one, by exact name or
11453
+ by a prefix ending in `*`; a package's own declaration always wins.
11454
+ `backend`, `frontend` and `shared` list app folders that
11455
+ count as that zone folder. Builds, dev servers, `nifra check` and the editor
11456
+ plugin all read this setting.
11457
+
11458
+ A project that serves no pages has no browser, so the zones do not apply to it. Declare that
11459
+ in its `package.json` with `{'"nifra": { "kind": "api" }'}`:
11460
+ `nifra check` then skips the zone checks and the server-only import check there,
11461
+ and treats every file as server code. It is read from the nearest `package.json`
11462
+ and only takes values `"api"` and `"web"`. A directory with web config
11463
+ (`nifra.config.ts` exporting `adapter`, or
11464
+ `backend/framework.ts`) keeps its zone checks under it, and
11465
+ `nifra check` reports the clash (NF-C037); builds and dev servers never read it.
11466
+
11467
+ A directory with `backend/app.ts` and no web config is read as an API-only app
11468
+ whether or not it declares the kind: `nifra routes`, `nifra context`,
11469
+ `nifra openapi` and `nifra mcp` load `backend/app.ts` alone,
11470
+ and the tools, resources and prompts its backend declares reach `nifra mcp`.
11471
+
11472
+ Those commands, the contract lock and the route lints see only what
11473
+ `backend/app.ts` exports. An API whose app is built by a factory such as
11474
+ `createApp(deps)`, with its real dependencies wired in another entry, exports the
11475
+ same app from `backend/app.ts` built with stand-in dependencies, so every operation
11476
+ is visible without a database or secrets; `nifra check` names an implemented
11477
+ operation it does not serve (NF-C039). See
11478
+ [the stand-in entry](/docs/types-first#stub-entry).
11479
+
10434
11480
  ### A route is two files
10435
11481
 
10436
11482
  The page renders; its backend half loads. A server-only export (`loader`,
@@ -10461,7 +11507,8 @@ import { notFound } from "@nifrajs/web"
10461
11507
  import type { Route } from "./+types/[id]"
10462
11508
 
10463
11509
  // 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.
11510
+ // but the schema does not declare is dropped before rendering; in development the console names
11511
+ // each dropped field, so a missing declaration never shows up as a silently blank page.
10465
11512
  export const loaderOutput = t.object({ name: t.string(), avatarUrl: t.string() })
10466
11513
 
10467
11514
  export async function loader({ params, api }: Route.LoaderArgs) {
@@ -10529,9 +11576,28 @@ page, `frontend/` or `shared/` fails the build.
10529
11576
  that contains what looks like a credential fails the build.
10530
11577
 
10531
11578
  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`.
11579
+ its source is sent), in both bundlers, for every framework, and in `nifra check`,
11580
+ which follows the imports the bundler follows: relative paths, tsconfig `paths`
11581
+ and `baseUrl`, `package.json` `imports`, and the
11582
+ `clientModule` the browser entry loads. An app on the older layout moves with `nifra migrate layout`: a dry run that
11583
+ reports every move, then `--write`. It follows the same imports, rewrites them
11584
+ and test-runner mocks (an alias through the same alias), moves the config's
11585
+ `clientModule` and component pipeline entries with their modules, adds the zone
11586
+ folders to tsconfig `include` and `.nifra/` to
11587
+ `.gitignore`, and leaves data files and folders zoned in
11588
+ `package.json` where they are. It reports what still needs a hand, such as an
11589
+ alias with no target that covers a moved file, or a specifier that names nothing installed.
11590
+
11591
+ An API-only app, one that declares `{'"nifra": { "kind": "api" }'}` or has no web
11592
+ config, no `routes/` and a `backend.ts` that exports
11593
+ `backend`, migrates as one: only
11594
+ `backend.ts` moves, to `backend/app.ts`, with the imports from it and
11595
+ to it. Nothing is split or zoned. The migration adds `backend` to tsconfig
11596
+ `include`, `.nifra/` to `.gitignore`, and the declaration
11597
+ to `package.json` when it is missing. Web config without `routes/`
11598
+ could be either kind of app, so the migration stops and asks which. Until an app moves,
11599
+ `nifra check` fails with NF-C038 on its root `backend.ts` or
11600
+ `framework.ts`, since no contract gate reads them there.
10535
11601
 
10536
11602
  ## Nifra - Contract-derived adversarial testing
10537
11603
 
@@ -10617,6 +11683,14 @@ await assertAdversarialContract(app, {
10617
11683
  Missing, invalid, or unsynthesizable witnesses become explicit coverage gaps. The default is
10618
11684
  fail-closed; `requireCoverage: false` makes gaps advisory.
10619
11685
 
11686
+ Declared `params` and `headers` get witnesses too, validated before any
11687
+ case runs, so a body or query case is rejected by the input it mutates. The harness never
11688
+ invents credentials: `authorization`, `cookie`, `x-api-key`
11689
+ and names that look like a key, token, secret or signature come from the witness and
11690
+ `prepareRequest` only, and are never mutated. A route with a `cookies`
11691
+ schema needs `headers.cookie` in its witness, and a missing credential is a gap that
11692
+ names it.
11693
+
10620
11694
  ### Auth and tenant context
10621
11695
 
10622
11696
  `prepareRequest` runs for every case and runtime. Use it to attach a test session,
@@ -10693,6 +11767,41 @@ await assertAdversarialContract(app, {
10693
11767
  })
10694
11768
  ```
10695
11769
 
11770
+ ### A web app's routes in a test
11771
+
11772
+ Give the test the same route manifest the app serves. `discoverRoutes(dir)` from
11773
+ `@nifrajs/web/fs` scans a routes directory the way the build does, so every page
11774
+ arrives with its `.backend.ts` half: its loader, action, output schemas and
11775
+ middleware. A built app can import the generated `server-manifest` instead, which
11776
+ bakes the same pairs into static imports.
11777
+
11778
+ ```ts
11779
+ // test/routes.test.ts - render the real routes in-process, each page with its backend half.
11780
+ import { expect, test } from "bun:test"
11781
+ import { createWebApp } from "@nifrajs/web"
11782
+ import { discoverRoutes } from "@nifrajs/web/fs"
11783
+ import { reactAdapter } from "@nifrajs/web-react"
11784
+
11785
+ const app = createWebApp({
11786
+ adapter: reactAdapter,
11787
+ // Scans routes/ the way the build does: a page and its .backend.ts half always arrive together.
11788
+ manifest: discoverRoutes(`${import.meta.dir}/../routes`),
11789
+ clientEntry: "/client.js",
11790
+ })
11791
+
11792
+ test("the users page runs its loader", async () => {
11793
+ const res = await app.fetch(new Request("http://localhost/users/7"))
11794
+ expect(res.status).toBe(200)
11795
+ })
11796
+ ```
11797
+
11798
+ `buildManifest(files, load)` pairs a page with its backend half only when both are
11799
+ in `files`. It is pure, so it cannot know a half is missing, and a hand-written list
11800
+ that names the page alone serves it without its loader and guards: the test passes against a
11801
+ route production never runs. `nifra doctor` flags a `buildManifest(...)`
11802
+ call whose array literal or `import.meta.glob` patterns reach a page but not the
11803
+ backend half beside it.
11804
+
10696
11805
  ### Browser end-to-end
10697
11806
 
10698
11807
  API flows stay in-process through `testSession` - typed, cookie-aware, no
@@ -10826,6 +11935,23 @@ implementation into the client.
10826
11935
  See [Framework contract](/docs/contract) for the decoupled form and its scaling
10827
11936
  guidance.
10828
11937
 
11938
+ The tooling reads the backend `backend/app.ts` exports. An app that
11939
+ `implement`s a contract somewhere else, such as a `createApp(deps)`
11940
+ factory, is invisible to the contract lock and the route lints until that file serves it.
11941
+ `nifra check` names each implemented operation the backend does not serve
11942
+ (NF-C039), and counts them when there is no `backend/app.ts` at all. Export the
11943
+ same app built with stand-in dependencies:
11944
+
11945
+ ```ts
11946
+ // backend/app.ts - the backend nifra's tooling reads
11947
+ import { createApp } from "../src/app"
11948
+ import { stubDeps } from "../src/stub-deps"
11949
+
11950
+ // The real entry builds the app with real dependencies elsewhere. This one builds the same routes with
11951
+ // stand-ins, so the contract lock, the typed client checks and `nifra routes` see every operation.
11952
+ export const backend = createApp(stubDeps())
11953
+ ```
11954
+
10829
11955
  ### OpenAPI
10830
11956
 
10831
11957
  The `openapi()` middleware builds an OpenAPI 3.1 document from your registered
@@ -10965,6 +12091,47 @@ input, so Nifra will not guess which app to point them at; a level that fuzzed y
10965
12091
  server by default would be a footgun, and a skipped route is reported rather than passed over
10966
12092
  in silence.
10967
12093
 
12094
+ An app that needs a session, real row ids or signed tokens passes them to the runner instead of
12095
+ rewriting requests inside the executor: `witnesses`, `prepareRequest`,
12096
+ `isRejected`, `include` and `maxMutationsPerInput` on
12097
+ `invariants` reach every L4 run, from `nifra levels` and the
12098
+ `nifra_levels` MCP tool alike. `setup` provisions a throwaway target only
12099
+ when L4 runs: it is called once, before the first L4 request, and its `teardown`
12100
+ always runs after the run, a failing one included. The example keeps its target in memory; a
12101
+ database or container belongs in your own `setup`.
12102
+
12103
+ ```ts
12104
+ // nifra.assurance.ts
12105
+ // doc-check: skip - the backend and its seed data are app-specific
12106
+ import { defineAssuranceConfig } from "@nifrajs/core/assurance"
12107
+ import { createBackend } from "./backend"
12108
+
12109
+ export default defineAssuranceConfig({
12110
+ source: createBackend({ rows: new Map() }),
12111
+ policy: { rules: [/* ... */] },
12112
+ invariants: {
12113
+ // Called once, before the first L4 request - never when this file is imported, so
12114
+ // `nifra check` and `nifra assure` provision nothing.
12115
+ setup: async () => {
12116
+ const rows = new Map([["row_1", { id: "row_1", owner: "fixture" }]])
12117
+ const target = createBackend({ rows })
12118
+ return {
12119
+ executor: (request) => target.fetch(request),
12120
+ teardown: () => rows.clear(), // runs after a failing run too
12121
+ }
12122
+ },
12123
+ // Known-good inputs for schemas L4 cannot synthesize (an opaque validator, a real id).
12124
+ witnesses: { "PATCH /rows/:id": { params: { id: "row_1" }, body: { title: "ok" } } },
12125
+ // Attach what the app needs on every case: a session, a signed token.
12126
+ prepareRequest: (request) => {
12127
+ const headers = new Headers(request.headers)
12128
+ headers.set("cookie", "session=fixture")
12129
+ return new Request(request, { headers })
12130
+ },
12131
+ },
12132
+ })
12133
+ ```
12134
+
10968
12135
  ### For coding agents
10969
12136
 
10970
12137
  The `nifra_levels` MCP tool returns the same
@@ -11149,6 +12316,62 @@ Part of the **[nifra](https://nifra.dev)** full-stack TypeScript framework - one
11149
12316
  For AI agents, see [`LLM.md`](./LLM.md) and the full corpus
11150
12317
  [`../../llms-full.txt`](../../llms-full.txt).
11151
12318
 
12319
+ ## @nifrajs/acp
12320
+
12321
+ An `AgentBackend` for agents that speak the [Agent Client Protocol](https://agentclientprotocol.com)
12322
+ (version 1) over stdio: Claude Code through `claude-code-acp`, Codex through `codex-acp`, Gemini CLI
12323
+ with `--experimental-acp`, or any other ACP agent. A Nifra host, RPC server, or app drives it like any
12324
+ other backend.
12325
+
12326
+ ```ts
12327
+ import { AcpBackend } from "@nifrajs/acp"
12328
+ import { CodingAgentHost } from "@nifrajs/coding-agent"
12329
+
12330
+ const backend = new AcpBackend({
12331
+ command: "claude-code-acp",
12332
+ env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY },
12333
+ })
12334
+ const host = new CodingAgentHost({ backend })
12335
+ ```
12336
+
12337
+ Each session is one agent process: `initialize` (protocol version 1, refused otherwise), an optional
12338
+ `authenticate` with `authMethod`, then `session/new` in the session's absolute `cwd`. A message is a
12339
+ `session/prompt`; its updates become protocol events:
12340
+
12341
+ | ACP update | Event |
12342
+ | --- | --- |
12343
+ | `agent_message_chunk` | `assistant.delta`, and the whole reply as `assistant.message` before a tool call and at the end |
12344
+ | `agent_thought_chunk` | `reasoning.delta` |
12345
+ | `tool_call`, `tool_call_update` | `tool.started`, then `tool.completed` once the status is `completed` or `failed` |
12346
+ | `plan` | `plan.updated` |
12347
+ | `usage_update` | `context.usage` (`used`, `size`) |
12348
+ | anything else | `x-acp.<sessionUpdate>` with a bounded copy of the update |
12349
+
12350
+ The stop reason ends the turn: `end_turn`, `max_tokens`, and `max_turn_requests` complete it (any but
12351
+ `end_turn` also reported as `x-acp.stop_reason`), `refusal` fails it, and `cancelled` stops it.
12352
+
12353
+ `session/request_permission` surfaces as `approval.required` (`capability` is `acp.<tool kind>`).
12354
+ `resolveApproval` answers with the agent's `allow_once` or `reject_once` option and never an "always"
12355
+ one, so the host's approval policy stays in charge; with no such option it answers `cancelled`. An
12356
+ approval the agent withdraws with `$/cancel_request` is answered and reported as `approval.resolved`
12357
+ with `approved: false`.
12358
+
12359
+ The client offers no filesystem or terminal methods: the agent uses its own tools, and those
12360
+ requests get JSON-RPC `-32601`. `cancel` answers open approvals `cancelled` and sends
12361
+ `session/cancel`; an agent that has not ended the turn after `cancelGraceMs` (default 2 seconds) is
12362
+ stopped, which ends its session.
12363
+
12364
+ The agent runs with `PATH`, `HOME`, locale, terminal, user, shell, and temp variables plus what `env`
12365
+ adds; nothing else from the parent environment passes, so give it its model credentials explicitly.
12366
+ Error text from the agent (a refused request, or the last error-looking stderr line when it exits)
12367
+ is cut to a bounded length with those values and common key shapes masked. stdout carries only
12368
+ JSON-RPC, one message per line, each at most `maxRecordBytes` (default 1 MiB).
12369
+
12370
+ The package passes `@nifrajs/agent-protocol/conformance` against a scripted ACP agent.
12371
+
12372
+ For AI agents, see [`LLM.md`](./LLM.md) and the full corpus
12373
+ [`../../llms-full.txt`](../../llms-full.txt).
12374
+
11152
12375
  ## @nifrajs/ag-ui
11153
12376
 
11154
12377
  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 +12492,11 @@ terminal events - the run is never re-executed. A disconnected run keeps going a
11269
12492
  real terminal events for that reconnect. Without an `evidenceLog` nothing can rejoin a dropped
11270
12493
  stream, so the disconnect aborts `ports.signal` and the turn suspends as `cancelled`.
11271
12494
 
12495
+ Exceeding `maxOutputBytes` detaches the viewer in the same way, including an oversized initial state
12496
+ snapshot. The response closes immediately, with an `output_limit` error frame when the remaining
12497
+ budget permits it. Without a log the turn is aborted; with a log it continues recording its real
12498
+ terminal result for reconnect rather than storing the viewer's output-limit error as the run result.
12499
+
11272
12500
  ```ts
11273
12501
  import { createMemoryAgentEvidenceLog } from "@nifrajs/agent/events"
11274
12502
 
@@ -11367,6 +12595,11 @@ dropped stream, so the disconnect aborts `ports.signal` and the turn suspends as
11367
12595
  `createMemoryAgentEvidenceLog` (from `@nifrajs/agent/events`) is the single-process dev/test
11368
12596
  reference; a durable, multi-process log is an adapter implementing the same `AgentEvidenceLog`
11369
12597
  interface.
12598
+
12599
+ One SSE response emits at most `maxOutputBytes` (default 4 MiB), so a client that stops reading
12600
+ cannot make the server queue a whole run. Past the cap the response ends with an `error` frame
12601
+ (`output_limit`): with an `evidenceLog` the run keeps recording and a `Last-Event-ID` reconnect reads
12602
+ on from where the response stopped; without one the run is cancelled.
11370
12603
  When more than one caller shares the route, also pass `evidenceOwner: (c) => <the caller's user or
11371
12604
  tenant id>`: turns are recorded and replayed under that owner, so another caller's reconnect or
11372
12605
  reused `turnId` finds none of them. Without it a turn id works as a bearer capability. The owner
@@ -11404,6 +12637,11 @@ counts.
11404
12637
  - **Ordered, deduplicated streaming.** A turn arrives as an ordered stream of `AgentEventView`s.
11405
12638
  Out-of-order and redelivered frames are reordered and de-duplicated by sequence; a bounded buffer
11406
12639
  skips an unfilled gap rather than stalling.
12640
+ - **Turns that survive a dropped connection.** When a turn's stream breaks mid-turn, `send` follows
12641
+ the turn again with `turn.attach` from the last seq it received (`resumeAttempts`, default 3;
12642
+ `resumeDelayMs`, default 250), so the views continue without a gap or a repeat. The host must keep
12643
+ turns (`resumableTurns` on `CodingAgentRpcServer`); one that cannot resume refuses, and `send`
12644
+ throws the original break.
11407
12645
  - **Cursor resume.** A persisted log can be replayed from a cursor. A cursor whose next record was
11408
12646
  evicted from the retained window asks the caller to resync.
11409
12647
  - **Approvals and handoffs.** Pending approvals list as identifiers; decisions and handoff outcomes go
@@ -11469,10 +12707,57 @@ const events = createAgentEventStream(256)
11469
12707
  events.push(/* a versioned AgentEvent */)
11470
12708
  ```
11471
12709
 
12710
+ `isAgentEvent` refuses an event past the protocol's bounds: 256 KiB characters of `text`, `prompt`, or
12711
+ error message, and 64 KiB per string inside `input`, `output`, or `details`. A backend keeps its
12712
+ events inside them with `splitAgentText` (a long reply as consecutive events that join back),
12713
+ `boundAgentText` (one field, cut with a marker), and `boundAgentValue` (a display copy of a tool's
12714
+ input or output; the tool itself still gets the whole value). `agentEventProblem` says why an event
12715
+ was refused, naming the field and the limit but never its content.
12716
+
12717
+ Three events carry what most coding agents report besides their reply: `reasoning.delta` (a piece
12718
+ of the model's reasoning, grouped by an optional `itemId`), `plan.updated` (the whole current plan,
12719
+ entries of `content`, `status`, and optional `priority`, each event replacing the last), and
12720
+ `context.usage` (`usedTokens` of the latest request, the model's `windowTokens` when known, and
12721
+ optional input, output, cached, and reasoning counts).
12722
+
12723
+ Any other frame a backend has of its own travels as an extension event:
12724
+ `type` is `x-<vendor>.<name>` (lowercase, at most 64 characters), with an optional `turnId` and a
12725
+ `data` value inside the same payload bounds. Hosts that do not know the type pass it through; a host
12726
+ that does can register a check for it.
12727
+
12728
+ ```ts
12729
+ yield { version: 1, sessionId, seq, at: Date.now(), type: "x-codex.plan", turnId, data: { steps } }
12730
+ ```
12731
+
11472
12732
  Use `@nifrajs/pi` for the Pi backend or implement `AgentBackend` for another
11473
12733
  runtime. The protocol is intentionally small so CLI, desktop, mobile, and CI
11474
12734
  clients can share it without pulling in the Nifra application framework.
11475
12735
 
12736
+ ## Conformance kit
12737
+
12738
+ `@nifrajs/agent-protocol/conformance` drives an `AgentBackend` through what a host relies on and
12739
+ returns a report, so an adapter for another agent can prove itself in any test runner:
12740
+
12741
+ ```ts
12742
+ import { checkAgentBackend } from "@nifrajs/agent-protocol/conformance"
12743
+
12744
+ const report = await checkAgentBackend({
12745
+ backend: () => new MyBackend(),
12746
+ cwd: process.cwd(),
12747
+ prompt: "say hi",
12748
+ slowPrompt: "wait until cancelled", // enables turn.busy and turn.cancel
12749
+ approvalPrompt: "write a file", // enables the approval checks
12750
+ })
12751
+ expect(report.checks.filter((check) => check.status === "failed")).toEqual([])
12752
+ ```
12753
+
12754
+ Each scenario gets a fresh backend and session. The checks: a valid session snapshot; a turn whose
12755
+ events are valid, ordered, of one turn, and end with exactly one terminal event; `session.completed`
12756
+ for a plain turn and an idle snapshot after it; a second message refused while a turn runs, without
12757
+ ending it; a cancel that ends with `session.stopped` and leaves the session usable; approvals allowed
12758
+ and denied; `undefined` from `resolveApproval` for an approval it never asked for or whose turn was
12759
+ cancelled; and no turn for a closed session.
12760
+
11476
12761
  ## Version 1 compatibility
11477
12762
 
11478
12763
  Version 1 is additive. Hosts advertise capabilities and clients use only the negotiated intersection;
@@ -11846,7 +13131,12 @@ await cache.invalidateTag(`user:${id}`)
11846
13131
  **immediately** while a **background** refresh runs (deduped). Latency stays flat; data self-heals.
11847
13132
  - **Miss/expired**: awaits `loader`. Concurrent misses for the same key share **one** call (no stampede).
11848
13133
  - 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()`**.
13134
+ - **Store outage**: a store `get` that throws or rejects is a miss, so the loader answers; a `set` that
13135
+ fails still returns the loaded value, and a `fence` that fails leaves that load uncached. Each
13136
+ failure goes to `onStoreError(error, { op })`, which by default logs one line per operation per
13137
+ minute without the key. The explicit `get`, `set`, `delete`, `invalidateTag` and `clear` still
13138
+ reject, so a failed invalidation is never silent.
13139
+ - **`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
13140
  - TTL: `ttlMs` (→ stale) + optional `swrMs` (→ served-stale window) + optional `tags`.
11851
13141
 
11852
13142
  ## Stores
@@ -11860,6 +13150,14 @@ opt-in. For a cache shared across instances, implement `CacheStore`
11860
13150
  const cache = createCache({ store: new RedisCacheStore(redis) })
11861
13151
  ```
11862
13152
 
13153
+ A store several caches or processes share should also implement the optional `fence(key, tags)` and
13154
+ `setIfFence(key, entry, tags, token)`. A `wrap` load takes a fence before it runs and stores its result
13155
+ through `setIfFence`, which must refuse, in one atomic step, when a `set` or `delete` of the key, an
13156
+ `invalidateTag` of one of its tags, or a `clear` ran since that fence, from any cache. Without them, a
13157
+ load running in one process can store a value another process invalidated meanwhile. `MemoryCache`
13158
+ implements both, and `cacheStoreCertificationProfile({ fencing: true })` from
13159
+ `@nifrajs/testing/certification` checks a store's implementation.
13160
+
11863
13161
  On **Cloudflare Workers** the in-memory cache is per-isolate and short-lived - back it with **CF KV** (or
11864
13162
  the Cache API) via a `CacheStore` for anything that should survive across requests/instances.
11865
13163
 
@@ -11867,7 +13165,8 @@ the Cache API) via a `CacheStore` for anything that should survive across reques
11867
13165
 
11868
13166
  `observer` receives one event per operation after it settles: `{ op, outcome, startedAt, durationMs,
11869
13167
  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
13168
+ finished background refresh (`op: "revalidate"`), `error` when the store or loader threw. A `wrap`
13169
+ that loaded through a store outage reports `miss`. An observer
11871
13170
  that throws is swallowed; it never changes a result. Without an observer the cache reads no extra clock.
11872
13171
 
11873
13172
  `cache.for(c)` binds a request (or job) context, and every event carries it. That is how
@@ -11890,7 +13189,7 @@ whenever it is set.
11890
13189
 
11891
13190
  ## API
11892
13191
 
11893
- - `createCache(options?)` → `Cache` - `{ store?, defaultTtlMs?, now?, onError?, observer?, beacon?, capabilities? }`.
13192
+ - `createCache(options?)` → `Cache` - `{ store?, defaultTtlMs?, now?, onError?, onStoreError?, observer?, beacon?, capabilities? }`.
11894
13193
  - `cache.wrap(key, loader, { ttlMs?, swrMs?, tags? })` · `get` · `has` · `set` · `delete` · `invalidateTag` · `clear`.
11895
13194
  - `cache.for(context)` → the same surface bound to a request or job context.
11896
13195
  - `MemoryCache({ maxEntries?, now? })` - the default store; implement `CacheStore` for your own.
@@ -11959,6 +13258,9 @@ the frontend asset `dist/manifest.json`.
11959
13258
  ## `nifra verify` - verification tiers
11960
13259
 
11961
13260
  `nifra check` is the fast project done-gate: TypeScript plus static contract and boundary diagnostics.
13261
+ It reads source without running it, with one exception: when the project has a `contracts.lock.json`,
13262
+ `nifra check` (with `--lints-only` too) imports `backend/app.ts`, running its top-level code, to compare
13263
+ the routes it serves against the lock.
11962
13264
  It is not a release claim. `nifra verify` runs the ordered local verification plan; its default mode
11963
13265
  is the feedback plan and reports which release gates it omits. CI and release automation should run
11964
13266
  `nifra verify --release` (or `bun run check:release`), which includes build, coverage, corpus,
@@ -12141,8 +13443,11 @@ const { ok, status, data, error } = await api.users({ id: "42" }).get()
12141
13443
  `api.files("report.json").get()`.
12142
13444
  - **Result, never exceptions.** Every call resolves to `{ ok, status, data, error }`,
12143
13445
  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`.
13446
+ pass `{ query, headers, signal }` as call options. A DELETE route with a body schema takes
13447
+ its body as an option: `api.items.delete({ body: { ids } })`. A call that gets no HTTP answer it
13448
+ can read has `status: 0` and the code `network_error`, `timeout`, `response_too_large`,
13449
+ `malformed_response` (a body its content type does not decode, like a proxy's HTML error page
13450
+ labelled JSON) or `invalid_path`.
12146
13451
  - **A param value is never a path step.** `.` and `..` are resolved by URL parsing in every
12147
13452
  encoding, so `api.users({ id: ".." }).delete()` sends nothing and returns `invalid_path`
12148
13453
  instead of reaching another route. `.subscribe()` reports it through `onError`; `.ws()` throws.
@@ -12198,6 +13503,14 @@ loopback debugger, set `exposeErrorStacks: true` (or pass
12198
13503
  `--expose-error-stacks` to `nifra-agent --rpc`) to include bounded exception
12199
13504
  stacks. This option is rejected for remote binding.
12200
13505
 
13506
+ Each `turn.send` SSE frame carries `id: <seq>`. By default a client that disconnects cancels its
13507
+ turn. With `resumableTurns: true` the turn keeps running: a client that comes back calls
13508
+ `turn.attach` with `{ afterSeq }` (or a `Last-Event-ID` header) to read the frames it missed and
13509
+ follow the rest. A cursor older than the held frames (`replayEvents`, default 1024; `replayBytes`,
13510
+ default 8 MiB) answers `409 resync_required`, so read `session.events` instead; a second
13511
+ `turn.send` while one runs answers `409 turn_running`; and a turn no client has followed for
13512
+ `detachedTimeoutMs` (default 2 minutes) is cancelled.
13513
+
12201
13514
  `nifra-agent` loads `.nifra/extensions/**` from `--cwd` only when started with
12202
13515
  `--extensions`. An extension module's top-level code runs as soon as it is
12203
13516
  imported, before its declared `capabilities` are checked, so pass the flag only
@@ -12211,7 +13524,10 @@ loading code you do not trust.
12211
13524
  an injected model implementation and bounded tools without importing a provider
12212
13525
  SDK. The conversation it sends the model is capped by `maxHistoryChars` (default
12213
13526
  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
13527
+ turn is always sent in full. `maxMessageChars` (default 64 KiB) caps the message `send` accepts and
13528
+ `maxReplyChars` (default 1 MiB) the model's reply; a reply longer than one event's text arrives as
13529
+ consecutive `assistant.message` events, and a tool's event shows a bounded copy of its input while
13530
+ the tool runs with all of it. `ReplayBackend` and `readReplayEvents` provide deterministic protocol
12215
13531
  replays for CI and Workbench regression tests.
12216
13532
 
12217
13533
  ## At-least-once run dispatch
@@ -12233,7 +13549,8 @@ Legacy `FileSessionStore` files remain supported for explicit local compatibilit
12233
13549
  `nifra-agent --migrate-session <id> --migrate-from <legacy-dir> --migrate-to <evidence-dir>` to
12234
13550
  create a separately validated, evidence-only target. The source is left untouched and the command
12235
13551
  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
13552
+ new directory yourself. `FileSessionStore` reads the migrated rows as content-free entries whose
13553
+ 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
13554
  target; a failed or interrupted migration leaves the active choice unchanged.
12238
13555
 
12239
13556
  For AI agents, see [`LLM.md`](./LLM.md) and the full corpus
@@ -12413,7 +13730,9 @@ route. Split groups before the compiler reports `TS2589`, rather than weakening
12413
13730
  client's `transport` option with the same registry. `@nifrajs/core/transport-codec` negotiates
12414
13731
  bounded HTTP representations and supplies the same frame/loader adapters for WebSockets and
12415
13732
  deferred data. Import `richWireCodec()` from `@nifrajs/core/transport-codec-rich`; the separate
12416
- subpath keeps rich-wire code out of plain JSON bundles.
13733
+ subpath keeps rich-wire code out of plain JSON bundles. The plugin decodes a codec body before
13734
+ routing, so a route's `bodyLimit` and its authentication run after the decode; its own `maxBytes`
13735
+ is the bound on that work.
12417
13736
  - **Typed data seam (opt in).** `@nifrajs/core/data` defines token-only operation contracts,
12418
13737
  `db.read`/`db.write` capability names, an opaque request-local `RlsScope`, typed adapter requests,
12419
13738
  drift snapshots, and `createDataPort(contract, adapter, { beacon: useCapability })`, which emits the
@@ -12451,7 +13770,9 @@ contract-first registry.
12451
13770
 
12452
13771
  `response` and `errors` are typed by default but are not runtime enforcement by themselves. Install
12453
13772
  `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.
13773
+ prevent extra fields from reaching the wire. That covers a `status(...)` or `c.json(...)` reply a
13774
+ handler returns or throws, a hook returns, or `onError` answers with. `warn` preserves the original
13775
+ payload during migration.
12455
13776
  Raw `Response`, streams, redirects, and `mountFetch` remain explicit transport escape hatches and
12456
13777
  must be governed by assurance/static review.
12457
13778
 
@@ -12559,6 +13880,7 @@ a Bun-`listen()`-style graceful `stop()` and opt-in signals.
12559
13880
 
12560
13881
  ```ts
12561
13882
  await serve(app, { port: 3000, signals: true }) // SIGTERM/SIGINT → graceful stop()
13883
+ await serve(app, { port: 3000, signals: { drainMs: 8_000 } }) // the same, with an 8 s drain window
12562
13884
  ```
12563
13885
 
12564
13886
  ## What you get for free
@@ -12594,6 +13916,13 @@ The plugin is enabled by default only when `NODE_ENV=development`. It records a
12594
13916
  and serves the overlay stream at `/_nifra/devtools`; remote hosts and cross-origin access stay denied
12595
13917
  unless explicitly allowed. Leave it unregistered in production for zero request-path overhead.
12596
13918
 
13919
+ Each SSE connection also has a finite pending-byte budget (`maxPendingBytes`, default 64 KiB),
13920
+ including keep-alive frames. A slow reader that fills that budget is closed and releases its
13921
+ connection slot. Already queued frames drain before EOF; the overflow frame is dropped. Browser
13922
+ `EventSource` reconnects automatically. Initial delivery includes only the newest history suffix
13923
+ that fits the byte budget. This is a recent trace view, so reconnects can have event-id gaps when
13924
+ older events no longer fit the history or connection budget; it does not guarantee lossless replay.
13925
+
12597
13926
  ## For AI agents
12598
13927
 
12599
13928
  Start with [`LLM.md`](./LLM.md) - this package's contract card (the exports you call + its footguns),
@@ -12780,7 +14109,8 @@ pass the request's bindings: `defineEnv(shape, { source: env })`.
12780
14109
  ## Secret safety
12781
14110
 
12782
14111
  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.
14112
+ secret). A BYO validator such as valibot may quote the value in its message; `defineEnv` replaces
14113
+ it with `<redacted>`. `defineEnv` returns a frozen object, so a validated config can't be mutated downstream.
12784
14114
 
12785
14115
  ## For AI agents
12786
14116
 
@@ -12875,6 +14205,15 @@ Resolvers read it as their third argument.
12875
14205
 
12876
14206
  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
14207
 
14208
+ Each reference subscriber queues at most 1024 events by default. If publication outruns consumption,
14209
+ the overloaded iterator detaches, clears its queued values, and its next pull rejects with
14210
+ `GraphqlPubSubOverflowError` (`code: "graphql_pubsub_overflow"`). Configure
14211
+ `createPubSub({ maxQueuedEvents: 256, overflow: "error" })` to change the finite limit. Explicit
14212
+ `"drop-oldest"` and `"drop-newest"` policies support consumers that accept missing events. Returning or
14213
+ throwing into an iterator clears its queue and terminates delivery. This is an event-count bound;
14214
+ publishers remain responsible for payload sizes. Synchronous `publish` does not provide producer
14215
+ backpressure or cross-instance delivery.
14216
+
12878
14217
  ```ts
12879
14218
  import { server } from "@nifrajs/core"
12880
14219
  import { websocket } from "@nifrajs/core/ws"
@@ -12897,6 +14236,8 @@ pubsub.publish("count", 1)
12897
14236
 
12898
14237
  The route must negotiate the `graphql-transport-ws` subprotocol at the WebSocket layer for a browser `graphql-ws` client to connect.
12899
14238
 
14239
+ 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.
14240
+
12900
14241
  ## Options
12901
14242
 
12902
14243
  | Option | Default | Purpose |
@@ -12909,12 +14250,12 @@ The route must negotiate the `graphql-transport-ws` subprotocol at the WebSocket
12909
14250
  | `allowedOrigins` | reflect `*` | CORS origin allowlist. |
12910
14251
  | `authorize` | - | Per-request guard; return `false` for 401. |
12911
14252
  | `enableGet` | `true` | Serve `GET /graphql` (queries only). |
12912
- | `subscriptions` | `false` | `true` (reuse schema/context) or explicit WS options. |
14253
+ | `subscriptions` | `false` | `true` (reuse schema, context, `authorize` and limits) or explicit WS options. |
12913
14254
  | `legacyJsonResponse` | `false` | Old always-200 `application/json` shape. |
12914
14255
 
12915
14256
  ## Responses
12916
14257
 
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.
14258
+ 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
14259
 
12919
14260
  ## License
12920
14261
 
@@ -13110,7 +14451,7 @@ framework runtime. The client companion to `@nifrajs/web-vanilla`: the server re
13110
14451
  (zero framework JS); islands attach behavior to it in place. No VDOM, no hydration re-render -
13111
14452
  the markup the server sent IS the initial state.
13112
14453
 
13113
- The full island bundle is just under 2 KB gzipped, and a size test keeps it there. Use
14454
+ The full island bundle is about 2 KB gzipped (2,080 B at most), and a size test keeps it there. Use
13114
14455
  it for small interactive widgets where a full framework runtime would be overkill.
13115
14456
 
13116
14457
  ## Server side (any adapter - `@nifrajs/web-vanilla` shown)
@@ -13176,7 +14517,10 @@ bind it, for example to show or hide the nested island.
13176
14517
 
13177
14518
  `signal(initial)` / `computed(fn)` / `effect(fn) → dispose` / `batch(fn)` - auto-tracking with
13178
14519
  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,
14520
+ into one flush. A `computed` runs on first read and stops by itself once nothing reads it: the
14521
+ next change to its inputs stops it instead of recomputing, and reading it again computes it afresh.
14522
+ Every effect an island's setup creates stops when `mountIslands`' disposer unmounts it; one created
14523
+ outside an island runs until its disposer is called. Island-scale by design: if a widget outgrows this (lists, ownership trees,
13180
14524
  async orchestration), that's the signal to use the Solid adapter for that page - both tiers are
13181
14525
  first-class.
13182
14526
 
@@ -13246,6 +14590,15 @@ const q = createQueue({ store: new RedisJobStore(redis) })
13246
14590
  Leasing is at-least-once: a leased job is hidden for `leaseMs`; a worker that dies mid-job releases it
13247
14591
  back automatically. Make handlers **idempotent**.
13248
14592
 
14593
+ A job's `timeoutMs` bounds each attempt: one still running after that many ms fails with a
14594
+ `JobTimeoutError` (then retries or dead-letters), frees its worker slot, and aborts `ctx.signal`. Pass the
14595
+ signal on to `fetch` or your driver, since a handler that ignores it keeps running unobserved. Keep
14596
+ `timeoutMs` below `leaseMs`, after which the job can be leased again.
14597
+
14598
+ ```ts
14599
+ q.define("render", { timeoutMs: 10_000, async handler(id, ctx) { await fetch(url(id), { signal: ctx.signal }) } })
14600
+ ```
14601
+
13249
14602
  `enqueue` may pass an optional `traceparent` (the producer's W3C trace context); a store should persist
13250
14603
  it and hand it back on every `lease`. A store that drops it still runs every job - a traced run only
13251
14604
  loses its link to the producer. `jobStoreCertificationProfile({ traceparent: true })` from
@@ -13309,10 +14662,10 @@ the run stays in the producer's trace.
13309
14662
  ## API
13310
14663
 
13311
14664
  - `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)`.
14665
+ - `queue.define(name, { handler, input?, retries?, timeoutMs? })` → typed `JobHandle` with `.enqueue(payload, { delayMs? | runAt?, traceparent? })` and `.for(context)`.
13313
14666
  - `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.
14667
+ - `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.
14668
+ - `queue.process()` - run one poll round (for Workers / custom drivers). `queue.drain()` - process until nothing is due or in flight.
13316
14669
  - `queue.counts()` → `{ pending, active, dead }`. `queue.store` - the underlying store.
13317
14670
  - Stores: `MemoryJobStore`. Backoff: `exponentialBackoff`, `fixedBackoff`, `noBackoff`.
13318
14671
 
@@ -13477,8 +14830,8 @@ Each layer refuses on its own, in this order:
13477
14830
 
13478
14831
  `explainPostgres` returns the plan (with `analyze`, executed inside the same transaction),
13479
14832
  `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.
14833
+ writes the SQL for a read-only role (`GRANT SELECT` on each allowed schema, with a `REVOKE` for each
14834
+ excluded table and its partitions) without running it.
13482
14835
 
13483
14836
  The live tests run against a throwaway server: set `NIFRA_TEST_POSTGRES_URL` to its superuser URL.
13484
14837
 
@@ -13533,6 +14886,9 @@ const app = server()
13533
14886
  - **`basicAuth(options)`** - Basic Auth plugin with constant-time static credential comparison or
13534
14887
  a custom verifier.
13535
14888
  - **`bearer(options)` / `apiKey(options)`** - token auth plugins with typed principals.
14889
+ - Required `basicAuth`, `bearer`, `apiKey` and `jwt` refuse in the core `authenticate()` stage:
14890
+ before the body is read or validated and before any `derive` or `beforeHandle`. A rate limiter
14891
+ that must count refused requests belongs in `onRequest`, where `rateLimit()` runs.
13536
14892
  - **`csrf({ secret })`** - signed double-submit CSRF protection plus Origin/Referer checking. The
13537
14893
  token travels in the `x-csrf-token` header; set `field: "_csrf"` to also accept it from a plain
13538
14894
  HTML form's hidden input (urlencoded or multipart, bodies up to `fieldMaxBytes`, default 64 KiB).
@@ -13636,9 +14992,10 @@ The mock server matches reflected routes and synthesizes values from supported J
13636
14992
  Unsupported or unsatisfiable schemas fail explicitly instead of returning known-invalid data. Use a
13637
14993
  fixed seed when responses must be replayable across local development and CI.
13638
14994
 
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`:
14995
+ A validator that implements Standard JSON Schema (`~standard.jsonSchema`, zod 4.2+ among them) mocks
14996
+ real data with no wiring, and an older zod 4 is converted with `z.toJSONSchema`. Routes whose response
14997
+ validator exposes neither mock as `{}` by default. Pass `reflectJsonSchema` to derive a schema from such
14998
+ a validator; for zod, the bridge ships as `@nifrajs/testing/zod`:
13642
14999
 
13643
15000
  ```ts
13644
15001
  import { zodJsonSchema } from "@nifrajs/testing/zod"
@@ -13709,10 +15066,12 @@ The optional `static` root is served before `app.fetch` for GET and HEAD request
13709
15066
  public asset directory and must contain nothing private: no secrets, source maps, build
13710
15067
  metadata, or user uploads. The directory and its ancestor path must be read-only and owned by
13711
15068
  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
15069
+ the file before opening it, and refuses it when its final file or any directory on its path became
15070
+ a symlink in the meantime: on macOS the open itself refuses such a path, and on Linux the opened
15071
+ descriptor's path is checked. On Windows and other platforms no file API can open without following a
15072
+ middle link, so a process that swaps a directory concurrently can still win a local TOCTOU race;
15073
+ the check there only makes it need repeated swaps. Mutable or attacker-writable static trees are
15074
+ therefore outside the adapter's guarantee on those platforms. Store user-controlled files outside the static root and serve them through
13716
15075
  an application or object-storage path with its own validation policy.
13717
15076
 
13718
15077
  Every file carries a strong `ETag` (from mtime and size), `Last-Modified`, and
@@ -13730,6 +15089,13 @@ process signals should be a deliberate choice):
13730
15089
  await serve(app, { port: 3000, signals: true })
13731
15090
  ```
13732
15091
 
15092
+ In-flight requests get 10 s to finish before the stop forces them closed. Set the window to fit
15093
+ the platform's kill grace period:
15094
+
15095
+ ```ts
15096
+ await serve(app, { port: 3000, signals: { drainMs: 8_000 } })
15097
+ ```
15098
+
13733
15099
  ## What you get for free
13734
15100
 
13735
15101
  The request timeout (`server({ requestTimeoutMs })` → `503 request_timeout`) and body-size
@@ -13737,6 +15103,10 @@ cap live **inside `app.fetch`**, not Bun's `listen()` - so they apply through th
13737
15103
  with no extra wiring. Slow-client / slow-loris protection is Node's built-in
13738
15104
  `requestTimeout` (300s) and `headersTimeout` (60s) defaults.
13739
15105
 
15106
+ A hook that throws past `app.fetch`, or a response Node refuses to write (a header value carrying
15107
+ 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
15108
+ with `req.clone()`, not `new Request(req)`, which Node's `Request` constructor refuses.
15109
+
13740
15110
  Works with any `{ fetch(req): Promise<Response> }` handler, not just nifra. ESM-only. MIT.
13741
15111
 
13742
15112
  ## For AI agents
@@ -13773,7 +15143,9 @@ const app = server()
13773
15143
  the inbound span as the parent. No inbound header → starts a fresh trace.
13774
15144
  - **Opens a span** with HTTP semantic-convention attributes (`http.request.method`, `url.path`,
13775
15145
  `http.response.status_code`, optional `service.name`), ended on response with duration + status
13776
- (`error` for 5xx, `ok` otherwise).
15146
+ (`error` for 5xx, `ok` otherwise). A request that matched a route reports the route template as
15147
+ `url.path`, so an id or a token in the path stays out of the trace backend; `rawUrlPath: true`
15148
+ reports the raw path instead.
13777
15149
  - **Exposes `c.trace`** (`{ traceId, spanId, parentSpanId?, sampled, traceparent }`) - spread
13778
15150
  `traceHeaders(c.trace)` into any downstream `fetch`/`ctx.api` call to continue the trace.
13779
15151
  - **Exposes `c.causality`** - a bounded, payload-free request node that survives durable command,
@@ -14026,7 +15398,7 @@ const upstream = createProxy({ upstream: "http://127.0.0.1:8081" })
14026
15398
  const app = server().mountFetch("/api", upstream, { stripPrefix: true })
14027
15399
  ```
14028
15400
 
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`.
15401
+ 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
15402
 
14031
15403
  ## Docs
14032
15404
 
@@ -14296,7 +15668,7 @@ const keys = await storage.list({ prefix: "avatars/" })
14296
15668
  | Adapter | Use | Notes |
14297
15669
  |---|---|---|
14298
15670
  | `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 |
15671
+ | `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
15672
  | `R2Storage(env.BUCKET)` | the edge (Cloudflare R2) | round-trips `contentType` + metadata; binding typed structurally (no `@cloudflare/workers-types`) |
14301
15673
 
14302
15674
  Implement `StorageAdapter` (`put` / `get` / `delete` / `exists` / `list`) for **S3 / GCS / anything else** -
@@ -14394,6 +15766,16 @@ The validator still decides which generated mutations are truly invalid; the sup
14394
15766
  weaken validation. A missing/invalid witness is a coverage gap, not a silent pass. Set
14395
15767
  `requireCoverage: false` only when gaps are intentionally advisory.
14396
15768
 
15769
+ Declared `params` and `headers` get witnesses too, validated before any case runs, so a body or query
15770
+ case is rejected by the input it mutates rather than by a placeholder param or a missing header. A `t`
15771
+ schema synthesizes them, the witness's `params` and `headers` override, and a route param the schema
15772
+ leaves out keeps a placeholder its pattern accepts. The harness never invents credentials:
15773
+ `authorization`, `proxy-authorization`, `cookie`, `x-api-key`, and names ending in `-key` or containing
15774
+ `token`, `secret` or `signature` come from the witness (and `prepareRequest`) only, and are never
15775
+ mutated; a schema that requires one the witness lacks is a `NO_WITNESS` gap that names it. Cookies are
15776
+ never synthesized either: a route with a `cookies` schema needs `headers.cookie` in its witness. A
15777
+ params or header field typed as bare text accepts any text, so it is not a `NO_REJECTED_MUTATION` gap.
15778
+
14397
15779
  To exercise adapter parity, pass runtime fetch targets. The reflected `app` remains the one source of
14398
15780
  the contract:
14399
15781
 
@@ -14415,10 +15797,13 @@ laboratory at production.
14415
15797
  ### Opaque validators (zod / valibot / arktype)
14416
15798
 
14417
15799
  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:
15800
+ automatically. So does any validator that implements Standard JSON Schema (`~standard.jsonSchema`,
15801
+ zod 4.2+ among them): the laboratory calls the schema's own converter, so nothing is resolved from disk
15802
+ and a monorepo run from its root reads an app's zod schemas too. An older zod 4 is converted with
15803
+ `z.toJSONSchema`, resolved from the project directory first, then from this package. A validator with
15804
+ neither validates requests fine, but the laboratory can't inspect it - those routes report `NO_WITNESS`
15805
+ and get only type-confusion mutations, and the gap says why when a conversion was tried and failed.
15806
+ Pass `reflectJsonSchema` to convert any other validator, or to choose the zod bridge explicitly:
14422
15807
 
14423
15808
  ```ts
14424
15809
  import { zodJsonSchema } from "@nifrajs/testing/zod"
@@ -14654,6 +16039,17 @@ done-gate, or `nifra mcp` to give the agent live project tools.
14654
16039
 
14655
16040
  ## @nifrajs/web
14656
16041
 
16042
+ Optional server components are implemented for React, Vue, Svelte and Solid in the post-4.0
16043
+ feature branch. React uses native RSC/Flight; the other adapters use native islands in validated
16044
+ server ranges. Definitions require closed input/public-props schemas and authorization. Refresh
16045
+ executes the actual owner GET, preserving admission, read capabilities, budgets and cancellation;
16046
+ output is private/no-store. Mutate through validated server functions or actions.
16047
+
16048
+ Use the `/server-components`, `/server-components/client`, `/server-components/islands` and
16049
+ `/server-components/build` exports with an explicit `componentPipeline` named config export.
16050
+ See [the server-components guide](https://nifra.dev/docs/server-components) and the four runnable
16051
+ repository examples for complete entries, native outlets, exact compiler pins and rollback.
16052
+
14657
16053
  Framework-agnostic SSR core for nifra - the render seam + HTML document orchestration.
14658
16054
 
14659
16055
  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`.
@@ -14885,7 +16281,8 @@ const registration = await registerWebMcpTools([addItem])
14885
16281
 
14886
16282
  The returned WebMCP `execute` function runs Nifra's existing input validation, capability, approval,
14887
16283
  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
16284
+ receipt envelope so an agent can verify whether the operation committed. A `signal` the host passes as
16285
+ `execute`'s second argument reaches the executor as `ctx.signal`. The normal human UI remains the
14889
16286
  source of page rendering and works when WebMCP is absent.
14890
16287
 
14891
16288
  Receipt output is retained only while its JSON encoding stays within the bounded `maxReceiptBytes`
@@ -15094,6 +16491,10 @@ done-gate, or `nifra mcp` to give the agent live project tools.
15094
16491
 
15095
16492
  `A2AAgentCard`, `A2AAgentSkill`, `A2ACardInfo`, `A2AMountableApp`, `A2ARouteContext`, `A2ATaskState`, `A2A_ERROR_CODES`, `A2A_PROTOCOL_VERSION`, `AGENT_CARD_WELL_KNOWN_PATH`, `MountA2AOptions`, `agentCard`, `mountA2A`
15096
16493
 
16494
+ ## @nifrajs/acp
16495
+
16496
+ `AcpBackend`, `AcpBackendOptions`, `AcpClientInfo`
16497
+
15097
16498
  ## @nifrajs/ag-ui
15098
16499
 
15099
16500
  `AgUIMountableApp`, `AgUIRouteContext`, `AgUIRunContext`, `AgUIRunUsage`, `MountAgUIOptions`, `mountAgUI`
@@ -15104,11 +16505,11 @@ done-gate, or `nifra mcp` to give the agent live project tools.
15104
16505
 
15105
16506
  ## @nifrajs/agent-app
15106
16507
 
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`
16508
+ `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
16509
 
15109
16510
  ## @nifrajs/agent-protocol
15110
16511
 
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`
16512
+ `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
16513
 
15113
16514
  ## @nifrajs/agent-review
15114
16515
 
@@ -15120,11 +16521,11 @@ done-gate, or `nifra mcp` to give the agent live project tools.
15120
16521
 
15121
16522
  ## @nifrajs/auth
15122
16523
 
15123
- `AuthorizationRequest`, `Authorizer`, `CsrfOptions`, `GuardOptions`, `KVNamespaceLike`, `KVSessionStore`, `MemorySessionStore`, `MemorySessionStoreOptions`, `Session`, `SessionContext`, `SessionCookieOptions`, `SessionManager`, `SessionOptions`, `SessionRecord`, `SessionStore`, `createSessions`, `csrf`, `isAuthorized`, `requireAuthorization`, `requireSession`, `requireUser`
16524
+ `AuthorizationRequest`, `Authorizer`, `CsrfOptions`, `GuardOptions`, `KVNamespaceLike`, `KVSessionStore`, `MemorySessionStore`, `MemorySessionStoreOptions`, `Session`, `SessionContext`, `SessionCookieOptions`, `SessionManager`, `SessionOptions`, `SessionRecord`, `SessionStore`, `UnauthenticatedResponse`, `createSessions`, `csrf`, `isAuthorized`, `requireAuthorization`, `requireSession`, `requireUser`
15124
16525
 
15125
16526
  ## @nifrajs/authjs
15126
16527
 
15127
- `AuthGuardOptions`, `AuthJSConfig`, `AuthJSOptions`, `Session`, `app`, `authjs`, `getSession`, `requireAuthUser`
16528
+ `AuthGuardOptions`, `AuthJSConfig`, `AuthJSOptions`, `Session`, `UnauthenticatedResponse`, `app`, `authjs`, `getSession`, `requireAuthUser`
15128
16529
 
15129
16530
  ## @nifrajs/aws-lambda
15130
16531
 
@@ -15132,19 +16533,19 @@ done-gate, or `nifra mcp` to give the agent live project tools.
15132
16533
 
15133
16534
  ## @nifrajs/better-auth
15134
16535
 
15135
- `AuthedOptions`, `BetterAuthLike`, `BetterAuthOptions`, `Principal`, `PrincipalFor`, `RequireSessionOptions`, `SessionOf`, `SessionUserOf`, `WithPrincipal`, `authed`, `betterAuth`, `getSession`, `requirePrincipal`, `requireSession`
16536
+ `AuthedOptions`, `BetterAuthLike`, `BetterAuthOptions`, `Principal`, `PrincipalFor`, `RequireSessionOptions`, `SessionOf`, `SessionUserOf`, `UnauthenticatedResponse`, `WithPrincipal`, `authed`, `betterAuth`, `getSession`, `requirePrincipal`, `requireSession`
15136
16537
 
15137
16538
  ## @nifrajs/cache
15138
16539
 
15139
- `Cache`, `CacheCapabilities`, `CacheEvent`, `CacheObserver`, `CacheOperation`, `CacheOptions`, `CacheOutcome`, `CacheStore`, `CapabilityBeacon`, `MemoryCache`, `MemoryCacheOptions`, `SetOptions`, `StoredEntry`, `WrapOptions`, `createCache`
16540
+ `Cache`, `CacheCapabilities`, `CacheEvent`, `CacheObserver`, `CacheOperation`, `CacheOptions`, `CacheOutcome`, `CacheStore`, `CacheStoreOp`, `CapabilityBeacon`, `MemoryCache`, `MemoryCacheOptions`, `SetOptions`, `StoredEntry`, `WrapOptions`, `createCache`
15140
16541
 
15141
16542
  ## @nifrajs/client
15142
16543
 
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`
16544
+ `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
16545
 
15145
16546
  ## @nifrajs/coding-agent
15146
16547
 
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`
16548
+ `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`, `isVerificationAvailable`, `migrateLegacySession`, `parseCapabilityManifest`, `parseSessionEvidenceRecord`, `readBoundedText`, `readProjectDiff`, `readReplayEvents`, `runNifraContext`, `runNifraReview`, `runNifraVerification`, `stableSessionEventCode`, `validateExtensionModule`
15148
16549
 
15149
16550
  ## @nifrajs/content
15150
16551
 
@@ -15152,7 +16553,7 @@ done-gate, or `nifra mcp` to give the agent live project tools.
15152
16553
 
15153
16554
  ## @nifrajs/core
15154
16555
 
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`
16556
+ `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`, `isSameOriginPath`, `isSameOriginRequest`, `jsonLogger`, `markAuthenticatedRequest`, `parseCookies`, `pathnameOf`, `redactLogFields`, `rejected`, `replacedRequestOf`, `routePatternOverlap`, `serializeCookie`, `server`, `signValue`, `silentLogger`, `status`, `toFetchHandler`, `toLambdaHandler`, `toNetlifyHandler`, `toVercelHandler`, `unsignValue`, `urlPartsOf`, `writableResponse`
15156
16557
 
15157
16558
  ## @nifrajs/cron
15158
16559
 
@@ -15180,7 +16581,7 @@ done-gate, or `nifra mcp` to give the agent live project tools.
15180
16581
 
15181
16582
  ## @nifrajs/graphql
15182
16583
 
15183
- `GraphqlContextBuilder`, `GraphqlContextInput`, `GraphqlHttpOptions`, `GraphqlPubSub`, `GraphqlWsOptions`, `MountGraphqlOptions`, `NifraContextLike`, `buildContext`, `createPubSub`, `graphqlWebSocket`, `mountGraphql`, `respondGraphql`
16584
+ `GraphqlContextBuilder`, `GraphqlContextInput`, `GraphqlHttpOptions`, `GraphqlPubSub`, `GraphqlPubSubOptions`, `GraphqlPubSubOverflowError`, `GraphqlWsOptions`, `MountGraphqlOptions`, `NifraContextLike`, `buildContext`, `createPubSub`, `graphqlWebSocket`, `mountGraphql`, `respondGraphql`
15184
16585
 
15185
16586
  ## @nifrajs/i18n
15186
16587
 
@@ -15196,11 +16597,11 @@ done-gate, or `nifra mcp` to give the agent live project tools.
15196
16597
 
15197
16598
  ## @nifrajs/islets
15198
16599
 
15199
- `BindableElement`, `BindableRoot`, `IslandContext`, `IslandHost`, `IslandScope`, `IslandSetup`, `IslandStrategy`, `MountIslandOptions`, `Signal`, `batch`, `bindScope`, `computed`, `effect`, `island`, `islandState`, `mountIslands`, `signal`
16600
+ `BindableElement`, `BindableRoot`, `Computed`, `IslandContext`, `IslandHost`, `IslandScope`, `IslandSetup`, `IslandStrategy`, `MountIslandOptions`, `Signal`, `batch`, `bindScope`, `computed`, `effect`, `island`, `islandState`, `mountIslands`, `signal`
15200
16601
 
15201
16602
  ## @nifrajs/jobs
15202
16603
 
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`
16604
+ `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
16605
 
15205
16606
  ## @nifrajs/mcp
15206
16607
 
@@ -15212,11 +16613,11 @@ done-gate, or `nifra mcp` to give the agent live project tools.
15212
16613
 
15213
16614
  ## @nifrajs/middleware
15214
16615
 
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`
16616
+ `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
16617
 
15217
16618
  ## @nifrajs/mock
15218
16619
 
15219
- `MockServer`, `MockServerOptions`, `MockableApp`, `MockableRoute`, `UnsupportedMockSchemaError`, `autoReflectJsonSchema`, `createMockServer`, `generateMockValue`
16620
+ `MockServer`, `MockServerOptions`, `MockableApp`, `MockableRoute`, `UnsupportedMockSchemaError`, `autoReflectJsonSchema`, `createMockServer`, `explainAutoReflectJsonSchema`, `generateMockValue`
15220
16621
 
15221
16622
  ## @nifrajs/node
15222
16623
 
@@ -15244,7 +16645,7 @@ done-gate, or `nifra mcp` to give the agent live project tools.
15244
16645
 
15245
16646
  ## @nifrajs/schema
15246
16647
 
15247
- `DeferredValue`, `ImportOpenAPIOptions`, `ImportedApiInventory`, `ImportedRoute`, `ImportedRouteQuery`, `NifraSchema`, `OpenAPIDocument`, `OpenAPIImportError`, `OpenAPIInfo`, `OpenApiInventoryDrift`, `Page`, `decodeCursor`, `diffOpenApiInventory`, `encodeCursor`, `fromTypeBox`, `importOpenAPI`, `paginate`, `registerFormat`, `t`, `toOpenAPI`, `toOpenAPIFromEvidence`
16648
+ `DeferredValue`, `ImportOpenAPIOptions`, `ImportedApiInventory`, `ImportedRoute`, `ImportedRouteQuery`, `JsonValue`, `NifraSchema`, `OpenAPIDocument`, `OpenAPIImportError`, `OpenAPIInfo`, `OpenAPIVersion`, `OpenApiInventoryDrift`, `Page`, `decodeCursor`, `diffOpenApiInventory`, `encodeCursor`, `fromTypeBox`, `importOpenAPI`, `paginate`, `registerFormat`, `t`, `toOpenAPI`, `toOpenAPIFromEvidence`
15248
16649
 
15249
16650
  ## @nifrajs/storage
15250
16651
 
@@ -15252,7 +16653,7 @@ done-gate, or `nifra mcp` to give the agent live project tools.
15252
16653
 
15253
16654
  ## @nifrajs/testing
15254
16655
 
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`
16656
+ `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
16657
 
15257
16658
  ## @nifrajs/ts-plugin
15258
16659
 
@@ -15264,7 +16665,7 @@ done-gate, or `nifra mcp` to give the agent live project tools.
15264
16665
 
15265
16666
  ## @nifrajs/web
15266
16667
 
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`
16668
+ `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`, `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
16669
 
15269
16670
  ## @nifrajs/web-preact
15270
16671
 
@@ -15292,7 +16693,7 @@ done-gate, or `nifra mcp` to give the agent live project tools.
15292
16693
 
15293
16694
  ## @nifrajs/webmcp
15294
16695
 
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`
16696
+ `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
16697
 
15297
16698
  ## @nifrajs/workers
15298
16699