@nifrajs/cli 1.13.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +17 -2
  2. package/dist/check.d.ts +17 -6
  3. package/dist/check.d.ts.map +1 -1
  4. package/dist/check.js +256 -15
  5. package/dist/check.js.map +1 -1
  6. package/dist/cli.d.ts +2 -3
  7. package/dist/cli.d.ts.map +1 -1
  8. package/dist/cli.js +58 -71
  9. package/dist/cli.js.map +1 -1
  10. package/dist/doctor.d.ts.map +1 -1
  11. package/dist/doctor.js +108 -48
  12. package/dist/doctor.js.map +1 -1
  13. package/dist/mcp-http.js +1 -1
  14. package/dist/mcp-http.js.map +1 -1
  15. package/dist/mcp.d.ts +2 -0
  16. package/dist/mcp.d.ts.map +1 -1
  17. package/dist/mcp.js +42 -0
  18. package/dist/mcp.js.map +1 -1
  19. package/dist/recipes/2.0.0.d.ts +9 -0
  20. package/dist/recipes/2.0.0.d.ts.map +1 -0
  21. package/dist/recipes/2.0.0.js +25 -0
  22. package/dist/recipes/2.0.0.js.map +1 -0
  23. package/dist/recipes/index.d.ts +7 -0
  24. package/dist/recipes/index.d.ts.map +1 -1
  25. package/dist/recipes/index.js +2 -0
  26. package/dist/recipes/index.js.map +1 -1
  27. package/dist/sync-manifest.d.ts +33 -0
  28. package/dist/sync-manifest.d.ts.map +1 -0
  29. package/dist/sync-manifest.js +97 -0
  30. package/dist/sync-manifest.js.map +1 -0
  31. package/dist/upgrade.d.ts +23 -2
  32. package/dist/upgrade.d.ts.map +1 -1
  33. package/dist/upgrade.js +144 -13
  34. package/dist/upgrade.js.map +1 -1
  35. package/docs/examples.json +69 -21
  36. package/docs/llms-full.txt +1186 -142
  37. package/docs/types.json +1914 -213
  38. package/package.json +9 -9
  39. package/src/check.ts +251 -18
  40. package/src/cli.ts +57 -73
  41. package/src/doctor.ts +133 -49
  42. package/src/mcp-http.ts +1 -1
  43. package/src/mcp.ts +52 -0
  44. package/src/recipes/2.0.0.ts +26 -0
  45. package/src/recipes/index.ts +10 -0
  46. package/src/sync-manifest.ts +111 -0
  47. package/src/upgrade.ts +163 -11
@@ -39,7 +39,7 @@ production backend (routing, validation, middleware, auth, WebSockets). Run it o
39
39
  `.listen()`:
40
40
 
41
41
  ```ts
42
- import { server } from "@nifrajs/core"
42
+ import { server } from "@nifrajs/core/server"
43
43
 
44
44
  server()
45
45
  .get("/", () => ({ hello: "world" }))
@@ -362,7 +362,7 @@ and `c.query` are the _validated_ types, and a bad request gets a structured
362
362
 
363
363
  ```ts
364
364
  // doc-check: skip — uses the third-party `zod` schema lib (any Standard Schema works); install it to run this.
365
- import { server } from "@nifrajs/core"
365
+ import { server } from "@nifrajs/core/server"
366
366
  import { z } from "zod" // any Standard Schema works: zod, valibot, arktype…
367
367
 
368
368
  export const app = server()
@@ -427,7 +427,7 @@ builder produces.
427
427
 
428
428
  ```ts
429
429
  // doc-check: skip — uses the third-party `zod` schema lib + an illustrative `users` repo; install zod to run this.
430
- import { defineContract, implement } from "@nifrajs/core"
430
+ import { defineContract, implement } from "@nifrajs/core/contract"
431
431
  import { z } from "zod"
432
432
 
433
433
  // 1. Declare the contract — methods, paths, and input schemas, no handlers.
@@ -945,7 +945,8 @@ resolves, you call `stream.close()`, or the client disconnects (
945
945
  Node, Deno, and the edge — no `new Function`, no per-runtime API.
946
946
 
947
947
  ```ts
948
- import { server, sse } from "@nifrajs/core"
948
+ import { server } from "@nifrajs/core/server"
949
+ import { sse } from "@nifrajs/core/sse"
949
950
 
950
951
  const app = server()
951
952
  // Your pub-sub of choice — subscribe returns an unsubscribe function.
@@ -1171,7 +1172,7 @@ plugins both depend on it), it wires its hooks once.
1171
1172
 
1172
1173
  ```ts
1173
1174
  // doc-check: skip — fragment: the outer `app`, `verify`, and `db` are your application's.
1174
- import { definePlugin } from "@nifrajs/core"
1175
+ import { definePlugin } from "@nifrajs/core/server"
1175
1176
 
1176
1177
  // A plugin is just (app) => app — call use/derive/decorate or register routes.
1177
1178
  // definePlugin adds a name so applying it twice (even transitively) is a no-op.
@@ -1206,7 +1207,7 @@ client — to `any`, with **no type error and no runtime error**.
1206
1207
  `server().use(betterAuth(auth)).get(...)` keeps every route typed.
1207
1208
 
1208
1209
  ```ts
1209
- import { defineRouterPlugin, server } from "@nifrajs/core"
1210
+ import { defineRouterPlugin, server } from "@nifrajs/core/server"
1210
1211
 
1211
1212
  // A plugin that mounts routes/hooks but adds NO context type: defineRouterPlugin (the clearer name for
1212
1213
  // defineIdentityPlugin) keeps app.use()'s return type EXACTLY the caller's server, so routes added after
@@ -1228,7 +1229,7 @@ short-circuit), `beforeHandle`/`afterHandle` (around the handler),
1228
1229
  error, 404). Hardening middleware uses the same hook model:
1229
1230
 
1230
1231
  ```ts
1231
- import { server } from "@nifrajs/core"
1232
+ import { server } from "@nifrajs/core/server"
1232
1233
  import { cors, rateLimit, securityHeaders, MemoryStore } from "@nifrajs/middleware"
1233
1234
 
1234
1235
  // Hardening middleware is a hook bundle (context-agnostic) — same app.use():
@@ -1244,7 +1245,7 @@ const app = server()
1244
1245
  `@nifrajs/middleware` seeds a few `definePlugin` plugins to build on:
1245
1246
 
1246
1247
  ```ts
1247
- import { server } from "@nifrajs/core"
1248
+ import { server } from "@nifrajs/core/server"
1248
1249
  import { requestId, logger, etag } from "@nifrajs/middleware"
1249
1250
 
1250
1251
  const app = server()
@@ -1309,7 +1310,7 @@ does custom, typed verification.
1309
1310
  clobbering one a handler already set.
1310
1311
 
1311
1312
  ```ts
1312
- import { server } from "@nifrajs/core"
1313
+ import { server } from "@nifrajs/core/server"
1313
1314
  import { compression, cacheControl } from "@nifrajs/middleware"
1314
1315
 
1315
1316
  const app = server()
@@ -1356,7 +1357,7 @@ connection. It short-circuits before the handler; a concurrent retry gets a `409
1356
1357
  guidance (shared store + DB constraint), and the `Set-Cookie` rule.
1357
1358
 
1358
1359
  ```ts
1359
- import { server } from "@nifrajs/core"
1360
+ import { server } from "@nifrajs/core/server"
1360
1361
  import { idempotency, MemoryIdempotencyStore } from "@nifrajs/middleware"
1361
1362
 
1362
1363
  // A retried POST with the same Idempotency-Key replays the first response instead of
@@ -1376,7 +1377,7 @@ Read the typed claims off the returned plugin — `auth.requireClaims(c.req)` (t
1376
1377
  compares static credentials in **constant time** (or takes a `verify` callback).
1377
1378
 
1378
1379
  ```ts
1379
- import { server } from "@nifrajs/core"
1380
+ import { server } from "@nifrajs/core/server"
1380
1381
  import { jwt, jwks, basicAuth } from "@nifrajs/middleware"
1381
1382
 
1382
1383
  // JWT (WebCrypto): an explicit algorithm allowlist is REQUIRED; alg:none and RSA/HMAC confusion are rejected.
@@ -1400,7 +1401,7 @@ user's response to another. `MemoryResponseCache` is per-instance and refuses
1400
1401
  pretty-prints JSON responses (capped, with an optional query toggle).
1401
1402
 
1402
1403
  ```ts
1403
- import { server } from "@nifrajs/core"
1404
+ import { server } from "@nifrajs/core/server"
1404
1405
  import { cache, MemoryResponseCache, prettyJson } from "@nifrajs/middleware"
1405
1406
 
1406
1407
  // Full response cache: pluggable store, Vary-aware keys, byte cap. Bypasses Set-Cookie and respects
@@ -1422,7 +1423,7 @@ no open redirect); `language` negotiates `Accept-Language` into
1422
1423
  is opt-in; `combine` bundles several middleware into one.
1423
1424
 
1424
1425
  ```ts
1425
- import { server } from "@nifrajs/core"
1426
+ import { server } from "@nifrajs/core/server"
1426
1427
  import { methodOverride, trimTrailingSlash, language, timing, poweredBy, combine } from "@nifrajs/middleware"
1427
1428
 
1428
1429
  const app = server()
@@ -1475,7 +1476,7 @@ bypasses that read path. `c.boundedBody(maxBytes?)` and
1475
1476
  `c.boundedJson<T>(maxBytes?)` extend the **same** cap to those routes.
1476
1477
 
1477
1478
  ```ts
1478
- import { server } from "@nifrajs/core"
1479
+ import { server } from "@nifrajs/core/server"
1479
1480
 
1480
1481
  const app = server()
1481
1482
 
@@ -1566,7 +1567,7 @@ the verified text for you to parse with your own schema.
1566
1567
 
1567
1568
  ```ts
1568
1569
  // doc-check: skip — fragment: `app`, `env`, `StripeEvent`, and the rotation keys are your application's.
1569
- import { verifyWebhook } from "@nifrajs/core"
1570
+ import { verifyWebhook } from "@nifrajs/core/webhook"
1570
1571
 
