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