@nifrajs/cli 2.10.0 → 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/README.md +7 -1
  2. package/dist/assure-hydration.d.ts +14 -0
  3. package/dist/assure-hydration.d.ts.map +1 -0
  4. package/dist/assure-hydration.js +483 -0
  5. package/dist/assure-hydration.js.map +1 -0
  6. package/dist/assure.d.ts +35 -0
  7. package/dist/assure.d.ts.map +1 -1
  8. package/dist/assure.js +222 -1
  9. package/dist/assure.js.map +1 -1
  10. package/dist/capabilities-tool.d.ts +13 -0
  11. package/dist/capabilities-tool.d.ts.map +1 -1
  12. package/dist/capabilities-tool.js +64 -1
  13. package/dist/capabilities-tool.js.map +1 -1
  14. package/dist/check.d.ts +133 -10
  15. package/dist/check.d.ts.map +1 -1
  16. package/dist/check.js +741 -148
  17. package/dist/check.js.map +1 -1
  18. package/dist/cli.d.ts +21 -14
  19. package/dist/cli.d.ts.map +1 -1
  20. package/dist/cli.js +288 -73
  21. package/dist/cli.js.map +1 -1
  22. package/dist/contract-proof.d.ts +49 -0
  23. package/dist/contract-proof.d.ts.map +1 -0
  24. package/dist/contract-proof.js +128 -0
  25. package/dist/contract-proof.js.map +1 -0
  26. package/dist/contracts.d.ts +22 -0
  27. package/dist/contracts.d.ts.map +1 -0
  28. package/dist/contracts.js +134 -0
  29. package/dist/contracts.js.map +1 -0
  30. package/dist/dev-bun-config.d.ts +17 -6
  31. package/dist/dev-bun-config.d.ts.map +1 -1
  32. package/dist/dev-bun-config.js +51 -14
  33. package/dist/dev-bun-config.js.map +1 -1
  34. package/dist/diagnostics.d.ts +23 -0
  35. package/dist/diagnostics.d.ts.map +1 -0
  36. package/dist/diagnostics.js +9 -0
  37. package/dist/diagnostics.js.map +1 -0
  38. package/dist/diff-tool.d.ts +2 -0
  39. package/dist/diff-tool.d.ts.map +1 -1
  40. package/dist/diff-tool.js +1 -1
  41. package/dist/diff-tool.js.map +1 -1
  42. package/dist/docs-search.d.ts +8 -3
  43. package/dist/docs-search.d.ts.map +1 -1
  44. package/dist/docs-search.js +36 -15
  45. package/dist/docs-search.js.map +1 -1
  46. package/dist/doctor.d.ts +38 -2
  47. package/dist/doctor.d.ts.map +1 -1
  48. package/dist/doctor.js +189 -7
  49. package/dist/doctor.js.map +1 -1
  50. package/dist/examples.d.ts +1 -2
  51. package/dist/examples.d.ts.map +1 -1
  52. package/dist/examples.js +37 -16
  53. package/dist/examples.js.map +1 -1
  54. package/dist/fix-recipes.d.ts +12 -0
  55. package/dist/fix-recipes.d.ts.map +1 -0
  56. package/dist/fix-recipes.js +106 -0
  57. package/dist/fix-recipes.js.map +1 -0
  58. package/dist/init-agents.d.ts.map +1 -1
  59. package/dist/init-agents.js +60 -4
  60. package/dist/init-agents.js.map +1 -1
  61. package/dist/internal/source-facts.d.ts +15 -0
  62. package/dist/internal/source-facts.d.ts.map +1 -0
  63. package/dist/internal/source-facts.js +82 -0
  64. package/dist/internal/source-facts.js.map +1 -0
  65. package/dist/internal/typescript-import.d.ts +9 -0
  66. package/dist/internal/typescript-import.d.ts.map +1 -1
  67. package/dist/internal/typescript-import.js +38 -0
  68. package/dist/internal/typescript-import.js.map +1 -1
  69. package/dist/introspect.d.ts +6 -2
  70. package/dist/introspect.d.ts.map +1 -1
  71. package/dist/introspect.js +60 -5
  72. package/dist/introspect.js.map +1 -1
  73. package/dist/load.d.ts +10 -0
  74. package/dist/load.d.ts.map +1 -1
  75. package/dist/load.js +1 -0
  76. package/dist/load.js.map +1 -1
  77. package/dist/manifest-tool.d.ts.map +1 -1
  78. package/dist/manifest-tool.js +14 -9
  79. package/dist/manifest-tool.js.map +1 -1
  80. package/dist/mcp-docs-tools.d.ts.map +1 -1
  81. package/dist/mcp-docs-tools.js +3 -0
  82. package/dist/mcp-docs-tools.js.map +1 -1
  83. package/dist/mcp-http.js +1 -1
  84. package/dist/mcp-root.d.ts +83 -0
  85. package/dist/mcp-root.d.ts.map +1 -0
  86. package/dist/mcp-root.js +221 -0
  87. package/dist/mcp-root.js.map +1 -0
  88. package/dist/mcp-run.d.ts.map +1 -1
  89. package/dist/mcp-run.js +18 -4
  90. package/dist/mcp-run.js.map +1 -1
  91. package/dist/mcp.d.ts +27 -4
  92. package/dist/mcp.d.ts.map +1 -1
  93. package/dist/mcp.js +360 -27
  94. package/dist/mcp.js.map +1 -1
  95. package/dist/pipeline-guard.d.ts +25 -10
  96. package/dist/pipeline-guard.d.ts.map +1 -1
  97. package/dist/pipeline-guard.js +35 -15
  98. package/dist/pipeline-guard.js.map +1 -1
  99. package/dist/pipeline-report.d.ts +34 -0
  100. package/dist/pipeline-report.d.ts.map +1 -0
  101. package/dist/pipeline-report.js +282 -0
  102. package/dist/pipeline-report.js.map +1 -0
  103. package/dist/project-path.d.ts +3 -0
  104. package/dist/project-path.d.ts.map +1 -0
  105. package/dist/project-path.js +36 -0
  106. package/dist/project-path.js.map +1 -0
  107. package/dist/replay.d.ts +12 -0
  108. package/dist/replay.d.ts.map +1 -0
  109. package/dist/replay.js +36 -0
  110. package/dist/replay.js.map +1 -0
  111. package/dist/rules/codes.d.ts +42 -0
  112. package/dist/rules/codes.d.ts.map +1 -0
  113. package/dist/rules/codes.js +52 -0
  114. package/dist/rules/codes.js.map +1 -0
  115. package/dist/rules/comment-markers.d.ts +10 -0
  116. package/dist/rules/comment-markers.d.ts.map +1 -0
  117. package/dist/rules/comment-markers.js +24 -0
  118. package/dist/rules/comment-markers.js.map +1 -0
  119. package/dist/rules/index.d.ts +31 -0
  120. package/dist/rules/index.d.ts.map +1 -0
  121. package/dist/rules/index.js +133 -0
  122. package/dist/rules/index.js.map +1 -0
  123. package/dist/rules/legacy.d.ts +7 -0
  124. package/dist/rules/legacy.d.ts.map +1 -0
  125. package/dist/rules/legacy.js +85 -0
  126. package/dist/rules/legacy.js.map +1 -0
  127. package/dist/rules/routes.d.ts +12 -0
  128. package/dist/rules/routes.d.ts.map +1 -0
  129. package/dist/rules/routes.js +142 -0
  130. package/dist/rules/routes.js.map +1 -0
  131. package/dist/rules/security.d.ts +10 -0
  132. package/dist/rules/security.d.ts.map +1 -0
  133. package/dist/rules/security.js +471 -0
  134. package/dist/rules/security.js.map +1 -0
  135. package/dist/search-terms.d.ts +2 -0
  136. package/dist/search-terms.d.ts.map +1 -1
  137. package/dist/search-terms.js +5 -1
  138. package/dist/search-terms.js.map +1 -1
  139. package/dist/types-search.d.ts +1 -2
  140. package/dist/types-search.d.ts.map +1 -1
  141. package/dist/types-search.js +33 -16
  142. package/dist/types-search.js.map +1 -1
  143. package/dist/verification.d.ts +9 -0
  144. package/dist/verification.d.ts.map +1 -1
  145. package/dist/verification.js +17 -1
  146. package/dist/verification.js.map +1 -1
  147. package/dist/work-graph.d.ts +123 -0
  148. package/dist/work-graph.d.ts.map +1 -0
  149. package/dist/work-graph.js +465 -0
  150. package/dist/work-graph.js.map +1 -0
  151. package/docs/examples.json +32 -2
  152. package/docs/llms-full.txt +1105 -27
  153. package/docs/types.json +1772 -137
  154. package/package.json +34 -9
  155. package/src/assure-hydration.ts +606 -0
  156. package/src/assure.ts +316 -2
  157. package/src/capabilities-tool.ts +80 -1
  158. package/src/check.ts +923 -169
  159. package/src/cli.ts +297 -76
  160. package/src/contract-proof.ts +194 -0
  161. package/src/contracts.ts +157 -0
  162. package/src/dev-bun-config.ts +57 -14
  163. package/src/diagnostics.ts +32 -0
  164. package/src/diff-tool.ts +1 -1
  165. package/src/docs-search.ts +45 -15
  166. package/src/doctor.ts +286 -8
  167. package/src/examples.ts +43 -15
  168. package/src/fix-recipes.ts +126 -0
  169. package/src/init-agents.ts +61 -4
  170. package/src/internal/source-facts.ts +133 -0
  171. package/src/internal/typescript-import.ts +29 -0
  172. package/src/introspect.ts +81 -6
  173. package/src/load.ts +11 -0
  174. package/src/manifest-tool.ts +16 -10
  175. package/src/mcp-docs-tools.ts +3 -0
  176. package/src/mcp-http.ts +1 -1
  177. package/src/mcp-root.ts +271 -0
  178. package/src/mcp-run.ts +19 -4
  179. package/src/mcp.ts +440 -34
  180. package/src/pipeline-guard.ts +44 -14
  181. package/src/pipeline-report.ts +341 -0
  182. package/src/project-path.ts +36 -0
  183. package/src/replay.ts +44 -0
  184. package/src/rules/codes.ts +57 -0
  185. package/src/rules/comment-markers.ts +26 -0
  186. package/src/rules/index.ts +175 -0
  187. package/src/rules/legacy.ts +93 -0
  188. package/src/rules/routes.ts +157 -0
  189. package/src/rules/security.ts +505 -0
  190. package/src/search-terms.ts +9 -4
  191. package/src/types-search.ts +39 -15
  192. package/src/verification.ts +18 -1
  193. package/src/work-graph.ts +613 -0
@@ -596,6 +596,23 @@ await api.search.get({ query: { page: "3" } }) // query string
596
596
  await api.users({ id: "1" }).posts({ postId: "2" }).get() // nested params
597
597
  ```
598
598
 
599
+ **Reserved proxy keys.** The client proxy resolves a fixed set of property names
600
+ _before_ path segments: the seven HTTP verbs
601
+ (`get`/`post`/`put`/`patch`/`delete`/
602
+ `head`/`options`, any casing) call the route, and
603
+ `subscribe`, `ws`, `index`, and `then` (exact
604
+ match) are the SSE, WebSocket, root-path, and thenable-guard keys. A route whose path
605
+ contains a static segment spelling one of these - `.post("/api/delete", …)` -
606
+ cannot be reached by **dot access**: `api.delete.post` resolves the
607
+ `delete` verb, not the segment. The typed spelling is a **call on the parent
608
+ node** - `api.api("delete").post()` sends `POST /api/delete` - the
609
+ same call params use, accepting exactly the colliding segment names. The client type rejects
610
+ the dot access at compile time with that guidance, and `nifra check` reports the
611
+ collision (`NF-C018`, advisory). Prefer a verb-free segment
612
+ (e.g. `/api/remove`) when you control the path; mark a route served only to
613
+ non-typed-client consumers with `// nifra-expect reserved-segment` above its
614
+ registration.
615
+
599
616
  ### Results never throw
600
617
 