1571
1572
  app.post("/webhooks/stripe", async (c) => {
1572
1573
  // Reads the raw body BOUNDED (DoS guard), verifies the HMAC CONSTANT-TIME, and only
@@ -1646,7 +1647,7 @@ Transient `5xx` aren't cached, so a failed call stays retryable.
1646
1647
  Every one is constant-time where it compares secrets and **fails closed** by default.
1647
1648
 
1648
1649
  ```ts
1649
- import { server } from "@nifrajs/core"
1650
+ import { server } from "@nifrajs/core/server"
1650
1651
  import { jwt, csrf, ipRestriction, bodyLimit } from "@nifrajs/middleware"
1651
1652
 
1652
1653
  const app = server()
@@ -1730,12 +1731,23 @@ treated as a substitute for static provenance.
1730
1731
 
1731
1732
  ```ts
1732
1733
  // doc-check: skip — combines route and assurance-config excerpts.
1733
- // route: exact effect declaration + runtime beacon at the owned adapter seam
1734
- import { useCapability } from "@nifrajs/core/capabilities"
1734
+ // route: exact effect declaration + correlated execution at the owned adapter seam
1735
+ import { executeCapability } from "@nifrajs/core/capabilities"
1736
+
1737
+ app.aroundCapability(async (effect, next) => {
1738
+ // Ask an entitlement service or short-lived approval gate using token-only metadata.
1739
+ // effect.signal aborts on request cancellation or the interceptor timeout.
1740
+ if (!policyAllows(effect.capability, effect.target)) return // deny fail-closed
1741
+ await next()
1742
+ }, { timeoutMs: 5_000 })
1735
1743
 
1736
1744
  app.post("/orders", { capabilities: ["db.write"] }, async (c) => {
1737
- useCapability(c, "db.write") // undeclared use fails before the effect
1738
- return orders.write(c.body)
1745
+ return executeCapability(
1746
+ c,
1747
+ "db.write",
1748
+ { target: "repo:orders" },
1749
+ ({ signal }) => orders.write(c.body, { signal }),
1750
+ )
1739
1751
  })
1740
1752
 
1741
1753
  // in nifra.assurance.ts, alongside policy:
@@ -1765,6 +1777,51 @@ Domain writes on `GET`/`HEAD` are hard violations. Each write definition may
1765
1777
  require request idempotency or durable command/provider-key evidence. The lockfile contains only
1766
1778
  method, path, and capability tokens—no payloads or tenant data—and CI never rewrites it.
1767
1779
 
1780
+ ### Durable approval, compensation, and reconciliation
1781
+
1782
+ The durable execution subpath turns the capability boundary into a crash-visible workflow.
1783
+ Approval resumes are HMAC-signed, expire, bind to the tenant, principal, capability, target, and
1784
+ digest, and are consumed atomically once. The effect journal marks execution before the provider
1785
+ call, so a crash after an external commit remains `ambiguous` for reconciliation instead
1786
+ of being retried blindly. Typed sagas persist compensation arguments separately from the sealed
1787
+ token-only ledger and compensate committed steps in reverse order with retry/backoff state.
1788
+
1789
+ ```ts
1790
+ // doc-check: skip — durable store implementations are deployment-specific.
1791
+ import {
1792
+ createApprovalCoordinator,
1793
+ createDurableEffectJournal,
1794
+ createSagaEngine,
1795
+ } from "@nifrajs/core/durable-execution"
1796
+ import { effectTracing } from "@nifrajs/otel/effects"
1797
+
1798
+ const effects = effectTracing({ exporter })
1799
+ app.use(effects)
1800
+
1801
+ const approval = createApprovalCoordinator({
1802
+ store: durableApprovalStore, // must declare durability: "durable"
1803
+ secret: approvalHmacKey, // 32+ random bytes, stored separately
1804
+ })
1805
+ const journal = createDurableEffectJournal({ store: durableEffectStore })
1806
+
1807
+ await executeCapability(c, "payments.charge", {
1808
+ target: "provider:stripe",
1809
+ digest,
1810
+ journal,
1811
+ approval: {
1812
+ gate: approval,
1813
+ tenantId: c.principal.tenantId,
1814
+ principalId: c.principal.userId,
1815
+ resumeToken,
1816
+ },
1817
+ }, ({ effectId, signal }) => payments.charge(input, { idempotencyKey: effectId, signal }))
1818
+
1819
+ const sagas = createSagaEngine({
1820
+ store: durableSagaStore,
1821
+ observer: effects.observer,
1822
+ })
1823
+ ```
1824
+
1768
1825
  ### Already built in
1769
1826
 
1770
1827
  These add to Nifra's standing defaults: strict-by-default schema validation (unknown fields
@@ -1782,7 +1839,7 @@ embeds a token), pass opt-in `valuePatterns` — `commonSecretPatterns` covers
1782
1839
  bearer tokens, JWTs, emails, and a few well-known key formats, or supply your own:
1783
1840
 
1784
1841
  ```ts
1785
- import { server, jsonLogger, commonSecretPatterns } from "@nifrajs/core"
1842
+ import { server, jsonLogger, commonSecretPatterns } from "@nifrajs/core/server"
1786
1843
 
1787
1844
  // Key-name redaction is always on; valuePatterns adds opt-in value + message scanning.
1788
1845
  const app = server({
@@ -2342,6 +2399,13 @@ Standard Schema
2342
2399
  , you can also bring Zod, Valibot, or ArkType (all eval-free) — only `t` also
2343
2400
  emits OpenAPI from the same definition.
2344
2401
 
2402
+ Size is the other edge axis. `t` is TypeBox-backed - it carries the whole JSON
2403
+ Schema type system - so a validated app on `t` is heavier than one on a lean
2404
+ validator. Validation is per route, so reach for `t` where you want the contract
2405
+ (OpenAPI/MCP for free) and Valibot where the Workers cold-start bundle is what matters: a
2406
+ Valibot-validated Nifra app measures **~16 KB gzipped**. Both are first-class, and you can
2407
+ mix them in one app.
2408
+
2345
2409
  ### Scheduled (cron)
2346
2410
 
2347
2411
  Pass `scheduled` to `toFetchHandler` to also export a Workers cron
@@ -2350,7 +2414,7 @@ typed `env` and `waitUntil` as your request handlers.
2350
2414
 
2351
2415
  ```ts
2352
2416
  // doc-check: skip — `env.KV` is typed by your app's `Env` (the ./app import), supplied by the reader.
2353
- import { toFetchHandler } from "@nifrajs/core"
2417
+ import { toFetchHandler } from "@nifrajs/core/server"
2354
2418
  import { app } from "./app"
2355
2419
 
2356
2420
  // Export fetch + a cron handler. Wire the schedule in wrangler.toml: [triggers] crons = ["0 * * * *"]
@@ -2369,15 +2433,18 @@ export default toFetchHandler(app, {
2369
2433
  through a generic — and runs on every runtime nifra serves: Bun, Deno, Node, and Cloudflare
2370
2434
  Workers.
2371
2435
 
2372
- The WebSocket runtime ships as a subpath, so apps that never use it don’t bundle it. Import
2373
- it once at your server entry; `app.ws()` without it fails loud at boot with
2374
- `WS_RUNTIME_MISSING`. (`@nifrajs/workers` imports it for you.)
2436
+ The WebSocket runtime ships as an opt-in plugin, so apps that never use it don’t bundle it.
2437
+ Enable it with `.use(websocket())` from `@nifrajs/core/ws`;
2438
+ `app.ws()` without it fails loud at registration with
2439
+ `WS_RUNTIME_MISSING`. It installs on that server instance only — the same
2440
+ `.use()` opt-in as `mcp()` and `streaming()`.
2375
2441
 
2376
2442
  ```ts
2377
- import { server } from "@nifrajs/core"
2378
- import "@nifrajs/core/ws" // registers the WebSocket runtime app.ws() needs
2443
+ import { server } from "@nifrajs/core/server"
2444
+ import { websocket } from "@nifrajs/core/ws" // .use(websocket()) enables app.ws()
2379
2445
 
2380
2446
  const app = server()
2447
+ .use(websocket())
2381
2448
  .get("/", () => ({ ok: true }))
2382
2449
  .ws("/echo", {
2383
2450
  open: (ws) => ws.send("welcome"),
@@ -2665,7 +2732,7 @@ SSR verified on real workerd — **this very site runs there**.
2665
2732
 
2666
2733
  ```ts
2667
2734
  // _worker.ts — the edge SSR entry
2668
- import { toFetchHandler } from "@nifrajs/core"
2735
+ import { toFetchHandler } from "@nifrajs/core/server"
2669
2736
  import { createWebApp } from "@nifrajs/web"
2670
2737
  import { reactAdapter } from "@nifrajs/web-react"
2671
2738
  import { clientEntry, manifest } from "./server-manifest" // generated by buildServer
@@ -2682,7 +2749,7 @@ Deploy: `wrangler pages deploy dist` (Pages) or `wrangler deploy`
2682
2749
 
2683
2750
  ## Nifra — CLI
2684
2751
 
2685
- > The zero-config Nifra CLI (`nifra dev`, `nifra build`, `nifra start`): true HMR, hashed client builds, and SSR serve.
2752
+ > The zero-config Nifra CLI (`nifra dev`, `nifra build`, `nifra start`): true HMR and complete target-specific deploy builds.
2686
2753
 
2687
2754
  `Nifra` is zero-config: it reads `routes/`, `framework.ts`, and
2688
2755
  (optionally) `backend.ts` from your project and wires the right
@@ -2691,57 +2758,62 @@ Deploy: `wrangler pages deploy dist` (Pages) or `wrangler deploy`
2691
2758
 
2692
2759
  ```ts
2693
2760
  nifra dev # true-HMR dev server (Vite middleware + nifra SSR) — http://localhost:4321
2694
- nifra build # bundle the client (content-hashed, code-split) + write dist/manifest.json (incl. CSS)
2695
- nifra start # serve the built client + SSR on Bun (assets, <link> stylesheets, matched-route preload)
2761
+ nifra build # full Bun deploy → dist/server.js + content-hashed dist/assets/ (default target: bun)
2762
+ nifra start # run dist/server.js on Bun
2763
+ nifra build --target cf-pages # also: node | deno | vercel | static; add --report for chunk sizes
2696
2764
 
2697
2765
  # dev + start share the default port 4321. Override per run: --port <n> (alias -p) or the PORT env var.
2698
- # flags: --port <n> (dev/start) · --out <dir> (build/start) · --poll (dev; containers/sandboxes)
2766
+ # flags: --port <n> (dev/start) · --out <dir> (build/start) · --target <t> (build) · --poll (dev)
2699
2767
  ```
2700
2768
 
2701
2769
  ### The conventions
2702
2770
 
2703
- Three files at the project root. `nifra dev` runs the Vite-backed
2771
+ Four conventions at the project root. `nifra dev` runs the Vite-backed
2704
2772
  [HMR dev server](/docs/dev); `nifra build` runs the Bun-native production
2705
- build (content-hashed, code-split, CSS bundled); `nifra start` serves the built
2706
- client and SSRs on Bun — assets with immutable caching + correct MIME, the matched route's
2707
- chunks preloaded, and the CSS bundle linked in every `<head>`.
2773
+ build (content-hashed client assets plus a target-specific server); `nifra start`
2774
+ runs the default Bun output. The generated server serves assets and SSR with matched-route
2775
+ chunks preloaded and route CSS linked in each `<head>`.
2708
2776
 
2709
2777
  ```ts
2710
2778
  my-app/
2711
2779
  routes/ # file-based routes (index.tsx, _layout.tsx, [id].tsx, …)
2712
- framework.ts # adapter + clientModule + plugins (above)
2780
+ framework.ts # deploy-safe render adapter
2781
+ nifra.config.ts # CLI-only client module + dev/build plugins
2713
2782
  backend.ts # export const backend = server()... (optional — the typed contract)
2714
2783
  ```
2715
2784
 
2716
2785
  ### framework.ts — naming the framework once
2717
2786
 
2718
- The single framework-specific file. Only `adapter` + `clientModule` are
2719
- required; the plugin/condition fields are extras the compiler frameworks need (React/Preact's
2720
- JSX is Bun-native, so they need none beyond the Vite plugin for dev HMR).
2787
+ Keep the render `adapter` in deploy-safe `framework.ts`. Put
2788
+ `clientModule`, Vite plugins, compiler plugins, conditions, and defines in CLI-only
2789
+ `nifra.config.ts`. This prevents build tooling and native dependencies from entering
2790
+ generated server bundles.
2721
2791
 
2722
2792
  ```ts
2723
- // doc-check: skip — needs the third-party @vitejs/plugin-react; install it to run this.
2724
- // framework.ts — the one place an app names its framework. The CLI reads these:
2725
- import react from "@vitejs/plugin-react"
2793
+ // framework.ts — deploy-safe; generated server entries import this file.
2726
2794
  import { reactAdapter } from "@nifrajs/web-react"
2727
2795
 
2728
2796
  export const adapter = reactAdapter
2797
+
2798
+ // nifra.config.ts — CLI-only build/dev tooling; never imported by a deployed server.
2799
+ // doc-check: skip — needs the third-party @vitejs/plugin-react; install it to run this.
2800
+ import react from "@vitejs/plugin-react"
2801
+ export { adapter } from "./framework"
2729
2802
  export const clientModule = "@nifrajs/web-react/client"
2730
2803
  export const vitePlugins = [react()] // dev HMR (Fast Refresh)
2731
2804
  // Vue/Svelte/Solid also export:
2732
2805
  // clientPlugins = [vueBunPlugin("dom")] // compile routes for the client build
2733
- // serverPlugins = [vueBunPlugin("ssr")] // compile routes for SSR (nifra start registers them)
2806
+ // serverPlugins = [vueBunPlugin("ssr")] // compile routes into the target's server bundle
2734
2807
  // conditions = ["solid"] // Solid: resolve solid-js to its source
2735
2808
  // define = { __VUE_OPTIONS_API__: "true", ... } // Vue feature flags
2736
2809
  ```
2737
2810
 
2738
2811
  ### Scope
2739
2812
 
2740
- `nifra build` + `nifra start` target a Bun (or Node) long-running server —
2741
- the common self-hosted case. For the edge (Cloudflare Workers / Vercel Edge / Deno Deploy),
2742
- use the per-target build entries from `create-nifra`'s `site` template (see
2743
- [Deployment](/docs/deployment)) — `buildServer` bundles a disk-less
2744
- worker those targets need. A `nifra build --target` for edge is a future addition.
2813
+ `nifra build` defaults to a self-hosted Bun server. Use `--target node`,
2814
+ `deno`, `cf-pages`, `vercel`, or `static` for another
2815
+ complete deploy shape (see [Deployment](/docs/deployment)). Run non-Bun outputs with
2816
+ the command printed by the build.
2745
2817
 
2746
2818
  ## Nifra — Dev & HMR
2747
2819
 
@@ -3170,6 +3242,110 @@ params. The error reads out the fix; add a schema to the route:
3170
3242
  `.get("/thing", { query: z.object({ page: z.string() }) }, h)`. Then
3171
3243
  `c.query` is the validated type and the client accepts a typed `query`.
3172
3244
 
3245
+ ###
3246
+ `TS2589` - "Type instantiation is excessively deep and possibly infinite" (one
3247
+ `server()` chain grew past ~95 routes)
3248
+
3249
+ The fluent builder's whole value - end-to-end inference, so `c.params.id` is typed
3250
+ straight from `:id` and the client is derived from `typeof app` - carries
3251
+ an **O(N) type-instantiation cost**. Each `.get(path, handler)` /
3252
+ `.post(...)` does two things at once: it computes the handler's context type from the
3253
+ path, _and_ it returns a server whose registry is one alias level deeper than the last.
3254
+ Neither strains the compiler alone; the **product** - recomputing the handler
3255
+ context while re-threading an ever-larger registry at every step - exhausts TypeScript's
3256
+ per-expression instantiation budget. A single chain hits `TS2589` at
3257
+ **~95-100 routes**.
3258
+
3259
+ [!NOTE]
3260
+
3261
+ This is a **healthy, growing app's wall, not abuse**. It is inherent to any
3262
+ builder that infers handler context _and_ accumulates a typed route registry (Elysia,
3263
+ tRPC, and Hono's typed clients cap the same way), so it is not fixable by reshaping the
3264
+ internal `AddRoute`. The fix is to use a shape that does not form the product.
3265
+
3266
+ **Fix 1 - split into domain groups and `.merge()` them.** Each group is
3267
+ its own short `server()` chain, so no single chain approaches the ceiling; a
3268
+ `.merge()` is one `R & R2` intersection with no per-call context work,
3269
+ so composing groups stays cheap. A 90-route single chain is inside the ceiling; 120 routes as
3270
+ four merged 30-route groups typecheck with full per-route fidelity.
3271
+
3272
+ ```ts
3273
+ import { server } from "@nifrajs/core"
3274
+
3275
+ // Each domain is its OWN short server() chain, kept well under the ~95-route ceiling.
3276
+ const users = server()
3277
+ .get("/users/:id", (c) => ({ id: c.params.id }))
3278
+ .post("/users", () => ({ created: true }))
3279
+
3280
+ const orders = server()
3281
+ .get("/orders/:id", (c) => ({ id: c.params.id }))
3282
+ .post("/orders", () => ({ placed: true }))
3283
+
3284
+ // merge() adds ONE R & R2 intersection per group - no per-call context recompute - so the whole
3285
+ // app stays inside tsc's budget no matter how many groups (or routes) you compose.
3286
+ export const app = server()
3287
+ .get("/health", () => ({ ok: true }))
3288
+ .merge(users)
3289
+ .merge(orders)
3290
+ ```
3291
+
3292
+ **Fix 2 - go contract-first, and stay flat at any route count.**
3293
+ `defineContract(...)` declares the entire registry as **one object type
3294
+ upfront**, and `implement(contract, handlers)` binds handlers to it. Nothing
3295
+ grows a registry per call, so there is **no ceiling at all** - this is the path for
3296
+ an API that will keep adding routes for years. Handlers stay checked against the contract exactly
3297
+ as inline routes are. See [Contract-first](/docs/contract).
3298
+
3299
+ ```ts
3300
+ import { defineContract, implement } from "@nifrajs/core/contract"
3301
+
3302
+ // One object type, declared upfront - NOT an N-deep stack of `Server<AddRoute<…>>` aliases.
3303
+ const contract = defineContract({
3304
+ getUser: { method: "GET", path: "/users/:id" },
3305
+ listUsers: { method: "GET", path: "/users" },
3306
+ createUser: { method: "POST", path: "/users" },
3307
+ // ...hundreds more operations stay flat: the registry is one type, not a growing chain.
3308
+ })
3309
+
3310
+ export const app = implement(contract, {
3311
+ getUser: (c) => ({ id: c.params.id }),
3312
+ listUsers: () => ({ users: [] as string[] }),
3313
+ createUser: () => ({ created: true }),
3314
+ })
3315
+ ```
3316
+
3317
+ ####
3318
+ `TS2345` from an unrelated `.merge()` - budget exhausted "at a distance"
3319
+
3320
+ Because the budget is per-expression and global to a compilation, a type-heavy construct
3321
+ _elsewhere_ in the program can push an otherwise-fine `.merge()` chain over the
3322
+ edge. It surfaces not as `TS2589` but as a `TS2345` assignability error
3323
+ naming an **uninstantiated** `Server` - the shape
3324
+ the server type collapses to when the compiler gives up mid-inference. The usual trigger is a
3325
+ **generic higher-order function that wraps the builder** (a
3326
+ `withX(app)` that threads the `Server` type through its own type
3327
+ parameters); merely having that file in the program can be enough. Treat these as real
3328
+ constraints of an inference-first framework:
3329
+
3330
+ -
3331
+ Keep **service-layer types flat and explicitly annotated** - do not let
3332
+ inference-heavy generics thread the `Server` / registry type through your own code.
3333
+
3334
+ -
3335
+ **Avoid generic HOF wrappers around the builder.** Wrap with a
3336
+ `defineRouterPlugin` (identity plugin) or compose with `.merge()` instead
3337
+ of a `withX(app)` that re-infers the whole server type.
3338
+
3339
+ -
3340
+ Put an explicit `Promise` return annotation on async callbacks that thread
3341
+ framework types, so the compiler stops re-deriving the awaited type at each use.
3342
+
3343
+ [!TIP]
3344
+
3345
+ Both fixes preserve full type fidelity - the client derived from `typeof app` is
3346
+ exactly as precise after a `.merge()` or an `implement()` as it is for an
3347
+ inline route. Splitting or going contract-first costs you nothing at the call site.
3348
+
3173
3349
  ### Still stuck?
3174
3350
 
3175
3351
  Run `nifra check --json` as the done-gate — it surfaces the import-chain leaks,
@@ -3195,7 +3371,7 @@ _over HTTP_. Both run the identical lifecycle — validation, middleware, contra
3195
3371
 
3196
3372
  ```ts
3197
3373
  // backend.ts — a normal @nifrajs/core server. Routes live at the full /api/... path.
3198
- import { server } from "@nifrajs/core"
3374
+ import { server } from "@nifrajs/core/server"
3199
3375
  import { t } from "@nifrajs/schema"
3200
3376
 
3201
3377
  export const backend = server()
@@ -3269,6 +3445,65 @@ Vite-backed dev server) routes every request through that same app's `fetch`. So
3269
3445
  the `/api/*` routes are served identically in development and production — there is
3270
3446
  nothing extra to wire for the dev loop.
3271
3447
 
3448
+ ### Mounting, composition & path semantics
3449
+
3450
+ A useful thing to know up front, because it is the opposite of some frameworks: in Nifra
3451
+ **composition and mounting are prefix-less, and paths stay absolute**. There is no
3452
+ per-mount base path, and nothing rewrites the request URL. Two mechanisms are involved and both
3453
+ leave the path untouched:
3454
+
3455
+ -
3456
+ **
3457
+ `.merge()` (and contract-first `implement()`)
3458
+ **
3459
+ - the composition escape hatch for large apps. `merge()` folds another server's
3460
+ routes into this one _at the exact paths they were defined_, into a single shared path
3461
+ space. It takes no prefix argument; a path+method collision throws a
3462
+ `RouteConfigError` at merge time (fail-closed), so groups never silently shadow
3463
+ each other. Each domain therefore owns its full absolute paths (`/api/listings`,
3464
+ `/api/agents`), exactly as it would standalone.
3465
+
3466
+ -
3467
+ **the `/api/*` auto-mount** - a path _guard_, not a rewrite.
3468
+ `createWebApp` hands the _same_ `Request` (its URL and body
3469
+ intact) to the backend when the pathname is exactly `apiPrefix` or under
3470
+ `apiPrefix + "/"`. The backend's own router then matches on
3471
+ `new URL(c.req.url).pathname` - the full, original path.
3472
+
3473
+ The consequence for a handler: **
3474
+ `c.req.url` always carries the full, original path
3475
+ **
3476
+ - the same URL the caller sent. The `/api` prefix is _not_ stripped before a
3477
+ merged or mounted route sees it, and there is no `pathOf`-style helper that strips
3478
+ it. Whether a route was defined inline, merged in from a group, or reached through the HTTP
3479
+ mount, the pathname it observes is the request's own.
3480
+
3481
+ ```ts
3482
+ // backend.ts - compose domains with .merge(). Each group owns its FULL absolute paths.
3483
+ import { server } from "@nifrajs/core/server"
3484
+
3485
+ const listings = server().get("/api/listings", (c) => {
3486
+ // c.req.url is the ORIGINAL request URL - .merge() and the /api mount never strip the prefix.
3487
+ const path = new URL(c.req.url).pathname // GET /api/listings -> "/api/listings"
3488
+ return { path }
3489
+ })
3490
+
3491
+ const agents = server().get("/api/agents", (c) => ({ ok: new URL(c.req.url).pathname }))
3492
+
3493
+ // merge() folds both groups into ONE flat path space at their defined paths; a path+method clash
3494
+ // throws a RouteConfigError right here at merge time (fail-closed), never a silent shadow at runtime.
3495
+ export const backend = server().merge(listings).merge(agents)
3496
+ ```
3497
+
3498
+ **The one gotcha:** because nothing strips the prefix, a backend that will be
3499
+ mounted (or merged) under `/api` must **declare its routes at the full path**
3500
+ - `server().post("/api/sync", …)`, _not_ `.post("/sync", …)` in the
3501
+ hope that `/api` gets peeled off. Define `/sync` and a browser
3502
+ `POST /api/sync` returns the backend's 404: the router looks up
3503
+ `/api/sync` and finds only `/sync`. `apiPrefix` chooses
3504
+ _which_ requests are handed to the backend; it does not rewrite them, so the route paths
3505
+ and the caller's paths are one and the same.
3506
+
3272
3507
  ### Calling /api/* from the browser
3273
3508
 
3274
3509
  With the routes mounted, a client island or component hits them with the same typed
@@ -3318,7 +3553,7 @@ to local policy, and exposes monotonic remaining time beside the existing cancel
3318
3553
  ### Clamp once at admission
3319
3554
 
3320
3555
  ```ts
3321
- import { server } from "@nifrajs/core"
3556
+ import { server } from "@nifrajs/core/server"
3322
3557
 
3323
3558
  const app = server({
3324
3559
  requestTimeoutMs: 2_000,
@@ -3357,6 +3592,177 @@ Configure a finite `requestTimeoutMs` before handing `c.budget` to a
3357
3592
  production adapter. The no-timeout compatibility path is deliberately unbounded and is not
3358
3593
  written to the wire.
3359
3594
 
3595
+ ## Nifra - Execution causality
3596
+
3597
+ > Carry one bounded, payload-free lineage across the durable seams a trace cannot span: commands, events, workflows, projections, and repairs.
3598
+
3599
+ A trace describes one observation tree. Causality survives the seams a trace cannot: a command
3600
+ commits an event, a workflow resumes tomorrow, a projection consumes that event, and
3601
+ reconciliation repairs the drift. Same execution, four identities that used to stop at the
3602
+ boundary.
3603
+
3604
+ ### Tokens only, by construction
3605
+
3606
+ A causality record has **no payload field**. There is nowhere to put a request
3607
+ body, a tenant identifier, or a URL, and unknown fields are rejected at every nesting level
3608
+ rather than carried along. Identities are bounded tokens, edge relations are bounded tokens,
3609
+ and the lineage stays safe to store and read during an incident.
3610
+
3611
+ ### Start at the boundary
3612
+
3613
+ ```ts
3614
+ import { createMemoryCausalityStore, startCausality } from "@nifrajs/core/causality"
3615
+
3616
+ const recorder = createMemoryCausalityStore()
3617
+ const requestId = crypto.randomUUID()
3618
+
3619
+ // One root node at the ingress boundary. executionId is the graph's identity:
3620
+ // generate it once, then propagate it.
3621
+ const step = startCausality("request", requestId, { executionId: crypto.randomUUID() })
3622
+
3623
+ // step.context propagates; step.record is what a durable adapter appends.
3624
+ await recorder.record(step.record)
3625
+ ```
3626
+
3627
+ ### Continue across seams
3628
+
3629
+ ```ts
3630
+ import { continueCausality, startCausality } from "@nifrajs/core/causality"
3631
+
3632
+ const step = startCausality("request", crypto.randomUUID(), {
3633
+ executionId: crypto.randomUUID(),
3634
+ })
3635
+
3636
+ // The command this request caused. The edge relation is a bounded token.
3637
+ const command = continueCausality(step.context, "command", crypto.randomUUID(), {
3638
+ relation: "caused",
3639
+ })
3640
+
3641
+ // The event that command emitted, inside the same commit.
3642
+ const event = continueCausality(command.context, "event", crypto.randomUUID(), {
3643
+ relation: "emitted",
3644
+ })
3645
+ ```
3646
+
3647
+ `joinCausality` merges several parents into one node, and refuses to join contexts
3648
+ from different executions: a graph that silently spanned two requests would be worse than no
3649
+ graph.
3650
+
3651
+ ### Propagation is a trust boundary
3652
+
3653
+ ```ts
3654
+ import {
3655
+ causalityHeaders,
3656
+ readCausalityHeaders,
3657
+ startCausality,
3658
+ } from "@nifrajs/core/causality"
3659
+
3660
+ const step = startCausality("request", crypto.randomUUID(), {
3661
+ executionId: crypto.randomUUID(),
3662
+ })
3663
+
3664
+ // Outbound: bounded headers only.
3665
+ await fetch("https://billing.internal/charge", {
3666
+ headers: causalityHeaders(step.context),
3667
+ })
3668
+
3669
+ // Inbound: never throws on hostile input.
3670
+ const request = new Request("https://api.example/charge")
3671
+ const parsed = readCausalityHeaders(request.headers)
3672
+ if (!parsed.success) {
3673
+ // "missing" | "incomplete" | "invalid" | "unknown-field" - start a fresh graph.
3674
+ }
3675
+ ```
3676
+
3677
+ Inbound headers are **untrusted by default**. A caller does not get to attach
3678
+ their request to your graph unless you say so, so a service-to-service seam opts in explicitly
3679
+ and anything that fails the check starts a fresh execution instead of being believed.
3680
+
3681
+ ### Storage is yours
3682
+
3683
+ `createMemoryCausalityStore()` is for development and tests. It is bounded, and it
3684
+ refuses to construct under `NODE_ENV=production`: in-memory evidence disappears on
3685
+ restart, which is exactly when an incident timeline matters. Production supplies a durable
3686
+ adapter behind the same `CausalityRecorder` interface, ideally appending in the same
3687
+ transaction as the effect it describes.
3688
+
3689
+ ### Traces still apply
3690
+
3691
+ The two models compose. `@nifrajs/otel` turns the nearest observed ancestor into a
3692
+ span link, so an event consumed tomorrow links back to the request that emitted it rather than
3693
+ inventing a trace identity that never existed.
3694
+
3695
+ ## Nifra - Adapter certification
3696
+
3697
+ > Prove a storage, cache, job, runtime, or event adapter against its interface and emit a hash-verifiable capability matrix.
3698
+
3699
+ An interface is a promise. Certification is how an adapter proves it kept it, in a form someone
3700
+ else can check.
3701
+
3702
+ ### Profiles
3703
+
3704
+ A profile is a named set of checks written against an interface, not an implementation:
3705
+ `cacheStoreCertificationProfile`, `jobStoreCertificationProfile`,
3706
+ `storageAdapterCertificationProfile`, `runtimeAdapterCertificationProfile`
3707
+ , and `eventDeliveryCertificationProfile`. Domain-specific seams define their own
3708
+ with `defineCertificationProfile`, which validates the profile at module load, so a
3709
+ check naming an undeclared capability fails before any adapter runs.
3710
+
3711
+ ### Certify an adapter
3712
+
3713
+ ```ts
3714
+ // doc-check: skip - createRedisCache stands in for the adapter under test
3715
+ import { test } from "bun:test"
3716
+ import {
3717
+ assertAdapterCertification,
3718
+ cacheStoreCertificationProfile,
3719
+ certifyAdapter,
3720
+ } from "@nifrajs/testing/certification"
3721
+
3722
+ test("the Redis cache adapter satisfies the cache contract", async () => {
3723
+ const report = await certifyAdapter({
3724
+ profile: cacheStoreCertificationProfile(),
3725
+ adapterId: "redis-cache",
3726
+ // A FRESH adapter per check, so one failure cannot contaminate the next.
3727
+ createAdapter: () => createRedisCache(url),
3728
+ cleanup: (adapter) => adapter.clear(),
3729
+ })
3730
+
3731
+ assertAdapterCertification(report) // throws, naming every failed check
3732
+ })
3733
+ ```
3734
+
3735
+ Profiles are structural and dependency-free. A third-party adapter can certify itself in its own
3736
+ test suite without adopting anything else, which is the point: trust becomes portable rather
3737
+ than something we vouch for.
3738
+
3739
+ ### The report is the evidence
3740
+
3741
+ ```ts
3742
+ {
3743
+ "schemaVersion": 1,
3744
+ "ok": true,
3745
+ "profile": { "id": "cache-store", "version": 1 },
3746
+ "adapterId": "redis-cache",
3747
+ "capabilities": [
3748
+ { "capability": "read-write", "status": "passed", "checks": ["set-get", "delete"] },
3749
+ { "capability": "tag-invalidation", "status": "passed", "checks": ["invalidate-tag"] }
3750
+ ],
3751
+ "evidenceHash": "9f2c..."
3752
+ }
3753
+ ```
3754
+
3755
+ The capability matrix is the useful artifact. It says what an adapter supports, per capability,
3756
+ with the checks that back each one. `verifyAdapterCertification` recomputes
3757
+ `evidenceHash`, so a stored report can be checked rather than believed.
3758
+
3759
+ ### Failures carry a class, not a message
3760
+
3761
+ A failed check records the error _class_ only. Provider messages routinely carry
3762
+ connection strings, tokens, and payload fragments, and evidence is meant to be shared, so it
3763
+ never becomes a place credentials leak. Cleanup errors cannot turn a failed functional check
3764
+ into a pass.
3765
+
3360
3766
  ## Nifra — Framework contract
3361
3767
 
3362
3768
  > The one-page contract: LoaderContext shape, loader/action/meta/head signatures, the client↔server boundary, and the build-output contract — every signature verified against the source.
@@ -3636,13 +4042,13 @@ or `env`. Keep server-only imports out of a route file's top level.
3636
4042
 
3637
4043
  ### The build-output contract
3638
4044
 
3639
- `nifra build` emits a content-hashed `dist/` plus a
3640
- `manifest.json` you hand to `createWebApp` (`entry`,
4045
+ The programmatic `buildClient()` API emits a content-hashed directory plus a
4046
+ `manifest.json` you can hand to `createWebApp` (`entry`,
3641
4047
  `routes` for per-route preload, `css` + `routeStyles` for
3642
4048
  `<link>` injection).
3643
4049
 
3644
4050
  ```ts
3645
- // `nifra build` → buildClient (Bun.build) writes dist/ + manifest.json. The contract:
4051
+ // buildClient() writes a browser bundle + manifest.json for custom build pipelines:
3646
4052
  {
3647
4053
  "entry": "/assets/_nifra-entry-HASH.js", // the bootstrap module script
3648
4054
  "assets": ["/assets/…"], // every emitted chunk URL
@@ -3727,6 +4133,73 @@ const paths = prerendered.map((p) => p.path)
3727
4133
  await Bun.write("./dist/_routes.json", JSON.stringify(cloudflarePagesRoutes({ prerendered: paths })))
3728
4134
  ```
3729
4135
 
4136
+ ### Publish a reusable router (library code vs app code)
4137
+
4138
+ The fluent `server().get(...).post(...)` builder is **app code**: each
4139
+ call both infers the handler's context from the path _and_ returns a
4140
+ `Server` whose type has one more route intersected in (`AddRoute` /
4141
+ `RouteInfoFor`). That accreting registry type lives happily inside your own program,
4142
+ but it does **not** cleanly survive TypeScript's `.d.ts` declaration
4143
+ emit - for a router you **publish** as a package, the compiler has to serialize
4144
+ the whole inferred chain into your `.d.ts` and chokes (a `TS2883`-class
4145
+ error: the inferred type is too large to name), which forces ugly casts on the boundary. So a
4146
+ distributable router is written **contract-first** instead.
4147
+
4148
+ The recipe: declare the API as a plain `defineContract({…})` object and
4149
+ **export it** (it is the consumer's typed-client source), then ship a factory that
4150
+ returns `implement(contract, handlers)`. The consumer mounts the router with one
4151
+ `.merge(...)` and derives its client straight from the exported contract value -
4152
+ `client(contract, url)`, no `typeof app` import crossing the package
4153
+ boundary. Pass a preamble as the third arg -
4154
+ `implement(contract, handlers, server().use(auth))` - to bake a middleware /
4155
+ `derive` chain into the router's routes (captured at registration).
4156
+
4157
+ ```ts
4158
+ // A PUBLISHABLE router - contract-first, so its types survive `.d.ts` emit for consumers.
4159
+ import { client } from "@nifrajs/client"
4160
+ import { defineContract, implement } from "@nifrajs/core/contract"
4161
+ import { server } from "@nifrajs/core/server"
4162
+ import { t } from "@nifrajs/schema"
4163
+
4164
+ // --- in the package (say @your-org/billing) ---
4165
+ // 1. The CONTRACT is ONE plain declared object - methods, paths, schemas, no handlers.
4166
+ // Export it: it is the single type the package publishes, and a consumer's typed client is
4167
+ // built from THIS value (not from `typeof app`), so no internal server type crosses the seam.
4168
+ export const billingContract = defineContract({
4169
+ getPlan: { method: "GET", path: "/billing/plan" },
4170
+ subscribe: { method: "POST", path: "/billing/subscribe", body: t.object({ plan: t.string() }) },
4171
+ })
4172
+
4173
+ // 2. A FACTORY closes over the host's dependencies and returns a real, mountable router.
4174
+ // `implement` binds handlers to the contract; each handler's context is typed from the contract
4175
+ // alone (`c.body` is { plan: string } below) - no per-route `AddRoute` inference accreting.
4176
+ export interface BillingDeps {
4177
+ charge(plan: string): Promise<{ id: string }>
4178
+ }
4179
+ export function createBillingRouter(deps: BillingDeps) {
4180
+ return implement(billingContract, {
4181
+ getPlan: () => ({ plan: "pro" }),
4182
+ subscribe: (c) => deps.charge(c.body.plan),
4183
+ })
4184
+ }
4185
+
4186
+ // --- in the consuming app ---
4187
+ // 3. Mount the router with ONE `.merge()` - no per-route `.get()/.post()` chain to re-infer.
4188
+ const app = server().merge(createBillingRouter({ charge: async (plan) => ({ id: plan }) }))
4189
+
4190
+ // 4. Build the client from the EXPORTED contract value - decoupled, no `typeof app` import needed.
4191
+ const api = client(billingContract, "https://api.example.com")
4192
+ const { data } = await api.billing.plan.get() // GET /billing/plan
4193
+ await api.billing.subscribe.post({ plan: "pro" }) // POST /billing/subscribe, body typed
4194
+ ```
4195
+
4196
+ Why this survives `.d.ts` emit where the fluent chain doesn't: the contract is a
4197
+ **plain declared object type** (`ContractShape`), fixed up front - there
4198
+ is no grow-the-registry-per-call inference to serialize, so its declaration is small and
4199
+ stable. The consumer still gets full end-to-end types both ways -
4200
+ `.merge()` threads the router's routes into the host app's registry, and
4201
+ `client(contract, url)` types every call from the same exported object.
4202
+
3730
4203
  See [SSG & ISR](/docs/rendering) for prerendering, [Dev & HMR](/docs/dev) for the
3731
4204
  two dev loops and the `--port` flag, and [Loaders & actions](/docs/data) for the
3732
4205
  data layer in depth.
@@ -3767,7 +4240,7 @@ server, and every handler reads it as a typed `c.db`. This is how the scaffold w
3767
4240
  it — and it keeps the client out of each handler's imports.
3768
4241
 
3769
4242
  ```ts
3770
- import { server } from "@nifrajs/core"
4243
+ import { server } from "@nifrajs/core/server"
3771
4244
  import { desc } from "drizzle-orm"
3772
4245
  import { db, notes } from "./db" // your Drizzle client + schema (what the scaffold generates)
3773
4246
 
@@ -3786,7 +4259,7 @@ Node, or `libSQL`/Turso anywhere. Open the connection once and query from your
3786
4259
  routes:
3787
4260
 
3788
4261
  ```ts
3789
- import { server } from "@nifrajs/core"
4262
+ import { server } from "@nifrajs/core/server"
3790
4263
  import { Database } from "bun:sqlite" // Node: better-sqlite3 · edge: Turso/libSQL
3791
4264
 
3792
4265
  // Open the connection ONCE at module scope (the driver pools internally).
@@ -3920,7 +4393,7 @@ directly:
3920
4393
 
3921
4394
  ```ts
3922
4395
  // doc-check: skip — illustrates a third-party driver (postgres + drizzle); install those to run it.
3923
- import { server } from "@nifrajs/core"
4396
+ import { server } from "@nifrajs/core/server"
3924
4397
  import { drizzle } from "drizzle-orm/postgres-js" // or node-postgres / neon-http
3925
4398
  import postgres from "postgres"
3926
4399
  import { eq } from "drizzle-orm"
@@ -3994,6 +4467,98 @@ Complete runnable references — a typed CRUD API you can diff your own against:
3994
4467
  `examples/db-sqlite` (the same API on raw `bun:sqlite`, parameterized
3995
4468
  queries, boundary validation).
3996
4469
 
4470
+ ## Nifra - Failure laboratory
4471
+
4472
+ > Replayable durable-failure schedules on virtual time: crash after commit, duplicate delivery, reordering, budget expiry, and checkpoint contention.
4473
+
4474
+ Durable systems fail on a schedule you never chose: the crash lands between the commit and the
4475
+ publish, the queue delivers twice, the events arrive backwards. The laboratory lets you choose
4476
+ that schedule, then replay it exactly.
4477
+
4478
+ ### Seven failures worth testing
4479
+
4480
+ -
4481
+ `crash` - stop dead at a named durability seam.
4482
+
4483
+ -
4484
+ `duplicate-delivery` - deliver the same batch N times.
4485
+
4486
+ -
4487
+ `reorder-events` - rotate a batch out of order.
4488
+
4489
+ -
4490
+ `delay` - advance virtual time, with no real sleeping.
4491
+
4492
+ -
4493
+ `expire-budget` - report zero remaining at a budget seam.
4494
+
4495
+ -
4496
+ `lose-provider-reply` - run the call, then lose only its reply.
4497
+
4498
+ -
4499
+ `contend-checkpoint` - make a checkpoint conflict.
4500
+
4501
+ ### Place the seams, schedule the failure
4502
+
4503
+ ```ts
4504
+ import { createFailureLab } from "@nifrajs/testing"
4505
+
4506
+ const lab = createFailureLab({
4507
+ seed: 77,
4508
+ schedule: [
4509
+ { kind: "duplicate-delivery", point: "relay.batch", copies: 3 },
4510
+ { kind: "reorder-events", point: "relay.batch" },
4511
+ ],
4512
+ })
4513
+
4514
+ // The adapter under test just names its seams. Same seed, same schedule,
4515
+ // same result - every run.
4516
+ const delivered = lab.deliveries("relay.batch", ["evt-a", "evt-b", "evt-c"])
4517
+ ```
4518
+
4519
+ The controller is an off-hot-path port. Your adapter names its seams; the schedule decides what
4520
+ happens at them. Time is virtual, so a 24-hour delay costs nothing and nothing is flaky.
4521
+
4522
+ ### Assert the invariant, not the output
4523
+
4524
+ ```ts
4525
+ // doc-check: skip - commit/publish/outboxStillPending stand in for the system under test
4526
+ import { runFailureScenario } from "@nifrajs/testing"
4527
+
4528
+ const report = await runFailureScenario(
4529
+ {
4530
+ name: "crash-after-commit",
4531
+ execute: async (lab) => {
4532
+ await commit()
4533
+ lab.checkpoint("outbox.after-commit") // crashes here, per the schedule
4534
+ await publish()
4535
+ return "published"
4536
+ },
4537
+ // The scenario passes only if the invariant survives the failure.
4538
+ verify: async ({ error }) => error !== undefined && (await outboxStillPending()),
4539
+ },
4540
+ { seed: 1, schedule: [{ kind: "crash", point: "outbox.after-commit" }] },
4541
+ )
4542
+
4543
+ report.ok // did the invariant hold?
4544
+ report.replay // { seed, schedule } - reproduces this exact run
4545
+ ```
4546
+
4547
+ A scenario passes only when `verify` returns true after the failure. Losing the
4548
+ provider reply is not a bug; losing the money is. That distinction is the whole point.
4549
+
4550
+ ### Evidence is tokens only
4551
+
4552
+ Injection evidence records the kind, the point, the occurrence, and the virtual time. Never
4553
+ payloads, provider results, exception messages, or stacks. A failing report carries the error
4554
+ _class_ and the exact `replay` inputs, so it reproduces from the report alone
4555
+ without leaking what flowed through the system.
4556
+
4557
+ ### Where it fits
4558
+
4559
+ The same engine backs `nifra levels` L4. Contract invariants fuzz the HTTP surface;
4560
+ the laboratory covers the durable paths underneath, where the interesting failures live.
4561
+
3997
4562
  ## Nifra — Fonts
3998
4563
 
3999
4564
  > Self-host web fonts with zero layout shift: a build-time Google Fonts downloader (the next/font equivalent) plus CLS-safe @font-face + preload primitives.
@@ -4096,62 +4661,196 @@ stylesheet can't redirect the download elsewhere — an SSRF gate), and caps eac
4096
4661
  Filenames are derived only from validated tokens plus a content hash, so there's no path
4097
4662
  traversal.
4098
4663
 
4099
- ## Nifra — Migrating from Express, Hono, Fastify & Elysia
4664
+ ## Nifra 2.0 migration guide — upgrade from 1.x
4100
4665
 
4101
- > Move a backend to Nifra: routing, middleware, validation, and body parsing map directly from Express, Hono, Fastify, and Elysia — and you gain a typed client, multi-runtime deploy, and optional SSR.
4666
+ > Upgrade a Nifra 1.x application to 2.0: automated package and import moves, opt-in runtime plugins, client and web changes, external auth mounts, and release verification.
4102
4667
 
4103
- Nifra's `server()` is a chainable, Web-standard router — the move from Express, Hono,
4104
- Fastify, or Elysia is mostly mechanical renames. What you gain: an end-to-end-typed client with
4105
- zero codegen, schema validation built into the route, the same app on Bun / Node / Deno / the
4106
- edge, and an SSR frontend whenever you want one.
4668
+ Nifra 2.0 removes the compatibility layer retained through 1.x and makes optional runtime
4669
+ systems explicit, instance-scoped plugins. The upgrade command handles deterministic package
4670
+ and import edits; this guide covers the structural changes it deliberately cannot guess.
4107
4671
 
4108
- ### How the concepts map
4672
+ ### 1. Run the executable upgrade
4109
4673
 
4110
- Express / Hono / Fastify
4111
- Nifra
4674
+ ```ts
4112
4675
 
4113
- `app.get(path, handler)`
4676
+ ```
4114
4677
 
4115
- `server().get(path, handler)` — chainable, fully type-inferred
4678
+ The command updates existing `@nifrajs/*`, `nifra`, and
4679
+ `create-nifra` dependency ranges to `2.0.0` while preserving
4680
+ caret/tilde/exact style. It also replaces the removed `@nifrajs/budget` dependency
4681
+ with `@nifrajs/core` and moves its imports to
4682
+ `@nifrajs/core/budget`. Dry-run is the default; `--write` applies and
4683
+ verifies with `nifra check`.
4116
4684
 
4117
- `req` / `res` · Hono `Context`
4685
+ ### 2. Install optional server systems explicitly
4118
4686
 
4119
- `c` — `c.req`, `c.params`, `c.query`; **return** a value (JSON) or a
4120
- `Response`
4687
+ Idempotency, the effect ledger, MCP declarations, SSE, WebSockets, and Node-direct resolution
4688
+ are no longer enabled by server options, side-effect imports, or process-global state. Install
4689
+ only the systems an app uses with `.use()`; registration fails loudly if a required
4690
+ plugin is missing.
4121
4691
 
4122
- `app.use(mw)` · Hono middleware
4692
+ ```ts
4123
4693
 
4124
- `.use()` + the `@nifrajs/middleware` pack (CORS, auth, rate limit, …)
4694
+ ```
4125
4695
 
4126
- `express.json()` / body parsing
4127
- built in — declare a schema and the parsed body is typed
4696
+ The `@nifrajs/node` adapter installs `nodeDirect()` automatically.
4697
+ Only applications calling `app.resolveNode()` directly need to install it.
4128
4698
 
4129
- Fastify JSON schema · `zod` middleware · Elysia `t`
4699
+ ### 3. Use the lean package entry points
4130
4700
 
4131
- `{ body, query, params }` validated by `t` (TypeBox) or any
4132
- [Standard Schema](https://standardschema.dev)
4701
+ The `@nifrajs/core` and `nifra` roots now expose the lean HTTP server
4702
+ surface. Prefer `@nifrajs/core/server` for `server()`, and import
4703
+ contracts, assurance, capabilities, budgets, manifests, reflection, SSE, WebSockets, and
4704
+ other optional systems from their documented subpaths.
4133
4705
 
4134
- `res.json(x)` / `reply.send(x)` / `c.json(x)`
4706
+ ```ts
4135
4707
 
4136
- `return x`
4708
+ ```
4137
4709
 
4138
- `app.listen(3000)`
4710
+ Contract-generated adversarial testing lives in `@nifrajs/testing`; the deprecated
4711
+ core invariant runner is removed.
4139
4712
 
4140
- `app.listen()` (Bun) · `@nifrajs/node` · `@nifrajs/deno` ·
4141
- `toFetchHandler` (edge)
4713
+ ### 4. Update full-stack backend mounts
4142
4714
 
4143
- OpenAPI plugins / swagger
4144
- generated from the same `t` schemas — no second source of truth
4715
+ `createWebApp` now auto-mounts a backend only through the platform-aware,
4716
+ symbol-keyed `BackendMount` interface. The old duck-typed
4717
+ `api.fetch(url, init)` convention is removed. Normal applications should pass the
4718
+ result of `inProcessClient(backend)` or `testClient(backend)`; both
4719
+ already implement the interface and forward `env` and `waitUntil`.
4145
4720
 
4146
- ### Express → Nifra
4721
+ ```ts
4147
4722
 
4148
- The handler returns its result instead of calling `res.json`, and validation moves
4149
- onto the route. The client then infers all of this for free.
4723
+ ```
4724
+
4725
+ ### 5. Declare external library mounts
4726
+
4727
+ Better Auth and similar libraries can own routes such as `/auth/**` outside the
4728
+ Nifra typed contract. A relative fetch to that library is intentional, but otherwise resembles
4729
+ a hand-rolled own-API call. Declare only the exact mounted prefix in
4730
+ `nifra.check.json` so `nifra check` does not keep CI red:
4150
4731
 
4151
4732
  ```ts
4152
- // Express
4153
- app.get("/users/:id", (req, res) => res.json({ id: req.params.id }))
4154
- app.post("/users", express.json(), (req, res) => res.status(201).json(create(req.body)))
4733
+
4734
+ ```
4735
+
4736
+ Prefixes are segment-anchored: `/auth` covers `/auth` and its children,
4737
+ never `/authors`. Traversal paths are not suppressed. The normalized allowlist is
4738
+ echoed in human, JSON, and MCP check results, so every bypass remains auditable. This option
4739
+ affects only the typed-client lint; it does not mount, authorize, or trust the library.
4740
+
4741
+ ### 6. Narrow typed-client failures by status
4742
+
4743
+ Routes with declared error schemas now return a failure union discriminated by HTTP status.
4744
+ After checking `!result.ok`, also narrow `result.status` before reading
4745
+ typed failure data. Undeclared statuses and transport status `0` remain unknown.
4746
+
4747
+ ```ts
4748
+
4749
+ ```
4750
+
4751
+ ### 7. Apply the remaining web and protocol changes
4752
+
4753
+ -
4754
+ Redirects accept an options object as their second argument.
4755
+
4756
+ ```ts
4757
+
4758
+ ```
4759
+
4760
+ -
4761
+ Replace the removed prerender wrapper with `enumerateStaticRoutes()`.
4762
+
4763
+ - Fragment navigation resolves element IDs only.
4764
+
4765
+ -
4766
+ MCP Apps metadata uses only `_meta.ui.resourceUri`; remove the deprecated flat
4767
+ `ui/resourceUri` key.
4768
+
4769
+ -
4770
+ Telemetry integrations use `ObservationAdapter`; the
4771
+ `AgentSpan`, `AgentSpanExporter`, and `SpanExporter` aliases
4772
+ are removed.
4773
+
4774
+ - Invalid HTTP method overrides always fail closed with 400.
4775
+
4776
+ -
4777
+ `nifra build` always emits a complete deploy directory and defaults to Bun;
4778
+ `nifra start` runs the generated `server.js`.
4779
+
4780
+ ### 8. Run the release gates
4781
+
4782
+ -
4783
+ Run `nifra check --json` and fix every error.
4784
+
4785
+ -
4786
+ If the project has `nifra.assurance.ts`, run `nifra assure --json`.
4787
+
4788
+ -
4789
+ If capabilities are configured, review a new snapshot and run
4790
+ `nifra capabilities check --json`.
4791
+
4792
+ - Run the application test suite and a production build.
4793
+
4794
+ -
4795
+ Exercise authentication, redirects, backend mounting, SSE/WebSocket routes, and each deploy
4796
+ adapter the application uses.
4797
+
4798
+ ## Nifra — Migrating from Express, Hono, Fastify & Elysia
4799
+
4800
+ > Move a backend to Nifra: routing, middleware, validation, and body parsing map directly from Express, Hono, Fastify, and Elysia — and you gain a typed client, multi-runtime deploy, and optional SSR.
4801
+
4802
+ Nifra's `server()` is a chainable, Web-standard router — the move from Express, Hono,
4803
+ Fastify, or Elysia is mostly mechanical renames. What you gain: an end-to-end-typed client with
4804
+ zero codegen, schema validation built into the route, the same app on Bun / Node / Deno / the
4805
+ edge, and an SSR frontend whenever you want one.
4806
+
4807
+ ### How the concepts map
4808
+
4809
+ Express / Hono / Fastify
4810
+ Nifra
4811
+
4812
+ `app.get(path, handler)`
4813
+
4814
+ `server().get(path, handler)` — chainable, fully type-inferred
4815
+
4816
+ `req` / `res` · Hono `Context`
4817
+
4818
+ `c` — `c.req`, `c.params`, `c.query`; **return** a value (JSON) or a
4819
+ `Response`
4820
+
4821
+ `app.use(mw)` · Hono middleware
4822
+
4823
+ `.use()` + the `@nifrajs/middleware` pack (CORS, auth, rate limit, …)
4824
+
4825
+ `express.json()` / body parsing
4826
+ built in — declare a schema and the parsed body is typed
4827
+
4828
+ Fastify JSON schema · `zod` middleware · Elysia `t`
4829
+
4830
+ `{ body, query, params }` validated by `t` (TypeBox) or any
4831
+ [Standard Schema](https://standardschema.dev)
4832
+
4833
+ `res.json(x)` / `reply.send(x)` / `c.json(x)`
4834
+
4835
+ `return x`
4836
+
4837
+ `app.listen(3000)`
4838
+
4839
+ `app.listen()` (Bun) · `@nifrajs/node` · `@nifrajs/deno` ·
4840
+ `toFetchHandler` (edge)
4841
+
4842
+ OpenAPI plugins / swagger
4843
+ generated from the same `t` schemas — no second source of truth
4844
+
4845
+ ### Express → Nifra
4846
+
4847
+ The handler returns its result instead of calling `res.json`, and validation moves
4848
+ onto the route. The client then infers all of this for free.
4849
+
4850
+ ```ts
4851
+ // Express
4852
+ app.get("/users/:id", (req, res) => res.json({ id: req.params.id }))
4853
+ app.post("/users", express.json(), (req, res) => res.status(201).json(create(req.body)))
4155
4854
 
4156
4855
  // nifra — return the value (or a Response); c is Web-standard; the body is parsed + validated
4157
4856
  server()
@@ -4438,7 +5137,7 @@ boundary _before_ your handler runs — invalid input is rejected with a 422, so
4438
5137
  handler only ever sees well-formed data.
4439
5138
 
4440
5139
  ```ts
4441
- import { server } from "@nifrajs/core"
5140
+ import { server } from "@nifrajs/core/server"
4442
5141
  import { GetUser } from "./schema"
4443
5142
 
4444
5143
  export const app = server().get("/users/:id", GetUser, (c) => {
@@ -4481,7 +5180,7 @@ routes and their schemas — generated lazily on first request, never hand-writt
4481
5180
  `ui: true` to also serve a Scalar reference page.
4482
5181
 
4483
5182
  ```ts
4484
- import { server } from "@nifrajs/core"
5183
+ import { server } from "@nifrajs/core/server"
4485
5184
  import { openapi } from "@nifrajs/middleware"
4486
5185
  import { GetUser } from "./schema" // the contract defined above
4487
5186
 
@@ -4523,10 +5222,132 @@ coupling — server code never ships to the client.
4523
5222
  The generated OpenAPI document is a structural subset of 3.1 derived from your schemas;
4524
5223
  it reflects exactly what the routes declare, not hand-authored prose.
4525
5224
 
5225
+ ## Nifra - Verification ladder
5226
+
5227
+ > Five cumulative levels, from a typed contract to contract-derived invariant tests, that say what a Nifra project actually proves.
5228
+
5229
+ "It compiles" and "it is secure" are different claims. The ladder makes the difference
5230
+ legible: five levels, each earned, so a project can state what it proves instead of what it
5231
+ hopes.
5232
+
5233
+ ### The five levels
5234
+
5235
+ Levels are **cumulative**. A level only counts when every level below it holds,
5236
+ so `achieved` is the highest rung with unbroken support underneath, and it is
5237
+ `-1` when even L0 fails.
5238
+
5239
+ -
5240
+ **L0 typed contract** - `nifra check` passes: the frontend and
5241
+ backend cannot have silently diverged.
5242
+
5243
+ -
5244
+ **L1 route assurance** - every route is classified, and the enforcement a rule
5245
+ requires is actually installed.
5246
+
5247
+ -
5248
+ **L2 capability lockfile** - declared effects match a reviewed lockfile, so a
5249
+ route that starts writing to the database shows up in review.
5250
+
5251
+ -
5252
+ **L3 route manifest** - the committed trust artifact still matches the routes,
5253
+ evidence, effects, and response classification it was emitted from.
5254
+
5255
+ -
5256
+ **L4 invariant-tested** - contract-derived invariants ran against an isolated
5257
+ executor with nothing skipped.
5258
+
5259
+ ### Run it
5260
+
5261
+ ```ts
5262
+ # What does this project prove right now?
5263
+ nifra levels
5264
+
5265
+ # Fail CI below a level the project already reached.
5266
+ nifra levels --min 2
5267
+
5268
+ # The same report an agent reads over MCP (nifra_levels).
5269
+ nifra levels --json
5270
+ ```
5271
+
5272
+ ```ts
5273
+ L0 typed contract ok
5274
+ L1 route assurance ok
5275
+ L2 capability lockfile ok
5276
+ L3 route manifest FAIL manifest drifted from routes: run `nifra manifest emit`
5277
+ L4 invariant-tested FAIL no isolated invariant executor configured
5278
+
5279
+ achieved: 2
5280
+ ```
5281
+
5282
+ Every level that does not hold explains itself, so the output is a work list rather than a
5283
+ verdict. `--min <n>` is the CI floor: pin it to the level you have reached and
5284
+ the build fails when a change quietly costs you a rung.
5285
+
5286
+ ### Wiring the levels
5287
+
5288
+ L0 needs nothing. L1 through L4 hang off `nifra.assurance.ts`, and each one you
5289
+ configure is a level you can start claiming.
5290
+
5291
+ ```ts
5292
+ // nifra.assurance.ts
5293
+ // doc-check: skip - policy rules and the isolated executor are app-specific
5294
+ import { defineAssuranceConfig } from "@nifrajs/core/assurance"
5295
+ import { backend } from "./backend"
5296
+
5297
+ export default defineAssuranceConfig({
5298
+ source: backend,
5299
+ policy: { rules: [/* ... */] },
5300
+ capabilities: { lockfile: "capabilities.lock.json", policy: { /* ... */ } },
5301
+ manifest: { path: "nifra.manifest.json" },
5302
+ invariants: {
5303
+ // L4 fuzzes the contract, so it needs an app you are willing to throw hostile
5304
+ // input at. Point this at an isolated instance, never a live one.
5305
+ executor: (request) => backend.fetch(request),
5306
+ },
5307
+ })
5308
+ ```
5309
+
5310
+ L4 requires an **explicit** executor. Invariant runs send deliberately hostile
5311
+ input, so nifra will not guess which app to point them at; a level that fuzzed your live
5312
+ server by default would be a footgun, and a skipped route is reported rather than passed over
5313
+ in silence.
5314
+
5315
+ ### For coding agents
5316
+
5317
+ The `nifra_levels` MCP tool returns the same
5318
+ `{ achieved, levels[] }` report. An agent can read what its change proved, and a
5319
+ level that regressed comes back with the reason it broke.
5320
+
4526
5321
  ---
4527
5322
 
4528
5323
  # Package reference (READMEs)
4529
5324
 
5325
+ ## @nifrajs/agent-telemetry
5326
+
5327
+ Child-span telemetry for AI tool calls made through Nifra tool and MCP endpoints.
5328
+
5329
+ ```sh
5330
+ bun add @nifrajs/agent-telemetry @nifrajs/otel
5331
+ ```
5332
+
5333
+ ```ts
5334
+ import { agentTelemetry, consoleAgentExporter } from "@nifrajs/agent-telemetry"
5335
+
5336
+ app.use(agentTelemetry({ exporter: consoleAgentExporter() }))
5337
+ ```
5338
+
5339
+ Requests under `/_nifra/tool/*` and requests to `/mcp` receive tool-name, input-size, output-size,
5340
+ duration, and status observations. When `@nifrajs/otel` owns the request observation, tool spans are
5341
+ attached as children; otherwise the plugin creates a standalone observation. Other routes pass through.
5342
+
5343
+ ## For AI agents
5344
+
5345
+ Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
5346
+ one cheap read instead of the whole corpus. For the wider framework: the repo's
5347
+ [`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
5348
+ [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
5349
+ done-gate, or `nifra mcp` to give the agent live project tools.
5350
+
4530
5351
  ## @nifrajs/auth
4531
5352
 
4532
5353
  Session primitives for nifra — signed-cookie + server-store sessions, CSRF, and route guards. Bring your own identity (Better Auth / Lucia / OAuth); nifra owns the session.
@@ -4586,7 +5407,7 @@ export const auth = createBetterAuth({
4586
5407
 
4587
5408
  ```ts
4588
5409
  // server.ts
4589
- import { server } from "@nifrajs/core"
5410
+ import { server } from "@nifrajs/core/server"
4590
5411
  import { betterAuth } from "@nifrajs/better-auth"
4591
5412
  import { auth } from "./auth"
4592
5413
 
@@ -4655,38 +5476,6 @@ one cheap read instead of the whole corpus. For the wider framework: the repo's
4655
5476
  [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
4656
5477
  done-gate, or `nifra mcp` to give the agent live project tools.
4657
5478
 
4658
- ## @nifrajs/budget
4659
-
4660
- Compatibility import for the request-deadline mechanics owned by `@nifrajs/core/budget`. Existing
4661
- Nifra, Hono, and Fetch consumers can keep this import unchanged; new core consumers may use the
4662
- subpath directly. Both paths resolve to the same implementation and classes.
4663
-
4664
- ```ts
4665
- import {
4666
- assertBudgetRemaining,
4667
- createRequestBudget,
4668
- withDeadlineHeader,
4669
- } from "@nifrajs/budget"
4670
-
4671
- const budget = createRequestBudget({
4672
- deadline: Date.now() + 2_000,
4673
- signal: request.signal,
4674
- })
4675
-
4676
- assertBudgetRemaining(budget, 100)
4677
- await fetch(url, {
4678
- signal: budget.signal,
4679
- headers: withDeadlineHeader(undefined, budget.child(50)),
4680
- })
4681
- ```
4682
-
4683
- The wire value is an absolute Unix epoch deadline. `remaining()` samples wall time once, then uses a
4684
- monotonic clock so NTP changes cannot extend or shorten admitted work. A budget does not choose retry,
4685
- provider, tenant, or money policy; it only answers how much time remains.
4686
-
4687
- Nifra route handlers receive the same primitive as `c.budget`. `c.signal` remains the cancellation
4688
- signal and aborts at the effective server deadline.
4689
-
4690
5479
  ## @nifrajs/cache
4691
5480
 
4692
5481
  Typed KV cache for nifra — **TTL**, **stale-while-revalidate**, **tag invalidation**, and **single-flight
@@ -4742,6 +5531,14 @@ the Cache API) via a `CacheStore` for anything that should survive across reques
4742
5531
  - `cache.wrap(key, loader, { ttlMs?, swrMs?, tags? })` · `get` · `has` · `set` · `delete` · `invalidateTag` · `clear`.
4743
5532
  - `MemoryCache({ maxEntries?, now? })` — the default store; implement `CacheStore` for your own.
4744
5533
 
5534
+ ## For AI agents
5535
+
5536
+ Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
5537
+ one cheap read instead of the whole corpus. For the wider framework: the repo's
5538
+ [`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
5539
+ [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
5540
+ done-gate, or `nifra mcp` to give the agent live project tools.
5541
+
4745
5542
  ## @nifrajs/cli
4746
5543
 
4747
5544
  The `nifra` CLI — zero-config dev/build/start for a nifra app, plus the agent surfaces (`nifra context`,
@@ -4750,8 +5547,8 @@ The `nifra` CLI — zero-config dev/build/start for a nifra app, plus the agent
4750
5547
 
4751
5548
  ```
4752
5549
  nifra dev [--port <n>] [--poll] True-HMR dev server (Vite + nifra SSR).
4753
- nifra build [--out <dir>] Content-hashed client bundle + manifest.json.
4754
- nifra start [--port <n>] [--out <dir>] Serve the built app (client + SSR) on Bun.
5550
+ nifra build [--out <dir>] [--target <t>] Complete deploy directory; target defaults to Bun.
5551
+ nifra start [--port <n>] [--out <dir>] Run the generated Bun server at <out>/server.js.
4755
5552
  nifra context Print this project's API + page routes + conventions.
4756
5553
  nifra mcp MCP server (stdio) exposing this project to a coding agent.
4757
5554
  nifra check [--json] Gate: typecheck + lints; --json includes fixes/suggestions.
@@ -4816,6 +5613,17 @@ For hand-rolled own-API `fetch()` calls, `nifra check` stays conservative: simpl
4816
5613
  that match a statically visible Nifra route get an exact typed-client rewrite diff; dynamic URLs, custom
4817
5614
  headers, bodies, query strings, or ambiguous routes fall back to manual steps.
4818
5615
 
5616
+ When a library such as Better Auth intentionally owns a mounted prefix outside the Nifra contract,
5617
+ declare that prefix in `nifra.check.json` so its relative calls do not keep CI red:
5618
+
5619
+ ```json
5620
+ { "externalMounts": ["/auth"] }
5621
+ ```
5622
+
5623
+ The match is segment-anchored (`/auth` never suppresses `/authors`) and traversal paths are rejected.
5624
+ The normalized prefixes are echoed in human, JSON, and MCP results so the lint bypass stays auditable.
5625
+ This suppresses only the typed-client diagnostic; it does not mount or authorize the external handler.
5626
+
4819
5627
  ## For AI coding agents
4820
5628
 
4821
5629
  **`nifra context`** prints this project's actual surface — API routes (from `backend.routes()`), page
@@ -4842,6 +5650,10 @@ an agent can act on the project, not just read about it. Core tools:
4842
5650
  - **`nifra_check`** — run the drift gate and get the structured `{ ok, typecheck, diagnostics[] }`
4843
5651
  result with suggestions, so the agent verifies its own work and fixes each diagnostic before finishing.
4844
5652
  - **`nifra_assure`** — evaluate `nifra.assurance.ts` and return the complete per-route evidence report.
5653
+ - **`nifra_levels`** - compute the cumulative verification ladder (L0 typed contract, L1 route
5654
+ assurance, L2 capability lockfile, L3 trust manifest, L4 contract invariants) and return
5655
+ `{ achieved, levels[] }`, so an agent can see what its change proves and read why a level it lost
5656
+ no longer holds.
4845
5657
  - **`nifra_doctor`** — catch imported packages missing from `package.json`; with `autoFix:true`, write
4846
5658
  safe dependency entries when a version can be inferred locally and return exact `bun add` commands
4847
5659
  for anything that needs a package-manager decision.
@@ -4959,7 +5771,7 @@ bun add @nifrajs/core
4959
5771
  ```
4960
5772
 
4961
5773
  ```ts
4962
- import { server } from "@nifrajs/core"
5774
+ import { server } from "@nifrajs/core/server"
4963
5775
 
4964
5776
  const app = server()
4965
5777
  .get("/users/:id", (c) => ({ id: c.params.id }))
@@ -4969,6 +5781,15 @@ const app = server()
4969
5781
  export type App = typeof app // hand this to @nifrajs/client for end-to-end types
4970
5782
  ```
4971
5783
 
5784
+ `@nifrajs/core` and `@nifrajs/core/server` expose the same lean common runtime. Optional systems are
5785
+ available only from their explicit subpaths, so an ordinary HTTP server never evaluates them:
5786
+
5787
+ ```ts
5788
+ import { defineContract, implement } from "@nifrajs/core/contract"
5789
+ import { startCausality } from "@nifrajs/core/causality"
5790
+ import { defineAssurancePolicy } from "@nifrajs/core/assurance"
5791
+ ```
5792
+
4972
5793
  - **Inline or contract-first.** Write routes inline (types inferred from the
4973
5794
  builder), or `defineContract(...)` + `implement(...)` for a decoupled, versionable
4974
5795
  surface — handlers lift over unchanged.
@@ -4989,6 +5810,33 @@ export type App = typeof app // hand this to @nifrajs/client for end-to-end type
4989
5810
  IP-restriction, and security-header modules publish reflection-safe enforcement evidence.
4990
5811
  An ordered `AssurancePolicy` classifies every route and fails closed on missing or forbidden
4991
5812
  evidence without adding work to the request path.
5813
+ - **Owned effect execution.** `executeCapability()` correlates intent and terminal evidence with an
5814
+ opaque `effectId`, records outcomes automatically, and forwards request cancellation. Add
5815
+ order-scoped `aroundCapability()` policies for async approval/admission; they receive token-only
5816
+ metadata, have bounded timeouts, and must call `next()` exactly once before the effect can run.
5817
+ - **Durable workflows (opt in).** `@nifrajs/core/durable-execution` provides tenant/principal-bound,
5818
+ signed single-use approval resumes; a durable effect journal + reconciliation scanner; and a typed
5819
+ saga state machine with reverse compensation, retry/backoff, and ambiguous-crash detection. Production
5820
+ constructors reject stores that do not declare `durability: "durable"`. Operational scans use bounded
5821
+ cursor pages through `reconcileEffectsPage()` / `reconcileSagasPage()`. Provider-confirmed manual
5822
+ review uses effect-ID-bound `resolveAmbiguity()`, followed by `resume()` or `compensate()`.
5823
+ - **Production durable adapters.** `@nifrajs/core/durable-adapters` supplies
5824
+ `PostgresDurableExecutionAdapter`, `SQLiteDurableExecutionAdapter`, and
5825
+ `DurableObjectExecutionAdapter`. Each exposes compatible `effects`, `approvals`, `sagas`, and
5826
+ `leases` stores. Run `runDurableExecutionAdapterConformance()` against the deployment backend.
5827
+ - **Bounded reconciliation workers.** `@nifrajs/core/reconciliation-worker` runs effect or saga
5828
+ scans under an atomic lease with durable cursor checkpoints, a finite page budget, bounded handler
5829
+ concurrency, filters, cancellation, and token-only metrics. A worker invocation always terminates.
5830
+ - **Rich wire values (opt in).** `@nifrajs/core/wire` round-trips dates, bigints, maps, sets, binary,
5831
+ shared references, and cycles through JSON transports. Decoding validates every reachable shape,
5832
+ preserves owned `__proto__` keys without prototype mutation, and enforces configurable node, depth,
5833
+ collection-entry, and decoded-byte limits.
5834
+ - **Versioned transport codecs (opt in).** Add `.use(transportCodecs(registry))` from
5835
+ `@nifrajs/core/transport-plugin` and configure the typed
5836
+ client's `transport` option with the same registry. `@nifrajs/core/transport-codec` negotiates
5837
+ bounded HTTP representations and supplies the same frame/loader adapters for WebSockets and
5838
+ deferred data. Import `richWireCodec()` from `@nifrajs/core/transport-codec-rich`; the separate
5839
+ subpath keeps rich-wire code out of plain JSON bundles.
4992
5840
 
4993
5841
  ```ts
4994
5842
  import { defineAssurancePolicy, evaluateRouteAssurance, NIFRA_ASSURANCE } from "@nifrajs/core/assurance"
@@ -5094,7 +5942,7 @@ deno add npm:@nifrajs/deno # or import npm:@nifrajs/deno directly
5094
5942
  ```
5095
5943
 
5096
5944
  ```ts
5097
- import { server } from "@nifrajs/core"
5945
+ import { server } from "@nifrajs/core/server"
5098
5946
  import { serve } from "@nifrajs/deno"
5099
5947
 
5100
5948
  const app = server().get("/users/:id", (c) => ({ id: c.params.id }))
@@ -5129,6 +5977,32 @@ one cheap read instead of the whole corpus. For the wider framework: the repo's
5129
5977
  [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
5130
5978
  done-gate, or `nifra mcp` to give the agent live project tools.
5131
5979
 
5980
+ ## @nifrajs/devtools
5981
+
5982
+ Interactive request-trace DevTools for `nifra dev`, with a secured SSE stream and browser overlay.
5983
+
5984
+ ```sh
5985
+ bun add -d @nifrajs/devtools
5986
+ ```
5987
+
5988
+ ```ts
5989
+ import { devtools } from "@nifrajs/devtools"
5990
+
5991
+ app.use(devtools())
5992
+ ```
5993
+
5994
+ The plugin is enabled by default only when `NODE_ENV=development`. It records a bounded event buffer
5995
+ and serves the overlay stream at `/_nifra/devtools`; remote hosts and cross-origin access stay denied
5996
+ unless explicitly allowed. Leave it unregistered in production for zero request-path overhead.
5997
+
5998
+ ## For AI agents
5999
+
6000
+ Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
6001
+ one cheap read instead of the whole corpus. For the wider framework: the repo's
6002
+ [`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
6003
+ [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
6004
+ done-gate, or `nifra mcp` to give the agent live project tools.
6005
+
5132
6006
  ## @nifrajs/env
5133
6007
 
5134
6008
  Typed, validated environment variables. Define a schema once at startup; get a **frozen, typed**
@@ -5215,6 +6089,14 @@ lineage metadata.
5215
6089
 
5216
6090
  See the [nifra docs](https://github.com/nifrajs/nifra#readme).
5217
6091
 
6092
+ ## For AI agents
6093
+
6094
+ Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
6095
+ one cheap read instead of the whole corpus. For the wider framework: the repo's
6096
+ [`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
6097
+ [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
6098
+ done-gate, or `nifra mcp` to give the agent live project tools.
6099
+
5218
6100
  ## @nifrajs/i18n
5219
6101
 
5220
6102
  Framework-agnostic i18n for nifra — locale negotiation + a tiny ICU message formatter on the platform Intl. Dependency-free.
@@ -5422,6 +6304,78 @@ export default {
5422
6304
  - `queue.counts()` → `{ pending, active, dead }`. `queue.store` — the underlying store.
5423
6305
  - Stores: `MemoryJobStore`. Backoff: `exponentialBackoff`, `fixedBackoff`, `noBackoff`.
5424
6306
 
6307
+ ## For AI agents
6308
+
6309
+ Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
6310
+ one cheap read instead of the whole corpus. For the wider framework: the repo's
6311
+ [`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
6312
+ [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
6313
+ done-gate, or `nifra mcp` to give the agent live project tools.
6314
+
6315
+ ## @nifrajs/mcp
6316
+
6317
+ Build transport-agnostic MCP servers and interactive MCP Apps for Nifra applications.
6318
+
6319
+ ```sh
6320
+ bun add @nifrajs/mcp
6321
+ ```
6322
+
6323
+ ```ts
6324
+ import { createMcpServer, defineMcpTool } from "@nifrajs/mcp"
6325
+
6326
+ const tools = [
6327
+ defineMcpTool({
6328
+ name: "health",
6329
+ description: "Report service health",
6330
+ handler: () => ({ text: "ok" }),
6331
+ }),
6332
+ ]
6333
+
6334
+ const mcp = createMcpServer({ name: "orders", version: "1.0.0", tools })
6335
+ ```
6336
+
6337
+ Mount `mcp.fetch` at `POST /mcp`. The package also exposes the JSON-RPC protocol and Streamable HTTP
6338
+ layers directly, plus `defineMcpWidget` and the React adapter for tool results that render UI in MCP
6339
+ hosts.
6340
+
6341
+ ## For AI agents
6342
+
6343
+ Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
6344
+ one cheap read instead of the whole corpus. For the wider framework: the repo's
6345
+ [`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
6346
+ [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
6347
+ done-gate, or `nifra mcp` to give the agent live project tools.
6348
+
6349
+ ## @nifrajs/mcp-db
6350
+
6351
+ Expose an allowlisted SQLite schema as a fail-closed MCP server, with opt-in read-only queries.
6352
+
6353
+ ```sh
6354
+ bun add @nifrajs/mcp-db
6355
+ ```
6356
+
6357
+ ```ts
6358
+ import { Database } from "bun:sqlite"
6359
+ import { serveDatabaseAsMcp } from "@nifrajs/mcp-db"
6360
+
6361
+ const mcp = serveDatabaseAsMcp(new Database("app.db"), {
6362
+ tables: ["habits", "entries"],
6363
+ })
6364
+ ```
6365
+
6366
+ Only `list_tables` and `describe_table` are exposed by default, restricted to the explicit table
6367
+ allowlist. `run_query` requires an authorization hook and is guarded by SQLite query-only mode,
6368
+ single-statement/read-only checks, query-plan verification, and bounded output. D1 is not supported
6369
+ because it cannot provide the same engine-level read-only guarantee.
6370
+
6371
+ ## For AI agents
6372
+
6373
+ Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
6374
+ one cheap read instead of the whole corpus. For the wider framework: the repo's
6375
+ [`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
6376
+ [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
6377
+ done-gate, or `nifra mcp` to give the agent live project tools.
6378
+
5425
6379
  ## @nifrajs/middleware
5426
6380
 
5427
6381
  Composable, dependency-light middleware for [nifra](../../README.md) — CORS, security headers, body
@@ -5433,7 +6387,7 @@ bun add @nifrajs/middleware
5433
6387
  ```
5434
6388
 
5435
6389
  ```ts
5436
- import { server } from "@nifrajs/core"
6390
+ import { server } from "@nifrajs/core/server"
5437
6391
  import {
5438
6392
  bodyLimit,
5439
6393
  cors,
@@ -5510,6 +6464,34 @@ one cheap read instead of the whole corpus. For the wider framework: the repo's
5510
6464
  [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
5511
6465
  done-gate, or `nifra mcp` to give the agent live project tools.
5512
6466
 
6467
+ ## @nifrajs/mock
6468
+
6469
+ Generate deterministic mock responses from a Nifra application's reflected response contracts.
6470
+
6471
+ ```sh
6472
+ bun add -d @nifrajs/mock
6473
+ ```
6474
+
6475
+ ```ts
6476
+ import { createMockServer } from "@nifrajs/mock"
6477
+ import { app } from "./app"
6478
+
6479
+ const mock = createMockServer(app, { seed: 42 })
6480
+ const response = await mock.fetch(new Request("http://local.test/users/1"))
6481
+ ```
6482
+
6483
+ The mock server matches reflected routes and synthesizes values from supported JSON Schema keywords.
6484
+ Unsupported or unsatisfiable schemas fail explicitly instead of returning known-invalid data. Use a
6485
+ fixed seed when responses must be replayable across local development and CI.
6486
+
6487
+ ## For AI agents
6488
+
6489
+ Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
6490
+ one cheap read instead of the whole corpus. For the wider framework: the repo's
6491
+ [`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
6492
+ [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
6493
+ done-gate, or `nifra mcp` to give the agent live project tools.
6494
+
5513
6495
  ## @nifrajs/node
5514
6496
 
5515
6497
  Run a [nifra](../../README.md) app on Node's `http` server — nifra's ergonomics, deployed
@@ -5520,7 +6502,7 @@ bun add @nifrajs/node # or: npm add @nifrajs/node
5520
6502
  ```
5521
6503
 
5522
6504
  ```ts
5523
- import { server } from "@nifrajs/core"
6505
+ import { server } from "@nifrajs/core/server"
5524
6506
  import { serve } from "@nifrajs/node"
5525
6507
 
5526
6508
  const app = server().get("/users/:id", (c) => ({ id: c.params.id }))
@@ -5644,8 +6626,7 @@ inventing trace identity.
5644
6626
 
5645
6627
  ## Adapters
5646
6628
 
5647
- Implement `ObservationAdapter` to send spans wherever you collect them. `SpanExporter` remains as a
5648
- backwards-compatible type alias:
6629
+ Implement `ObservationAdapter` to send spans wherever you collect them:
5649
6630
 
5650
6631
  ```ts
5651
6632
  interface ObservationAdapter {
@@ -5678,6 +6659,37 @@ one cheap read instead of the whole corpus. For the wider framework: the repo's
5678
6659
  [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
5679
6660
  done-gate, or `nifra mcp` to give the agent live project tools.
5680
6661
 
6662
+ ## @nifrajs/prompt
6663
+
6664
+ Provider-neutral, type-safe prompts with Standard Schema input and validated structured output.
6665
+
6666
+ ```sh
6667
+ bun add @nifrajs/prompt
6668
+ ```
6669
+
6670
+ ```ts
6671
+ import { prompt } from "@nifrajs/prompt"
6672
+ import { t } from "@nifrajs/schema"
6673
+
6674
+ const extract = prompt("Extract the contact from the text.")
6675
+ .input(t.object({ text: t.string() }))
6676
+ .output(t.object({ name: t.string(), email: t.string({ format: "email" }) }))
6677
+
6678
+ const contact = await extract.run({ text }, { complete: callYourProvider })
6679
+ ```
6680
+
6681
+ The package imports no vendor SDK. Supply a `complete` adapter for your provider; inputs are validated
6682
+ before the call and model output is parsed and validated before it reaches application code. An optional
6683
+ `heal` hook can repair and retry invalid structured output.
6684
+
6685
+ ## For AI agents
6686
+
6687
+ Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
6688
+ one cheap read instead of the whole corpus. For the wider framework: the repo's
6689
+ [`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
6690
+ [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
6691
+ done-gate, or `nifra mcp` to give the agent live project tools.
6692
+
5681
6693
  ## @nifrajs/runner
5682
6694
 
5683
6695
  Run requests through a nifra app and capture **structured results** — the shared engine behind the
@@ -5727,7 +6739,7 @@ bun add @nifrajs/schema
5727
6739
  ```
5728
6740
 
5729
6741
  ```ts
5730
- import { server } from "@nifrajs/core"
6742
+ import { server } from "@nifrajs/core/server"
5731
6743
  import { t, toOpenAPI } from "@nifrajs/schema"
5732
6744
 
5733
6745
  const app = server().post("/users", { body: t.object({ name: t.string(), age: t.integer() }) }, (c) => ({
@@ -5747,6 +6759,22 @@ const openapi = toOpenAPI(app) // OpenAPI 3.1
5747
6759
  `operationId`s); also works on a live app. Routes using a BYO Standard Schema are
5748
6760
  emitted without a detailed schema (Standard Schema exposes no JSON Schema).
5749
6761
 
6762
+ ### `t` vs bring-your-own (bundle size)
6763
+
6764
+ `t` is the batteries-included default because a TypeBox schema *is* a JSON Schema: it gives you
6765
+ OpenAPI + MCP `tools/list` for free and compiles to a fast validator. That completeness has a
6766
+ cost - TypeBox carries the whole JSON Schema type system, so `t` adds meaningful bytes to a bundle.
6767
+
6768
+ nifra validates through **Standard Schema**, so any Standard-Schema validator works on a route with
6769
+ no adapter - `{ body: v.object({ ... }) }` with valibot, for instance. A validated app built on a
6770
+ lean validator like valibot is a fraction of the size (measured ~16 KB gzipped), which matters for
6771
+ edge/Workers cold-starts. The trade is the one noted above: a bring-your-own validator exposes no
6772
+ JSON Schema, so those routes carry no OpenAPI/MCP detail.
6773
+
6774
+ Rule of thumb: reach for `t` when you want the contract (OpenAPI/MCP) for free; bring valibot (or any
6775
+ Standard-Schema validator) when bundle size on the edge is the priority. Both are first-class - the
6776
+ choice is per route, and you can mix them in one app.
6777
+
5750
6778
  ### `toOpenAPI` coverage
5751
6779
 
5752
6780
  It emits `paths`, `parameters` (path + object query), `requestBody`, and `responses`, plus:
@@ -5835,6 +6863,14 @@ import { assertSafeKey, StorageKeyError } from "@nifrajs/storage"
5835
6863
  - `assertStorageAdapterConformance({ createAdapter })` — dependency-free executable adapter contract.
5836
6864
  - Optional interfaces: `PagedStorageAdapter` · `PresignableStorageAdapter` · `MovableStorageAdapter`.
5837
6865
 
6866
+ ## For AI agents
6867
+
6868
+ Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
6869
+ one cheap read instead of the whole corpus. For the wider framework: the repo's
6870
+ [`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
6871
+ [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
6872
+ done-gate, or `nifra mcp` to give the agent live project tools.
6873
+
5838
6874
  ## @nifrajs/testing
5839
6875
 
5840
6876
  Contract-derived adversarial testing and stateful in-process helpers for Nifra apps.
@@ -6004,6 +7040,14 @@ that every call carries and captures cookies via a shared jar.
6004
7040
 
6005
7041
  For a **stateless** request (no cookies), use `testClient` from `@nifrajs/client` directly.
6006
7042
 
7043
+ ## For AI agents
7044
+
7045
+ Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
7046
+ one cheap read instead of the whole corpus. For the wider framework: the repo's
7047
+ [`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
7048
+ [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
7049
+ done-gate, or `nifra mcp` to give the agent live project tools.
7050
+
6007
7051
  ## @nifrajs/uploads
6008
7052
 
6009
7053
  File-upload hardening for nifra — magic-byte MIME detection, size + type validation, short-TTL signed download URLs, and EXIF/metadata stripping. Dependency-free, edge-safe.
@@ -6330,7 +7374,7 @@ done-gate, or `nifra mcp` to give the agent live project tools.
6330
7374
 
6331
7375
  ## @nifrajs/agent-telemetry
6332
7376
 
6333
- `AgentSpan`, `AgentSpanExporter`, `agentTelemetry`, `AgentTelemetryOptions`, `consoleAgentExporter`
7377
+ `agentTelemetry`, `AgentTelemetryOptions`, `consoleAgentExporter`
6334
7378
 
6335
7379
  ## @nifrajs/auth
6336
7380
 
@@ -6338,7 +7382,7 @@ done-gate, or `nifra mcp` to give the agent live project tools.
6338
7382
 
6339
7383
  ## @nifrajs/better-auth
6340
7384
 
6341
- `betterAuth`, `BetterAuthLike`, `BetterAuthOptions`, `getSession`, `requireSession`, `RequireSessionOptions`, `SessionOf`
7385
+ `authed`, `AuthedOptions`, `betterAuth`, `BetterAuthLike`, `BetterAuthOptions`, `getSession`, `Principal`, `PrincipalFor`, `requirePrincipal`, `requireSession`, `RequireSessionOptions`, `SessionOf`, `SessionUserOf`, `WithPrincipal`
6342
7386
 
6343
7387
  ## @nifrajs/cache
6344
7388
 
@@ -6346,7 +7390,7 @@ done-gate, or `nifra mcp` to give the agent live project tools.
6346
7390
 
6347
7391
  ## @nifrajs/client
6348
7392
 
6349
- `ActionArgs`, `ActionData`, `ApiError`, `ApiProxy`, `client`, `ClientOptions`, `FetchFn`, `inProcessClient`, `InProcessClient`, `Jsonify`, `LoaderArgs`, `LoaderData`, `RegistryOf`, `Result`, `SubscribeOptions`, `Subscription`, `testClient`, `Treaty`, `TreatyFromRegistry`
7393
+ `ActionArgs`, `ActionData`, `ApiError`, `ApiProxy`, `client`, `ClientOptions`, `ClientRetryOptions`, `FetchFn`, `inProcessClient`, `InProcessClient`, `InProcessClientOptions`, `Jsonify`, `LoaderArgs`, `LoaderData`, `RegistryOf`, `ResponseContractViolation`, `Result`, `SubscribeOptions`, `Subscription`, `testClient`, `Treaty`, `TreatyFromRegistry`, `WsCallOptions`, `WsHandle`
6350
7394
 
6351
7395
  ## @nifrajs/content
6352
7396
 
@@ -6354,7 +7398,7 @@ done-gate, or `nifra mcp` to give the agent live project tools.
6354
7398
 
6355
7399
  ## @nifrajs/core
6356
7400
 
6357
- `AdmissionController`, `AdmissionDecision`, `AnyServer`, `attachWebSocket`, `commonSecretPatterns`, `Context`, `ContextForOp`, `ContractShape`, `CookieOptions`, `defineContract`, `defineIdentityPlugin`, `definePlugin`, `defineRouterPlugin`, `DurableObjectNamespaceLike`, `ExecutionContext`, `FRAMEWORK_NAME`, `FrameworkError`, `FrameworkName`, `Handler`, `HandlersFor`, `IdentityPlugin`, `implement`, `InferInput`, `InferOutput`, `jsonLogger`, `LogFields`, `Logger`, `McpPromptDescriptor`, `McpResourceDescriptor`, `Method`, `METHODS`, `Middleware`, `NifraPlugin`, `NifraWebSocket`, `NodeServeOutcome`, `OnRequestResult`, `OperationDef`, `Params`, `parseCookies`, `Platform`, `Prettify`, `PromptArgument`, `PromptMessage`, `redactLogFields`, `RedactOptions`, `Registry`, `RegistryFor`, `RegistryFromImpl`, `ResponseControls`, `ResponseDef`, `ResponseFinalization`, `robots`, `RobotsOptions`, `RobotsRule`, `RouteConfigError`, `RouteConfigErrorCode`, `RouteDescriptor`, `RouteInfo`, `Router`, `RouterMatch`, `RouteSchema`, `RunningServer`, `ScheduledController`, `ScheduledHandler`, `serializeCookie`, `server`, `Server`, `ServerOptions`, `SignatureEncoding`, `signValue`, `silentLogger`, `sitemap`, `SitemapChangeFreq`, `SitemapEntry`, `SitemapOptions`, `sse`, `SSEContext`, `SSEInit`, `SSEMessage`, `SSEStream`, `StandardIssue`, `StandardResult`, `StandardSchemaV1`, `StandardTypes`, `StandardWebSocket`, `toFetchHandler`, `ToolAnnotations`, `TopicRegistry`, `typedSSEStream`, `TypedSSEStream`, `unsignValue`, `validateStandard`, `ValidationOutcome`, `verifyWebhook`, `VerifyWebhookOptions`, `Version`, `VERSION`, `WebhookFailureReason`, `WebhookProvider`, `WebhookResult`, `WebSocketContext`, `WebSocketData`, `WebSocketHandler`, `WebSocketUpgradeOutcome`
7401
+ `Version`, `VERSION`
6358
7402
 
6359
7403
  ## @nifrajs/cron
6360
7404
 
@@ -6394,7 +7438,7 @@ done-gate, or `nifra mcp` to give the agent live project tools.
6394
7438
 
6395
7439
  ## @nifrajs/mcp
6396
7440
 
6397
- `bridgeScript`, `createMcpServer`, `CreateMcpServerOptions`, `defineMcpTool`, `DefineMcpToolOptions`, `defineMcpWidget`, `DefineMcpWidgetOptions`, `McpAppBridge`, `McpHttpOptions`, `McpServer`, `McpToolHandlerResult`, `McpUiIntent`, `McpWidget`, `respondMcpHttp`, `uiResourceMeta`, `widgetDocument`
7441
+ `bridgeScript`, `createMcpServer`, `CreateMcpServerOptions`, `defineMcpTool`, `DefineMcpToolOptions`, `defineMcpWidget`, `DefineMcpWidgetOptions`, `InferOutput`, `McpAppBridge`, `McpHttpOptions`, `McpServer`, `McpToolHandlerResult`, `McpUiIntent`, `McpWidget`, `respondMcpHttp`, `StandardIssue`, `StandardResult`, `StandardSchemaV1`, `uiResourceMeta`, `widgetDocument`
6398
7442
 
6399
7443
  ## @nifrajs/mcp-db
6400
7444
 
@@ -6414,7 +7458,7 @@ done-gate, or `nifra mcp` to give the agent live project tools.
6414
7458
 
6415
7459
  ## @nifrajs/otel
6416
7460
 
6417
- `ActiveObservation`, `AttributeValue`, `causalitySpanLink`, `combineObservationAdapters`, `consoleSpanExporter`, `createObservationLifecycle`, `EndObservation`, `formatTraceparent`, `generateSpanId`, `generateTraceId`, `NifraSpan`, `ObservationAdapter`, `ObservationClock`, `ObservationContext`, `ObservationLifecycle`, `ObservationLifecycleOptions`, `ObservationLink`, `ObservationParent`, `ParsedTraceparent`, `parseTraceparent`, `SpanExporter`, `SpanStatus`, `StartObservation`, `TraceContext`, `traceHeaders`, `tracing`, `TracingOptions`
7461
+ `ActiveObservation`, `AttributeValue`, `causalitySpanLink`, `combineObservationAdapters`, `consoleSpanExporter`, `createObservationLifecycle`, `effectTracing`, `EffectTracingOptions`, `EffectTracingPlugin`, `EndObservation`, `formatTraceparent`, `generateSpanId`, `generateTraceId`, `NifraSpan`, `ObservationAdapter`, `ObservationClock`, `ObservationContext`, `ObservationLifecycle`, `ObservationLifecycleOptions`, `ObservationLink`, `ObservationParent`, `otlpExporter`, `OtlpExporter`, `OtlpExporterOptions`, `ParsedTraceparent`, `parseTraceparent`, `SpanStatus`, `StartObservation`, `TraceContext`, `traceHeaders`, `tracing`, `TracingOptions`
6418
7462
 
6419
7463
  ## @nifrajs/prompt
6420
7464
 
@@ -6442,7 +7486,7 @@ done-gate, or `nifra mcp` to give the agent live project tools.
6442
7486
 
6443
7487
  ## @nifrajs/web
6444
7488
 
6445
- `Action`, `ACTION_GLOBAL`, `apiKey`, `assertRenderAdapterConformance`, `BrowserNavigate`, `buildManifest`, `CachedResponse`, `CacheStore`, `canonical`, `clientEntry`, `ClientRouter`, `ClientRouterOptions`, `createClientRouter`, `createMatcher`, `createMutation`, `createQueryClient`, `createWebApp`, `CreateWebAppOptions`, `DATA_GLOBAL`, `DATA_HEADER`, `DEFAULT_DEV_PORT`, `defer`, `Deferred`, `DehydratedState`, `disableDraft`, `DRAFT_COOKIE`, `DraftCookieControls`, `enableDraft`, `EnableDraftOptions`, `enumeratePrerenderedPaths`, `enumerateStaticRoutes`, `Fetcher`, `FetcherState`, `FetchRouteData`, `filePathToPattern`, `filePathToPatterns`, `FontDisplay`, `fontFace`, `FontFace`, `fontPreload`, `FontPreloadInput`, `FontSource`, `generateClientEntry`, `GenerateClientEntryOptions`, `generateServerManifest`, `GenerateServerManifestOptions`, `getBrowserNavigate`, `GetStaticPaths`, `hashQueryKey`, `InfiniteData`, `InfiniteQueryHandle`, `InfiniteQueryOptions`, `isDraftEnabled`, `ISR_REVALIDATE_HEADER`, `ISR_STATUS_HEADER`, `ISRApp`, `ISROptions`, `ISRPlatform`, `jsonLd`, `KVCacheStore`, `KVCacheStoreOptions`, `KVNamespaceLike`, `LayoutEntry`, `LinkDescriptor`, `Loader`, `LoaderContext`, `manifest`, `Manifest`, `MemoryCacheStore`, `MemoryCacheStoreOptions`, `mergeHeads`, `meta`, `Meta`, `MetaArgs`, `MetaInput`, `MountRouterOptions`, `MutationCallbacks`, `MutationHandle`, `MutationState`, `MutationStatus`, `NavigateOptions`, `openGraph`, `OpenGraphInput`, `PRE_HYDRATION_GUARD`, `QueryClient`, `QueryClientOptions`, `QueryHandle`, `QueryOptions`, `QueryState`, `QueryStatus`, `redirect`, `REDIRECT_HEADER`, `RedirectOptions`, `RenderAdapter`, `RenderAdapterConformanceError`, `RenderAdapterConformanceFixture`, `RenderedPage`, `renderPage`, `RenderPageOptions`, `renderPageResult`, `RenderProps`, `resolveMeta`, `revalidate`, `REVALIDATE_HEADER`, `revalidateEndpoint`, `RevalidateEndpointOptions`, `RevalidateResult`, `ROUTE_GLOBAL`, `RouteEntry`, `RouteMatch`, `RouteModule`, `RoutePattern`, `RouterState`, `routeStyles`, `ScriptDescriptor`, `serializeData`, `ServerOnly`, `setBrowserNavigate`, `StaticPath`, `StaticPaths`, `StaticRoutes`, `styles`, `Submission`, `SubmitOptions`, `withISR`
7489
+ `Action`, `ACTION_GLOBAL`, `apiKey`, `assertRenderAdapterConformance`, `BrowserNavigate`, `buildManifest`, `CachedResponse`, `CacheStore`, `canonical`, `clientEntry`, `ClientRouter`, `ClientRouterOptions`, `createClientRouter`, `createMatcher`, `createMutation`, `createQueryClient`, `createWebApp`, `CreateWebAppOptions`, `DATA_GLOBAL`, `DATA_HEADER`, `DEFAULT_DEV_PORT`, `defer`, `Deferred`, `DehydratedState`, `disableDraft`, `DRAFT_COOKIE`, `DraftCookieControls`, `enableDraft`, `EnableDraftOptions`, `enumerateStaticRoutes`, `Fetcher`, `FetcherState`, `FetchRouteData`, `filePathToPattern`, `filePathToPatterns`, `FontDisplay`, `fontFace`, `FontFace`, `fontPreload`, `FontPreloadInput`, `FontSource`, `generateClientEntry`, `GenerateClientEntryOptions`, `generateServerManifest`, `GenerateServerManifestOptions`, `getBrowserNavigate`, `GetStaticPaths`, `hashQueryKey`, `InfiniteData`, `InfiniteQueryHandle`, `InfiniteQueryOptions`, `isDraftEnabled`, `ISR_REVALIDATE_HEADER`, `ISR_STATUS_HEADER`, `ISRApp`, `ISROptions`, `ISRPlatform`, `jsonLd`, `KVCacheStore`, `KVCacheStoreOptions`, `KVNamespaceLike`, `LayoutEntry`, `LinkDescriptor`, `Loader`, `LoaderContext`, `manifest`, `Manifest`, `MemoryCacheStore`, `MemoryCacheStoreOptions`, `mergeHeads`, `meta`, `Meta`, `MetaArgs`, `MetaInput`, `MountRouterOptions`, `MutationCallbacks`, `MutationHandle`, `MutationState`, `MutationStatus`, `NavigateOptions`, `openGraph`, `OpenGraphInput`, `PRE_HYDRATION_GUARD`, `QueryClient`, `QueryClientOptions`, `QueryHandle`, `QueryOptions`, `QueryState`, `QueryStatus`, `redirect`, `REDIRECT_HEADER`, `RedirectOptions`, `RenderAdapter`, `RenderAdapterConformanceError`, `RenderAdapterConformanceFixture`, `RenderedPage`, `renderPage`, `RenderPageOptions`, `renderPageResult`, `RenderProps`, `resolveMeta`, `revalidate`, `REVALIDATE_HEADER`, `revalidateEndpoint`, `RevalidateEndpointOptions`, `RevalidateResult`, `ROUTE_GLOBAL`, `RouteEntry`, `RouteMatch`, `RouteModule`, `RoutePattern`, `RouterState`, `routeStyles`, `ScriptDescriptor`, `serializeData`, `ServerOnly`, `setBrowserNavigate`, `StaticPath`, `StaticPaths`, `StaticRoutes`, `styles`, `Submission`, `SubmitOptions`, `withISR`
6446
7490
 
6447
7491
  ## @nifrajs/web-preact
6448
7492