@nifrajs/cli 1.12.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -2
- package/dist/check.d.ts +17 -6
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +256 -15
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts +2 -3
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +58 -71
- package/dist/cli.js.map +1 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +131 -61
- package/dist/doctor.js.map +1 -1
- package/dist/mcp-http.js +1 -1
- package/dist/mcp-http.js.map +1 -1
- package/dist/mcp.d.ts +2 -0
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +42 -0
- package/dist/mcp.js.map +1 -1
- package/dist/recipes/2.0.0.d.ts +9 -0
- package/dist/recipes/2.0.0.d.ts.map +1 -0
- package/dist/recipes/2.0.0.js +25 -0
- package/dist/recipes/2.0.0.js.map +1 -0
- package/dist/recipes/index.d.ts +7 -0
- package/dist/recipes/index.d.ts.map +1 -1
- package/dist/recipes/index.js +2 -0
- package/dist/recipes/index.js.map +1 -1
- package/dist/sync-manifest.d.ts +33 -0
- package/dist/sync-manifest.d.ts.map +1 -0
- package/dist/sync-manifest.js +97 -0
- package/dist/sync-manifest.js.map +1 -0
- package/dist/upgrade.d.ts +23 -2
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +144 -13
- package/dist/upgrade.js.map +1 -1
- package/docs/examples.json +69 -21
- package/docs/llms-full.txt +1065 -104
- package/docs/types.json +933 -181
- package/package.json +9 -9
- package/src/check.ts +251 -18
- package/src/cli.ts +57 -73
- package/src/doctor.ts +160 -61
- package/src/mcp-http.ts +1 -1
- package/src/mcp.ts +52 -0
- package/src/recipes/2.0.0.ts +26 -0
- package/src/recipes/index.ts +10 -0
- package/src/sync-manifest.ts +111 -0
- package/src/upgrade.ts +163 -11
package/docs/llms-full.txt
CHANGED
|
@@ -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
|
|
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()
|
|
@@ -1782,7 +1783,7 @@ embeds a token), pass opt-in `valuePatterns` — `commonSecretPatterns` covers
|
|
|
1782
1783
|
bearer tokens, JWTs, emails, and a few well-known key formats, or supply your own:
|
|
1783
1784
|
|
|
1784
1785
|
```ts
|
|
1785
|
-
import { server, jsonLogger, commonSecretPatterns } from "@nifrajs/core"
|
|
1786
|
+
import { server, jsonLogger, commonSecretPatterns } from "@nifrajs/core/server"
|
|
1786
1787
|
|
|
1787
1788
|
// Key-name redaction is always on; valuePatterns adds opt-in value + message scanning.
|
|
1788
1789
|
const app = server({
|
|
@@ -2342,6 +2343,13 @@ Standard Schema
|
|
|
2342
2343
|
, you can also bring Zod, Valibot, or ArkType (all eval-free) — only `t` also
|
|
2343
2344
|
emits OpenAPI from the same definition.
|
|
2344
2345
|
|
|
2346
|
+
Size is the other edge axis. `t` is TypeBox-backed - it carries the whole JSON
|
|
2347
|
+
Schema type system - so a validated app on `t` is heavier than one on a lean
|
|
2348
|
+
validator. Validation is per route, so reach for `t` where you want the contract
|
|
2349
|
+
(OpenAPI/MCP for free) and Valibot where the Workers cold-start bundle is what matters: a
|
|
2350
|
+
Valibot-validated Nifra app measures **~16 KB gzipped**. Both are first-class, and you can
|
|
2351
|
+
mix them in one app.
|
|
2352
|
+
|
|
2345
2353
|
### Scheduled (cron)
|
|
2346
2354
|
|
|
2347
2355
|
Pass `scheduled` to `toFetchHandler` to also export a Workers cron
|
|
@@ -2350,7 +2358,7 @@ typed `env` and `waitUntil` as your request handlers.
|
|
|
2350
2358
|
|
|
2351
2359
|
```ts
|
|
2352
2360
|
// 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"
|
|
2361
|
+
import { toFetchHandler } from "@nifrajs/core/server"
|
|
2354
2362
|
import { app } from "./app"
|
|
2355
2363
|
|
|
2356
2364
|
// Export fetch + a cron handler. Wire the schedule in wrangler.toml: [triggers] crons = ["0 * * * *"]
|
|
@@ -2369,15 +2377,18 @@ export default toFetchHandler(app, {
|
|
|
2369
2377
|
through a generic — and runs on every runtime nifra serves: Bun, Deno, Node, and Cloudflare
|
|
2370
2378
|
Workers.
|
|
2371
2379
|
|
|
2372
|
-
The WebSocket runtime ships as
|
|
2373
|
-
it
|
|
2374
|
-
`
|
|
2380
|
+
The WebSocket runtime ships as an opt-in plugin, so apps that never use it don’t bundle it.
|
|
2381
|
+
Enable it with `.use(websocket())` from `@nifrajs/core/ws`;
|
|
2382
|
+
`app.ws()` without it fails loud at registration with
|
|
2383
|
+
`WS_RUNTIME_MISSING`. It installs on that server instance only — the same
|
|
2384
|
+
`.use()` opt-in as `mcp()` and `streaming()`.
|
|
2375
2385
|
|
|
2376
2386
|
```ts
|
|
2377
|
-
import { server } from "@nifrajs/core"
|
|
2378
|
-
import "@nifrajs/core/ws" //
|
|
2387
|
+
import { server } from "@nifrajs/core/server"
|
|
2388
|
+
import { websocket } from "@nifrajs/core/ws" // .use(websocket()) enables app.ws()
|
|
2379
2389
|
|
|
2380
2390
|
const app = server()
|
|
2391
|
+
.use(websocket())
|
|
2381
2392
|
.get("/", () => ({ ok: true }))
|
|
2382
2393
|
.ws("/echo", {
|
|
2383
2394
|
open: (ws) => ws.send("welcome"),
|
|
@@ -2665,7 +2676,7 @@ SSR verified on real workerd — **this very site runs there**.
|
|
|
2665
2676
|
|
|
2666
2677
|
```ts
|
|
2667
2678
|
// _worker.ts — the edge SSR entry
|
|
2668
|
-
import { toFetchHandler } from "@nifrajs/core"
|
|
2679
|
+
import { toFetchHandler } from "@nifrajs/core/server"
|
|
2669
2680
|
import { createWebApp } from "@nifrajs/web"
|
|
2670
2681
|
import { reactAdapter } from "@nifrajs/web-react"
|
|
2671
2682
|
import { clientEntry, manifest } from "./server-manifest" // generated by buildServer
|
|
@@ -2682,7 +2693,7 @@ Deploy: `wrangler pages deploy dist` (Pages) or `wrangler deploy`
|
|
|
2682
2693
|
|
|
2683
2694
|
## Nifra — CLI
|
|
2684
2695
|
|
|
2685
|
-
> The zero-config Nifra CLI (`nifra dev`, `nifra build`, `nifra start`): true HMR
|
|
2696
|
+
> The zero-config Nifra CLI (`nifra dev`, `nifra build`, `nifra start`): true HMR and complete target-specific deploy builds.
|
|
2686
2697
|
|
|
2687
2698
|
`Nifra` is zero-config: it reads `routes/`, `framework.ts`, and
|
|
2688
2699
|
(optionally) `backend.ts` from your project and wires the right
|
|
@@ -2691,57 +2702,62 @@ Deploy: `wrangler pages deploy dist` (Pages) or `wrangler deploy`
|
|
|
2691
2702
|
|
|
2692
2703
|
```ts
|
|
2693
2704
|
nifra dev # true-HMR dev server (Vite middleware + nifra SSR) — http://localhost:4321
|
|
2694
|
-
nifra build #
|
|
2695
|
-
nifra start #
|
|
2705
|
+
nifra build # full Bun deploy → dist/server.js + content-hashed dist/assets/ (default target: bun)
|
|
2706
|
+
nifra start # run dist/server.js on Bun
|
|
2707
|
+
nifra build --target cf-pages # also: node | deno | vercel | static; add --report for chunk sizes
|
|
2696
2708
|
|
|
2697
2709
|
# 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
|
|
2710
|
+
# flags: --port <n> (dev/start) · --out <dir> (build/start) · --target <t> (build) · --poll (dev)
|
|
2699
2711
|
```
|
|
2700
2712
|
|
|
2701
2713
|
### The conventions
|
|
2702
2714
|
|
|
2703
|
-
|
|
2715
|
+
Four conventions at the project root. `nifra dev` runs the Vite-backed
|
|
2704
2716
|
[HMR dev server](/docs/dev); `nifra build` runs the Bun-native production
|
|
2705
|
-
build (content-hashed
|
|
2706
|
-
|
|
2707
|
-
chunks preloaded
|
|
2717
|
+
build (content-hashed client assets plus a target-specific server); `nifra start`
|
|
2718
|
+
runs the default Bun output. The generated server serves assets and SSR with matched-route
|
|
2719
|
+
chunks preloaded and route CSS linked in each `<head>`.
|
|
2708
2720
|
|
|
2709
2721
|
```ts
|
|
2710
2722
|
my-app/
|
|
2711
2723
|
routes/ # file-based routes (index.tsx, _layout.tsx, [id].tsx, …)
|
|
2712
|
-
framework.ts #
|
|
2724
|
+
framework.ts # deploy-safe render adapter
|
|
2725
|
+
nifra.config.ts # CLI-only client module + dev/build plugins
|
|
2713
2726
|
backend.ts # export const backend = server()... (optional — the typed contract)
|
|
2714
2727
|
```
|
|
2715
2728
|
|
|
2716
2729
|
### framework.ts — naming the framework once
|
|
2717
2730
|
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2731
|
+
Keep the render `adapter` in deploy-safe `framework.ts`. Put
|
|
2732
|
+
`clientModule`, Vite plugins, compiler plugins, conditions, and defines in CLI-only
|
|
2733
|
+
`nifra.config.ts`. This prevents build tooling and native dependencies from entering
|
|
2734
|
+
generated server bundles.
|
|
2721
2735
|
|
|
2722
2736
|
```ts
|
|
2723
|
-
//
|
|
2724
|
-
// framework.ts — the one place an app names its framework. The CLI reads these:
|
|
2725
|
-
import react from "@vitejs/plugin-react"
|
|
2737
|
+
// framework.ts — deploy-safe; generated server entries import this file.
|
|
2726
2738
|
import { reactAdapter } from "@nifrajs/web-react"
|
|
2727
2739
|
|
|
2728
2740
|
export const adapter = reactAdapter
|
|
2741
|
+
|
|
2742
|
+
// nifra.config.ts — CLI-only build/dev tooling; never imported by a deployed server.
|
|
2743
|
+
// doc-check: skip — needs the third-party @vitejs/plugin-react; install it to run this.
|
|
2744
|
+
import react from "@vitejs/plugin-react"
|
|
2745
|
+
export { adapter } from "./framework"
|
|
2729
2746
|
export const clientModule = "@nifrajs/web-react/client"
|
|
2730
2747
|
export const vitePlugins = [react()] // dev HMR (Fast Refresh)
|
|
2731
2748
|
// Vue/Svelte/Solid also export:
|
|
2732
2749
|
// clientPlugins = [vueBunPlugin("dom")] // compile routes for the client build
|
|
2733
|
-
// serverPlugins = [vueBunPlugin("ssr")] // compile routes
|
|
2750
|
+
// serverPlugins = [vueBunPlugin("ssr")] // compile routes into the target's server bundle
|
|
2734
2751
|
// conditions = ["solid"] // Solid: resolve solid-js to its source
|
|
2735
2752
|
// define = { __VUE_OPTIONS_API__: "true", ... } // Vue feature flags
|
|
2736
2753
|
```
|
|
2737
2754
|
|
|
2738
2755
|
### Scope
|
|
2739
2756
|
|
|
2740
|
-
`nifra build`
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
worker those targets need. A `nifra build --target` for edge is a future addition.
|
|
2757
|
+
`nifra build` defaults to a self-hosted Bun server. Use `--target node`,
|
|
2758
|
+
`deno`, `cf-pages`, `vercel`, or `static` for another
|
|
2759
|
+
complete deploy shape (see [Deployment](/docs/deployment)). Run non-Bun outputs with
|
|
2760
|
+
the command printed by the build.
|
|
2745
2761
|
|
|
2746
2762
|
## Nifra — Dev & HMR
|
|
2747
2763
|
|
|
@@ -3170,6 +3186,110 @@ params. The error reads out the fix; add a schema to the route:
|
|
|
3170
3186
|
`.get("/thing", { query: z.object({ page: z.string() }) }, h)`. Then
|
|
3171
3187
|
`c.query` is the validated type and the client accepts a typed `query`.
|
|
3172
3188
|
|
|
3189
|
+
###
|
|
3190
|
+
`TS2589` - "Type instantiation is excessively deep and possibly infinite" (one
|
|
3191
|
+
`server()` chain grew past ~95 routes)
|
|
3192
|
+
|
|
3193
|
+
The fluent builder's whole value - end-to-end inference, so `c.params.id` is typed
|
|
3194
|
+
straight from `:id` and the client is derived from `typeof app` - carries
|
|
3195
|
+
an **O(N) type-instantiation cost**. Each `.get(path, handler)` /
|
|
3196
|
+
`.post(...)` does two things at once: it computes the handler's context type from the
|
|
3197
|
+
path, _and_ it returns a server whose registry is one alias level deeper than the last.
|
|
3198
|
+
Neither strains the compiler alone; the **product** - recomputing the handler
|
|
3199
|
+
context while re-threading an ever-larger registry at every step - exhausts TypeScript's
|
|
3200
|
+
per-expression instantiation budget. A single chain hits `TS2589` at
|
|
3201
|
+
**~95-100 routes**.
|
|
3202
|
+
|
|
3203
|
+
[!NOTE]
|
|
3204
|
+
|
|
3205
|
+
This is a **healthy, growing app's wall, not abuse**. It is inherent to any
|
|
3206
|
+
builder that infers handler context _and_ accumulates a typed route registry (Elysia,
|
|
3207
|
+
tRPC, and Hono's typed clients cap the same way), so it is not fixable by reshaping the
|
|
3208
|
+
internal `AddRoute`. The fix is to use a shape that does not form the product.
|
|
3209
|
+
|
|
3210
|
+
**Fix 1 - split into domain groups and `.merge()` them.** Each group is
|
|
3211
|
+
its own short `server()` chain, so no single chain approaches the ceiling; a
|
|
3212
|
+
`.merge()` is one `R & R2` intersection with no per-call context work,
|
|
3213
|
+
so composing groups stays cheap. A 90-route single chain is inside the ceiling; 120 routes as
|
|
3214
|
+
four merged 30-route groups typecheck with full per-route fidelity.
|
|
3215
|
+
|
|
3216
|
+
```ts
|
|
3217
|
+
import { server } from "@nifrajs/core"
|
|
3218
|
+
|
|
3219
|
+
// Each domain is its OWN short server() chain, kept well under the ~95-route ceiling.
|
|
3220
|
+
const users = server()
|
|
3221
|
+
.get("/users/:id", (c) => ({ id: c.params.id }))
|
|
3222
|
+
.post("/users", () => ({ created: true }))
|
|
3223
|
+
|
|
3224
|
+
const orders = server()
|
|
3225
|
+
.get("/orders/:id", (c) => ({ id: c.params.id }))
|
|
3226
|
+
.post("/orders", () => ({ placed: true }))
|
|
3227
|
+
|
|
3228
|
+
// merge() adds ONE R & R2 intersection per group - no per-call context recompute - so the whole
|
|
3229
|
+
// app stays inside tsc's budget no matter how many groups (or routes) you compose.
|
|
3230
|
+
export const app = server()
|
|
3231
|
+
.get("/health", () => ({ ok: true }))
|
|
3232
|
+
.merge(users)
|
|
3233
|
+
.merge(orders)
|
|
3234
|
+
```
|
|
3235
|
+
|
|
3236
|
+
**Fix 2 - go contract-first, and stay flat at any route count.**
|
|
3237
|
+
`defineContract(...)` declares the entire registry as **one object type
|
|
3238
|
+
upfront**, and `implement(contract, handlers)` binds handlers to it. Nothing
|
|
3239
|
+
grows a registry per call, so there is **no ceiling at all** - this is the path for
|
|
3240
|
+
an API that will keep adding routes for years. Handlers stay checked against the contract exactly
|
|
3241
|
+
as inline routes are. See [Contract-first](/docs/contract).
|
|
3242
|
+
|
|
3243
|
+
```ts
|
|
3244
|
+
import { defineContract, implement } from "@nifrajs/core/contract"
|
|
3245
|
+
|
|
3246
|
+
// One object type, declared upfront - NOT an N-deep stack of `Server<AddRoute<…>>` aliases.
|
|
3247
|
+
const contract = defineContract({
|
|
3248
|
+
getUser: { method: "GET", path: "/users/:id" },
|
|
3249
|
+
listUsers: { method: "GET", path: "/users" },
|
|
3250
|
+
createUser: { method: "POST", path: "/users" },
|
|
3251
|
+
// ...hundreds more operations stay flat: the registry is one type, not a growing chain.
|
|
3252
|
+
})
|
|
3253
|
+
|
|
3254
|
+
export const app = implement(contract, {
|
|
3255
|
+
getUser: (c) => ({ id: c.params.id }),
|
|
3256
|
+
listUsers: () => ({ users: [] as string[] }),
|
|
3257
|
+
createUser: () => ({ created: true }),
|
|
3258
|
+
})
|
|
3259
|
+
```
|
|
3260
|
+
|
|
3261
|
+
####
|
|
3262
|
+
`TS2345` from an unrelated `.merge()` - budget exhausted "at a distance"
|
|
3263
|
+
|
|
3264
|
+
Because the budget is per-expression and global to a compilation, a type-heavy construct
|
|
3265
|
+
_elsewhere_ in the program can push an otherwise-fine `.merge()` chain over the
|
|
3266
|
+
edge. It surfaces not as `TS2589` but as a `TS2345` assignability error
|
|
3267
|
+
naming an **uninstantiated** `Server` - the shape
|
|
3268
|
+
the server type collapses to when the compiler gives up mid-inference. The usual trigger is a
|
|
3269
|
+
**generic higher-order function that wraps the builder** (a
|
|
3270
|
+
`withX(app)` that threads the `Server` type through its own type
|
|
3271
|
+
parameters); merely having that file in the program can be enough. Treat these as real
|
|
3272
|
+
constraints of an inference-first framework:
|
|
3273
|
+
|
|
3274
|
+
-
|
|
3275
|
+
Keep **service-layer types flat and explicitly annotated** - do not let
|
|
3276
|
+
inference-heavy generics thread the `Server` / registry type through your own code.
|
|
3277
|
+
|
|
3278
|
+
-
|
|
3279
|
+
**Avoid generic HOF wrappers around the builder.** Wrap with a
|
|
3280
|
+
`defineRouterPlugin` (identity plugin) or compose with `.merge()` instead
|
|
3281
|
+
of a `withX(app)` that re-infers the whole server type.
|
|
3282
|
+
|
|
3283
|
+
-
|
|
3284
|
+
Put an explicit `Promise` return annotation on async callbacks that thread
|
|
3285
|
+
framework types, so the compiler stops re-deriving the awaited type at each use.
|
|
3286
|
+
|
|
3287
|
+
[!TIP]
|
|
3288
|
+
|
|
3289
|
+
Both fixes preserve full type fidelity - the client derived from `typeof app` is
|
|
3290
|
+
exactly as precise after a `.merge()` or an `implement()` as it is for an
|
|
3291
|
+
inline route. Splitting or going contract-first costs you nothing at the call site.
|
|
3292
|
+
|
|
3173
3293
|
### Still stuck?
|
|
3174
3294
|
|
|
3175
3295
|
Run `nifra check --json` as the done-gate — it surfaces the import-chain leaks,
|
|
@@ -3195,7 +3315,7 @@ _over HTTP_. Both run the identical lifecycle — validation, middleware, contra
|
|
|
3195
3315
|
|
|
3196
3316
|
```ts
|
|
3197
3317
|
// backend.ts — a normal @nifrajs/core server. Routes live at the full /api/... path.
|
|
3198
|
-
import { server } from "@nifrajs/core"
|
|
3318
|
+
import { server } from "@nifrajs/core/server"
|
|
3199
3319
|
import { t } from "@nifrajs/schema"
|
|
3200
3320
|
|
|
3201
3321
|
export const backend = server()
|
|
@@ -3269,6 +3389,65 @@ Vite-backed dev server) routes every request through that same app's `fetch`. So
|
|
|
3269
3389
|
the `/api/*` routes are served identically in development and production — there is
|
|
3270
3390
|
nothing extra to wire for the dev loop.
|
|
3271
3391
|
|
|
3392
|
+
### Mounting, composition & path semantics
|
|
3393
|
+
|
|
3394
|
+
A useful thing to know up front, because it is the opposite of some frameworks: in Nifra
|
|
3395
|
+
**composition and mounting are prefix-less, and paths stay absolute**. There is no
|
|
3396
|
+
per-mount base path, and nothing rewrites the request URL. Two mechanisms are involved and both
|
|
3397
|
+
leave the path untouched:
|
|
3398
|
+
|
|
3399
|
+
-
|
|
3400
|
+
**
|
|
3401
|
+
`.merge()` (and contract-first `implement()`)
|
|
3402
|
+
**
|
|
3403
|
+
- the composition escape hatch for large apps. `merge()` folds another server's
|
|
3404
|
+
routes into this one _at the exact paths they were defined_, into a single shared path
|
|
3405
|
+
space. It takes no prefix argument; a path+method collision throws a
|
|
3406
|
+
`RouteConfigError` at merge time (fail-closed), so groups never silently shadow
|
|
3407
|
+
each other. Each domain therefore owns its full absolute paths (`/api/listings`,
|
|
3408
|
+
`/api/agents`), exactly as it would standalone.
|
|
3409
|
+
|
|
3410
|
+
-
|
|
3411
|
+
**the `/api/*` auto-mount** - a path _guard_, not a rewrite.
|
|
3412
|
+
`createWebApp` hands the _same_ `Request` (its URL and body
|
|
3413
|
+
intact) to the backend when the pathname is exactly `apiPrefix` or under
|
|
3414
|
+
`apiPrefix + "/"`. The backend's own router then matches on
|
|
3415
|
+
`new URL(c.req.url).pathname` - the full, original path.
|
|
3416
|
+
|
|
3417
|
+
The consequence for a handler: **
|
|
3418
|
+
`c.req.url` always carries the full, original path
|
|
3419
|
+
**
|
|
3420
|
+
- the same URL the caller sent. The `/api` prefix is _not_ stripped before a
|
|
3421
|
+
merged or mounted route sees it, and there is no `pathOf`-style helper that strips
|
|
3422
|
+
it. Whether a route was defined inline, merged in from a group, or reached through the HTTP
|
|
3423
|
+
mount, the pathname it observes is the request's own.
|
|
3424
|
+
|
|
3425
|
+
```ts
|
|
3426
|
+
// backend.ts - compose domains with .merge(). Each group owns its FULL absolute paths.
|
|
3427
|
+
import { server } from "@nifrajs/core/server"
|
|
3428
|
+
|
|
3429
|
+
const listings = server().get("/api/listings", (c) => {
|
|
3430
|
+
// c.req.url is the ORIGINAL request URL - .merge() and the /api mount never strip the prefix.
|
|
3431
|
+
const path = new URL(c.req.url).pathname // GET /api/listings -> "/api/listings"
|
|
3432
|
+
return { path }
|
|
3433
|
+
})
|
|
3434
|
+
|
|
3435
|
+
const agents = server().get("/api/agents", (c) => ({ ok: new URL(c.req.url).pathname }))
|
|
3436
|
+
|
|
3437
|
+
// merge() folds both groups into ONE flat path space at their defined paths; a path+method clash
|
|
3438
|
+
// throws a RouteConfigError right here at merge time (fail-closed), never a silent shadow at runtime.
|
|
3439
|
+
export const backend = server().merge(listings).merge(agents)
|
|
3440
|
+
```
|
|
3441
|
+
|
|
3442
|
+
**The one gotcha:** because nothing strips the prefix, a backend that will be
|
|
3443
|
+
mounted (or merged) under `/api` must **declare its routes at the full path**
|
|
3444
|
+
- `server().post("/api/sync", …)`, _not_ `.post("/sync", …)` in the
|
|
3445
|
+
hope that `/api` gets peeled off. Define `/sync` and a browser
|
|
3446
|
+
`POST /api/sync` returns the backend's 404: the router looks up
|
|
3447
|
+
`/api/sync` and finds only `/sync`. `apiPrefix` chooses
|
|
3448
|
+
_which_ requests are handed to the backend; it does not rewrite them, so the route paths
|
|
3449
|
+
and the caller's paths are one and the same.
|
|
3450
|
+
|
|
3272
3451
|
### Calling /api/* from the browser
|
|
3273
3452
|
|
|
3274
3453
|
With the routes mounted, a client island or component hits them with the same typed
|
|
@@ -3318,7 +3497,7 @@ to local policy, and exposes monotonic remaining time beside the existing cancel
|
|
|
3318
3497
|
### Clamp once at admission
|
|
3319
3498
|
|
|
3320
3499
|
```ts
|
|
3321
|
-
import { server } from "@nifrajs/core"
|
|
3500
|
+
import { server } from "@nifrajs/core/server"
|
|
3322
3501
|
|
|
3323
3502
|
const app = server({
|
|
3324
3503
|
requestTimeoutMs: 2_000,
|
|
@@ -3357,6 +3536,177 @@ Configure a finite `requestTimeoutMs` before handing `c.budget` to a
|
|
|
3357
3536
|
production adapter. The no-timeout compatibility path is deliberately unbounded and is not
|
|
3358
3537
|
written to the wire.
|
|
3359
3538
|
|
|
3539
|
+
## Nifra - Execution causality
|
|
3540
|
+
|
|
3541
|
+
> Carry one bounded, payload-free lineage across the durable seams a trace cannot span: commands, events, workflows, projections, and repairs.
|
|
3542
|
+
|
|
3543
|
+
A trace describes one observation tree. Causality survives the seams a trace cannot: a command
|
|
3544
|
+
commits an event, a workflow resumes tomorrow, a projection consumes that event, and
|
|
3545
|
+
reconciliation repairs the drift. Same execution, four identities that used to stop at the
|
|
3546
|
+
boundary.
|
|
3547
|
+
|
|
3548
|
+
### Tokens only, by construction
|
|
3549
|
+
|
|
3550
|
+
A causality record has **no payload field**. There is nowhere to put a request
|
|
3551
|
+
body, a tenant identifier, or a URL, and unknown fields are rejected at every nesting level
|
|
3552
|
+
rather than carried along. Identities are bounded tokens, edge relations are bounded tokens,
|
|
3553
|
+
and the lineage stays safe to store and read during an incident.
|
|
3554
|
+
|
|
3555
|
+
### Start at the boundary
|
|
3556
|
+
|
|
3557
|
+
```ts
|
|
3558
|
+
import { createMemoryCausalityStore, startCausality } from "@nifrajs/core/causality"
|
|
3559
|
+
|
|
3560
|
+
const recorder = createMemoryCausalityStore()
|
|
3561
|
+
const requestId = crypto.randomUUID()
|
|
3562
|
+
|
|
3563
|
+
// One root node at the ingress boundary. executionId is the graph's identity:
|
|
3564
|
+
// generate it once, then propagate it.
|
|
3565
|
+
const step = startCausality("request", requestId, { executionId: crypto.randomUUID() })
|
|
3566
|
+
|
|
3567
|
+
// step.context propagates; step.record is what a durable adapter appends.
|
|
3568
|
+
await recorder.record(step.record)
|
|
3569
|
+
```
|
|
3570
|
+
|
|
3571
|
+
### Continue across seams
|
|
3572
|
+
|
|
3573
|
+
```ts
|
|
3574
|
+
import { continueCausality, startCausality } from "@nifrajs/core/causality"
|
|
3575
|
+
|
|
3576
|
+
const step = startCausality("request", crypto.randomUUID(), {
|
|
3577
|
+
executionId: crypto.randomUUID(),
|
|
3578
|
+
})
|
|
3579
|
+
|
|
3580
|
+
// The command this request caused. The edge relation is a bounded token.
|
|
3581
|
+
const command = continueCausality(step.context, "command", crypto.randomUUID(), {
|
|
3582
|
+
relation: "caused",
|
|
3583
|
+
})
|
|
3584
|
+
|
|
3585
|
+
// The event that command emitted, inside the same commit.
|
|
3586
|
+
const event = continueCausality(command.context, "event", crypto.randomUUID(), {
|
|
3587
|
+
relation: "emitted",
|
|
3588
|
+
})
|
|
3589
|
+
```
|
|
3590
|
+
|
|
3591
|
+
`joinCausality` merges several parents into one node, and refuses to join contexts
|
|
3592
|
+
from different executions: a graph that silently spanned two requests would be worse than no
|
|
3593
|
+
graph.
|
|
3594
|
+
|
|
3595
|
+
### Propagation is a trust boundary
|
|
3596
|
+
|
|
3597
|
+
```ts
|
|
3598
|
+
import {
|
|
3599
|
+
causalityHeaders,
|
|
3600
|
+
readCausalityHeaders,
|
|
3601
|
+
startCausality,
|
|
3602
|
+
} from "@nifrajs/core/causality"
|
|
3603
|
+
|
|
3604
|
+
const step = startCausality("request", crypto.randomUUID(), {
|
|
3605
|
+
executionId: crypto.randomUUID(),
|
|
3606
|
+
})
|
|
3607
|
+
|
|
3608
|
+
// Outbound: bounded headers only.
|
|
3609
|
+
await fetch("https://billing.internal/charge", {
|
|
3610
|
+
headers: causalityHeaders(step.context),
|
|
3611
|
+
})
|
|
3612
|
+
|
|
3613
|
+
// Inbound: never throws on hostile input.
|
|
3614
|
+
const request = new Request("https://api.example/charge")
|
|
3615
|
+
const parsed = readCausalityHeaders(request.headers)
|
|
3616
|
+
if (!parsed.success) {
|
|
3617
|
+
// "missing" | "incomplete" | "invalid" | "unknown-field" - start a fresh graph.
|
|
3618
|
+
}
|
|
3619
|
+
```
|
|
3620
|
+
|
|
3621
|
+
Inbound headers are **untrusted by default**. A caller does not get to attach
|
|
3622
|
+
their request to your graph unless you say so, so a service-to-service seam opts in explicitly
|
|
3623
|
+
and anything that fails the check starts a fresh execution instead of being believed.
|
|
3624
|
+
|
|
3625
|
+
### Storage is yours
|
|
3626
|
+
|
|
3627
|
+
`createMemoryCausalityStore()` is for development and tests. It is bounded, and it
|
|
3628
|
+
refuses to construct under `NODE_ENV=production`: in-memory evidence disappears on
|
|
3629
|
+
restart, which is exactly when an incident timeline matters. Production supplies a durable
|
|
3630
|
+
adapter behind the same `CausalityRecorder` interface, ideally appending in the same
|
|
3631
|
+
transaction as the effect it describes.
|
|
3632
|
+
|
|
3633
|
+
### Traces still apply
|
|
3634
|
+
|
|
3635
|
+
The two models compose. `@nifrajs/otel` turns the nearest observed ancestor into a
|
|
3636
|
+
span link, so an event consumed tomorrow links back to the request that emitted it rather than
|
|
3637
|
+
inventing a trace identity that never existed.
|
|
3638
|
+
|
|
3639
|
+
## Nifra - Adapter certification
|
|
3640
|
+
|
|
3641
|
+
> Prove a storage, cache, job, runtime, or event adapter against its interface and emit a hash-verifiable capability matrix.
|
|
3642
|
+
|
|
3643
|
+
An interface is a promise. Certification is how an adapter proves it kept it, in a form someone
|
|
3644
|
+
else can check.
|
|
3645
|
+
|
|
3646
|
+
### Profiles
|
|
3647
|
+
|
|
3648
|
+
A profile is a named set of checks written against an interface, not an implementation:
|
|
3649
|
+
`cacheStoreCertificationProfile`, `jobStoreCertificationProfile`,
|
|
3650
|
+
`storageAdapterCertificationProfile`, `runtimeAdapterCertificationProfile`
|
|
3651
|
+
, and `eventDeliveryCertificationProfile`. Domain-specific seams define their own
|
|
3652
|
+
with `defineCertificationProfile`, which validates the profile at module load, so a
|
|
3653
|
+
check naming an undeclared capability fails before any adapter runs.
|
|
3654
|
+
|
|
3655
|
+
### Certify an adapter
|
|
3656
|
+
|
|
3657
|
+
```ts
|
|
3658
|
+
// doc-check: skip - createRedisCache stands in for the adapter under test
|
|
3659
|
+
import { test } from "bun:test"
|
|
3660
|
+
import {
|
|
3661
|
+
assertAdapterCertification,
|
|
3662
|
+
cacheStoreCertificationProfile,
|
|
3663
|
+
certifyAdapter,
|
|
3664
|
+
} from "@nifrajs/testing/certification"
|
|
3665
|
+
|
|
3666
|
+
test("the Redis cache adapter satisfies the cache contract", async () => {
|
|
3667
|
+
const report = await certifyAdapter({
|
|
3668
|
+
profile: cacheStoreCertificationProfile(),
|
|
3669
|
+
adapterId: "redis-cache",
|
|
3670
|
+
// A FRESH adapter per check, so one failure cannot contaminate the next.
|
|
3671
|
+
createAdapter: () => createRedisCache(url),
|
|
3672
|
+
cleanup: (adapter) => adapter.clear(),
|
|
3673
|
+
})
|
|
3674
|
+
|
|
3675
|
+
assertAdapterCertification(report) // throws, naming every failed check
|
|
3676
|
+
})
|
|
3677
|
+
```
|
|
3678
|
+
|
|
3679
|
+
Profiles are structural and dependency-free. A third-party adapter can certify itself in its own
|
|
3680
|
+
test suite without adopting anything else, which is the point: trust becomes portable rather
|
|
3681
|
+
than something we vouch for.
|
|
3682
|
+
|
|
3683
|
+
### The report is the evidence
|
|
3684
|
+
|
|
3685
|
+
```ts
|
|
3686
|
+
{
|
|
3687
|
+
"schemaVersion": 1,
|
|
3688
|
+
"ok": true,
|
|
3689
|
+
"profile": { "id": "cache-store", "version": 1 },
|
|
3690
|
+
"adapterId": "redis-cache",
|
|
3691
|
+
"capabilities": [
|
|
3692
|
+
{ "capability": "read-write", "status": "passed", "checks": ["set-get", "delete"] },
|
|
3693
|
+
{ "capability": "tag-invalidation", "status": "passed", "checks": ["invalidate-tag"] }
|
|
3694
|
+
],
|
|
3695
|
+
"evidenceHash": "9f2c..."
|
|
3696
|
+
}
|
|
3697
|
+
```
|
|
3698
|
+
|
|
3699
|
+
The capability matrix is the useful artifact. It says what an adapter supports, per capability,
|
|
3700
|
+
with the checks that back each one. `verifyAdapterCertification` recomputes
|
|
3701
|
+
`evidenceHash`, so a stored report can be checked rather than believed.
|
|
3702
|
+
|
|
3703
|
+
### Failures carry a class, not a message
|
|
3704
|
+
|
|
3705
|
+
A failed check records the error _class_ only. Provider messages routinely carry
|
|
3706
|
+
connection strings, tokens, and payload fragments, and evidence is meant to be shared, so it
|
|
3707
|
+
never becomes a place credentials leak. Cleanup errors cannot turn a failed functional check
|
|
3708
|
+
into a pass.
|
|
3709
|
+
|
|
3360
3710
|
## Nifra — Framework contract
|
|
3361
3711
|
|
|
3362
3712
|
> 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 +3986,13 @@ or `env`. Keep server-only imports out of a route file's top level.
|
|
|
3636
3986
|
|
|
3637
3987
|
### The build-output contract
|
|
3638
3988
|
|
|
3639
|
-
|
|
3640
|
-
`manifest.json` you hand to `createWebApp` (`entry`,
|
|
3989
|
+
The programmatic `buildClient()` API emits a content-hashed directory plus a
|
|
3990
|
+
`manifest.json` you can hand to `createWebApp` (`entry`,
|
|
3641
3991
|
`routes` for per-route preload, `css` + `routeStyles` for
|
|
3642
3992
|
`<link>` injection).
|
|
3643
3993
|
|
|
3644
3994
|
```ts
|
|
3645
|
-
//
|
|
3995
|
+
// buildClient() writes a browser bundle + manifest.json for custom build pipelines:
|
|
3646
3996
|
{
|
|
3647
3997
|
"entry": "/assets/_nifra-entry-HASH.js", // the bootstrap module script
|
|
3648
3998
|
"assets": ["/assets/…"], // every emitted chunk URL
|
|
@@ -3727,6 +4077,73 @@ const paths = prerendered.map((p) => p.path)
|
|
|
3727
4077
|
await Bun.write("./dist/_routes.json", JSON.stringify(cloudflarePagesRoutes({ prerendered: paths })))
|
|
3728
4078
|
```
|
|
3729
4079
|
|
|
4080
|
+
### Publish a reusable router (library code vs app code)
|
|
4081
|
+
|
|
4082
|
+
The fluent `server().get(...).post(...)` builder is **app code**: each
|
|
4083
|
+
call both infers the handler's context from the path _and_ returns a
|
|
4084
|
+
`Server` whose type has one more route intersected in (`AddRoute` /
|
|
4085
|
+
`RouteInfoFor`). That accreting registry type lives happily inside your own program,
|
|
4086
|
+
but it does **not** cleanly survive TypeScript's `.d.ts` declaration
|
|
4087
|
+
emit - for a router you **publish** as a package, the compiler has to serialize
|
|
4088
|
+
the whole inferred chain into your `.d.ts` and chokes (a `TS2883`-class
|
|
4089
|
+
error: the inferred type is too large to name), which forces ugly casts on the boundary. So a
|
|
4090
|
+
distributable router is written **contract-first** instead.
|
|
4091
|
+
|
|
4092
|
+
The recipe: declare the API as a plain `defineContract({…})` object and
|
|
4093
|
+
**export it** (it is the consumer's typed-client source), then ship a factory that
|
|
4094
|
+
returns `implement(contract, handlers)`. The consumer mounts the router with one
|
|
4095
|
+
`.merge(...)` and derives its client straight from the exported contract value -
|
|
4096
|
+
`client(contract, url)`, no `typeof app` import crossing the package
|
|
4097
|
+
boundary. Pass a preamble as the third arg -
|
|
4098
|
+
`implement(contract, handlers, server().use(auth))` - to bake a middleware /
|
|
4099
|
+
`derive` chain into the router's routes (captured at registration).
|
|
4100
|
+
|
|
4101
|
+
```ts
|
|
4102
|
+
// A PUBLISHABLE router - contract-first, so its types survive `.d.ts` emit for consumers.
|
|
4103
|
+
import { client } from "@nifrajs/client"
|
|
4104
|
+
import { defineContract, implement } from "@nifrajs/core/contract"
|
|
4105
|
+
import { server } from "@nifrajs/core/server"
|
|
4106
|
+
import { t } from "@nifrajs/schema"
|
|
4107
|
+
|
|
4108
|
+
// --- in the package (say @your-org/billing) ---
|
|
4109
|
+
// 1. The CONTRACT is ONE plain declared object - methods, paths, schemas, no handlers.
|
|
4110
|
+
// Export it: it is the single type the package publishes, and a consumer's typed client is
|
|
4111
|
+
// built from THIS value (not from `typeof app`), so no internal server type crosses the seam.
|
|
4112
|
+
export const billingContract = defineContract({
|
|
4113
|
+
getPlan: { method: "GET", path: "/billing/plan" },
|
|
4114
|
+
subscribe: { method: "POST", path: "/billing/subscribe", body: t.object({ plan: t.string() }) },
|
|
4115
|
+
})
|
|
4116
|
+
|
|
4117
|
+
// 2. A FACTORY closes over the host's dependencies and returns a real, mountable router.
|
|
4118
|
+
// `implement` binds handlers to the contract; each handler's context is typed from the contract
|
|
4119
|
+
// alone (`c.body` is { plan: string } below) - no per-route `AddRoute` inference accreting.
|
|
4120
|
+
export interface BillingDeps {
|
|
4121
|
+
charge(plan: string): Promise<{ id: string }>
|
|
4122
|
+
}
|
|
4123
|
+
export function createBillingRouter(deps: BillingDeps) {
|
|
4124
|
+
return implement(billingContract, {
|
|
4125
|
+
getPlan: () => ({ plan: "pro" }),
|
|
4126
|
+
subscribe: (c) => deps.charge(c.body.plan),
|
|
4127
|
+
})
|
|
4128
|
+
}
|
|
4129
|
+
|
|
4130
|
+
// --- in the consuming app ---
|
|
4131
|
+
// 3. Mount the router with ONE `.merge()` - no per-route `.get()/.post()` chain to re-infer.
|
|
4132
|
+
const app = server().merge(createBillingRouter({ charge: async (plan) => ({ id: plan }) }))
|
|
4133
|
+
|
|
4134
|
+
// 4. Build the client from the EXPORTED contract value - decoupled, no `typeof app` import needed.
|
|
4135
|
+
const api = client(billingContract, "https://api.example.com")
|
|
4136
|
+
const { data } = await api.billing.plan.get() // GET /billing/plan
|
|
4137
|
+
await api.billing.subscribe.post({ plan: "pro" }) // POST /billing/subscribe, body typed
|
|
4138
|
+
```
|
|
4139
|
+
|
|
4140
|
+
Why this survives `.d.ts` emit where the fluent chain doesn't: the contract is a
|
|
4141
|
+
**plain declared object type** (`ContractShape`), fixed up front - there
|
|
4142
|
+
is no grow-the-registry-per-call inference to serialize, so its declaration is small and
|
|
4143
|
+
stable. The consumer still gets full end-to-end types both ways -
|
|
4144
|
+
`.merge()` threads the router's routes into the host app's registry, and
|
|
4145
|
+
`client(contract, url)` types every call from the same exported object.
|
|
4146
|
+
|
|
3730
4147
|
See [SSG & ISR](/docs/rendering) for prerendering, [Dev & HMR](/docs/dev) for the
|
|
3731
4148
|
two dev loops and the `--port` flag, and [Loaders & actions](/docs/data) for the
|
|
3732
4149
|
data layer in depth.
|
|
@@ -3767,7 +4184,7 @@ server, and every handler reads it as a typed `c.db`. This is how the scaffold w
|
|
|
3767
4184
|
it — and it keeps the client out of each handler's imports.
|
|
3768
4185
|
|
|
3769
4186
|
```ts
|
|
3770
|
-
import { server } from "@nifrajs/core"
|
|
4187
|
+
import { server } from "@nifrajs/core/server"
|
|
3771
4188
|
import { desc } from "drizzle-orm"
|
|
3772
4189
|
import { db, notes } from "./db" // your Drizzle client + schema (what the scaffold generates)
|
|
3773
4190
|
|
|
@@ -3786,7 +4203,7 @@ Node, or `libSQL`/Turso anywhere. Open the connection once and query from your
|
|
|
3786
4203
|
routes:
|
|
3787
4204
|
|
|
3788
4205
|
```ts
|
|
3789
|
-
import { server } from "@nifrajs/core"
|
|
4206
|
+
import { server } from "@nifrajs/core/server"
|
|
3790
4207
|
import { Database } from "bun:sqlite" // Node: better-sqlite3 · edge: Turso/libSQL
|
|
3791
4208
|
|
|
3792
4209
|
// Open the connection ONCE at module scope (the driver pools internally).
|
|
@@ -3920,7 +4337,7 @@ directly:
|
|
|
3920
4337
|
|
|
3921
4338
|
```ts
|
|
3922
4339
|
// doc-check: skip — illustrates a third-party driver (postgres + drizzle); install those to run it.
|
|
3923
|
-
import { server } from "@nifrajs/core"
|
|
4340
|
+
import { server } from "@nifrajs/core/server"
|
|
3924
4341
|
import { drizzle } from "drizzle-orm/postgres-js" // or node-postgres / neon-http
|
|
3925
4342
|
import postgres from "postgres"
|
|
3926
4343
|
import { eq } from "drizzle-orm"
|
|
@@ -3994,6 +4411,98 @@ Complete runnable references — a typed CRUD API you can diff your own against:
|
|
|
3994
4411
|
`examples/db-sqlite` (the same API on raw `bun:sqlite`, parameterized
|
|
3995
4412
|
queries, boundary validation).
|
|
3996
4413
|
|
|
4414
|
+
## Nifra - Failure laboratory
|
|
4415
|
+
|
|
4416
|
+
> Replayable durable-failure schedules on virtual time: crash after commit, duplicate delivery, reordering, budget expiry, and checkpoint contention.
|
|
4417
|
+
|
|
4418
|
+
Durable systems fail on a schedule you never chose: the crash lands between the commit and the
|
|
4419
|
+
publish, the queue delivers twice, the events arrive backwards. The laboratory lets you choose
|
|
4420
|
+
that schedule, then replay it exactly.
|
|
4421
|
+
|
|
4422
|
+
### Seven failures worth testing
|
|
4423
|
+
|
|
4424
|
+
-
|
|
4425
|
+
`crash` - stop dead at a named durability seam.
|
|
4426
|
+
|
|
4427
|
+
-
|
|
4428
|
+
`duplicate-delivery` - deliver the same batch N times.
|
|
4429
|
+
|
|
4430
|
+
-
|
|
4431
|
+
`reorder-events` - rotate a batch out of order.
|
|
4432
|
+
|
|
4433
|
+
-
|
|
4434
|
+
`delay` - advance virtual time, with no real sleeping.
|
|
4435
|
+
|
|
4436
|
+
-
|
|
4437
|
+
`expire-budget` - report zero remaining at a budget seam.
|
|
4438
|
+
|
|
4439
|
+
-
|
|
4440
|
+
`lose-provider-reply` - run the call, then lose only its reply.
|
|
4441
|
+
|
|
4442
|
+
-
|
|
4443
|
+
`contend-checkpoint` - make a checkpoint conflict.
|
|
4444
|
+
|
|
4445
|
+
### Place the seams, schedule the failure
|
|
4446
|
+
|
|
4447
|
+
```ts
|
|
4448
|
+
import { createFailureLab } from "@nifrajs/testing"
|
|
4449
|
+
|
|
4450
|
+
const lab = createFailureLab({
|
|
4451
|
+
seed: 77,
|
|
4452
|
+
schedule: [
|
|
4453
|
+
{ kind: "duplicate-delivery", point: "relay.batch", copies: 3 },
|
|
4454
|
+
{ kind: "reorder-events", point: "relay.batch" },
|
|
4455
|
+
],
|
|
4456
|
+
})
|
|
4457
|
+
|
|
4458
|
+
// The adapter under test just names its seams. Same seed, same schedule,
|
|
4459
|
+
// same result - every run.
|
|
4460
|
+
const delivered = lab.deliveries("relay.batch", ["evt-a", "evt-b", "evt-c"])
|
|
4461
|
+
```
|
|
4462
|
+
|
|
4463
|
+
The controller is an off-hot-path port. Your adapter names its seams; the schedule decides what
|
|
4464
|
+
happens at them. Time is virtual, so a 24-hour delay costs nothing and nothing is flaky.
|
|
4465
|
+
|
|
4466
|
+
### Assert the invariant, not the output
|
|
4467
|
+
|
|
4468
|
+
```ts
|
|
4469
|
+
// doc-check: skip - commit/publish/outboxStillPending stand in for the system under test
|
|
4470
|
+
import { runFailureScenario } from "@nifrajs/testing"
|
|
4471
|
+
|
|
4472
|
+
const report = await runFailureScenario(
|
|
4473
|
+
{
|
|
4474
|
+
name: "crash-after-commit",
|
|
4475
|
+
execute: async (lab) => {
|
|
4476
|
+
await commit()
|
|
4477
|
+
lab.checkpoint("outbox.after-commit") // crashes here, per the schedule
|
|
4478
|
+
await publish()
|
|
4479
|
+
return "published"
|
|
4480
|
+
},
|
|
4481
|
+
// The scenario passes only if the invariant survives the failure.
|
|
4482
|
+
verify: async ({ error }) => error !== undefined && (await outboxStillPending()),
|
|
4483
|
+
},
|
|
4484
|
+
{ seed: 1, schedule: [{ kind: "crash", point: "outbox.after-commit" }] },
|
|
4485
|
+
)
|
|
4486
|
+
|
|
4487
|
+
report.ok // did the invariant hold?
|
|
4488
|
+
report.replay // { seed, schedule } - reproduces this exact run
|
|
4489
|
+
```
|
|
4490
|
+
|
|
4491
|
+
A scenario passes only when `verify` returns true after the failure. Losing the
|
|
4492
|
+
provider reply is not a bug; losing the money is. That distinction is the whole point.
|
|
4493
|
+
|
|
4494
|
+
### Evidence is tokens only
|
|
4495
|
+
|
|
4496
|
+
Injection evidence records the kind, the point, the occurrence, and the virtual time. Never
|
|
4497
|
+
payloads, provider results, exception messages, or stacks. A failing report carries the error
|
|
4498
|
+
_class_ and the exact `replay` inputs, so it reproduces from the report alone
|
|
4499
|
+
without leaking what flowed through the system.
|
|
4500
|
+
|
|
4501
|
+
### Where it fits
|
|
4502
|
+
|
|
4503
|
+
The same engine backs `nifra levels` L4. Contract invariants fuzz the HTTP surface;
|
|
4504
|
+
the laboratory covers the durable paths underneath, where the interesting failures live.
|
|
4505
|
+
|
|
3997
4506
|
## Nifra — Fonts
|
|
3998
4507
|
|
|
3999
4508
|
> 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,6 +4605,140 @@ stylesheet can't redirect the download elsewhere — an SSRF gate), and caps eac
|
|
|
4096
4605
|
Filenames are derived only from validated tokens plus a content hash, so there's no path
|
|
4097
4606
|
traversal.
|
|
4098
4607
|
|
|
4608
|
+
## Nifra 2.0 migration guide — upgrade from 1.x
|
|
4609
|
+
|
|
4610
|
+
> 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.
|
|
4611
|
+
|
|
4612
|
+
Nifra 2.0 removes the compatibility layer retained through 1.x and makes optional runtime
|
|
4613
|
+
systems explicit, instance-scoped plugins. The upgrade command handles deterministic package
|
|
4614
|
+
and import edits; this guide covers the structural changes it deliberately cannot guess.
|
|
4615
|
+
|
|
4616
|
+
### 1. Run the executable upgrade
|
|
4617
|
+
|
|
4618
|
+
```ts
|
|
4619
|
+
|
|
4620
|
+
```
|
|
4621
|
+
|
|
4622
|
+
The command updates existing `@nifrajs/*`, `nifra`, and
|
|
4623
|
+
`create-nifra` dependency ranges to `2.0.0` while preserving
|
|
4624
|
+
caret/tilde/exact style. It also replaces the removed `@nifrajs/budget` dependency
|
|
4625
|
+
with `@nifrajs/core` and moves its imports to
|
|
4626
|
+
`@nifrajs/core/budget`. Dry-run is the default; `--write` applies and
|
|
4627
|
+
verifies with `nifra check`.
|
|
4628
|
+
|
|
4629
|
+
### 2. Install optional server systems explicitly
|
|
4630
|
+
|
|
4631
|
+
Idempotency, the effect ledger, MCP declarations, SSE, WebSockets, and Node-direct resolution
|
|
4632
|
+
are no longer enabled by server options, side-effect imports, or process-global state. Install
|
|
4633
|
+
only the systems an app uses with `.use()`; registration fails loudly if a required
|
|
4634
|
+
plugin is missing.
|
|
4635
|
+
|
|
4636
|
+
```ts
|
|
4637
|
+
|
|
4638
|
+
```
|
|
4639
|
+
|
|
4640
|
+
The `@nifrajs/node` adapter installs `nodeDirect()` automatically.
|
|
4641
|
+
Only applications calling `app.resolveNode()` directly need to install it.
|
|
4642
|
+
|
|
4643
|
+
### 3. Use the lean package entry points
|
|
4644
|
+
|
|
4645
|
+
The `@nifrajs/core` and `nifra` roots now expose the lean HTTP server
|
|
4646
|
+
surface. Prefer `@nifrajs/core/server` for `server()`, and import
|
|
4647
|
+
contracts, assurance, capabilities, budgets, manifests, reflection, SSE, WebSockets, and
|
|
4648
|
+
other optional systems from their documented subpaths.
|
|
4649
|
+
|
|
4650
|
+
```ts
|
|
4651
|
+
|
|
4652
|
+
```
|
|
4653
|
+
|
|
4654
|
+
Contract-generated adversarial testing lives in `@nifrajs/testing`; the deprecated
|
|
4655
|
+
core invariant runner is removed.
|
|
4656
|
+
|
|
4657
|
+
### 4. Update full-stack backend mounts
|
|
4658
|
+
|
|
4659
|
+
`createWebApp` now auto-mounts a backend only through the platform-aware,
|
|
4660
|
+
symbol-keyed `BackendMount` interface. The old duck-typed
|
|
4661
|
+
`api.fetch(url, init)` convention is removed. Normal applications should pass the
|
|
4662
|
+
result of `inProcessClient(backend)` or `testClient(backend)`; both
|
|
4663
|
+
already implement the interface and forward `env` and `waitUntil`.
|
|
4664
|
+
|
|
4665
|
+
```ts
|
|
4666
|
+
|
|
4667
|
+
```
|
|
4668
|
+
|
|
4669
|
+
### 5. Declare external library mounts
|
|
4670
|
+
|
|
4671
|
+
Better Auth and similar libraries can own routes such as `/auth/**` outside the
|
|
4672
|
+
Nifra typed contract. A relative fetch to that library is intentional, but otherwise resembles
|
|
4673
|
+
a hand-rolled own-API call. Declare only the exact mounted prefix in
|
|
4674
|
+
`nifra.check.json` so `nifra check` does not keep CI red:
|
|
4675
|
+
|
|
4676
|
+
```ts
|
|
4677
|
+
|
|
4678
|
+
```
|
|
4679
|
+
|
|
4680
|
+
Prefixes are segment-anchored: `/auth` covers `/auth` and its children,
|
|
4681
|
+
never `/authors`. Traversal paths are not suppressed. The normalized allowlist is
|
|
4682
|
+
echoed in human, JSON, and MCP check results, so every bypass remains auditable. This option
|
|
4683
|
+
affects only the typed-client lint; it does not mount, authorize, or trust the library.
|
|
4684
|
+
|
|
4685
|
+
### 6. Narrow typed-client failures by status
|
|
4686
|
+
|
|
4687
|
+
Routes with declared error schemas now return a failure union discriminated by HTTP status.
|
|
4688
|
+
After checking `!result.ok`, also narrow `result.status` before reading
|
|
4689
|
+
typed failure data. Undeclared statuses and transport status `0` remain unknown.
|
|
4690
|
+
|
|
4691
|
+
```ts
|
|
4692
|
+
|
|
4693
|
+
```
|
|
4694
|
+
|
|
4695
|
+
### 7. Apply the remaining web and protocol changes
|
|
4696
|
+
|
|
4697
|
+
-
|
|
4698
|
+
Redirects accept an options object as their second argument.
|
|
4699
|
+
|
|
4700
|
+
```ts
|
|
4701
|
+
|
|
4702
|
+
```
|
|
4703
|
+
|
|
4704
|
+
-
|
|
4705
|
+
Replace the removed prerender wrapper with `enumerateStaticRoutes()`.
|
|
4706
|
+
|
|
4707
|
+
- Fragment navigation resolves element IDs only.
|
|
4708
|
+
|
|
4709
|
+
-
|
|
4710
|
+
MCP Apps metadata uses only `_meta.ui.resourceUri`; remove the deprecated flat
|
|
4711
|
+
`ui/resourceUri` key.
|
|
4712
|
+
|
|
4713
|
+
-
|
|
4714
|
+
Telemetry integrations use `ObservationAdapter`; the
|
|
4715
|
+
`AgentSpan`, `AgentSpanExporter`, and `SpanExporter` aliases
|
|
4716
|
+
are removed.
|
|
4717
|
+
|
|
4718
|
+
- Invalid HTTP method overrides always fail closed with 400.
|
|
4719
|
+
|
|
4720
|
+
-
|
|
4721
|
+
`nifra build` always emits a complete deploy directory and defaults to Bun;
|
|
4722
|
+
`nifra start` runs the generated `server.js`.
|
|
4723
|
+
|
|
4724
|
+
### 8. Run the release gates
|
|
4725
|
+
|
|
4726
|
+
-
|
|
4727
|
+
Run `nifra check --json` and fix every error.
|
|
4728
|
+
|
|
4729
|
+
-
|
|
4730
|
+
If the project has `nifra.assurance.ts`, run `nifra assure --json`.
|
|
4731
|
+
|
|
4732
|
+
-
|
|
4733
|
+
If capabilities are configured, review a new snapshot and run
|
|
4734
|
+
`nifra capabilities check --json`.
|
|
4735
|
+
|
|
4736
|
+
- Run the application test suite and a production build.
|
|
4737
|
+
|
|
4738
|
+
-
|
|
4739
|
+
Exercise authentication, redirects, backend mounting, SSE/WebSocket routes, and each deploy
|
|
4740
|
+
adapter the application uses.
|
|
4741
|
+
|
|
4099
4742
|
## Nifra — Migrating from Express, Hono, Fastify & Elysia
|
|
4100
4743
|
|
|
4101
4744
|
> 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.
|
|
@@ -4438,7 +5081,7 @@ boundary _before_ your handler runs — invalid input is rejected with a 422, so
|
|
|
4438
5081
|
handler only ever sees well-formed data.
|
|
4439
5082
|
|
|
4440
5083
|
```ts
|
|
4441
|
-
import { server } from "@nifrajs/core"
|
|
5084
|
+
import { server } from "@nifrajs/core/server"
|
|
4442
5085
|
import { GetUser } from "./schema"
|
|
4443
5086
|
|
|
4444
5087
|
export const app = server().get("/users/:id", GetUser, (c) => {
|
|
@@ -4481,7 +5124,7 @@ routes and their schemas — generated lazily on first request, never hand-writt
|
|
|
4481
5124
|
`ui: true` to also serve a Scalar reference page.
|
|
4482
5125
|
|
|
4483
5126
|
```ts
|
|
4484
|
-
import { server } from "@nifrajs/core"
|
|
5127
|
+
import { server } from "@nifrajs/core/server"
|
|
4485
5128
|
import { openapi } from "@nifrajs/middleware"
|
|
4486
5129
|
import { GetUser } from "./schema" // the contract defined above
|
|
4487
5130
|
|
|
@@ -4523,10 +5166,132 @@ coupling — server code never ships to the client.
|
|
|
4523
5166
|
The generated OpenAPI document is a structural subset of 3.1 derived from your schemas;
|
|
4524
5167
|
it reflects exactly what the routes declare, not hand-authored prose.
|
|
4525
5168
|
|
|
5169
|
+
## Nifra - Verification ladder
|
|
5170
|
+
|
|
5171
|
+
> Five cumulative levels, from a typed contract to contract-derived invariant tests, that say what a Nifra project actually proves.
|
|
5172
|
+
|
|
5173
|
+
"It compiles" and "it is secure" are different claims. The ladder makes the difference
|
|
5174
|
+
legible: five levels, each earned, so a project can state what it proves instead of what it
|
|
5175
|
+
hopes.
|
|
5176
|
+
|
|
5177
|
+
### The five levels
|
|
5178
|
+
|
|
5179
|
+
Levels are **cumulative**. A level only counts when every level below it holds,
|
|
5180
|
+
so `achieved` is the highest rung with unbroken support underneath, and it is
|
|
5181
|
+
`-1` when even L0 fails.
|
|
5182
|
+
|
|
5183
|
+
-
|
|
5184
|
+
**L0 typed contract** - `nifra check` passes: the frontend and
|
|
5185
|
+
backend cannot have silently diverged.
|
|
5186
|
+
|
|
5187
|
+
-
|
|
5188
|
+
**L1 route assurance** - every route is classified, and the enforcement a rule
|
|
5189
|
+
requires is actually installed.
|
|
5190
|
+
|
|
5191
|
+
-
|
|
5192
|
+
**L2 capability lockfile** - declared effects match a reviewed lockfile, so a
|
|
5193
|
+
route that starts writing to the database shows up in review.
|
|
5194
|
+
|
|
5195
|
+
-
|
|
5196
|
+
**L3 route manifest** - the committed trust artifact still matches the routes,
|
|
5197
|
+
evidence, effects, and response classification it was emitted from.
|
|
5198
|
+
|
|
5199
|
+
-
|
|
5200
|
+
**L4 invariant-tested** - contract-derived invariants ran against an isolated
|
|
5201
|
+
executor with nothing skipped.
|
|
5202
|
+
|
|
5203
|
+
### Run it
|
|
5204
|
+
|
|
5205
|
+
```ts
|
|
5206
|
+
# What does this project prove right now?
|
|
5207
|
+
nifra levels
|
|
5208
|
+
|
|
5209
|
+
# Fail CI below a level the project already reached.
|
|
5210
|
+
nifra levels --min 2
|
|
5211
|
+
|
|
5212
|
+
# The same report an agent reads over MCP (nifra_levels).
|
|
5213
|
+
nifra levels --json
|
|
5214
|
+
```
|
|
5215
|
+
|
|
5216
|
+
```ts
|
|
5217
|
+
L0 typed contract ok
|
|
5218
|
+
L1 route assurance ok
|
|
5219
|
+
L2 capability lockfile ok
|
|
5220
|
+
L3 route manifest FAIL manifest drifted from routes: run `nifra manifest emit`
|
|
5221
|
+
L4 invariant-tested FAIL no isolated invariant executor configured
|
|
5222
|
+
|
|
5223
|
+
achieved: 2
|
|
5224
|
+
```
|
|
5225
|
+
|
|
5226
|
+
Every level that does not hold explains itself, so the output is a work list rather than a
|
|
5227
|
+
verdict. `--min <n>` is the CI floor: pin it to the level you have reached and
|
|
5228
|
+
the build fails when a change quietly costs you a rung.
|
|
5229
|
+
|
|
5230
|
+
### Wiring the levels
|
|
5231
|
+
|
|
5232
|
+
L0 needs nothing. L1 through L4 hang off `nifra.assurance.ts`, and each one you
|
|
5233
|
+
configure is a level you can start claiming.
|
|
5234
|
+
|
|
5235
|
+
```ts
|
|
5236
|
+
// nifra.assurance.ts
|
|
5237
|
+
// doc-check: skip - policy rules and the isolated executor are app-specific
|
|
5238
|
+
import { defineAssuranceConfig } from "@nifrajs/core/assurance"
|
|
5239
|
+
import { backend } from "./backend"
|
|
5240
|
+
|
|
5241
|
+
export default defineAssuranceConfig({
|
|
5242
|
+
source: backend,
|
|
5243
|
+
policy: { rules: [/* ... */] },
|
|
5244
|
+
capabilities: { lockfile: "capabilities.lock.json", policy: { /* ... */ } },
|
|
5245
|
+
manifest: { path: "nifra.manifest.json" },
|
|
5246
|
+
invariants: {
|
|
5247
|
+
// L4 fuzzes the contract, so it needs an app you are willing to throw hostile
|
|
5248
|
+
// input at. Point this at an isolated instance, never a live one.
|
|
5249
|
+
executor: (request) => backend.fetch(request),
|
|
5250
|
+
},
|
|
5251
|
+
})
|
|
5252
|
+
```
|
|
5253
|
+
|
|
5254
|
+
L4 requires an **explicit** executor. Invariant runs send deliberately hostile
|
|
5255
|
+
input, so nifra will not guess which app to point them at; a level that fuzzed your live
|
|
5256
|
+
server by default would be a footgun, and a skipped route is reported rather than passed over
|
|
5257
|
+
in silence.
|
|
5258
|
+
|
|
5259
|
+
### For coding agents
|
|
5260
|
+
|
|
5261
|
+
The `nifra_levels` MCP tool returns the same
|
|
5262
|
+
`{ achieved, levels[] }` report. An agent can read what its change proved, and a
|
|
5263
|
+
level that regressed comes back with the reason it broke.
|
|
5264
|
+
|
|
4526
5265
|
---
|
|
4527
5266
|
|
|
4528
5267
|
# Package reference (READMEs)
|
|
4529
5268
|
|
|
5269
|
+
## @nifrajs/agent-telemetry
|
|
5270
|
+
|
|
5271
|
+
Child-span telemetry for AI tool calls made through Nifra tool and MCP endpoints.
|
|
5272
|
+
|
|
5273
|
+
```sh
|
|
5274
|
+
bun add @nifrajs/agent-telemetry @nifrajs/otel
|
|
5275
|
+
```
|
|
5276
|
+
|
|
5277
|
+
```ts
|
|
5278
|
+
import { agentTelemetry, consoleAgentExporter } from "@nifrajs/agent-telemetry"
|
|
5279
|
+
|
|
5280
|
+
app.use(agentTelemetry({ exporter: consoleAgentExporter() }))
|
|
5281
|
+
```
|
|
5282
|
+
|
|
5283
|
+
Requests under `/_nifra/tool/*` and requests to `/mcp` receive tool-name, input-size, output-size,
|
|
5284
|
+
duration, and status observations. When `@nifrajs/otel` owns the request observation, tool spans are
|
|
5285
|
+
attached as children; otherwise the plugin creates a standalone observation. Other routes pass through.
|
|
5286
|
+
|
|
5287
|
+
## For AI agents
|
|
5288
|
+
|
|
5289
|
+
Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
|
|
5290
|
+
one cheap read instead of the whole corpus. For the wider framework: the repo's
|
|
5291
|
+
[`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
|
|
5292
|
+
[`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
|
|
5293
|
+
done-gate, or `nifra mcp` to give the agent live project tools.
|
|
5294
|
+
|
|
4530
5295
|
## @nifrajs/auth
|
|
4531
5296
|
|
|
4532
5297
|
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 +5351,7 @@ export const auth = createBetterAuth({
|
|
|
4586
5351
|
|
|
4587
5352
|
```ts
|
|
4588
5353
|
// server.ts
|
|
4589
|
-
import { server } from "@nifrajs/core"
|
|
5354
|
+
import { server } from "@nifrajs/core/server"
|
|
4590
5355
|
import { betterAuth } from "@nifrajs/better-auth"
|
|
4591
5356
|
import { auth } from "./auth"
|
|
4592
5357
|
|
|
@@ -4655,38 +5420,6 @@ one cheap read instead of the whole corpus. For the wider framework: the repo's
|
|
|
4655
5420
|
[`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
|
|
4656
5421
|
done-gate, or `nifra mcp` to give the agent live project tools.
|
|
4657
5422
|
|
|
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
5423
|
## @nifrajs/cache
|
|
4691
5424
|
|
|
4692
5425
|
Typed KV cache for nifra — **TTL**, **stale-while-revalidate**, **tag invalidation**, and **single-flight
|
|
@@ -4742,6 +5475,14 @@ the Cache API) via a `CacheStore` for anything that should survive across reques
|
|
|
4742
5475
|
- `cache.wrap(key, loader, { ttlMs?, swrMs?, tags? })` · `get` · `has` · `set` · `delete` · `invalidateTag` · `clear`.
|
|
4743
5476
|
- `MemoryCache({ maxEntries?, now? })` — the default store; implement `CacheStore` for your own.
|
|
4744
5477
|
|
|
5478
|
+
## For AI agents
|
|
5479
|
+
|
|
5480
|
+
Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
|
|
5481
|
+
one cheap read instead of the whole corpus. For the wider framework: the repo's
|
|
5482
|
+
[`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
|
|
5483
|
+
[`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
|
|
5484
|
+
done-gate, or `nifra mcp` to give the agent live project tools.
|
|
5485
|
+
|
|
4745
5486
|
## @nifrajs/cli
|
|
4746
5487
|
|
|
4747
5488
|
The `nifra` CLI — zero-config dev/build/start for a nifra app, plus the agent surfaces (`nifra context`,
|
|
@@ -4750,8 +5491,8 @@ The `nifra` CLI — zero-config dev/build/start for a nifra app, plus the agent
|
|
|
4750
5491
|
|
|
4751
5492
|
```
|
|
4752
5493
|
nifra dev [--port <n>] [--poll] True-HMR dev server (Vite + nifra SSR).
|
|
4753
|
-
nifra build [--out <dir>]
|
|
4754
|
-
nifra start [--port <n>] [--out <dir>]
|
|
5494
|
+
nifra build [--out <dir>] [--target <t>] Complete deploy directory; target defaults to Bun.
|
|
5495
|
+
nifra start [--port <n>] [--out <dir>] Run the generated Bun server at <out>/server.js.
|
|
4755
5496
|
nifra context Print this project's API + page routes + conventions.
|
|
4756
5497
|
nifra mcp MCP server (stdio) exposing this project to a coding agent.
|
|
4757
5498
|
nifra check [--json] Gate: typecheck + lints; --json includes fixes/suggestions.
|
|
@@ -4816,6 +5557,17 @@ For hand-rolled own-API `fetch()` calls, `nifra check` stays conservative: simpl
|
|
|
4816
5557
|
that match a statically visible Nifra route get an exact typed-client rewrite diff; dynamic URLs, custom
|
|
4817
5558
|
headers, bodies, query strings, or ambiguous routes fall back to manual steps.
|
|
4818
5559
|
|
|
5560
|
+
When a library such as Better Auth intentionally owns a mounted prefix outside the Nifra contract,
|
|
5561
|
+
declare that prefix in `nifra.check.json` so its relative calls do not keep CI red:
|
|
5562
|
+
|
|
5563
|
+
```json
|
|
5564
|
+
{ "externalMounts": ["/auth"] }
|
|
5565
|
+
```
|
|
5566
|
+
|
|
5567
|
+
The match is segment-anchored (`/auth` never suppresses `/authors`) and traversal paths are rejected.
|
|
5568
|
+
The normalized prefixes are echoed in human, JSON, and MCP results so the lint bypass stays auditable.
|
|
5569
|
+
This suppresses only the typed-client diagnostic; it does not mount or authorize the external handler.
|
|
5570
|
+
|
|
4819
5571
|
## For AI coding agents
|
|
4820
5572
|
|
|
4821
5573
|
**`nifra context`** prints this project's actual surface — API routes (from `backend.routes()`), page
|
|
@@ -4842,6 +5594,10 @@ an agent can act on the project, not just read about it. Core tools:
|
|
|
4842
5594
|
- **`nifra_check`** — run the drift gate and get the structured `{ ok, typecheck, diagnostics[] }`
|
|
4843
5595
|
result with suggestions, so the agent verifies its own work and fixes each diagnostic before finishing.
|
|
4844
5596
|
- **`nifra_assure`** — evaluate `nifra.assurance.ts` and return the complete per-route evidence report.
|
|
5597
|
+
- **`nifra_levels`** - compute the cumulative verification ladder (L0 typed contract, L1 route
|
|
5598
|
+
assurance, L2 capability lockfile, L3 trust manifest, L4 contract invariants) and return
|
|
5599
|
+
`{ achieved, levels[] }`, so an agent can see what its change proves and read why a level it lost
|
|
5600
|
+
no longer holds.
|
|
4845
5601
|
- **`nifra_doctor`** — catch imported packages missing from `package.json`; with `autoFix:true`, write
|
|
4846
5602
|
safe dependency entries when a version can be inferred locally and return exact `bun add` commands
|
|
4847
5603
|
for anything that needs a package-manager decision.
|
|
@@ -4959,7 +5715,7 @@ bun add @nifrajs/core
|
|
|
4959
5715
|
```
|
|
4960
5716
|
|
|
4961
5717
|
```ts
|
|
4962
|
-
import { server } from "@nifrajs/core"
|
|
5718
|
+
import { server } from "@nifrajs/core/server"
|
|
4963
5719
|
|
|
4964
5720
|
const app = server()
|
|
4965
5721
|
.get("/users/:id", (c) => ({ id: c.params.id }))
|
|
@@ -4969,6 +5725,15 @@ const app = server()
|
|
|
4969
5725
|
export type App = typeof app // hand this to @nifrajs/client for end-to-end types
|
|
4970
5726
|
```
|
|
4971
5727
|
|
|
5728
|
+
`@nifrajs/core` and `@nifrajs/core/server` expose the same lean common runtime. Optional systems are
|
|
5729
|
+
available only from their explicit subpaths, so an ordinary HTTP server never evaluates them:
|
|
5730
|
+
|
|
5731
|
+
```ts
|
|
5732
|
+
import { defineContract, implement } from "@nifrajs/core/contract"
|
|
5733
|
+
import { startCausality } from "@nifrajs/core/causality"
|
|
5734
|
+
import { defineAssurancePolicy } from "@nifrajs/core/assurance"
|
|
5735
|
+
```
|
|
5736
|
+
|
|
4972
5737
|
- **Inline or contract-first.** Write routes inline (types inferred from the
|
|
4973
5738
|
builder), or `defineContract(...)` + `implement(...)` for a decoupled, versionable
|
|
4974
5739
|
surface — handlers lift over unchanged.
|
|
@@ -5094,7 +5859,7 @@ deno add npm:@nifrajs/deno # or import npm:@nifrajs/deno directly
|
|
|
5094
5859
|
```
|
|
5095
5860
|
|
|
5096
5861
|
```ts
|
|
5097
|
-
import { server } from "@nifrajs/core"
|
|
5862
|
+
import { server } from "@nifrajs/core/server"
|
|
5098
5863
|
import { serve } from "@nifrajs/deno"
|
|
5099
5864
|
|
|
5100
5865
|
const app = server().get("/users/:id", (c) => ({ id: c.params.id }))
|
|
@@ -5129,6 +5894,32 @@ one cheap read instead of the whole corpus. For the wider framework: the repo's
|
|
|
5129
5894
|
[`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
|
|
5130
5895
|
done-gate, or `nifra mcp` to give the agent live project tools.
|
|
5131
5896
|
|
|
5897
|
+
## @nifrajs/devtools
|
|
5898
|
+
|
|
5899
|
+
Interactive request-trace DevTools for `nifra dev`, with a secured SSE stream and browser overlay.
|
|
5900
|
+
|
|
5901
|
+
```sh
|
|
5902
|
+
bun add -d @nifrajs/devtools
|
|
5903
|
+
```
|
|
5904
|
+
|
|
5905
|
+
```ts
|
|
5906
|
+
import { devtools } from "@nifrajs/devtools"
|
|
5907
|
+
|
|
5908
|
+
app.use(devtools())
|
|
5909
|
+
```
|
|
5910
|
+
|
|
5911
|
+
The plugin is enabled by default only when `NODE_ENV=development`. It records a bounded event buffer
|
|
5912
|
+
and serves the overlay stream at `/_nifra/devtools`; remote hosts and cross-origin access stay denied
|
|
5913
|
+
unless explicitly allowed. Leave it unregistered in production for zero request-path overhead.
|
|
5914
|
+
|
|
5915
|
+
## For AI agents
|
|
5916
|
+
|
|
5917
|
+
Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
|
|
5918
|
+
one cheap read instead of the whole corpus. For the wider framework: the repo's
|
|
5919
|
+
[`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
|
|
5920
|
+
[`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
|
|
5921
|
+
done-gate, or `nifra mcp` to give the agent live project tools.
|
|
5922
|
+
|
|
5132
5923
|
## @nifrajs/env
|
|
5133
5924
|
|
|
5134
5925
|
Typed, validated environment variables. Define a schema once at startup; get a **frozen, typed**
|
|
@@ -5215,6 +6006,14 @@ lineage metadata.
|
|
|
5215
6006
|
|
|
5216
6007
|
See the [nifra docs](https://github.com/nifrajs/nifra#readme).
|
|
5217
6008
|
|
|
6009
|
+
## For AI agents
|
|
6010
|
+
|
|
6011
|
+
Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
|
|
6012
|
+
one cheap read instead of the whole corpus. For the wider framework: the repo's
|
|
6013
|
+
[`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
|
|
6014
|
+
[`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
|
|
6015
|
+
done-gate, or `nifra mcp` to give the agent live project tools.
|
|
6016
|
+
|
|
5218
6017
|
## @nifrajs/i18n
|
|
5219
6018
|
|
|
5220
6019
|
Framework-agnostic i18n for nifra — locale negotiation + a tiny ICU message formatter on the platform Intl. Dependency-free.
|
|
@@ -5422,6 +6221,78 @@ export default {
|
|
|
5422
6221
|
- `queue.counts()` → `{ pending, active, dead }`. `queue.store` — the underlying store.
|
|
5423
6222
|
- Stores: `MemoryJobStore`. Backoff: `exponentialBackoff`, `fixedBackoff`, `noBackoff`.
|
|
5424
6223
|
|
|
6224
|
+
## For AI agents
|
|
6225
|
+
|
|
6226
|
+
Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
|
|
6227
|
+
one cheap read instead of the whole corpus. For the wider framework: the repo's
|
|
6228
|
+
[`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
|
|
6229
|
+
[`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
|
|
6230
|
+
done-gate, or `nifra mcp` to give the agent live project tools.
|
|
6231
|
+
|
|
6232
|
+
## @nifrajs/mcp
|
|
6233
|
+
|
|
6234
|
+
Build transport-agnostic MCP servers and interactive MCP Apps for Nifra applications.
|
|
6235
|
+
|
|
6236
|
+
```sh
|
|
6237
|
+
bun add @nifrajs/mcp
|
|
6238
|
+
```
|
|
6239
|
+
|
|
6240
|
+
```ts
|
|
6241
|
+
import { createMcpServer, defineMcpTool } from "@nifrajs/mcp"
|
|
6242
|
+
|
|
6243
|
+
const tools = [
|
|
6244
|
+
defineMcpTool({
|
|
6245
|
+
name: "health",
|
|
6246
|
+
description: "Report service health",
|
|
6247
|
+
handler: () => ({ text: "ok" }),
|
|
6248
|
+
}),
|
|
6249
|
+
]
|
|
6250
|
+
|
|
6251
|
+
const mcp = createMcpServer({ name: "orders", version: "1.0.0", tools })
|
|
6252
|
+
```
|
|
6253
|
+
|
|
6254
|
+
Mount `mcp.fetch` at `POST /mcp`. The package also exposes the JSON-RPC protocol and Streamable HTTP
|
|
6255
|
+
layers directly, plus `defineMcpWidget` and the React adapter for tool results that render UI in MCP
|
|
6256
|
+
hosts.
|
|
6257
|
+
|
|
6258
|
+
## For AI agents
|
|
6259
|
+
|
|
6260
|
+
Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
|
|
6261
|
+
one cheap read instead of the whole corpus. For the wider framework: the repo's
|
|
6262
|
+
[`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
|
|
6263
|
+
[`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
|
|
6264
|
+
done-gate, or `nifra mcp` to give the agent live project tools.
|
|
6265
|
+
|
|
6266
|
+
## @nifrajs/mcp-db
|
|
6267
|
+
|
|
6268
|
+
Expose an allowlisted SQLite schema as a fail-closed MCP server, with opt-in read-only queries.
|
|
6269
|
+
|
|
6270
|
+
```sh
|
|
6271
|
+
bun add @nifrajs/mcp-db
|
|
6272
|
+
```
|
|
6273
|
+
|
|
6274
|
+
```ts
|
|
6275
|
+
import { Database } from "bun:sqlite"
|
|
6276
|
+
import { serveDatabaseAsMcp } from "@nifrajs/mcp-db"
|
|
6277
|
+
|
|
6278
|
+
const mcp = serveDatabaseAsMcp(new Database("app.db"), {
|
|
6279
|
+
tables: ["habits", "entries"],
|
|
6280
|
+
})
|
|
6281
|
+
```
|
|
6282
|
+
|
|
6283
|
+
Only `list_tables` and `describe_table` are exposed by default, restricted to the explicit table
|
|
6284
|
+
allowlist. `run_query` requires an authorization hook and is guarded by SQLite query-only mode,
|
|
6285
|
+
single-statement/read-only checks, query-plan verification, and bounded output. D1 is not supported
|
|
6286
|
+
because it cannot provide the same engine-level read-only guarantee.
|
|
6287
|
+
|
|
6288
|
+
## For AI agents
|
|
6289
|
+
|
|
6290
|
+
Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
|
|
6291
|
+
one cheap read instead of the whole corpus. For the wider framework: the repo's
|
|
6292
|
+
[`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
|
|
6293
|
+
[`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
|
|
6294
|
+
done-gate, or `nifra mcp` to give the agent live project tools.
|
|
6295
|
+
|
|
5425
6296
|
## @nifrajs/middleware
|
|
5426
6297
|
|
|
5427
6298
|
Composable, dependency-light middleware for [nifra](../../README.md) — CORS, security headers, body
|
|
@@ -5433,7 +6304,7 @@ bun add @nifrajs/middleware
|
|
|
5433
6304
|
```
|
|
5434
6305
|
|
|
5435
6306
|
```ts
|
|
5436
|
-
import { server } from "@nifrajs/core"
|
|
6307
|
+
import { server } from "@nifrajs/core/server"
|
|
5437
6308
|
import {
|
|
5438
6309
|
bodyLimit,
|
|
5439
6310
|
cors,
|
|
@@ -5510,6 +6381,34 @@ one cheap read instead of the whole corpus. For the wider framework: the repo's
|
|
|
5510
6381
|
[`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
|
|
5511
6382
|
done-gate, or `nifra mcp` to give the agent live project tools.
|
|
5512
6383
|
|
|
6384
|
+
## @nifrajs/mock
|
|
6385
|
+
|
|
6386
|
+
Generate deterministic mock responses from a Nifra application's reflected response contracts.
|
|
6387
|
+
|
|
6388
|
+
```sh
|
|
6389
|
+
bun add -d @nifrajs/mock
|
|
6390
|
+
```
|
|
6391
|
+
|
|
6392
|
+
```ts
|
|
6393
|
+
import { createMockServer } from "@nifrajs/mock"
|
|
6394
|
+
import { app } from "./app"
|
|
6395
|
+
|
|
6396
|
+
const mock = createMockServer(app, { seed: 42 })
|
|
6397
|
+
const response = await mock.fetch(new Request("http://local.test/users/1"))
|
|
6398
|
+
```
|
|
6399
|
+
|
|
6400
|
+
The mock server matches reflected routes and synthesizes values from supported JSON Schema keywords.
|
|
6401
|
+
Unsupported or unsatisfiable schemas fail explicitly instead of returning known-invalid data. Use a
|
|
6402
|
+
fixed seed when responses must be replayable across local development and CI.
|
|
6403
|
+
|
|
6404
|
+
## For AI agents
|
|
6405
|
+
|
|
6406
|
+
Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
|
|
6407
|
+
one cheap read instead of the whole corpus. For the wider framework: the repo's
|
|
6408
|
+
[`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
|
|
6409
|
+
[`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
|
|
6410
|
+
done-gate, or `nifra mcp` to give the agent live project tools.
|
|
6411
|
+
|
|
5513
6412
|
## @nifrajs/node
|
|
5514
6413
|
|
|
5515
6414
|
Run a [nifra](../../README.md) app on Node's `http` server — nifra's ergonomics, deployed
|
|
@@ -5520,7 +6419,7 @@ bun add @nifrajs/node # or: npm add @nifrajs/node
|
|
|
5520
6419
|
```
|
|
5521
6420
|
|
|
5522
6421
|
```ts
|
|
5523
|
-
import { server } from "@nifrajs/core"
|
|
6422
|
+
import { server } from "@nifrajs/core/server"
|
|
5524
6423
|
import { serve } from "@nifrajs/node"
|
|
5525
6424
|
|
|
5526
6425
|
const app = server().get("/users/:id", (c) => ({ id: c.params.id }))
|
|
@@ -5644,8 +6543,7 @@ inventing trace identity.
|
|
|
5644
6543
|
|
|
5645
6544
|
## Adapters
|
|
5646
6545
|
|
|
5647
|
-
Implement `ObservationAdapter` to send spans wherever you collect them
|
|
5648
|
-
backwards-compatible type alias:
|
|
6546
|
+
Implement `ObservationAdapter` to send spans wherever you collect them:
|
|
5649
6547
|
|
|
5650
6548
|
```ts
|
|
5651
6549
|
interface ObservationAdapter {
|
|
@@ -5678,6 +6576,37 @@ one cheap read instead of the whole corpus. For the wider framework: the repo's
|
|
|
5678
6576
|
[`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
|
|
5679
6577
|
done-gate, or `nifra mcp` to give the agent live project tools.
|
|
5680
6578
|
|
|
6579
|
+
## @nifrajs/prompt
|
|
6580
|
+
|
|
6581
|
+
Provider-neutral, type-safe prompts with Standard Schema input and validated structured output.
|
|
6582
|
+
|
|
6583
|
+
```sh
|
|
6584
|
+
bun add @nifrajs/prompt
|
|
6585
|
+
```
|
|
6586
|
+
|
|
6587
|
+
```ts
|
|
6588
|
+
import { prompt } from "@nifrajs/prompt"
|
|
6589
|
+
import { t } from "@nifrajs/schema"
|
|
6590
|
+
|
|
6591
|
+
const extract = prompt("Extract the contact from the text.")
|
|
6592
|
+
.input(t.object({ text: t.string() }))
|
|
6593
|
+
.output(t.object({ name: t.string(), email: t.string({ format: "email" }) }))
|
|
6594
|
+
|
|
6595
|
+
const contact = await extract.run({ text }, { complete: callYourProvider })
|
|
6596
|
+
```
|
|
6597
|
+
|
|
6598
|
+
The package imports no vendor SDK. Supply a `complete` adapter for your provider; inputs are validated
|
|
6599
|
+
before the call and model output is parsed and validated before it reaches application code. An optional
|
|
6600
|
+
`heal` hook can repair and retry invalid structured output.
|
|
6601
|
+
|
|
6602
|
+
## For AI agents
|
|
6603
|
+
|
|
6604
|
+
Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
|
|
6605
|
+
one cheap read instead of the whole corpus. For the wider framework: the repo's
|
|
6606
|
+
[`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
|
|
6607
|
+
[`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
|
|
6608
|
+
done-gate, or `nifra mcp` to give the agent live project tools.
|
|
6609
|
+
|
|
5681
6610
|
## @nifrajs/runner
|
|
5682
6611
|
|
|
5683
6612
|
Run requests through a nifra app and capture **structured results** — the shared engine behind the
|
|
@@ -5727,7 +6656,7 @@ bun add @nifrajs/schema
|
|
|
5727
6656
|
```
|
|
5728
6657
|
|
|
5729
6658
|
```ts
|
|
5730
|
-
import { server } from "@nifrajs/core"
|
|
6659
|
+
import { server } from "@nifrajs/core/server"
|
|
5731
6660
|
import { t, toOpenAPI } from "@nifrajs/schema"
|
|
5732
6661
|
|
|
5733
6662
|
const app = server().post("/users", { body: t.object({ name: t.string(), age: t.integer() }) }, (c) => ({
|
|
@@ -5747,6 +6676,22 @@ const openapi = toOpenAPI(app) // OpenAPI 3.1
|
|
|
5747
6676
|
`operationId`s); also works on a live app. Routes using a BYO Standard Schema are
|
|
5748
6677
|
emitted without a detailed schema (Standard Schema exposes no JSON Schema).
|
|
5749
6678
|
|
|
6679
|
+
### `t` vs bring-your-own (bundle size)
|
|
6680
|
+
|
|
6681
|
+
`t` is the batteries-included default because a TypeBox schema *is* a JSON Schema: it gives you
|
|
6682
|
+
OpenAPI + MCP `tools/list` for free and compiles to a fast validator. That completeness has a
|
|
6683
|
+
cost - TypeBox carries the whole JSON Schema type system, so `t` adds meaningful bytes to a bundle.
|
|
6684
|
+
|
|
6685
|
+
nifra validates through **Standard Schema**, so any Standard-Schema validator works on a route with
|
|
6686
|
+
no adapter - `{ body: v.object({ ... }) }` with valibot, for instance. A validated app built on a
|
|
6687
|
+
lean validator like valibot is a fraction of the size (measured ~16 KB gzipped), which matters for
|
|
6688
|
+
edge/Workers cold-starts. The trade is the one noted above: a bring-your-own validator exposes no
|
|
6689
|
+
JSON Schema, so those routes carry no OpenAPI/MCP detail.
|
|
6690
|
+
|
|
6691
|
+
Rule of thumb: reach for `t` when you want the contract (OpenAPI/MCP) for free; bring valibot (or any
|
|
6692
|
+
Standard-Schema validator) when bundle size on the edge is the priority. Both are first-class - the
|
|
6693
|
+
choice is per route, and you can mix them in one app.
|
|
6694
|
+
|
|
5750
6695
|
### `toOpenAPI` coverage
|
|
5751
6696
|
|
|
5752
6697
|
It emits `paths`, `parameters` (path + object query), `requestBody`, and `responses`, plus:
|
|
@@ -5835,6 +6780,14 @@ import { assertSafeKey, StorageKeyError } from "@nifrajs/storage"
|
|
|
5835
6780
|
- `assertStorageAdapterConformance({ createAdapter })` — dependency-free executable adapter contract.
|
|
5836
6781
|
- Optional interfaces: `PagedStorageAdapter` · `PresignableStorageAdapter` · `MovableStorageAdapter`.
|
|
5837
6782
|
|
|
6783
|
+
## For AI agents
|
|
6784
|
+
|
|
6785
|
+
Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
|
|
6786
|
+
one cheap read instead of the whole corpus. For the wider framework: the repo's
|
|
6787
|
+
[`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
|
|
6788
|
+
[`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
|
|
6789
|
+
done-gate, or `nifra mcp` to give the agent live project tools.
|
|
6790
|
+
|
|
5838
6791
|
## @nifrajs/testing
|
|
5839
6792
|
|
|
5840
6793
|
Contract-derived adversarial testing and stateful in-process helpers for Nifra apps.
|
|
@@ -6004,6 +6957,14 @@ that every call carries and captures cookies via a shared jar.
|
|
|
6004
6957
|
|
|
6005
6958
|
For a **stateless** request (no cookies), use `testClient` from `@nifrajs/client` directly.
|
|
6006
6959
|
|
|
6960
|
+
## For AI agents
|
|
6961
|
+
|
|
6962
|
+
Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
|
|
6963
|
+
one cheap read instead of the whole corpus. For the wider framework: the repo's
|
|
6964
|
+
[`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
|
|
6965
|
+
[`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
|
|
6966
|
+
done-gate, or `nifra mcp` to give the agent live project tools.
|
|
6967
|
+
|
|
6007
6968
|
## @nifrajs/uploads
|
|
6008
6969
|
|
|
6009
6970
|
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 +7291,7 @@ done-gate, or `nifra mcp` to give the agent live project tools.
|
|
|
6330
7291
|
|
|
6331
7292
|
## @nifrajs/agent-telemetry
|
|
6332
7293
|
|
|
6333
|
-
`
|
|
7294
|
+
`agentTelemetry`, `AgentTelemetryOptions`, `consoleAgentExporter`
|
|
6334
7295
|
|
|
6335
7296
|
## @nifrajs/auth
|
|
6336
7297
|
|
|
@@ -6338,7 +7299,7 @@ done-gate, or `nifra mcp` to give the agent live project tools.
|
|
|
6338
7299
|
|
|
6339
7300
|
## @nifrajs/better-auth
|
|
6340
7301
|
|
|
6341
|
-
`betterAuth`, `BetterAuthLike`, `BetterAuthOptions`, `getSession`, `requireSession`, `RequireSessionOptions`, `SessionOf`
|
|
7302
|
+
`authed`, `AuthedOptions`, `betterAuth`, `BetterAuthLike`, `BetterAuthOptions`, `getSession`, `Principal`, `PrincipalFor`, `requirePrincipal`, `requireSession`, `RequireSessionOptions`, `SessionOf`, `SessionUserOf`, `WithPrincipal`
|
|
6342
7303
|
|
|
6343
7304
|
## @nifrajs/cache
|
|
6344
7305
|
|
|
@@ -6346,7 +7307,7 @@ done-gate, or `nifra mcp` to give the agent live project tools.
|
|
|
6346
7307
|
|
|
6347
7308
|
## @nifrajs/client
|
|
6348
7309
|
|
|
6349
|
-
`ActionArgs`, `ActionData`, `ApiError`, `ApiProxy`, `client`, `ClientOptions`, `FetchFn`, `inProcessClient`, `InProcessClient`, `Jsonify`, `LoaderArgs`, `LoaderData`, `RegistryOf`, `Result`, `SubscribeOptions`, `Subscription`, `testClient`, `Treaty`, `TreatyFromRegistry`
|
|
7310
|
+
`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
7311
|
|
|
6351
7312
|
## @nifrajs/content
|
|
6352
7313
|
|
|
@@ -6354,7 +7315,7 @@ done-gate, or `nifra mcp` to give the agent live project tools.
|
|
|
6354
7315
|
|
|
6355
7316
|
## @nifrajs/core
|
|
6356
7317
|
|
|
6357
|
-
`
|
|
7318
|
+
`Version`, `VERSION`
|
|
6358
7319
|
|
|
6359
7320
|
## @nifrajs/cron
|
|
6360
7321
|
|
|
@@ -6394,7 +7355,7 @@ done-gate, or `nifra mcp` to give the agent live project tools.
|
|
|
6394
7355
|
|
|
6395
7356
|
## @nifrajs/mcp
|
|
6396
7357
|
|
|
6397
|
-
`bridgeScript`, `createMcpServer`, `CreateMcpServerOptions`, `defineMcpTool`, `DefineMcpToolOptions`, `defineMcpWidget`, `DefineMcpWidgetOptions`, `McpAppBridge`, `McpHttpOptions`, `McpServer`, `McpToolHandlerResult`, `McpUiIntent`, `McpWidget`, `respondMcpHttp`, `uiResourceMeta`, `widgetDocument`
|
|
7358
|
+
`bridgeScript`, `createMcpServer`, `CreateMcpServerOptions`, `defineMcpTool`, `DefineMcpToolOptions`, `defineMcpWidget`, `DefineMcpWidgetOptions`, `InferOutput`, `McpAppBridge`, `McpHttpOptions`, `McpServer`, `McpToolHandlerResult`, `McpUiIntent`, `McpWidget`, `respondMcpHttp`, `StandardIssue`, `StandardResult`, `StandardSchemaV1`, `uiResourceMeta`, `widgetDocument`
|
|
6398
7359
|
|
|
6399
7360
|
## @nifrajs/mcp-db
|
|
6400
7361
|
|
|
@@ -6414,7 +7375,7 @@ done-gate, or `nifra mcp` to give the agent live project tools.
|
|
|
6414
7375
|
|
|
6415
7376
|
## @nifrajs/otel
|
|
6416
7377
|
|
|
6417
|
-
`ActiveObservation`, `AttributeValue`, `causalitySpanLink`, `combineObservationAdapters`, `consoleSpanExporter`, `createObservationLifecycle`, `EndObservation`, `formatTraceparent`, `generateSpanId`, `generateTraceId`, `NifraSpan`, `ObservationAdapter`, `ObservationClock`, `ObservationContext`, `ObservationLifecycle`, `ObservationLifecycleOptions`, `ObservationLink`, `ObservationParent`, `
|
|
7378
|
+
`ActiveObservation`, `AttributeValue`, `causalitySpanLink`, `combineObservationAdapters`, `consoleSpanExporter`, `createObservationLifecycle`, `EndObservation`, `formatTraceparent`, `generateSpanId`, `generateTraceId`, `NifraSpan`, `ObservationAdapter`, `ObservationClock`, `ObservationContext`, `ObservationLifecycle`, `ObservationLifecycleOptions`, `ObservationLink`, `ObservationParent`, `otlpExporter`, `OtlpExporter`, `OtlpExporterOptions`, `ParsedTraceparent`, `parseTraceparent`, `SpanStatus`, `StartObservation`, `TraceContext`, `traceHeaders`, `tracing`, `TracingOptions`
|
|
6418
7379
|
|
|
6419
7380
|
## @nifrajs/prompt
|
|
6420
7381
|
|
|
@@ -6442,7 +7403,7 @@ done-gate, or `nifra mcp` to give the agent live project tools.
|
|
|
6442
7403
|
|
|
6443
7404
|
## @nifrajs/web
|
|
6444
7405
|
|
|
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`, `
|
|
7406
|
+
`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
7407
|
|
|
6447
7408
|
## @nifrajs/web-preact
|
|
6448
7409
|
|