601
618
  Every call resolves to a discriminated `Result`: branch on `ok` (or
@@ -1743,6 +1760,12 @@ does custom, typed verification.
1743
1760
  `cacheControl()` sets `Cache-Control` on matching responses without
1744
1761
  clobbering one a handler already set.
1745
1762
 
1763
+ **Security note (BREACH):** any HTTP compression makes the response size depend on its
1764
+ content, so a response that mixes a secret with attacker-reflected request input can leak
1765
+ the secret through the compressed length. Nifra's CSRF tokens are HMAC-signed per session
1766
+ and safe to compress, but if a route reflects request input alongside a per-request secret,
1767
+ exclude it via the `compressible` option or route scoping.
1768
+
1746
1769
  ```ts
1747
1770
  import { server } from "@nifrajs/core/server"
1748
1771
  import { compression, cacheControl } from "@nifrajs/middleware"
@@ -1894,12 +1917,13 @@ createWebApp({
1894
1917
 
1895
1918
  ## Nifra - security & hardening
1896
1919
 
1897
- > Bounded request bodies, magic-byte file-upload validation, constant-time webhook verification, and idempotency-key replay - the hardening primitives every production app needs, built in.
1920
+ > Bounded request bodies, JSON prototype-poisoning defense, magic-byte file-upload validation, constant-time webhook verification, security response headers, and idempotency-key replay - the hardening primitives every production app needs, built in.
1898
1921
 
1899
1922
  The pieces every production endpoint needs - a body-size cap for raw routes, real file-type
1900
1923
  validation, constant-time webhook verification, and idempotent retries - ship as first-party
1901
1924
  primitives. All are **edge-safe** (WebCrypto, no `node:crypto`) and run unchanged
1902
- on Bun, Node, Deno, and Workers.
1925
+ on Bun, Node, Deno, and Workers. For how these defaults stack up against Hono, Fastify,
1926
+ Express, and Elysia, see [security posture, compared](/docs/security-comparison).
1903
1927
 
1904
1928
  ### Responses that cannot leak more than they declare
1905
1929
 
@@ -1992,6 +2016,65 @@ bad JSON a `400` - thrown as control-flow `Response`s the lifecycle catches,
1992
2016
  so the cap can't be silently skipped. Pass a larger `maxBytes` for an upload route, a
1993
2017
  smaller one to tighten an endpoint.
1994
2018
 
2019
+ The cap is enforced on the bytes actually **delivered**, never on the number the caller wrote in
2020
+ `Content-Length`. That distinction is the whole defense: an adapter that rebuilds a
2021
+ request from an event envelope, or any code assembling a `Request` by hand, can carry a
2022
+ header claiming 5 bytes over a payload of five megabytes. A runtime HTTP parser cannot lie that way,
2023
+ because it framed the body at the declared length itself, so nifra's ingress adapters mark their
2024
+ requests as runtime-framed and skip the recount entirely:
2025
+ `listen()` on Bun, `toFetchHandler(app)` on Workers and edge,
2026
+ `@nifrajs/deno`, and `@nifrajs/node` (which reads exact byte counts in the
2027
+ first place). Nothing to configure on any of them.
2028
+
2029
+ Only a request arriving through a path nifra ships no adapter for pays the extra pass over the body.
2030
+ If you serve an edge runtime by exporting the app directly rather than through
2031
+ `toFetchHandler`, `server({ trustBodyFraming: true })` asserts that every
2032
+ `app.fetch` request came from the platform's own parser. It is an assertion about
2033
+ deployment topology, so do not set it on an app whose `fetch` is also called with
2034
+ requests built from untrusted input. It trusts the _frame_, never the cap: an over-cap declared
2035
+ length is still a `413`, and a body with no `Content-Length` still goes
2036
+ through the streaming guard.
2037
+
2038
+ ### Prototype-poisoning defense
2039
+
2040
+ Every JSON body nifra parses - the schema-validated route and `c.boundedJson`
2041
+ alike - is screened for the prototype-pollution shape: an own `__proto__` key, or
2042
+ a `constructor` whose value carries a `prototype`. `JSON.parse`
2043
+ creates these as ordinary data properties, and the damage lands later, when innocent code does
2044
+ `{ ...body }`, `Object.assign(target, body)`, or a deep merge and
2045
+ walks that key onto a real prototype. The screen is on by default.
2046
+
2047
+ ```ts
2048
+ import { server } from "@nifrajs/core/server"
2049
+
2050
+ // Default is "reject". A JSON body with an own `__proto__` key - or a `constructor`
2051
+ // carrying a `prototype` - is the exact shape that turns a later innocent { ...body }
2052
+ // merge or Object.assign into prototype pollution. It answers the SAME flat 400 as
2053
+ // malformed JSON, so an attacker learns nothing from the response.
2054
+ const app = server({ protoPoisoning: "reject" }) // "strip" | "ignore" also available
2055
+
2056
+ app.post("/profile", async (c) => {
2057
+ const body = await c.boundedJson<{ name: string }>() // and the schema-route path
2058
+ // Reached only for a clean payload. Under "strip" the offending keys are deleted and
2059
+ // the handler sees the cleaned object; under "ignore" the body passes through as-is.
2060
+ return { name: body.name }
2061
+ })
2062
+ ```
2063
+
2064
+ `protoPoisoning` is a server option with three settings.
2065
+ ` "reject"` (the default) answers the same flat `400` as
2066
+ malformed JSON - indistinguishable on the wire, so a probe learns nothing.
2067
+ `"strip"` deletes the offending keys and hands the handler the cleaned
2068
+ value, siblings intact. `"ignore"` parses as-is, for a route you are sure
2069
+ never merges body input into another object. A string _value_ of `"__proto__"`
2070
+ is legal data and never triggers - only an own key of that name does.
2071
+
2072
+ The check is sound against escape smuggling: a `__proto__`-spelled key
2073
+ parses to the same own property, so it is caught the same way. And it is cheap on the common
2074
+ path - a clean body pays a substring pre-scan only; the deep walk runs solely when the raw text
2075
+ actually contains a suspect token, so an honest payload is never charged for the tree it does
2076
+ not have.
2077
+
1995
2078
  ### File uploads - `@nifrajs/uploads`
1996
2079
 
1997
2080
  A dependency-free package for the upload-hardening basics. `validateUpload` enforces a
@@ -2171,6 +2254,37 @@ trustworthy client IP can be derived, and never trusts `X-Forwarded-For` unless
2171
2254
  bodies before routing (fails closed with `411` on a length-less body). The read-time
2172
2255
  guard above (`c.boundedBody` / schema cap) remains the source of truth.
2173
2256
 
2257
+ ### Security response headers - `securityHeaders`
2258
+
2259
+ `securityHeaders()` sets a safe-by-default response header set on every response,
2260
+ errors and 404s included. Three are always on - `X-Content-Type-Options: nosniff`,
2261
+ `X-Frame-Options: DENY`, and `Referrer-Policy: no-referrer`. The
2262
+ cross-origin isolation headers (`COOP`/`COEP`/`CORP`),
2263
+ `Permissions-Policy`, `Content-Security-Policy`, and `HSTS` are
2264
+ opt-in, because each one can break a working app (embedding, popups, HTTP) and so is a
2265
+ deliberate choice, never a silent default.
2266
+
2267
+ ```ts
2268
+ import { server } from "@nifrajs/core/server"
2269
+ import { securityHeaders } from "@nifrajs/middleware"
2270
+
2271
+ // Always on (covering errors and 404s too): X-Content-Type-Options: nosniff,
2272
+ // X-Frame-Options: DENY, Referrer-Policy: no-referrer. The rest are opt-in - each is a
2273
+ // deliberate cross-origin decision, so nifra never turns them on behind your back:
2274
+ const app = server().use(securityHeaders({
2275
+ crossOriginOpenerPolicy: "same-origin", // isolate the browsing-context group
2276
+ crossOriginResourcePolicy: "same-origin", // block cross-origin embedding of your responses
2277
+ crossOriginEmbedderPolicy: "require-corp", // enable crossOriginIsolated (SharedArrayBuffer, …)
2278
+ permissionsPolicy: "camera=(), geolocation=()",
2279
+ contentSecurityPolicy: "default-src 'self'",
2280
+ hsts: { maxAge: 63072000, includeSubDomains: true, preload: true }, // opt in once HTTPS-only
2281
+ }))
2282
+ ```
2283
+
2284
+ Every value is fixed at construction, so the headers are declared statically rather than
2285
+ written by a response hook - an app whose response middleware is only this keeps the fused
2286
+ native response lanes. A route that sets one of these names itself keeps its own value.
2287
+
2174
2288
  ### Route assurance - prove every route is guarded
2175
2289
 
2176
2290
  Installing security middleware is not the same as proving it covers every route. Nifra&apos;s
@@ -2313,8 +2427,11 @@ const sagas = createSagaEngine({
2313
2427
  ### Already built in
2314
2428
 
2315
2429
  These add to Nifra's standing defaults: strict-by-default schema validation (unknown fields
2316
- rejected), SSR serialization that escapes every inline-script value, signed-cookie sessions + CSRF
2317
- + route guards ([@nifrajs/auth](/docs/auth)), bearer/apiKey auth + a shared-store rate
2430
+ rejected), SSR serialization that escapes every inline-script value,
2431
+ `__Host-`/`__Secure-` cookie-prefix enforcement (a cookie whose
2432
+ attributes violate its prefix contract fails at serialization rather than being silently
2433
+ dropped by the browser), signed-cookie sessions + CSRF + route guards
2434
+ ([@nifrajs/auth](/docs/auth)), bearer/apiKey auth + a shared-store rate
2318
2435
  limiter ([@nifrajs/middleware](/docs/plugins)), and a hardened image-resize endpoint
2319
2436
  ([@nifrajs/image/server](/docs/images)).
2320
2437
 
@@ -3004,10 +3121,13 @@ you bring JSON catalogs.
3004
3121
 
3005
3122
  ### Negotiate the locale
3006
3123
 
3007
- `negotiateLocale` picks the best supported locale from a cookie (an explicit choice),
3008
- then `Accept-Language` (quality-ranked, with `fr-CA`→`fr`
3009
- base-subtag fallback), else your default. Resolve it in a loader and return just that locale's
3010
- messages.
3124
+ `negotiateLocale` picks the best supported locale from a `?lang=` query
3125
+ parameter (an explicit ask), then a cookie (a remembered choice), then
3126
+ ` Accept-Language` (quality-ranked, with `fr-CA`→`fr`
3127
+ base-subtag fallback), else your default. The answer is always drawn from your
3128
+ ` locales` allow-list - request input is matched, never echoed - so a hostile
3129
+ ` ?lang=` can't reach the response. Resolve it in a loader and return just that
3130
+ locale's messages.
3011
3131
 
3012
3132
  ```ts
3013
3133
  // In a loader: resolve the locale + return only that catalog's messages.
@@ -3015,12 +3135,34 @@ import { negotiateLocale } from "@nifrajs/i18n"
3015
3135
  import { catalogs, locales } from "../catalogs"
3016
3136
 
3017
3137
  export async function loader({ request }: { request: Request }) {
3018
- const q = new URL(request.url).searchParams.get("lang")
3019
- const locale = q && locales.includes(q) ? q : negotiateLocale(request, { locales, defaultLocale: "en", cookie: "lang" })
3020
- return { locale, messages: catalogs[locale] } // cookie → Accept-Language → default
3138
+ const locale = negotiateLocale(request, { locales, defaultLocale: "en", queryParam: "lang", cookie: "lang" })
3139
+ return { locale, messages: catalogs[locale] } // ?lang= → cookie → Accept-Language → default
3021
3140
  }
3022
3141
  ```
3023
3142
 
3143
+ On the server, `localeDetector()` (from `@nifrajs/i18n/detector`, needs
3144
+ ` @nifrajs/core`) wraps the same negotiation as a plugin: handlers read
3145
+ ` c.locale`/`c.localeSource`, responses carry
3146
+ ` Content-Language`. With `persist: true` it writes the locale cookie
3147
+ ** only** when an explicit `?lang=` choice differs from the cookie - a
3148
+ header-derived guess is never pinned, and plain requests never grow a
3149
+ `Set-Cookie`, so responses stay cacheable. (`@nifrajs/middleware`'s
3150
+ ` language()` is the header-only sibling; use one or the other.)
3151
+
3152
+ ```ts
3153
+ // Or as a server plugin: c.locale on every handler, Content-Language on every response.
3154
+ import { server } from "@nifrajs/core"
3155
+ import { localeDetector } from "@nifrajs/i18n/detector"
3156
+
3157
+ const app = server().use(localeDetector({
3158
+ locales: ["en", "fr", "de"],
3159
+ defaultLocale: "en",
3160
+ queryParam: "lang",
3161
+ cookie: "locale",
3162
+ persist: true, // pin an explicit ?lang= choice into the cookie
3163
+ })).get("/", (c) => c.json({ locale: c.locale, via: c.localeSource }))
3164
+ ```
3165
+
3024
3166
  ### Format messages
3025
3167
 
3026
3168
  `createFormatter(locale, messages)` → `{ t, n, d }`. `t`
@@ -3108,6 +3250,14 @@ export default { fetch: (req: Request, env: Env, ctx: ExecutionContext) =>
3108
3250
  app.fetch(req, { env, waitUntil: ctx.waitUntil.bind(ctx) }) }
3109
3251
  ```
3110
3252
 
3253
+ Export through `toFetchHandler` rather than handing the app out directly. Besides
3254
+ `env` and `ctx.waitUntil`, it tells nifra the request came from the
3255
+ platform's own HTTP parser, which lets the
3256
+ [bounded-body cap](/docs/security) trust the declared
3257
+ `Content-Length` as a real transport frame instead of re-counting every JSON body.
3258
+ A bare `export default app` has no such signal and pays the extra pass unless you
3259
+ set `server({ trustBodyFraming: true })`.
3260
+
3111
3261
  ### Background work
3112
3262
 
3113
3263
  `c.waitUntil(promise)` keeps work alive after the response is sent - cache writes,
@@ -3511,6 +3661,30 @@ content-typed, with an immutable cache - before the app runs, leaving the SSR fa
3511
3661
  untouched: `serve(app, {'{ port, static: { dir: new URL("./assets/", import.meta.url) } }'})`.
3512
3662
  On Cloudflare/Vercel the platform serves assets, so you don't need it there.
3513
3663
 
3664
+ ### AWS Lambda
3665
+
3666
+ `@nifrajs/aws-lambda` - API Gateway HTTP APIs (payload v2) and Lambda Function
3667
+ URLs. `handle(app)` is the buffered handler; `streamHandle(app)`
3668
+ streams responses on Function URLs with `InvokeMode: RESPONSE_STREAM`. Build the
3669
+ app at module scope (once per container). The adapter decodes the body and checks its real
3670
+ size before a `Request` exists, assembles headers + the v2 `cookies`
3671
+ array in one place, decides response base64 by what the bytes actually are, and feeds
3672
+ `sourceIp` into `c.clientIp`. `waitUntil` work settles
3673
+ before the container freezes. REST APIs (payload v1) and ALB are not supported.
3674
+
3675
+ ```ts
3676
+ // handler.ts - one app per container, built at module scope
3677
+ import { handle, streamHandle, type LambdaEnv } from "@nifrajs/aws-lambda"
3678
+ import { server } from "@nifrajs/core"
3679
+
3680
+ const app = server<LambdaEnv>()
3681
+ .get("/", (c) => c.json({ ip: c.clientIp, requestId: c.env.context?.awsRequestId }))
3682
+
3683
+ export const handler = handle(app) // API Gateway HTTP API (v2) + Function URLs
3684
+ // or, on a Function URL with InvokeMode: RESPONSE_STREAM:
3685
+ // export const handler = streamHandle(app)
3686
+ ```
3687
+
3514
3688
  ### Cloudflare Workers / Pages (the edge)
3515
3689
 
3516
3690
  `buildServer` bundles with edge conditions (`workerd`,
@@ -3545,23 +3719,35 @@ Deploy: `wrangler pages deploy dist` (Pages) or `wrangler deploy`
3545
3719
  `server.ts` to hand-write. (`create-nifra` scaffolds the conventions.)
3546
3720
 
3547
3721
  ```ts
3548
- nifra dev # true-HMR dev server (Vite middleware + Nifra SSR) - http://localhost:4321
3722
+ nifra dev # true-HMR dev server (Bun native HMR + Nifra SSR) - http://localhost:4321
3723
+ nifra dev --vite # the Vite middleware pipeline instead (automatic when vitePlugins are your ONLY transforms)
3549
3724
  nifra build # full Bun deploy → dist/server.js + content-hashed dist/assets/ (default target: bun)
3550
3725
  nifra start # run dist/server.js on Bun
3551
3726
  nifra build --target cf-pages # also: node | deno | vercel | static; add --report for chunk sizes
3552
3727
 
3553
3728
  # dev + start share the default port 4321. Override per run: --port <n> (alias -p) or the PORT env var.
3554
3729
  # flags: --port <n> (dev/start) · --out <dir> (build/start) · --target <t> (build) · --poll (dev)
3730
+ # --vite | --bun (dev/build) force the pipeline
3555
3731
  ```
3556
3732
 
3557
3733
  ### The conventions
3558
3734
 
3559
- Four conventions at the project root. `nifra dev` runs the Vite-backed
3735
+ Four conventions at the project root. `nifra dev` runs the Bun-native
3560
3736
  [HMR dev server](/docs/dev); `nifra build` runs the Bun-native production
3561
3737
  build (content-hashed client assets plus a target-specific server); `nifra start`
3562
3738
  runs the default Bun output. The generated server serves assets and SSR with matched-route
3563
3739
  chunks preloaded and route CSS linked in each `<head>`.
3564
3740
 
3741
+ **One rule picks the bundler, and it is the same rule in both phases** - so a
3742
+ project can never be bundled by one toolchain in dev and the other in production. Bun runs
3743
+ everything, except an app whose _only_ transforms are `vitePlugins`: the Bun
3744
+ pipeline cannot run those, so that app gets Vite for dev and build alike, and says so.
3745
+ `--vite` / `--bun` force the choice; `--bun` is refused on a
3746
+ Vite-only app rather than dropping its transforms silently. Every `dev` and
3747
+ `build` run prints which bundler it picked and why, and `nifra check`
3748
+ answers the same question without starting a server - see
3749
+ [which pipeline runs, when](/docs/dev) for the full table.
3750
+
3565
3751
  ```ts
3566
3752
  my-app/
3567
3753
  routes/ # file-based routes (index.tsx, _layout.tsx, [id].tsx, …)
@@ -3844,9 +4030,9 @@ distinct.
3844
4030
 
3845
4031
  Nifra gives you two local development loops, and the rule between them is that one
3846
4032
  toolchain owns a whole phase. **Both** give you React Fast Refresh with state
3847
- preserved. Use `nifra dev` (Vite) for the plugin ecosystem, or
3848
- `nifra dev --bun` for one bundler across dev and prod with no Vite dependency.
3849
- Both serve your real SSR app locally, and neither mixes the two bundlers in one process.
4033
+ preserved. Bun is the default; Vite takes over automatically when your config's only
4034
+ transforms are `vitePlugins`. Both serve your real SSR app locally, and neither
4035
+ mixes the two bundlers in one process.
3850
4036
 
3851
4037
  ### Two loops, same app
3852
4038
 
@@ -3859,7 +4045,7 @@ use when
3859
4045
 
3860
4046
  Bun HMR + Fast Refresh (native)
3861
4047
  none (Bun only)
3862
- one bundler dev+prod; no Vite dep (no CSS Modules)
4048
+ one bundler dev+prod, no Vite dep; CSS Modules included
3863
4049
 
3864
4050
  `@nifrajs/web/vite`
3865
4051
 
@@ -3869,6 +4055,79 @@ true HMR (Fast Refresh / framework HMR)
3869
4055
 
3870
4056
  state-preserving UI iteration
3871
4057
 
4058
+ ### Which pipeline runs, when
4059
+
4060
+ One rule decides the bundler, and it decides it the same way for `nifra dev` and
4061
+ `nifra build` - your dev loop and your production build are never on different
4062
+ toolchains.
4063
+
4064
+ your config
4065
+ command
4066
+ pipeline
4067
+ why
4068
+
4069
+ no plugins at all
4070
+
4071
+ `nifra dev` / `nifra build`
4072
+
4073
+ Bun
4074
+ the default: no Vite dependency, one bundler across dev and prod
4075
+
4076
+ `clientPlugins` and/or `serverPlugins`
4077
+
4078
+ `nifra dev` / `nifra build`
4079
+
4080
+ Bun
4081
+ those slots are Bun.build plugins; Vite would never call them
4082
+
4083
+ `vitePlugins` only
4084
+
4085
+ `nifra dev` / `nifra build`
4086
+
4087
+ Vite (automatic)
4088
+ the Bun build cannot run them, so staying on Bun would silently drop your transforms
4089
+
4090
+ `vitePlugins` only
4091
+
4092
+ `--bun`
4093
+
4094
+ error
4095
+ nifra refuses rather than build an app with its compiler switched off
4096
+
4097
+ any
4098
+
4099
+ `--vite`
4100
+
4101
+ Vite (forced)
4102
+
4103
+ exact dev/prod client resolution for `conditions`, or a Vite-only plugin
4104
+
4105
+ any
4106
+
4107
+ `--bun`
4108
+
4109
+ Bun (forced)
4110
+ allowed whenever no transform would be lost
4111
+
4112
+ You never have to work this out from the table. Every `nifra dev` and
4113
+ `nifra build` run prints the answer under its banner, and
4114
+ `nifra check` and `nifra doctor` report it - with the config hazards
4115
+ below - without starting a server:
4116
+
4117
+ ```ts
4118
+ # doc-check: skip - fragment: terminal output, not source.
4119
+ $ nifra dev
4120
+ nifra dev (bun) → http://localhost:3000
4121
+ bundler: bun (default; --vite to switch)
4122
+
4123
+ $ nifra build
4124
+ nifra build (node, vite) → dist/server/server.js
4125
+ bundler: vite (auto: this app's only transforms are `vitePlugins` (svelte), which the Bun build cannot run)
4126
+
4127
+ $ nifra check
4128
+ • bundler: vite (auto: this app's only transforms are `vitePlugins` (svelte), which the Bun build cannot run)
4129
+ ```
4130
+
3872
4131
  ### State-preserving HMR
3873
4132
 
3874
4133
  Use `createViteDevServer` when you want component edits to update the browser
@@ -4012,10 +4271,12 @@ a component-only module swaps its markup with `useState` state intact, no reload
4012
4271
  boundary rule is the same as Vite's (see below). The real prize is that dev and production use
4013
4272
  the _same bundler_, so the dev/prod seam disappears.
4014
4273
 
4015
- One gap: **CSS Modules**. Bun's dev-server bundler has no
4016
- `*.module.css` transform (its production `Bun.build` does), so the CLI
4017
- refuses `--bun` for a CSS-Modules app rather than serving a broken client. Plain CSS
4018
- and Tailwind work normally.
4274
+ **CSS Modules included.** Bun's dev-server bundler has no
4275
+ `*.module.css` transform of its own, so the CLI hands it the same one the
4276
+ production `Bun.build` uses - and a class hashes to the same scoped name on every
4277
+ pipeline. Calling `createDevServer` as a library instead of through the CLI, you
4278
+ supply that plugin yourself through your own `bunfig.toml`; plain CSS and Tailwind
4279
+ need nothing either way.
4019
4280
 
4020
4281
  **
4021
4282
  Server functions and `*.server` modules work under `--bun`
@@ -4044,7 +4305,8 @@ source. A divergence between the loops is treated as a bug, not a caveat.
4044
4305
  import { createDevServer } from "@nifrajs/web/dev"
4045
4306
  // Bun.serve bundles + hot-reloads the client; Bun's runtime resolves SSR. An edit reloads the
4046
4307
  // changed module graph - with React Fast Refresh (state preserved) applied natively by Bun, no plugin.
4047
- // CSS + the entry URL come from Bun. Plain CSS/Tailwind work; *.module.css does not (Bun's dev bundler).
4308
+ // CSS + the entry URL come from Bun. Plain CSS/Tailwind work as-is; for *.module.css as a library
4309
+ // caller, pass the production CSS Modules plugin through your own bunfig (nifra dev --bun does it for you).
4048
4310
  const server = await createDevServer({ routesDir, outDir, clientModule, createApp })
4049
4311
  ```
4050
4312
 
@@ -4161,6 +4423,105 @@ own stylesheet. Pass `manifest.routeStyles` to `createWebApp` alongside
4161
4423
  `styles`, and Nifra links the matched route's CSS during SSR. In
4162
4424
  **dev**, Vite injects per-module CSS.
4163
4425
 
4426
+ ### Gotchas
4427
+
4428
+ The rule "one toolchain owns a whole phase" is what keeps the two loops honest, and most of
4429
+ what follows is a consequence of it. None of these are things you have to memorise before you
4430
+ start - they are the edges you can hit later, collected in one place.
4431
+
4432
+ #### Keep the adapter out of your config file
4433
+
4434
+ Split `framework.ts` (the adapter, imported by your server and edge entries) from
4435
+ `nifra.config.ts` (the CLI's config, where the Vite plugins and SFC compilers live).
4436
+ If your server entry reaches the adapter _through_ the config, everything the config
4437
+ imports is bundled into your production server - the Vite plugin, the framework compiler, and
4438
+ their native bindings. It builds without complaint and then fails at startup with a missing
4439
+ native binding, which reads like a broken install rather than a config-shape problem.
4440
+
4441
+ ```ts
4442
+ // doc-check: skip - fragment: two files from one app, shown together.
4443
+ // framework.ts - imported by your SERVER and edge entries. Adapter only, nothing else.
4444
+ import { svelteAdapter } from "@nifrajs/web-svelte"
4445
+ export const adapter = svelteAdapter
4446
+
4447
+ // nifra.config.ts - imported ONLY by the CLI, which runs on Bun. Compilers and Vite plugins
4448
+ // belong here. Re-export the adapter; never define it here, or the dev toolchain it pulls in
4449
+ // (Vite, the SFC compiler, their native bindings) is bundled into your production server.
4450
+ import { svelte } from "@sveltejs/vite-plugin-svelte"
4451
+ export { adapter } from "./framework"
4452
+ export const clientModule = "@nifrajs/web-svelte/client"
4453
+ export const vitePlugins = [svelte()]
4454
+ ```
4455
+
4456
+ #### Plugins go in the slot that matches the pipeline
4457
+
4458
+ `vitePlugins` run on the Vite pipeline; `clientPlugins` and
4459
+ `serverPlugins` run on the Bun one. A plugin in the wrong slot is not an error, it
4460
+ is a _no-op_ - your transform silently does not run. Nifra classifies each plugin by its
4461
+ hook shape and refuses a mismatch rather than letting the build succeed without it. The same
4462
+ check is why `--bun` throws for an app whose only transforms are
4463
+ `vitePlugins`, instead of building without them.
4464
+
4465
+ #### `conditions` does not reach the Bun dev client bundle
4466
+
4467
+ On the Bun pipeline, `conditions` governs SSR - `nifra dev` passes them
4468
+ to the runtime when it re-execs - and it governs the production client bundle. It cannot govern
4469
+ the client bundle the dev server serves: Bun's dev-server bundler accepts no resolve
4470
+ conditions, and there is no `bunfig.toml` key for them either (a top-level
4471
+ `conditions`, or one under `[run]`, `[serve.static]` or
4472
+ `[bundle]`, parses and is ignored). So a package with an `exports` map
4473
+ can resolve to one file in dev and another in `nifra build`. Nifra says so once at
4474
+ startup rather than letting you find out in production. If your app depends on exact dev/prod
4475
+ client resolution, run `nifra dev --vite`.
4476
+
4477
+ Related, if you ever pass the flag to `bun` yourself: it takes
4478
+ **one condition per flag**. The comma form is accepted and matches nothing,
4479
+ because Bun reads the whole string as a single condition name.
4480
+
4481
+ ```ts
4482
+ # Resolve conditions on the Bun dev pipeline.
4483
+ # SSR honours `conditions` - nifra passes them to the runtime when it re-execs.
4484
+ # The CLIENT bundle does not: Bun's dev-server bundler takes no resolve conditions,
4485
+ # from bunfig.toml or anywhere else. nifra warns once at startup when it matters.
4486
+ nifra dev --vite # exact dev/prod client resolution, if your app needs it
4487
+
4488
+ # If you invoke bun yourself: ONE FLAG PER CONDITION.
4489
+ bun --conditions=browser --conditions=development ./app.ts # correct
4490
+ bun --conditions=browser,development ./app.ts # ONE condition named "browser,development"
4491
+ ```
4492
+
4493
+ #### Editing a non-route file updates SSR too
4494
+
4495
+ On the Bun pipeline the dev server tracks the module graph your routes import, so editing a
4496
+ component, a helper, or a server module several levels down is reflected in the next SSR
4497
+ render - not just in the browser bundle. Modules you did _not_ touch keep their
4498
+ identity, so a module-level singleton stays single across the reload. Two limits are
4499
+ deliberate: files under `node_modules` are not tracked (they are dependencies, not
4500
+ your source), and a specifier only participates if it is relative and written literally - a
4501
+ computed or aliased specifier is invisible to a static graph. A third-party Bun plugin that
4502
+ rewrites imports needs to call `rewriteSsrImports` for its output to stay tracked.
4503
+
4504
+ #### An adapter with a compiled asset needs the dev server's loader
4505
+
4506
+ This one matters only if you are writing a render adapter. On the Vite pipeline Vite owns SSR
4507
+ resolution, which is what lets `resolve.dedupe` reach the server and keeps your app
4508
+ on one copy of the framework. Adapter packages themselves stay external to that graph, because
4509
+ the adapter's context object has to be the one instance your app imported. If such an adapter
4510
+ must load a _compiled_ asset on the server - Svelte's chain component is one - a plain
4511
+ `import` reaches a runtime with no compiler for it, and registering a second
4512
+ compiler in the runtime is worse: the tree then holds two copies of the framework runtime, and
4513
+ context written by one half is invisible to the other. Load it through
4514
+ `ssrModuleLoader()` instead, and take the framework's _server renderer_ from
4515
+ there as well - a component compiled by one toolchain has to render through the renderer that
4516
+ toolchain resolved.
4517
+
4518
+ #### Scoped class names are identical across pipelines
4519
+
4520
+ A CSS Modules class hashes to the same scoped name whichever pipeline produced it, and across
4521
+ machines - the hash is keyed on the class name plus the module's _package-relative_
4522
+ path, never an absolute one. A selector written against a generated name behaves the same under
4523
+ `nifra dev`, `nifra dev --bun`, and `nifra build`.
4524
+
4164
4525
  ## Nifra - Troubleshooting
4165
4526
 
4166
4527
  > Fixes keyed on the literal error strings Nifra prints: `reached the client bundle` (a node:/native import in the browser bundle), `server-only module reached the client bundle` (the server-only marker), and `resolveDispatcher` / `Invalid hook call` (duplicate React).
@@ -5370,7 +5731,9 @@ const app = server().merge(createBillingRouter({ charge: async (plan) => ({ id:
5370
5731
  // 4. Build the client from the EXPORTED contract value - decoupled, no `typeof app` import needed.
5371
5732
  const api = client(billingContract, "https://api.example.com")
5372
5733
  const { data } = await api.billing.plan.get() // GET /billing/plan
5373
- await api.billing.subscribe.post({ plan: "pro" }) // POST /billing/subscribe, body typed
5734
+ // "subscribe" is a reserved client word (the SSE subscriber), so this segment is reached by
5735
+ // CALLING the parent node with the segment name - same request, same typed body.
5736
+ await api.billing("subscribe").post({ plan: "pro" }) // POST /billing/subscribe, body typed
5374
5737
  ```
5375
5738
 
5376
5739
  Why this survives `.d.ts` emit where the fluent chain doesn't: the contract is a
@@ -6268,6 +6631,488 @@ backend you can also ship on its own. Start with [Getting started](/docs).
6268
6631
  Moving a backend (Express, Hono, Fastify, Elysia) instead? See
6269
6632
  [Migrating a backend](/docs/migrate-backend).
6270
6633
 
6634
+ ## Nifra - Migrate route by route
6635
+
6636
+ > Keep an existing Hono, Elysia, Express, or Workers app running while moving routes into typed Nifra handlers one at a time.
6637
+
6638
+ Keep the old app live while you move its surface into Nifra. Mount the legacy fetch handler at
6639
+ a prefix, add typed Nifra routes as they are ready, then delete the mount when the prefix is
6640
+ empty.
6641
+
6642
+ [!WARNING] Mounted handlers are an escape hatch. Their request and response shapes are outside
6643
+ Nifra's typed route contract, response schemas, and response-contract enforcement. Keep the
6644
+ mount small and temporary, and use typed routes for every migrated endpoint.
6645
+
6646
+ ### 1. Mount the existing app
6647
+
6648
+ `mountFetch` accepts any handler with the Web fetch shape. Use a path ending in
6649
+ `/*` for a subtree. By default the legacy app sees the original URL. Set
6650
+ `stripPrefix: true` when the legacy app declares paths relative to its mount point.
6651
+ The platform object is forwarded as the second argument when the runtime supplies one.
6652
+
6653
+ ```ts
6654
+ import { Hono } from "hono"
6655
+ import { server } from "@nifrajs/core/server"
6656
+ import { t } from "@nifrajs/schema"
6657
+
6658
+ const legacy = new Hono()
6659
+ legacy.get("/users", (c) => c.json({ source: "hono", users: [] }))
6660
+ legacy.post("/users", async (c) => c.json(await c.req.json()))
6661
+
6662
+ export const app = server()
6663
+ // The wildcard is a literal mount prefix, not a typed Nifra route.
6664
+ .mountFetch("/legacy/*", legacy.fetch, { stripPrefix: true })
6665
+ // Typed routes take precedence, so move one route out of Hono at a time.
6666
+ .get("/legacy/users/:id", (c) => ({ id: c.params.id, source: "nifra" }))
6667
+ .post(
6668
+ "/users",
6669
+ { body: t.object({ name: t.string() }) },
6670
+ (c) => ({ id: crypto.randomUUID(), name: c.body.name }),
6671
+ )
6672
+ ```
6673
+
6674
+ ### 2. Move the routes
6675
+
6676
+ - Choose one legacy endpoint and add it as a typed Nifra route.
6677
+
6678
+ - Keep the mount in place for the remaining endpoints.
6679
+
6680
+ - Add a body, query, or params schema so the trust boundary is explicit.
6681
+
6682
+ - Run `nifra check` and the route tests, then repeat.
6683
+
6684
+ Typed routes win when both surfaces match. That lets a new Nifra route replace its legacy
6685
+ counterpart without changing the mount prefix or adding a second server.
6686
+
6687
+ ### 3. Remove the escape hatch
6688
+
6689
+ When the legacy prefix has no remaining routes, remove `mountFetch` and delete the
6690
+ old app. The remaining routes now have typed params, validated inputs, reflected contracts,
6691
+ and the end-to-end typed client.
6692
+
6693
+ ### Other legacy handlers
6694
+
6695
+ The same seam works with Elysia, an Express app exposed through an adapter, or a raw Workers
6696
+ handler. Pass its fetch-compatible function directly. For a Workers-style handler, the
6697
+ platform argument carries bindings and `waitUntil`; mounted code remains responsible
6698
+ for its own framework-specific request and response conventions.
6699
+
6700
+ ## Nifra security posture vs Hono, Fastify, Express, Elysia
6701
+
6702
+ > A dimension-by-dimension security comparison of Nifra against the current releases of Hono, Fastify, Express, and Elysia - which hardening each framework ships by default, which is opt-in, and which is left to third-party packages.
6703
+
6704
+ Which hardening each framework actually ships. A dimension-by-dimension read of Nifra&rsquo;s
6705
+ security defaults against the **current releases** of Hono, Fastify, Express, and Elysia -
6706
+ what is on by default, what is opt-in, and what is left to a third-party package. For how each
6707
+ Nifra primitive works, see [Security & hardening](/docs/security).
6708
+
6709
+ ### The honest verdict
6710
+
6711
+ Nifra is **at or above parity on every dimension where a comparison is meaningful**, and
6712
+ ahead of all four on several:
6713
+
6714
+ -
6715
+ **Ahead of all four:** CORS misconfiguration resistance, redirect safety, WebSocket
6716
+ origin policy, rate-limiter safe-by-construction design, route-level security evidence
6717
+ (`nifra assure`), response over-exposure enforcement, upload content
6718
+ verification, env-independent error responses, client-IP trust model.
6719
+
6720
+ -
6721
+ **Par with the best-in-class:** body limits (Fastify class), JSON prototype-poisoning
6722
+ rejection (par Fastify, default-on), schema validation (par Fastify/Elysia), CSRF (par
6723
+ Fastify), security headers (par Hono, first-party), cookie prefixes (par Hono), sessions,
6724
+ JWT hardening, static serving, supply chain (par Hono&rsquo;s zero-dependency core).
6725
+
6726
+ **One caveat no capability table removes.** Hono, Fastify, and Express have orders of
6727
+ magnitude more production exposure and more external researchers looking at them. Nifra&rsquo;s
6728
+ protections are implementation-verified and unit-tested, but have not had that volume of
6729
+ adversarial attention or a third-party audit. Ongoing scrutiny is the part only time buys.
6730
+
6731
+ ### How this was measured
6732
+
6733
+ Nifra side: an implementation-level read of every security-relevant module across
6734
+ `core`, `middleware`, `auth`, `node`,
6735
+ `web`, `uploads`, and `cli` - defaults and failure modes, not
6736
+ marketing claims. Competitor side: current official documentation for defaults, verified as of
6737
+ August 2026 against Hono 4.12.x, Fastify 5.x, Express 5.1 / 4.21.x, and Elysia 1.4.x. The
6738
+ comparison unit is what a developer gets **by default** and what the framework makes
6739
+ **impossible to get wrong**, not what a maximally careful expert could configure - because
6740
+ most real-world security incidents are misconfiguration-by-default.
6741
+
6742
+ ### Summary matrix
6743
+
6744
+ Legend: **B** built-in and on by default, **b** built-in opt-in, **O** official
6745
+ add-on package, **3** third-party / community, **-** absent. The verdict column is Nifra
6746
+ relative to the field.
6747
+
6748
+ Dimension
6749
+ Nifra
6750
+ Hono
6751
+ Fastify
6752
+ Express
6753
+ Elysia
6754
+ Verdict
6755
+
6756
+ Default body-size cap
6757
+ B 1 MB
6758
+ - (opt-in mw)
6759
+ B 1 MiB
6760
+ - (needs body-parser)
6761
+ - (runtime cap only)
6762
+ par Fastify; ahead of the rest
6763
+
6764
+ Bounded streaming reads
6765
+ B
6766
+ b
6767
+ B
6768
+ O
6769
+ -
6770
+ par-plus
6771
+
6772
+ Body cap on delivered bytes (not the claimed length)
6773
+ B
6774
+ - (keys on Content-Length)
6775
+ b
6776
+ -
6777
+ -
6778
+ ahead
6779
+
6780
+ Proto-pollution-inert parsers (query / cookie / form / params)
6781
+ B null-proto
6782
+ B
6783
+ B
6784
+ B (5.x simple parser)
6785
+ B
6786
+ par
6787
+
6788
+ JSON proto-key rejection
6789
+ B (reject default)
6790
+ -
6791
+ B (proto + constructor)
6792
+ -
6793
+ b (schema-strip)
6794
+ par Fastify
6795
+
6796
+ Route ReDoS surface
6797
+ B none (strict grammar)
6798
+ B
6799
+ B (find-my-way)
6800
+ B (path-to-regexp)
6801
+ B
6802
+ par
6803
+
6804
+ Input schema validation
6805
+ B per-route Standard Schema
6806
+ b (validator mw)
6807
+ B ajv
6808
+ -
6809
+ B TypeBox
6810
+ par with best
6811
+
6812
+ Response over-exposure prevention
6813
+ B contract warn/enforce
6814
+ -
6815
+ b (serializer)
6816
+ -
6817
+ B normalize
6818
+ ahead (with Elysia)
6819
+
6820
+ Error responses leak-free by construction
6821
+ B env-independent flat 500
6822
+ B
6823
+ B (mostly)
6824
+ b (dev leaks stacks)
6825
+ B (mostly)
6826
+ ahead
6827
+
6828
+ CORS safe-by-construction
6829
+ B fail-loud ctor
6830
+ b (reflect-with-creds configurable)
6831
+ O
6832
+ O
6833
+ O
6834
+ ahead
6835
+
6836
+ CSRF
6837
+ B two layers (origin + signed double-submit)
6838
+ b (origin heuristic)
6839
+ O token-based
6840
+ 3 (csurf deprecated)
6841
+ -
6842
+ ahead; par Fastify
6843
+
6844
+ Security headers (first-party)
6845
+ b (nosniff/XFO/RP on; COOP/COEP/CORP/PP/CSP/HSTS opt-in)
6846
+ b (broader default set once added)
6847
+ O helmet
6848
+ 3 helmet
6849
+ 3
6850
+ par Hono; ahead of the helmet-dependent ones
6851
+
6852
+ Cookie signing + safe serialization
6853
+ B
6854
+ b
6855
+ O
6856
+ O
6857
+ B
6858
+ par-plus
6859
+
6860
+ `__Host-`/`__Secure-` prefix enforcement
6861
+
6862
+ B (throws on violation)
6863
+ B (throws on violation)
6864
+ O
6865
+ O
6866
+ -
6867
+ par Hono
6868
+
6869
+ Sessions (HttpOnly, SameSite, fixation, fail-closed)
6870
+ B
6871
+ -
6872
+ O
6873
+ 3 (express-session)
6874
+ 3
6875
+ ahead
6876
+
6877
+ JWT hardening (alg allowlist, none-reject, exp required)
6878
+ B
6879
+ b (no allowlist, exp optional)
6880
+ O (fast-jwt)
6881
+ 3
6882
+ O (jose)
6883
+ par-plus
6884
+
6885
+ Rate limiting
6886
+ B forced-decision ctor
6887
+ 3
6888
+ O
6889
+ 3
6890
+ 3
6891
+ ahead
6892
+
6893
+ Client IP / proxy trust
6894
+ B explicit, fail-closed
6895
+ b (per-runtime helpers)
6896
+ B (trustProxy)
6897
+ B (manual trust proxy)
6898
+ -
6899
+ ahead
6900
+
6901
+ Reverse-proxy hop-by-hop hygiene
6902
+ B (strips both directions)
6903
+ b (proxy helper)
6904
+ O (http-proxy)
6905
+ 3
6906
+ -
6907
+ ahead
6908
+
6909
+ Load shedding / overload
6910
+ B admission (in-flight + loop lag)
6911
+ -
6912
+ O under-pressure
6913
+ -
6914
+ -
6915
+ ahead; par Fastify
6916
+
6917
+ Static serving traversal defense
6918
+ B 7-layer + dotfile deny
6919
+ 3
6920
+ O (send-based)
6921
+ O (send-based)
6922
+ O
6923
+ ahead
6924
+
6925
+ Redirect safety (open-redirect + CRLF)
6926
+ B same-origin default
6927
+ -
6928
+ -
6929
+ -
6930
+ -
6931
+ ahead (unique)
6932
+
6933
+ WebSocket origin check
6934
+ B same-origin default
6935
+ -
6936
+ -
6937
+ -
6938
+ -
6939
+ ahead (unique)
6940
+
6941
+ Upload content verification (magic bytes, EXIF strip)
6942
+ B package
6943
+ -
6944
+ O limits only
6945
+ 3 (multer)
6946
+ b (t.File type/size)
6947
+ ahead
6948
+
6949
+ Constant-time webhook verification
6950
+ B (multi-key rotation)
6951
+ -
6952
+ -
6953
+ -
6954
+ -
6955
+ ahead (unique)
6956
+
6957
+ Idempotency-key replay
6958
+ B (atomic-claim store)
6959
+ -
6960
+ -
6961
+ -
6962
+ -
6963
+ ahead (unique)
6964
+
6965
+ Supply chain (runtime deps in core)
6966
+ 0
6967
+ 0
6968
+ ~dozen
6969
+ many
6970
+ few
6971
+ par with Hono
6972
+
6973
+ Machine-checkable route security evidence
6974
+
6975
+ B (`nifra assure`)
6976
+
6977
+ -
6978
+ -
6979
+ -
6980
+ -
6981
+ unique
6982
+
6983
+ Security lint (fail-open, non-constant-time, PII logs)
6984
+ B (NF-S001..007)
6985
+ -
6986
+ -
6987
+ -
6988
+ -
6989
+ unique
6990
+
6991
+ ### Dimension detail
6992
+
6993
+ #### Body limits and bounded reads
6994
+
6995
+ Nifra applies a 1 MB default cap to the schema lane and to
6996
+ `c.boundedBody`/`c.boundedJson`: an over-cap `Content-Length`
6997
+ is rejected before buffering (413), a malformed one is a 400, and a chunked or length-less
6998
+ body is drained through a true streaming byte counter that cancels at the cap. The cap is
6999
+ charged on the bytes actually **delivered**, never on the number a caller wrote in the
7000
+ header - so an adapter that rebuilds a request from an event envelope cannot smuggle a large
7001
+ payload behind a small declared length. Fastify enforces its 1 MiB default on real bytes and
7002
+ is the gold standard among the four; Nifra matches it. Hono&rsquo;s `bodyLimit` is
7003
+ an opt-in middleware that keys on `Content-Length`; Express relies on
7004
+ body-parser&rsquo;s 100 KB default only if you mount it; Elysia inherits the runtime cap
7005
+ (Bun&rsquo;s 128 MB).
7006
+
7007
+ #### Prototype pollution
7008
+
7009
+ Every hand-rolled parser (query, cookies, form, route params) produces null-prototype objects,
7010
+ so `__proto__`/`constructor` keys are inert there by construction. The
7011
+ JSON body lane **rejects** the poisoning shape by default - an own `__proto__`
7012
+ key, or a `constructor` carrying a `prototype` - answering the same flat
7013
+ 400 as malformed JSON, with `"strip"` and `"ignore"`
7014
+ as alternatives. That matches Fastify, whose `onProtoPoisoning` and
7015
+ `onConstructorPoisoning` both default to `&apos;error&apos;`. Hono and
7016
+ Express do not reject proto keys in the JSON body; Elysia strips undeclared keys only on a
7017
+ validated route via its schema normalization.
7018
+
7019
+ #### Validation and response contracts
7020
+
7021
+ Nifra does per-route Standard Schema validation for body/query/params, and - the
7022
+ differentiating half - **response** schemas as an enforced upper bound:
7023
+ `"warn"` logs undeclared fields, `"enforce"` strips
7024
+ them. That kills the `passwordHash`-in-the-response class at the framework layer.
7025
+ Fastify offers the same protection through a declared response schema (opt-in); Elysia&rsquo;s
7026
+ exactMirror normalization strips undeclared response fields and is its strongest default.
7027
+ Hono&rsquo;s validator is opt-in with no response-side story, and Express ships nothing.
7028
+
7029
+ #### CORS, CSRF, and error handling
7030
+
7031
+ Nifra&rsquo;s `cors()` throws at construction when `credentials: true`
7032
+ meets `origin: "*"`, never reflects an origin by default with credentials,
7033
+ and lands headers on error responses too - it is **the only one of the five where the
7034
+ dangerous configuration is a construction-time error**. The other four leave
7035
+ reflect-with-credentials configurable. CSRF is two independent layers (Origin/Referer check
7036
+ plus a signed double-submit token, timing-safe); Hono&rsquo;s CSRF is an Origin /
7037
+ Sec-Fetch-Site heuristic, Fastify&rsquo;s official token package is comparable, Express&rsquo;s
7038
+ csurf is deprecated, and Elysia ships none in-tree. Uncaught errors become a flat 500
7039
+ unconditionally - not env-gated - so no configuration leaks a stack to the wire; Express&rsquo;s
7040
+ dev handler printing stacks when `NODE_ENV` is unset is the perennial
7041
+ counter-example.
7042
+
7043
+ #### Headers, cookies, sessions, JWT
7044
+
7045
+ Both Nifra and Hono ship a first-party security-headers middleware you add with
7046
+ `use()`. Nifra&rsquo;s turns on nosniff, `X-Frame-Options: DENY`, and
7047
+ `Referrer-Policy: no-referrer`, and keeps the headers that can break a working app
7048
+ (COOP/COEP/CORP, Permissions-Policy, CSP, HSTS) opt-in by design; Hono&rsquo;s covers a broader
7049
+ list on by default once added, HSTS included. Cookie serialization enforces RFC 6265 name
7050
+ tokens, caps at 4096 bytes, and enforces `__Host-`/`__Secure-` prefix
7051
+ preconditions at serialization time, throwing on a violation - the same behavior Hono&rsquo;s
7052
+ cookie helper has, and stricter than a silent browser drop. Sessions are always HMAC-signed,
7053
+ HttpOnly, SameSite=Lax, fail-closed, with `regenerate()` for fixation - no
7054
+ competitor ships sessions in-tree. JWT verification demands an algorithm allowlist, rejects
7055
+ `alg: none`, blocks algorithm confusion at the key level, and requires
7056
+ `exp` by default; Hono&rsquo;s JWT middleware takes a single required algorithm with
7057
+ no enforced allowlist and treats `exp` as optional, so Nifra is stricter there.
7058
+
7059
+ #### Static serving, redirects, uploads, proxying
7060
+
7061
+ Static serving runs seven layers - decode with 400 on malformed input, NUL rejection,
7062
+ segment-precise `..` rejection, backslash rejection, containment via
7063
+ `resolve`+`relative`, a `realpath` symlink re-check, and a
7064
+ default **dotfile deny** (a served `.env`/`.git` answers 404).
7065
+ `redirect()` requires a same-origin path by default and rejects CR/LF - Nifra is the
7066
+ only one of the five with a safe-by-default redirect. Uploads verify the real type by magic
7067
+ bytes and strip EXIF/GPS by re-encode, where the others do size limits at best.
7068
+ `@nifrajs/proxy` strips the RFC 9110 hop-by-hop set - plus every
7069
+ `Connection`-nominated header - in **both** directions by default.
7070
+
7071
+ #### Rate limiting, IP trust, load shedding
7072
+
7073
+ The rate limiter&rsquo;s constructor refuses to run until you choose a key source, so the
7074
+ &ldquo;trust proxy = spoofable keys&rdquo; footgun is a construction-time error;
7075
+ `X-Forwarded-For` is ignored at the default and counted from the right only when you
7076
+ declare trusted proxies. `c.clientIp` is the socket peer unless you pass an explicit
7077
+ trust config, and short chains fail closed. Admission sheds on live capacity evidence
7078
+ (in-flight count plus event-loop lag). Fastify&rsquo;s under-pressure is the only peer for load
7079
+ shedding; nothing else in the field forces the key decision or fails closed on IP trust.
7080
+
7081
+ ### Beyond parity - what none of the four ship
7082
+
7083
+ -
7084
+ **Route assurance evidence.** Middleware publishes machine-checkable per-route claims
7085
+ (`AUTHENTICATED`, `BODY_BOUNDED`, `CSRF`,
7086
+ `RATE_LIMITED`, `IP_RESTRICTED`, `SECURITY_HEADERS`,
7087
+ `IDEMPOTENCY_KEY`), and `nifra assure` fails CI when a policy is unmet.
7088
+ Evidence is honest: an escape hatch suppresses its own claim.
7089
+
7090
+ -
7091
+ **Security lint rules** - fail-open gate detection, non-constant-time secret comparison,
7092
+ sensitive values in log calls, and more, running in `nifra check`.
7093
+
7094
+ -
7095
+ **Fail-loud constructors as a design norm** - CORS credentials+wildcard, rate-limit
7096
+ missing key decision, CSRF short secrets, in-memory stores in production - all
7097
+ construction-time errors, not runtime surprises.
7098
+
7099
+ -
7100
+ **Response-contract enforce mode, admission load-shedding, idempotency keys, magic-byte +
7101
+ EXIF-strip uploads, and constant-time webhook verification** - each exists somewhere as a
7102
+ third-party add-on for the others; Nifra ships them first-party with assurance integration.
7103
+
7104
+ ### Where each alternative is still the right call
7105
+
7106
+ This is a security read, not a blanket &ldquo;pick Nifra&rdquo;. Hono&rsquo;s run-anywhere
7107
+ minimalism and its zero-dependency core are genuinely excellent; Fastify&rsquo;s maturity,
7108
+ ecosystem, and years of adversarial attention are real advantages a young framework cannot
7109
+ claim; Express&rsquo;s ubiquity means every hire already knows it. Nifra&rsquo;s argument is
7110
+ narrower and honest: it makes the dangerous configurations _unrepresentable_, ships the
7111
+ hardening primitives first-party, and lets `nifra assure` prove in CI that every
7112
+ route is guarded. For the full capability and throughput comparison, see
7113
+ [vs other frameworks](/docs/comparison) and the
7114
+ [per-framework pages](/compare).
7115
+
6271
7116
  ## Nifra - Contract-derived adversarial testing
6272
7117
 
6273
7118
  > Turn route schemas into hostile input, response-conformance, replay, shrinking, and runtime-matrix tests with @nifrajs/testing.
@@ -6650,6 +7495,33 @@ level that regressed comes back with the reason it broke.
6650
7495
 
6651
7496
  # Package reference (READMEs)
6652
7497
 
7498
+ ## @nifrajs/agent
7499
+
7500
+ Provider-neutral, typed, resumable agent turns over Nifra tool contracts.
7501
+
7502
+ The package owns one bounded turn at a time. Model providers, state, approvals, budgets, idempotency,
7503
+ and telemetry are ports. The package never imports a model vendor and its state adapter stores only
7504
+ token-only turn evidence.
7505
+
7506
+ ```ts
7507
+ import { createAgentState, runAgent } from "@nifrajs/agent"
7508
+
7509
+ const result = await runAgent(definition, { value: input }, ports, {
7510
+ state: createAgentState("support-1"),
7511
+ maxTurns: 8,
7512
+ })
7513
+ ```
7514
+
7515
+ The public in-memory adapters are for local development and tests. Durable state, provider
7516
+ credentials, and operated policy remain adapter concerns.
7517
+
7518
+ Execution policies can be required by a tool contract. `createLocalProcessAdapter` applies cwd and
7519
+ environment filtering, timeouts, and cancellation to child processes. The local adapter is NOT a
7520
+ security boundary. Without OS-level sandboxing it contains crashes and accidents, not hostile code.
7521
+
7522
+ For the compact agent contract, see [`LLM.md`](./LLM.md). The full machine-readable corpus is
7523
+ [`../../llms-full.txt`](../../llms-full.txt).
7524
+
6653
7525
  ## @nifrajs/agent-telemetry
6654
7526
 
6655
7527
  Child-span telemetry for AI tool calls made through Nifra tool and MCP endpoints.
@@ -6706,6 +7578,59 @@ one cheap read instead of the whole corpus. For the wider framework: the repo's
6706
7578
  [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
6707
7579
  done-gate, or `nifra mcp` to give the agent live project tools.
6708
7580
 
7581
+ ## @nifrajs/aws-lambda
7582
+
7583
+ AWS Lambda adapter for nifra - API Gateway HTTP API (payload v2) and Lambda Function URLs, buffered or streaming, with post-decode body limits and single-site header merging. Dependency-free.
7584
+
7585
+ 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`.
7586
+
7587
+ ## Install
7588
+
7589
+ ```sh
7590
+ bun add @nifrajs/aws-lambda
7591
+ ```
7592
+
7593
+ ## Use
7594
+
7595
+ ```ts
7596
+ // handler.ts - one app per container, built at module scope
7597
+ import { handle, streamHandle, type LambdaEnv } from "@nifrajs/aws-lambda"
7598
+ import { server } from "@nifrajs/core"
7599
+
7600
+ const app = server<LambdaEnv>()
7601
+ .get("/", (c) => c.json({ ip: c.clientIp, requestId: c.env.context?.awsRequestId }))
7602
+
7603
+ export const handler = handle(app) // API Gateway HTTP API (v2) + Function URLs
7604
+ // or, on a Function URL with InvokeMode: RESPONSE_STREAM:
7605
+ // export const handler = streamHandle(app)
7606
+ ```
7607
+
7608
+ How it holds the line:
7609
+
7610
+ - **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`.
7611
+ - **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.
7612
+ - **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.
7613
+ - **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.
7614
+ - **Flat errors.** Uncaught failures collapse to the same `{"ok":false,"error":"internal_error"}` 500 as every other nifra runtime - the event is never echoed.
7615
+ - **Freeze-safe `waitUntil`.** Background work settles before the handler returns, so Lambda's container freeze cannot strand it.
7616
+
7617
+ The event and invocation context ride on `c.env.event` / `c.env.context` (type your app `server<LambdaEnv>()`). REST APIs (payload v1) and ALB events are out of scope; API Gateway does not support response streaming - `streamHandle` is for Function URLs.
7618
+
7619
+ ## Docs
7620
+
7621
+ - Reference: <https://nifra.dev/docs>
7622
+ - AI-readable: <https://nifra.dev/llms.txt>
7623
+
7624
+ MIT
7625
+
7626
+ ## For AI agents
7627
+
7628
+ Start with [`LLM.md`](./LLM.md) - this package's contract card (the exports you call + its footguns),
7629
+ one cheap read instead of the whole corpus. For the wider framework: the repo's
7630
+ [`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
7631
+ [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
7632
+ done-gate, or `nifra mcp` to give the agent live project tools.
7633
+
6709
7634
  ## @nifrajs/better-auth
6710
7635
 
6711
7636
  Mount [better-auth](https://better-auth.com) into a [nifra](https://github.com/nifra) app: one
@@ -6877,7 +7802,7 @@ The `nifra` CLI - zero-config dev/build/start for a nifra app, plus the agent su
6877
7802
  `routes/`).
6878
7803
 
6879
7804
  ```
6880
- nifra dev [--port <n>] [--poll] True-HMR dev server (Vite + nifra SSR).
7805
+ nifra dev [--port <n>] [--poll] True-HMR dev server (Bun + nifra SSR; Vite for a Vite-only app).
6881
7806
  nifra build [--out <dir>] [--target <t>] Complete deploy directory; target defaults to Bun.
6882
7807
  nifra start [--port <n>] [--out <dir>] Run the generated Bun server at <out>/server.js.
6883
7808
  nifra context Print this project's API + page routes + conventions.
@@ -7015,6 +7940,12 @@ Claude Code:
7015
7940
  claude mcp add nifra -- nifra mcp
7016
7941
  ```
7017
7942
 
7943
+ ## Structured verification
7944
+
7945
+ `nifra check --json` returns stable diagnostic codes and machine-actionable evidence. `nifra assure --json --strict` returns one versioned bundle with explicit pass, fail, and skip gate results plus a `green` or `red` verdict. Use `nifra fix --code NF-...` for a registered fix recipe, and `nifra contracts snapshot` to opt into route contract drift detection.
7946
+
7947
+ Assurance configs may append application-supplied rule packs. Pack codes must not use the reserved `NF-` prefix. Rules are pure source scans and run after the built-in rules.
7948
+
7018
7949
  Run the client from (or point it at) your project root. The protocol is hand-rolled
7019
7950
  (newline-delimited JSON-RPC 2.0) - no SDK dependency.
7020
7951
 
@@ -7458,6 +8389,27 @@ Part of the **[nifra](https://nifra.dev)** full-stack TypeScript framework - one
7458
8389
  bun add @nifrajs/i18n
7459
8390
  ```
7460
8391
 
8392
+ ## Locale detection
8393
+
8394
+ `negotiateLocale()` / `resolveLocale()` are pure: query parameter → cookie → `Accept-Language` →
8395
+ default, always answering from your `locales` allow-list (request input is matched, never echoed).
8396
+ The server plugin lives at `@nifrajs/i18n/detector` and needs `@nifrajs/core`:
8397
+
8398
+ ```ts
8399
+ import { localeDetector } from "@nifrajs/i18n/detector"
8400
+
8401
+ app.use(
8402
+ localeDetector({
8403
+ locales: ["en", "fr", "de"],
8404
+ defaultLocale: "en",
8405
+ queryParam: "lang",
8406
+ cookie: "locale",
8407
+ persist: true, // pin an explicit ?lang= choice into the cookie
8408
+ }),
8409
+ )
8410
+ // handlers see c.locale / c.localeSource; responses carry Content-Language
8411
+ ```
8412
+
7461
8413
  ## Docs
7462
8414
 
7463
8415
  - Reference: <https://nifra.dev/docs>
@@ -7937,6 +8889,17 @@ await serve(app, {
7937
8889
  })
7938
8890
  ```
7939
8891
 
8892
+ By default, `request.url` reflects the inbound `Host` header. Do not use that value for
8893
+ security decisions unless the deployment validates it. Set `allowedHosts` to reject unknown
8894
+ host authorities, or set `canonicalHost` behind a trusted proxy to build URLs from a fixed
8895
+ authority. If neither option is set, treat the host in `request.url` as client-controlled.
8896
+
8897
+ ## Static files
8898
+
8899
+ The optional `static` root is served before `app.fetch` for GET and HEAD requests. It is a
8900
+ public asset directory and must contain nothing private: no secrets, source maps, build
8901
+ metadata, or user uploads.
8902
+
7940
8903
  ## Graceful shutdown on signals
7941
8904
 
7942
8905
  Opt in to SIGTERM/SIGINT handling so a `docker stop` / Ctrl-C drains in-flight requests
@@ -8091,6 +9054,45 @@ one cheap read instead of the whole corpus. For the wider framework: the repo's
8091
9054
  [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
8092
9055
  done-gate, or `nifra mcp` to give the agent live project tools.
8093
9056
 
9057
+ ## @nifrajs/proxy
9058
+
9059
+ Reverse proxy for nifra - stream requests to one fixed upstream origin with hop-by-hop hygiene, SSRF-proof by construction. Dependency-free.
9060
+
9061
+ 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`.
9062
+
9063
+ ## Install
9064
+
9065
+ ```sh
9066
+ bun add @nifrajs/proxy
9067
+ ```
9068
+
9069
+ ## Use
9070
+
9071
+ ```ts
9072
+ import { server } from "@nifrajs/core"
9073
+ import { createProxy } from "@nifrajs/proxy"
9074
+
9075
+ const upstream = createProxy({ upstream: "http://127.0.0.1:8081" })
9076
+ const app = server().mountFetch("/api", upstream, { stripPrefix: true })
9077
+ ```
9078
+
9079
+ 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, forwarding metadata (`X-Forwarded-*`) is dropped unless `forwardClientIp: true`, 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`.
9080
+
9081
+ ## Docs
9082
+
9083
+ - Reference: <https://nifra.dev/docs>
9084
+ - AI-readable: <https://nifra.dev/llms.txt>
9085
+
9086
+ MIT
9087
+
9088
+ ## For AI agents
9089
+
9090
+ Start with [`LLM.md`](./LLM.md) - this package's contract card (the exports you call + its footguns),
9091
+ one cheap read instead of the whole corpus. For the wider framework: the repo's
9092
+ [`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
9093
+ [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
9094
+ done-gate, or `nifra mcp` to give the agent live project tools.
9095
+
8094
9096
  ## @nifrajs/runner
8095
9097
 
8096
9098
  Run requests through a nifra app and capture **structured results** - the shared engine behind the
@@ -8204,6 +9206,69 @@ one cheap read instead of the whole corpus. For the wider framework: the repo's
8204
9206
  [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
8205
9207
  done-gate, or `nifra mcp` to give the agent live project tools.
8206
9208
 
9209
+ ## @nifrajs/skills
9210
+
9211
+ Portable agent skills for [Nifra](../../README.md) - one `SKILL.md` bundle, published to every agent
9212
+ surface that reads a `skills/` directory.
9213
+
9214
+ A skill is loaded on demand: the agent sees only each skill's name and description until it decides
9215
+ one applies, then reads the body. That keeps Nifra's conventions out of the context window until they
9216
+ matter.
9217
+
9218
+ ```
9219
+ skills/
9220
+ nifra/SKILL.md entry point - MCP-first rule, the core loop, the non-negotiables
9221
+ nifra-api/SKILL.md typed routes, schemas, middleware, WebSockets, the never-throwing client
9222
+ nifra-web/SKILL.md file routing, loaders/actions, SSR, islands, the client/server boundary
9223
+ nifra-verify/SKILL.md check, assure, capabilities, manifest diff, the verification ladder
9224
+ ```
9225
+
9226
+ ## Install
9227
+
9228
+ **Pi**
9229
+
9230
+ ```sh
9231
+ pi install npm:@nifrajs/skills
9232
+ ```
9233
+
9234
+ **Claude Code**
9235
+
9236
+ ```sh
9237
+ /plugin marketplace add nifrajs/nifra
9238
+ /plugin install nifra@nifra
9239
+ ```
9240
+
9241
+ **Anything that reads a skills directory** (Cursor, `.agents/skills`, a project `.pi/skills`)
9242
+
9243
+ ```sh
9244
+ bun add -d @nifrajs/skills
9245
+ cp -R node_modules/@nifrajs/skills/skills/* .agents/skills/
9246
+ ```
9247
+
9248
+ ## These skills are deliberately thin
9249
+
9250
+ They teach shape and rules, not signatures. Signatures come from the MCP server, which is typechecked
9251
+ against the version installed in the project being edited:
9252
+
9253
+ ```sh
9254
+ bunx @nifrajs/cli init-agents # register the MCP server in an existing app
9255
+ ```
9256
+
9257
+ Not in a Nifra project? The docs tools are hosted at `https://mcp.nifra.dev`.
9258
+
9259
+ That split is the anti-drift design: a skill that restated the API would rot on the next release,
9260
+ while a skill that says "ask `nifra_types`" stays correct. Keep it that way when editing these files -
9261
+ if you find yourself pasting a signature in, reach for a tool reference instead.
9262
+
9263
+ ## For AI agents
9264
+
9265
+ Start with [`LLM.md`](./LLM.md) - this package's contract card, one cheap read instead of the whole
9266
+ corpus. For the wider framework: the repo's [`AGENTS.md`](../../AGENTS.md) is the copy-paste quick
9267
+ reference, and [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run
9268
+ `nifra check` as the done-gate, or `nifra mcp` to give the agent live project tools.
9269
+
9270
+ MIT licensed.
9271
+
8207
9272
  ## @nifrajs/storage
8208
9273
 
8209
9274
  Blob storage for nifra - one `StorageAdapter` interface, several adapters. The **persistence half of
@@ -8464,6 +9529,7 @@ that every call carries and captures cookies via a shared jar.
8464
9529
  - `testSession<App>(app, { origin?, cookies? })` → `{ client: Treaty<App>, cookies: CookieJar }`.
8465
9530
  - `cookieJar()` → `CookieJar` - `header()` · `applyTo(headers)` · `store(response)` · `set` · `get` · `clear` · `size`.
8466
9531
  Honours removal (`Max-Age=0` / past `Expires`); other cookie attributes are ignored (in-process, same-origin).
9532
+ - `proveIdempotency({ run, runs? })` → a token-only proof that canonical effect ledgers replay identically.
8467
9533
 
8468
9534
  For a **stateless** request (no cookies), use `testClient` from `@nifrajs/client` directly.
8469
9535
 
@@ -8837,6 +9903,10 @@ done-gate, or `nifra mcp` to give the agent live project tools.
8837
9903
 
8838
9904
  # Complete export index (generated from each package's src/index.ts)
8839
9905
 
9906
+ ## @nifrajs/agent
9907
+
9908
+ `AgentApprovalPort`, `AgentApprovalResult`, `AgentDefinition`, `AgentModelPort`, `AgentModelRequest`, `AgentModelResponse`, `AgentPendingContinuation`, `AgentPendingKind`, `AgentPorts`, `AgentRunResult`, `AgentStateStore`, `AgentStatus`, `AgentStepEvidence`, `AgentTelemetryPort`, `AgentToolContract`, `AgentToolDescriptor`, `AgentToolResult`, `AgentTranscript`, `AgentTurnBaseResult`, `AgentTurnError`, `AgentTurnInput`, `AgentTurnResult`, `AgentTurnState`, `createAgentState`, `createLocalProcessAdapter`, `ExecutionPolicy`, `ExecutionPolicyAdapter`, `LOCAL_PROCESS_LIMITATION`, `LocalProcessAdapter`, `LocalProcessAdapterOptions`, `LocalProcessPolicyError`, `LocalProcessRequest`, `LocalProcessResult`, `MemoryAgentStateStore`, `replayAgent`, `resumeAgent`, `runAgent`, `RunAgentOptions`, `turn`
9909
+
8840
9910
  ## @nifrajs/agent-telemetry
8841
9911
 
8842
9912
  `agentTelemetry`, `AgentTelemetryOptions`, `consoleAgentExporter`
@@ -8845,6 +9915,10 @@ done-gate, or `nifra mcp` to give the agent live project tools.
8845
9915
 
8846
9916
  `AuthorizationRequest`, `Authorizer`, `createSessions`, `csrf`, `CsrfOptions`, `GuardOptions`, `isAuthorized`, `KVNamespaceLike`, `KVSessionStore`, `MemorySessionStore`, `MemorySessionStoreOptions`, `requireAuthorization`, `requireSession`, `requireUser`, `Session`, `SessionContext`, `SessionCookieOptions`, `SessionManager`, `SessionOptions`, `SessionRecord`, `SessionStore`
8847
9917
 
9918
+ ## @nifrajs/aws-lambda
9919
+
9920
+ `FetchHandler`, `handle`, `handler`, `LambdaContext`, `LambdaEnv`, `LambdaEvent`, `LambdaOptions`, `LambdaPlatform`, `LambdaResult`, `ResponseStream`, `streamHandle`
9921
+
8848
9922
  ## @nifrajs/better-auth
8849
9923
 
8850
9924
  `authed`, `AuthedOptions`, `betterAuth`, `BetterAuthLike`, `BetterAuthOptions`, `getSession`, `Principal`, `PrincipalFor`, `requirePrincipal`, `requireSession`, `RequireSessionOptions`, `SessionOf`, `SessionUserOf`, `WithPrincipal`
@@ -8887,7 +9961,7 @@ done-gate, or `nifra mcp` to give the agent live project tools.
8887
9961
 
8888
9962
  ## @nifrajs/i18n
8889
9963
 
8890
- `createFormatter`, `Formatter`, `Locale`, `Messages`, `negotiateLocale`, `NegotiateOptions`
9964
+ `createFormatter`, `Formatter`, `Locale`, `LocaleParts`, `LocaleSource`, `Messages`, `negotiateLocale`, `NegotiateOptions`, `ResolvedLocale`, `resolveLocale`
8891
9965
 
8892
9966
  ## @nifrajs/image
8893
9967
 
@@ -8929,6 +10003,10 @@ done-gate, or `nifra mcp` to give the agent live project tools.
8929
10003
 
8930
10004
  `prompt`, `Prompt`, `PromptInputError`, `PromptMessage`, `PromptOutputError`, `PromptRequest`, `PromptResponseFormat`, `RunOptions`
8931
10005
 
10006
+ ## @nifrajs/proxy
10007
+
10008
+ `createProxy`, `ProxyContext`, `ProxyHandler`, `ProxyOptions`
10009
+
8932
10010
  ## @nifrajs/runner
8933
10011
 
8934
10012
  `AppLike`, `RequestSpec`, `runApp`, `RunOptions`, `runRequest`, `RunResult`
@@ -8943,7 +10021,7 @@ done-gate, or `nifra mcp` to give the agent live project tools.
8943
10021
 
8944
10022
  ## @nifrajs/testing
8945
10023
 
8946
- `AdapterCertificationError`, `AdapterCertificationProfile`, `AdapterCertificationReport`, `AdversarialContractError`, `AdversarialContractOptions`, `AdversarialContractReport`, `AdversarialContractResult`, `AppLike`, `assertAdapterCertification`, `assertAdversarialContract`, `assertIncidentReplays`, `cacheStoreCertificationProfile`, `CapturedRequest`, `CapturedRequestInput`, `captureIncident`, `CaptureIncidentOptions`, `CertifiableCacheEntry`, `CertifiableCacheStore`, `CertifiableDomainEvent`, `CertifiableEventDeliveryAdapter`, `CertifiableEventRecord`, `CertifiableJobStore`, `CertifiableRuntimeAdapter`, `CertifiableRuntimeServer`, `CertifiableStorageAdapter`, `CertificationCapabilityEvidence`, `CertificationCheck`, `CertificationCheckEvidence`, `certifyAdapter`, `ContractCaseContext`, `ContractCaseKind`, `ContractCoverageGap`, `ContractCoverageGapCode`, `ContractReplay`, `ContractRuntime`, `ContractTarget`, `ContractTestApp`, `ContractWitness`, `cookieJar`, `CookieJar`, `createFailureLab`, `defineCertificationProfile`, `eventDeliveryCertificationProfile`, `FailureDirective`, `FailureEvidence`, `FailureInjectedError`, `FailureKind`, `FailureLab`, `FailureLabOptions`, `FailureReplay`, `FailureScenario`, `FailureScenarioReport`, `generateRegressionTest`, `GenerateRegressionTestOptions`, `IncidentCapsule`, `IncidentReplayError`, `IncidentReplayResult`, `jobStoreCertificationProfile`, `redactForEmission`, `replayIncident`, `ReplayIncidentOptions`, `runAdversarialContract`, `runFailureScenario`, `runtimeAdapterCertificationProfile`, `shapeOf`, `storageAdapterCertificationProfile`, `testSession`, `TestSession`, `TestSessionOptions`, `verifyAdapterCertification`
10024
+ `AdapterCertificationError`, `AdapterCertificationProfile`, `AdapterCertificationReport`, `AdversarialContractError`, `AdversarialContractOptions`, `AdversarialContractReport`, `AdversarialContractResult`, `AppLike`, `assertAdapterCertification`, `assertAdversarialContract`, `assertIncidentReplays`, `assertTrajectoryInvariants`, `cacheStoreCertificationProfile`, `CapturedRequest`, `CapturedRequestInput`, `captureIncident`, `CaptureIncidentOptions`, `CertifiableCacheEntry`, `CertifiableCacheStore`, `CertifiableDomainEvent`, `CertifiableEventDeliveryAdapter`, `CertifiableEventRecord`, `CertifiableJobStore`, `CertifiableRuntimeAdapter`, `CertifiableRuntimeServer`, `CertifiableStorageAdapter`, `CertificationCapabilityEvidence`, `CertificationCheck`, `CertificationCheckEvidence`, `certifyAdapter`, `checkTrajectoryInvariants`, `ContractCaseContext`, `ContractCaseKind`, `ContractCoverageGap`, `ContractCoverageGapCode`, `ContractReplay`, `ContractRuntime`, `ContractTarget`, `ContractTestApp`, `ContractWitness`, `cookieJar`, `CookieJar`, `createFailureLab`, `createTrajectoryTranscript`, `CreateTrajectoryTranscriptOptions`, `defineCertificationProfile`, `defineFaultProfile`, `EffectLedger`, `eventDeliveryCertificationProfile`, `FailureDirective`, `FailureEvidence`, `FailureInjectedError`, `FailureKind`, `FailureLab`, `FailureLabOptions`, `FailureReplay`, `FailureScenario`, `FailureScenarioReport`, `FaultProfile`, `FaultProfileFault`, `FaultProfileFaultKind`, `FaultProfileReport`, `FaultProfileScenario`, `FaultProfileScenarioReport`, `generateRegressionTest`, `GenerateRegressionTestOptions`, `httpToolAdapter`, `IdempotencyDivergence`, `IdempotencyProof`, `IncidentCapsule`, `IncidentReplayError`, `IncidentReplayResult`, `inProcessToolAdapter`, `jobStoreCertificationProfile`, `mcpToolAdapter`, `proveIdempotency`, `recordTrajectory`, `redactForEmission`, `referenceFaultProfile`, `replayIncident`, `ReplayIncidentOptions`, `replayTrajectory`, `ReplayTrajectoryOptions`, `runAdversarialContract`, `runFailureScenario`, `runFaultProfile`, `RunFaultProfileOptions`, `runtimeAdapterCertificationProfile`, `runTrajectory`, `shapeOf`, `storageAdapterCertificationProfile`, `testSession`, `TestSession`, `TestSessionOptions`, `testToolAdapter`, `TrajectoryInvariantId`, `TrajectoryInvariantOptions`, `TrajectoryInvariantResult`, `trajectoryRegressionId`, `TrajectoryReplayResult`, `TrajectoryRun`, `TrajectoryTranscript`, `verifyAdapterCertification`
8947
10025
 
8948
10026
  ## @nifrajs/ts-plugin
8949
10027
 
@@ -8955,7 +10033,7 @@ done-gate, or `nifra mcp` to give the agent live project tools.
8955
10033
 
8956
10034
  ## @nifrajs/web
8957
10035
 
8958
- `Action`, `ACTION_GLOBAL`, `apiKey`, `assertRenderAdapterConformance`, `Blocker`, `BlockerController`, `BlockerFunction`, `BlockerLocation`, `BlockerState`, `BrowserNavigate`, `buildManifest`, `CachedResponse`, `CacheStore`, `canonical`, `clientEntry`, `ClientRouter`, `ClientRouterOptions`, `createClientRouter`, `createMatcher`, `createMutation`, `createQueryClient`, `createWebApp`, `CreateWebAppOptions`, `DATA_GLOBAL`, `DATA_HEADER`, `DEFAULT_DEV_PORT`, `defer`, `Deferred`, `DehydratedState`, `disableDraft`, `DRAFT_COOKIE`, `DraftCookieControls`, `enableDraft`, `EnableDraftOptions`, `enumerateStaticRoutes`, `Fetcher`, `FetcherState`, `FetchRouteData`, `filePathToPattern`, `filePathToPatterns`, `FontDisplay`, `fontFace`, `FontFace`, `fontPreload`, `FontPreloadInput`, `FontSource`, `gate`, `generateClientEntry`, `GenerateClientEntryOptions`, `generateRouteSearchTypes`, `GenerateRouteSearchTypesOptions`, `generateServerManifest`, `GenerateServerManifestOptions`, `getBrowserNavigate`, `GetStaticPaths`, `gone`, `hashQueryKey`, `IDLE_BLOCKER`, `InertScriptType`, `InfiniteData`, `InfiniteQueryHandle`, `InfiniteQueryOptions`, `isDraftEnabled`, `ISR_REVALIDATE_HEADER`, `ISR_REVALIDATE_TAGS_HEADER`, `ISR_STATUS_HEADER`, `ISRApp`, `ISROptions`, `ISRPlatform`, `jsonLd`, `KVCacheStore`, `KVCacheStoreOptions`, `KVNamespaceLike`, `LAYOUT_DATA_GLOBAL`, `LayoutEntry`, `LinkDescriptor`, `Loader`, `LoaderContext`, `manifest`, `Manifest`, `MemoryCacheStore`, `MemoryCacheStoreOptions`, `mergeHeads`, `meta`, `Meta`, `MetaArgs`, `MetaInput`, `MountRouterOptions`, `MutationCallbacks`, `MutationHandle`, `MutationState`, `MutationStatus`, `NAV_FROM_HEADER`, `NavigateFunction`, `NavigateOptions`, `NavigateSearchOf`, `NavigateTargetInput`, `notFound`, `openGraph`, `OpenGraphInput`, `PRE_HYDRATION_GUARD`, `previewEndpoint`, `PreviewEndpointOptions`, `PublicDirCache`, `QueryClient`, `QueryClientOptions`, `QueryHandle`, `QueryOptions`, `QueryState`, `QueryStatus`, `redirect`, `REDIRECT_HEADER`, `RedirectOptions`, `registerBlocker`, `RenderAdapter`, `RenderAdapterConformanceError`, `RenderAdapterConformanceFixture`, `RenderAssemblyCache`, `RenderedPage`, `renderPage`, `RenderPageInput`, `RenderPageOptions`, `renderPageResult`, `RenderProps`, `resolveMeta`, `resolveNavigate`, `resolvePublicPath`, `RETAIN_HEADER`, `revalidate`, `REVALIDATE_HEADER`, `revalidateEndpoint`, `RevalidateEndpointOptions`, `RevalidateResult`, `ROOT_ATTRIBUTE`, `ROUTE_GLOBAL`, `RouteEntry`, `RouteMatch`, `RouteModule`, `RoutePattern`, `RouterState`, `RouteSearch`, `routeStyles`, `ScriptDescriptor`, `searchOf`, `SearchOf`, `searchOfChain`, `serializeData`, `serializeSearch`, `servePublicDir`, `ServePublicDirOptions`, `SERVER_FN_MODULE`, `SERVER_ONLY_MODULE`, `ServerOnly`, `setBlockerController`, `setBrowserNavigate`, `StaticPath`, `StaticPaths`, `StaticRoutes`, `STATUS_HEADER`, `statusPage`, `StatusPageOptions`, `styles`, `Submission`, `SubmitOptions`, `unsafeInlineScript`, `UnsafeScriptDescriptor`, `withISR`
10036
+ `Action`, `ACTION_GLOBAL`, `apiKey`, `assertRenderAdapterConformance`, `Blocker`, `BlockerController`, `BlockerFunction`, `BlockerLocation`, `BlockerState`, `BrowserNavigate`, `buildManifest`, `CachedResponse`, `CacheStore`, `canonical`, `clientEntry`, `ClientRouter`, `ClientRouterOptions`, `createClientRouter`, `createMatcher`, `createMutation`, `createQueryClient`, `createWebApp`, `CreateWebAppOptions`, `DATA_GLOBAL`, `DATA_HEADER`, `DEFAULT_DEV_PORT`, `defer`, `Deferred`, `DehydratedState`, `disableDraft`, `DRAFT_COOKIE`, `DraftCookieControls`, `enableDraft`, `EnableDraftOptions`, `enumerateStaticRoutes`, `Fetcher`, `FetcherState`, `FetchRouteData`, `filePathToPattern`, `filePathToPatterns`, `FontDisplay`, `fontFace`, `FontFace`, `fontPreload`, `FontPreloadInput`, `FontSource`, `gate`, `generateClientEntry`, `GenerateClientEntryOptions`, `generateRouteSearchTypes`, `GenerateRouteSearchTypesOptions`, `generateServerManifest`, `GenerateServerManifestOptions`, `getBrowserNavigate`, `GetStaticPaths`, `gone`, `hashQueryKey`, `IDLE_BLOCKER`, `InertScriptType`, `InfiniteData`, `InfiniteQueryHandle`, `InfiniteQueryOptions`, `isDraftEnabled`, `ISR_REVALIDATE_HEADER`, `ISR_REVALIDATE_TAGS_HEADER`, `ISR_STATUS_HEADER`, `ISRApp`, `ISROptions`, `ISRPlatform`, `jsonLd`, `KVCacheStore`, `KVCacheStoreOptions`, `KVNamespaceLike`, `LAYOUT_DATA_GLOBAL`, `LayoutEntry`, `LinkDescriptor`, `Loader`, `LoaderContext`, `manifest`, `Manifest`, `MemoryCacheStore`, `MemoryCacheStoreOptions`, `mergeHeads`, `meta`, `Meta`, `MetaArgs`, `MetaDescriptor`, `MetaInput`, `MountRouterOptions`, `MutationCallbacks`, `MutationHandle`, `MutationState`, `MutationStatus`, `NAV_FROM_HEADER`, `NavigateFunction`, `NavigateOptions`, `NavigateSearchOf`, `NavigateTargetInput`, `notFound`, `openGraph`, `OpenGraphInput`, `PRE_HYDRATION_GUARD`, `previewEndpoint`, `PreviewEndpointOptions`, `PublicDirCache`, `QueryClient`, `QueryClientOptions`, `QueryHandle`, `QueryOptions`, `QueryState`, `QueryStatus`, `redirect`, `REDIRECT_HEADER`, `RedirectOptions`, `registerBlocker`, `RenderAdapter`, `RenderAdapterConformanceError`, `RenderAdapterConformanceFixture`, `RenderAssemblyCache`, `RenderedPage`, `renderPage`, `RenderPageInput`, `RenderPageOptions`, `renderPageResult`, `RenderProps`, `resolveMeta`, `resolveNavigate`, `resolvePublicPath`, `RETAIN_HEADER`, `revalidate`, `REVALIDATE_HEADER`, `revalidateEndpoint`, `RevalidateEndpointOptions`, `RevalidateResult`, `ROOT_ATTRIBUTE`, `ROUTE_GLOBAL`, `RouteEntry`, `RouteMatch`, `RouteModule`, `RoutePattern`, `RouterState`, `RouteSearch`, `routeStyles`, `ScriptDescriptor`, `searchOf`, `SearchOf`, `searchOfChain`, `serializeData`, `serializeSearch`, `servePublicDir`, `ServePublicDirOptions`, `SERVER_FN_MODULE`, `SERVER_ONLY_MODULE`, `ServerOnly`, `setBlockerController`, `setBrowserNavigate`, `setSsrModuleLoader`, `ssrModuleLoader`, `SsrModuleLoader`, `StaticPath`, `StaticPaths`, `StaticRoutes`, `STATUS_HEADER`, `statusPage`, `StatusPageOptions`, `styles`, `Submission`, `SubmitOptions`, `unsafeInlineScript`, `UnsafeScriptDescriptor`, `withISR`
8959
10037
 
8960
10038
  ## @nifrajs/web-preact
8961
10039