@nifrajs/cli 0.1.0-alpha.1 → 0.1.0-alpha.3
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 +8 -0
- package/dist/check.d.ts +82 -3
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +326 -11
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts +11 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +178 -17
- package/dist/cli.js.map +1 -1
- package/dist/init-agents.d.ts +58 -0
- package/dist/init-agents.d.ts.map +1 -0
- package/dist/init-agents.js +126 -0
- package/dist/init-agents.js.map +1 -0
- package/dist/introspect.d.ts +77 -2
- package/dist/introspect.d.ts.map +1 -1
- package/dist/introspect.js +138 -9
- package/dist/introspect.js.map +1 -1
- package/dist/mcp-render.d.ts.map +1 -1
- package/dist/mcp-render.js +99 -8
- package/dist/mcp-render.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +49 -21
- package/dist/mcp.js.map +1 -1
- package/dist/openapi-tool.d.ts +12 -3
- package/dist/openapi-tool.d.ts.map +1 -1
- package/dist/openapi-tool.js +21 -4
- package/dist/openapi-tool.js.map +1 -1
- package/dist/port.d.ts +144 -0
- package/dist/port.d.ts.map +1 -0
- package/dist/port.js +498 -0
- package/dist/port.js.map +1 -0
- package/docs/examples.json +91 -133
- package/docs/llms-full.txt +1090 -35
- package/package.json +6 -5
- package/src/check.ts +398 -14
- package/src/cli.ts +176 -21
- package/src/init-agents.ts +176 -0
- package/src/introspect.ts +208 -8
- package/src/mcp-render.ts +106 -11
- package/src/mcp.ts +56 -20
- package/src/openapi-tool.ts +24 -4
- package/src/port.ts +663 -0
package/docs/examples.json
CHANGED
|
@@ -6,40 +6,64 @@
|
|
|
6
6
|
"code": "import { client } from \"@nifrajs/client\"\nimport type { app } from \"./server\"\n\n// Infers the server's types directly — no codegen, no schema duplication.\nconst api = client<typeof app>(\"https://api.example.com\")\n\nconst { data } = await api.users({ id: \"1\" }).get() // path param → /users/1\nawait api.users.post({ name: \"Ada\" }) // POST body\nawait api.search.get({ query: { page: \"3\" } }) // query string\nawait api.users({ id: \"1\" }).posts({ postId: \"2\" }).get() // nested params"
|
|
7
7
|
},
|
|
8
8
|
{
|
|
9
|
-
"name": "
|
|
10
|
-
"topic": "Nifra —
|
|
11
|
-
"slug": "
|
|
12
|
-
"code": "
|
|
9
|
+
"name": "SETUP",
|
|
10
|
+
"topic": "Nifra — Auth & sessions",
|
|
11
|
+
"slug": "auth",
|
|
12
|
+
"code": "// auth.ts — one session manager. LAZY so a route module can import it without shipping it to the\n// browser (see the warning below). Store mode keeps data server-side; cookie mode (no store) is stateless.\nimport { createSessions, MemorySessionStore } from \"@nifrajs/auth\"\n\nlet manager: ReturnType<typeof createSessions> | undefined\nexport const getSessions = () => (manager ??= createSessions({\n secret: process.env.SESSION_SECRET!, // ≥ 16 chars; rotating it invalidates all sessions\n store: new MemorySessionStore(), // prod: new KVSessionStore(env.SESSIONS)\n // cookie: { secure: false }, // local http dev only\n}))"
|
|
13
13
|
},
|
|
14
14
|
{
|
|
15
|
-
"name": "
|
|
16
|
-
"topic": "Nifra —
|
|
17
|
-
"slug": "
|
|
18
|
-
"code": "
|
|
15
|
+
"name": "BACKEND",
|
|
16
|
+
"topic": "Nifra — Backends & API (dev and prod)",
|
|
17
|
+
"slug": "backends",
|
|
18
|
+
"code": "// backend.ts — a normal @nifrajs/core server. Routes live at the full /api/... path.\nimport { server } from \"@nifrajs/core\"\nimport { t } from \"@nifrajs/schema\"\n\nexport const backend = server()\n .post(\"/api/sync\", { body: t.object({ cursor: t.string() }) }, async (c) => {\n // validated body; runs the same whether called in-process (a loader) or over HTTP (a client fetch)\n return { applied: 12, nextCursor: c.body.cursor }\n })\n .get(\"/api/me\", (c) => ({ id: c.cookies.session ?? null }))"
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
|
-
"name": "
|
|
22
|
-
"topic": "Nifra —
|
|
23
|
-
"slug": "
|
|
24
|
-
"code": "//
|
|
21
|
+
"name": "CLIENT",
|
|
22
|
+
"topic": "Nifra — Backends & API (dev and prod)",
|
|
23
|
+
"slug": "backends",
|
|
24
|
+
"code": "// A browser island / client component hits the mounted HTTP routes with the typed client.\nimport { client } from \"@nifrajs/client\"\nimport type { backend } from \"./backend\"\n\nconst api = client<typeof backend>(\"\") // same-origin: /api/sync is served by createWebApp's mount\nexport async function runSync(cursor: string) {\n const { data, error } = await api.api.sync.post({ cursor })\n return error ? { applied: 0 } : data\n}"
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
|
-
"name": "
|
|
28
|
-
"topic": "Nifra —
|
|
29
|
-
"slug": "
|
|
30
|
-
"code": "//
|
|
27
|
+
"name": "LOADER",
|
|
28
|
+
"topic": "Nifra — Backends & API (dev and prod)",
|
|
29
|
+
"slug": "backends",
|
|
30
|
+
"code": "// routes/index.tsx — a loader calls the backend IN-PROCESS via ctx.api (no network).\nimport type { LoaderContext } from \"@nifrajs/web\"\n\nexport async function loader(ctx: LoaderContext) {\n const api = ctx.api as { me: { get(): Promise<{ data: { id: string | null } }> } }\n const res = await api.me.get() // in-process: full validation/middleware, no HTTP round-trip\n return { me: res.data }\n}"
|
|
31
31
|
},
|
|
32
32
|
{
|
|
33
|
-
"name": "
|
|
34
|
-
"topic": "Nifra —
|
|
35
|
-
"slug": "
|
|
36
|
-
"code": "//
|
|
33
|
+
"name": "WIRE",
|
|
34
|
+
"topic": "Nifra — Backends & API (dev and prod)",
|
|
35
|
+
"slug": "backends",
|
|
36
|
+
"code": "// server.ts (prod) — createWebApp serves pages AND auto-mounts the backend at /api/*.\nimport { inProcessClient } from \"@nifrajs/client\"\nimport { createWebApp } from \"@nifrajs/web\"\nimport { reactAdapter } from \"@nifrajs/web-react\"\nimport { backend } from \"./backend\"\nimport { clientEntry, manifest } from \"./server-manifest\"\n\nexport const app = createWebApp({\n adapter: reactAdapter,\n manifest,\n clientEntry,\n api: inProcessClient(backend), // → ctx.api in loaders/actions AND auto-mounted at /api/*\n // apiPrefix: \"/api\", // the default; pass \"\" to disable the HTTP mount (pages only)\n})\n\n// Bun: Bun.serve({ fetch: app.fetch }). No `if (pathname.startsWith(\"/api/\")) …` branch needed —\n// POST /api/sync, GET /api/me, etc. are dispatched to the backend BEFORE the page router sees them."
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"name": "DUAL_API",
|
|
40
|
+
"topic": "Nifra — Framework contract",
|
|
41
|
+
"slug": "contract",
|
|
42
|
+
"code": "// TWO ways to handle a request — pick by who is calling.\nimport type { LoaderContext } from \"@nifrajs/web\"\n\n// (1) A page's own data + mutations → route exports. A PUBLIC POST is a route ACTION\n// (routes/contact.tsx) — the browser POSTs the route's own URL, nifra runs `action`:\nexport async function action(ctx: LoaderContext) {\n const form = await ctx.request.formData() // the browser POSTed this route\n return { ok: true, body: String(form.get(\"body\")) } // → props.actionData on re-render\n}\n\n// (2) ctx.api is the IN-PROCESS backend client (the inProcessClient(server) the app was given).\n// It is LOADERS-ONLY: the SSR backend registers GET handlers, so a .post() to ctx.api 405s.\nexport async function loader(ctx: LoaderContext) {\n // ✅ a GET in-process — no HTTP hop. (A .post() to ctx.api would 405; write an `action` instead.)\n return { id: ctx.params.id }\n}\n\n// Rule of thumb: data INTO a page → loader + ctx.api (GET). A mutation FROM the browser\n// (a form/fetch POST) → an `action` export on the route. Don't `inProcessClient(api).post()`."
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"name": "PARAM_404",
|
|
46
|
+
"topic": "Nifra — Framework contract",
|
|
47
|
+
"slug": "contract",
|
|
48
|
+
"code": "// routes/users/[id].tsx — plain SSR runs the loader for ANY :id value. `fallback: \"404\"`\n// only takes effect under prerender/CDN, so on on-demand SSR you MUST guard and 404 yourself.\nimport type { LoaderContext } from \"@nifrajs/web\"\n\ndeclare function lookupUser(id: string): Promise<{ id: string } | null>\n\nexport async function loader(ctx: LoaderContext) {\n // ctx.params.id is `string | undefined` (an optional segment) — default it before the lookup.\n const user = await lookupUser(ctx.params.id ?? \"\")\n if (!user) {\n // CONTROL FLOW: a thrown Response is an INTENTIONAL HTTP response — it propagates untouched\n // (no _error boundary, no 500). This is how you 404 a missing record.\n throw new Response(\"Not Found\", { status: 404 })\n }\n return { user }\n}\n\ndeclare function lookupUser(id: string): Promise<{ name: string } | null>\n\n// The throw contract, stated once:\n// • throw a Response → that exact HTTP response is sent (404 / redirect() / 403 / …). Control flow.\n// • throw an Error → renders the nearest _error boundary if one exists, else a 500. A bug.\n// So NEVER `throw new Error(\"not found\")` to mean 404 — that is a 500. Throw a 404 Response."
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"name": "PUBLIC_ENV",
|
|
52
|
+
"topic": "Nifra — Framework contract",
|
|
53
|
+
"slug": "contract",
|
|
54
|
+
"code": "// process.env in the CLIENT bundle is compiled at build time:\n// process.env → ({}) (a bare read won't crash hydration)\n// process.env.NODE_ENV → \"production\" (the build mode — frameworks' prod/dev branch)\n// process.env.SECRET_KEY → undefined (unprefixed → never exposed; no secret leak)\n// process.env.PUBLIC_API_URL → \"https://api.example.com\" (PUBLIC_-prefixed → baked in by value)\n//\n// So to ship a value to the browser, give it a PUBLIC_ prefix in the BUILD environment:\n// PUBLIC_API_URL=https://api.example.com bun run build\n// then read process.env.PUBLIC_API_URL in app code — it becomes a string literal after the build.\nimport { buildClient } from \"@nifrajs/web/build\"\n\n// Override the prefix (or disable auto-exposure) via buildClient:\nawait buildClient({\n routesDir: \"./routes\",\n outDir: \"./dist\",\n clientModule: \"@nifrajs/web-react/client\",\n publicEnvPrefix: \"NIFRA_PUBLIC_\", // default \"PUBLIC_\"; \"\" disables auto-exposure entirely\n})"
|
|
37
55
|
},
|
|
38
56
|
{
|
|
39
|
-
"name": "
|
|
40
|
-
"topic": "Nifra —
|
|
41
|
-
"slug": "
|
|
42
|
-
"code": "//
|
|
57
|
+
"name": "SEO_EXAMPLE",
|
|
58
|
+
"topic": "Nifra — Framework contract",
|
|
59
|
+
"slug": "contract",
|
|
60
|
+
"code": "// routes/articles/[slug].tsx — a complete SEO head: canonical + Open Graph + Twitter + JSON-LD,\n// with ABSOLUTE URLs built from `origin`. The three helpers (canonical/openGraph/jsonLd) are public\n// exports of @nifrajs/web; `origin` is the third MetaArgs field (after data + params).\nimport { canonical, jsonLd, openGraph, type MetaArgs } from \"@nifrajs/web\"\n\n// CAVEAT: meta() runs in BOTH SSR and client navigation, so it has NO server env — never read\n// `process.env` or the request here. The framework injects `origin` (the site's scheme+host, e.g.\n// \"https://news.example.com\") server-side from the request AND on the client from location.origin, so\n// the two match and an absolute og:url/canonical/og:image never drifts between SSR and a soft-nav. For\n// anything else server-only (an API base, a CDN host), thread it through the LOADER, not meta().\nexport const meta = ({ data, params, origin }: MetaArgs) => {\n const article = data as { title: string; summary: string; image: string; published: string }\n const url = `${origin}/articles/${params.slug}` // absolute, from the injected origin\n const image = `${origin}${article.image}` // og:image MUST be absolute for crawlers\n return {\n title: article.title,\n meta: [\n { name: \"description\", content: article.summary },\n // og:* — openGraph emits only the props you pass (+ og:type, here \"article\").\n ...openGraph({\n title: article.title,\n description: article.summary,\n url,\n image,\n type: \"article\",\n }),\n // twitter:* — a large-image summary card sharing the same absolute image.\n { name: \"twitter:card\", content: \"summary_large_image\" },\n { name: \"twitter:title\", content: article.title },\n { name: \"twitter:description\", content: article.summary },\n { name: \"twitter:image\", content: image },\n ],\n link: [canonical(url)], // <link rel=\"canonical\"> — the authoritative URL\n // JSON-LD structured data — escaped for safe <script> embedding by the head renderer.\n script: [\n jsonLd({\n \"@context\": \"https://schema.org\",\n \"@type\": \"NewsArticle\",\n headline: article.title,\n image,\n datePublished: article.published,\n mainEntityOfPage: url,\n }),\n ],\n }\n}"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"name": "STATIC_EMIT",
|
|
64
|
+
"topic": "Nifra — Framework contract",
|
|
65
|
+
"slug": "contract",
|
|
66
|
+
"code": "// build.ts — emit static HTML for opted-in routes. prerenderRoutes + cloudflarePagesRoutes\n// are PUBLIC (exported from @nifrajs/web/build) — no need to boot a server and curl it.\nimport { buildClient, prerenderRoutes, cloudflarePagesRoutes } from \"@nifrajs/web/build\"\nimport { discoverRoutes } from \"@nifrajs/web/fs\"\n\n// 1. Build the client bundle first (so the app references the hashed entry).\nawait buildClient({ routesDir: \"./routes\", outDir: \"./dist\", clientModule: \"@nifrajs/web-react/client\" })\n\n// 2. Drive the app's own fetch to render each opted-in route → dist/<path>/index.html + _data.json.\n// Static routes opt in with `export const prerender = true`; dynamic routes enumerate concrete\n// params with `export const getStaticPaths` (+ a `fallback: \"ssr\" | \"404\"`).\nconst { app } = await import(\"./server\")\nconst { prerendered } = await prerenderRoutes({\n app, // the built createWebApp (a { fetch } is enough)\n routes: discoverRoutes(\"./routes\").routes,\n outDir: \"./dist\",\n})\n\n// 3. Hybrid deploy: a Cloudflare Pages _routes.json that serves the prerendered HTML + _data.json\n// from the CDN and falls everything else through to the SSR worker.\nconst paths = prerendered.map((p) => p.path)\nawait Bun.write(\"./dist/_routes.json\", JSON.stringify(cloudflarePagesRoutes({ prerendered: paths })))"
|
|
43
67
|
},
|
|
44
68
|
{
|
|
45
69
|
"name": "CONTENT",
|
|
@@ -53,23 +77,11 @@
|
|
|
53
77
|
"slug": "database",
|
|
54
78
|
"code": "import { server } from \"@nifrajs/core\"\nimport { desc } from \"drizzle-orm\"\nimport { db, notes } from \"./db\" // your Drizzle client + schema (what the scaffold generates)\n\n// decorate() hangs the client on the context ONCE — every handler then reads it as `c.db`, fully typed.\nexport const app = server()\n .decorate(\"db\", db)\n .get(\"/notes\", async (c) => c.db.select().from(notes).orderBy(desc(notes.createdAt)))\n\nexport type App = typeof app"
|
|
55
79
|
},
|
|
56
|
-
{
|
|
57
|
-
"name": "EDGE",
|
|
58
|
-
"topic": "Nifra — Databases",
|
|
59
|
-
"slug": "database",
|
|
60
|
-
"code": "import { server } from \"@nifrajs/core\"\n\n// On Workers there's no raw TCP, so use an HTTP-based driver. D1 is a typed platform binding:\ninterface Env { DB: D1Database }\n\nconst app = server<Env>().get(\"/users/:id\", async (c) => {\n const user = await c.env.DB.prepare(\"SELECT * FROM users WHERE id = ?\").bind(c.params.id).first()\n return user ?? new Response(\"Not found\", { status: 404 })\n})"
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
"name": "POSTGRES",
|
|
64
|
-
"topic": "Nifra — Databases",
|
|
65
|
-
"slug": "database",
|
|
66
|
-
"code": "import { server } from \"@nifrajs/core\"\nimport { drizzle } from \"drizzle-orm/postgres-js\" // or node-postgres / neon-http\nimport postgres from \"postgres\"\nimport { eq } from \"drizzle-orm\"\nimport { users } from \"./schema\"\n\nconst db = drizzle(postgres(process.env.DATABASE_URL!))\n\nexport const app = server().get(\"/users/:id\", async (c) => {\n const [user] = await db.select().from(users).where(eq(users.id, Number(c.params.id)))\n return user ?? new Response(\"Not found\", { status: 404 })\n})"
|
|
67
|
-
},
|
|
68
80
|
{
|
|
69
81
|
"name": "SQLITE",
|
|
70
82
|
"topic": "Nifra — Databases",
|
|
71
83
|
"slug": "database",
|
|
72
|
-
"code": "import { server } from \"@nifrajs/core\"\nimport { Database } from \"bun:sqlite\" // Node: better-sqlite3 · edge: Turso/libSQL\n\n// Open the connection ONCE at module scope (the driver pools internally).\nconst db = new Database(\"app.db\")\nconst byId = db.query<{ id: number; name: string }>(\"SELECT * FROM users WHERE id = ?\")\n\nexport const app = server().get(\"/users/:id\", (c) =>\n // Parameterized (?) — never interpolate user input into SQL.\n byId.get(Number(c.params.id)) ?? new Response(\"Not found\", { status: 404 }),\n)"
|
|
84
|
+
"code": "import { server } from \"@nifrajs/core\"\nimport { Database } from \"bun:sqlite\" // Node: better-sqlite3 · edge: Turso/libSQL\n\n// Open the connection ONCE at module scope (the driver pools internally).\nconst db = new Database(\"app.db\")\nconst byId = db.query<{ id: number; name: string }, [number]>(\"SELECT * FROM users WHERE id = ?\")\n\nexport const app = server().get(\"/users/:id\", (c) =>\n // Parameterized (?) — never interpolate user input into SQL.\n byId.get(Number(c.params.id)) ?? new Response(\"Not found\", { status: 404 }),\n)"
|
|
73
85
|
},
|
|
74
86
|
{
|
|
75
87
|
"name": "CF",
|
|
@@ -77,24 +89,6 @@
|
|
|
77
89
|
"slug": "deployment",
|
|
78
90
|
"code": "// _worker.ts — the edge SSR entry\nimport { toFetchHandler } from \"@nifrajs/core\"\nimport { createWebApp } from \"@nifrajs/web\"\nimport { reactAdapter } from \"@nifrajs/web-react\"\nimport { clientEntry, manifest } from \"./server-manifest\" // generated by buildServer\n\nconst app = createWebApp({ adapter: reactAdapter, manifest, clientEntry })\nexport default toFetchHandler(app) // a Workers/Pages fetch handler\n\n// build.ts — buildClient (→ /assets) + buildServer (edge conditions → _worker.js)\nimport { buildClient, buildServer } from \"@nifrajs/web/build\""
|
|
79
91
|
},
|
|
80
|
-
{
|
|
81
|
-
"name": "BUN_DEV",
|
|
82
|
-
"topic": "Nifra — Dev & HMR",
|
|
83
|
-
"slug": "dev",
|
|
84
|
-
"code": "// dev.ts — Bun-native live reload\nimport { createDevServer } from \"@nifrajs/web/dev\"\n// builds the client on boot, serves SSR, watches routes, full-page live-reload on change.\nconst server = await createDevServer({ routesDir, outDir, clientModule, createApp })"
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
"name": "VITE_DEV",
|
|
88
|
-
"topic": "Nifra — Dev & HMR",
|
|
89
|
-
"slug": "dev",
|
|
90
|
-
"code": "// dev.ts — state-preserving HMR for supported UI adapters\nimport react from \"@vitejs/plugin-react\" // your framework's official Vite plugin\nimport { createWebApp } from \"@nifrajs/web\"\nimport { discoverRoutes } from \"@nifrajs/web/fs\"\nimport { createViteDevServer } from \"@nifrajs/web/vite\"\nimport { reactAdapter } from \"@nifrajs/web-react\"\nimport { backend } from \"./backend\"\n\nconst routesDir = `${import.meta.dir}/routes`\nconst server = await createViteDevServer({\n root: import.meta.dir,\n routesDir,\n clientModule: \"@nifrajs/web-react/client\",\n plugins: [react()], // Vue: @vitejs/plugin-vue, Svelte: …, etc.\n port: Number(Bun.env.PORT ?? 3000),\n createApp: (clientEntry, importQuery) =>\n createWebApp({\n adapter: reactAdapter,\n manifest: discoverRoutes(routesDir, { importQuery }),\n clientEntry,\n api: inProcessClient(backend),\n }),\n})"
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
"name": "CRON",
|
|
94
|
-
"topic": "Nifra — Edge & bindings",
|
|
95
|
-
"slug": "edge",
|
|
96
|
-
"code": "import { toFetchHandler } from \"@nifrajs/core\"\nimport { app } from \"./app\"\n\n// Export fetch + a cron handler. Wire the schedule in wrangler.toml: [triggers] crons = [\"0 * * * *\"]\nexport default toFetchHandler(app, {\n scheduled: (controller, { env, waitUntil }) =>\n waitUntil(env.KV.put(\"last-run\", String(controller.scheduledTime))),\n})"
|
|
97
|
-
},
|
|
98
92
|
{
|
|
99
93
|
"name": "AUTOMATED",
|
|
100
94
|
"topic": "Nifra — Fonts",
|
|
@@ -117,25 +111,19 @@
|
|
|
117
111
|
"name": "SWAP",
|
|
118
112
|
"topic": "Nifra — Frameworks",
|
|
119
113
|
"slug": "frameworks",
|
|
120
|
-
"code": "// Server — pick an adapter. Everything else is identical across all five frameworks:\n// the same routes, loaders, actions, streaming, <Await>, fetchers, query cache.\nimport { createWebApp } from \"@nifrajs/web\"\nimport { reactAdapter } from \"@nifrajs/web-react\" // ← or web-solid · web-vue · web-preact · web-svelte\n\nconst app = createWebApp({\n adapter: reactAdapter, // the one line that changes per framework\n manifest,\n clientEntry, // the built client bundle (from buildClient's manifest)\n
|
|
114
|
+
"code": "// Server — pick an adapter. Everything else is identical across all five frameworks:\n// the same routes, loaders, actions, streaming, <Await>, fetchers, query cache.\nimport { createWebApp } from \"@nifrajs/web\"\nimport { reactAdapter } from \"@nifrajs/web-react\" // ← or web-solid · web-vue · web-preact · web-svelte\nimport { clientEntry, manifest } from \"./server-manifest\" // generated by buildServer\n\nconst app = createWebApp({\n adapter: reactAdapter, // the one line that changes per framework\n manifest,\n clientEntry, // the built client bundle (from buildClient's manifest)\n})"
|
|
121
115
|
},
|
|
122
116
|
{
|
|
123
117
|
"name": "NEGOTIATE",
|
|
124
118
|
"topic": "Nifra — i18n",
|
|
125
119
|
"slug": "i18n",
|
|
126
|
-
"code": "// In a loader: resolve the locale + return only that catalog's messages.\nimport { negotiateLocale } from \"@nifrajs/i18n\"\nimport { catalogs, locales } from \"../catalogs\"\n\nexport async function loader({ request }) {\n const q = new URL(request.url).searchParams.get(\"lang\")\n const locale = q && locales.includes(q) ? q : negotiateLocale(request, { locales, defaultLocale: \"en\", cookie: \"lang\" })\n return { locale, messages: catalogs[locale] } // cookie → Accept-Language → default\n}"
|
|
127
|
-
},
|
|
128
|
-
{
|
|
129
|
-
"name": "BACKENDS",
|
|
130
|
-
"topic": "Nifra — images",
|
|
131
|
-
"slug": "images",
|
|
132
|
-
"code": "import { createImageHandler } from \"@nifrajs/image/server\"\nimport { sharpImageBackend, wasmImageBackend } from \"@nifrajs/image/backends\"\n\n// Node — libvips via sharp. Pass your own import (nifra never depends on it):\nimport sharp from \"sharp\"\ncreateImageHandler({ backend: sharpImageBackend(sharp), root: \"./public\" })\n\n// Edge (Workers / Vercel-Edge / Deno-Deploy) — pure-WASM codecs, e.g. jSquash:\nimport decodeJpeg from \"@jsquash/jpeg/decode\"\nimport resize from \"@jsquash/resize\"\nimport encodeWebp from \"@jsquash/webp/encode\" // + @jsquash/png, @jsquash/jpeg encoders\ncreateImageHandler({\n allowedOrigins: [\"https://cdn.example\"],\n backend: wasmImageBackend({\n decode: decodeJpeg, // → { data, width, height } (RGBA)\n resize: (img, width, height) => resize(img, { width, height }),\n encode: async (img, format) => new Uint8Array(await encodeWebp(img)), // switch by format\n }),\n})"
|
|
120
|
+
"code": "// In a loader: resolve the locale + return only that catalog's messages.\nimport { negotiateLocale } from \"@nifrajs/i18n\"\nimport { catalogs, locales } from \"../catalogs\"\n\nexport async function loader({ request }: { request: Request }) {\n const q = new URL(request.url).searchParams.get(\"lang\")\n const locale = q && locales.includes(q) ? q : negotiateLocale(request, { locales, defaultLocale: \"en\", cookie: \"lang\" })\n return { locale, messages: catalogs[locale] } // cookie → Accept-Language → default\n}"
|
|
133
121
|
},
|
|
134
122
|
{
|
|
135
123
|
"name": "DIMENSIONS",
|
|
136
124
|
"topic": "Nifra — images",
|
|
137
125
|
"slug": "images",
|
|
138
|
-
"code": "import { imageDimensions, readImageDimensions } from \"@nifrajs/image\"\n\n// Pure-JS header read — PNG / JPEG / GIF / WebP. No decode, no codec, no deps.\nconst info = await readImageDimensions(Bun.file(\"public/hero.jpg\"))\n// → { width: 1200, height: 630, format: \"jpeg\" } (null if unrecognized)\n\n// Build-time tooling: pre-read sizes into a manifest so <Image> is CLS-safe\n// without hardcoding width/height at every call site
|
|
126
|
+
"code": "import { imageDimensions, readImageDimensions } from \"@nifrajs/image\"\n\n// Pure-JS header read — PNG / JPEG / GIF / WebP. No decode, no codec, no deps.\nconst info = await readImageDimensions(Bun.file(\"public/hero.jpg\"))\n// → { width: 1200, height: 630, format: \"jpeg\" } (null if unrecognized)\n\n// Build-time tooling: pre-read sizes into a manifest so <Image> is CLS-safe\n// without hardcoding width/height at every call site. The sync variant takes the\n// raw header bytes you already have in memory (no file read):\nconst headerBytes = await Bun.file(\"public/hero.jpg\").bytes()\nimageDimensions(headerBytes)"
|
|
139
127
|
},
|
|
140
128
|
{
|
|
141
129
|
"name": "LOADERS",
|
|
@@ -147,7 +135,7 @@
|
|
|
147
135
|
"name": "SIGNING",
|
|
148
136
|
"topic": "Nifra — images",
|
|
149
137
|
"slug": "images",
|
|
150
|
-
"code": "// Lock the endpoint to URLs YOU minted — kills resize-bombing (w/q enumeration).\n// SAME secret on loader + handler; it's server-only (inject from env, like a session secret).\nimport { selfHostedLoader, signImageUrl } from \"@nifrajs/image\"\nimport { createImageHandler } from \"@nifrajs/image/server\"\n\nconst resize = selfHostedLoader({ endpoint: \"/_image\", secret:
|
|
138
|
+
"code": "// Lock the endpoint to URLs YOU minted — kills resize-bombing (w/q enumeration).\n// SAME secret on loader + handler; it's server-only (inject from env, like a session secret).\nimport { selfHostedLoader, signImageUrl } from \"@nifrajs/image\"\nimport { createImageHandler } from \"@nifrajs/image/server\"\n\nconst IMAGE_SECRET = process.env.IMAGE_SECRET! // server-only; on Workers read c.env.IMAGE_SECRET instead\n\nconst resize = selfHostedLoader({ endpoint: \"/_image\", secret: IMAGE_SECRET })\n// → /_image?src=…&w=800&s=<hmac> (stable: an SSR-signed URL hydrates + caches identically)\n\nconst image = createImageHandler({ root: \"./public\", signing: { secret: IMAGE_SECRET } })\n// any request without a valid &s= (forged / tampered / expired) → 403, before any fetch or decode\n\n// Time-limited links to private images (server-side only):\nconst url = signImageUrl(\"/_image\", { src: \"/private/a.jpg\", width: 800 }, {\n secret: IMAGE_SECRET,\n expiresIn: 300, // seconds → adds &exp=\n})"
|
|
151
139
|
},
|
|
152
140
|
{
|
|
153
141
|
"name": "CLIENT",
|
|
@@ -161,23 +149,11 @@
|
|
|
161
149
|
"slug": "index",
|
|
162
150
|
"code": "import { server } from \"@nifrajs/core\"\n\nserver()\n .get(\"/\", () => ({ hello: \"world\" }))\n .get(\"/users/:id\", (c) => ({ id: c.params.id }))\n .listen(3000)"
|
|
163
151
|
},
|
|
164
|
-
{
|
|
165
|
-
"name": "OPTIMISTIC",
|
|
166
|
-
"topic": "Nifra — Optimistic UI & fetchers",
|
|
167
|
-
"slug": "mutations",
|
|
168
|
-
"code": "import { useFetcher } from \"@nifrajs/web-react/fetcher\"\n\n// Optimistic UI: while a submit is in flight, read the expected value from the in-flight\n// submission's FormData; the real data takes over when the action resolves, and a failed\n// submit reverts automatically — no manual rollback.\nconst fetcher = useFetcher(\"todo:\" + id)\nconst pending = fetcher.submission?.formData.get(\"done\") // the value you just submitted\nconst done = pending != null ? pending === \"true\" : todo.done // optimistic value wins while pending\n\nfunction toggle() {\n const fd = new FormData()\n fd.set(\"id\", String(id))\n fd.set(\"done\", String(!done))\n fetcher.submit(\"/todos\", fd) // submit(actionPath, body) — runs the route's action\n}"
|
|
169
|
-
},
|
|
170
|
-
{
|
|
171
|
-
"name": "AUTHN",
|
|
172
|
-
"topic": "Nifra — Plugins & middleware",
|
|
173
|
-
"slug": "plugins",
|
|
174
|
-
"code": "import { bearer, apiKey } from \"@nifrajs/middleware\"\n\n// Bearer tokens — verify returns your principal (its type is inferred), 401s missing/invalid:\nconst auth = bearer({ verify: (token) => lookupUser(token) }) // AuthPlugin<User>\napp\n .use(auth) // guards routes defined after it\n .get(\"/me\", (c) => auth.requirePrincipal(c.req)) // typed principal, or throws 401\n\n// API keys via a header (default x-api-key) — a fixed set compared in CONSTANT TIME…\napp.use(apiKey({ keys: [process.env.API_KEY!] })) // matched key becomes the principal\n// …or custom (DB-backed) verification; 'optional' lets unauthenticated requests through:\napp.use(apiKey({ verify: (key) => db.apiKeys.find(key), optional: true }))"
|
|
175
|
-
},
|
|
176
152
|
{
|
|
177
153
|
"name": "CACHING",
|
|
178
154
|
"topic": "Nifra — Plugins & middleware",
|
|
179
155
|
"slug": "plugins",
|
|
180
|
-
"code": "import { cache, MemoryResponseCache, prettyJson } from \"@nifrajs/middleware\"\n\n// Full response cache: pluggable store, Vary-aware keys, byte cap. Bypasses Set-Cookie and respects\n// Cache-Control (no-store/private). MemoryResponseCache is per-instance — refuses prod unless opted in.\napp.use(cache({ store: new MemoryResponseCache(), ttlMs: 30_000, vary: [\"accept-language\"] }))\napp.use(prettyJson()) // pretty-print JSON responses (size-capped; optional ?pretty query toggle)"
|
|
156
|
+
"code": "import { server } from \"@nifrajs/core\"\nimport { cache, MemoryResponseCache, prettyJson } from \"@nifrajs/middleware\"\n\n// Full response cache: pluggable store, Vary-aware keys, byte cap. Bypasses Set-Cookie and respects\n// Cache-Control (no-store/private). MemoryResponseCache is per-instance — refuses prod unless opted in.\nconst app = server()\napp.use(cache({ store: new MemoryResponseCache(), ttlMs: 30_000, vary: [\"accept-language\"] }))\napp.use(prettyJson()) // pretty-print JSON responses (size-capped; optional ?pretty query toggle)"
|
|
181
157
|
},
|
|
182
158
|
{
|
|
183
159
|
"name": "IDENTITY",
|
|
@@ -189,49 +165,37 @@
|
|
|
189
165
|
"name": "JWT_BASIC",
|
|
190
166
|
"topic": "Nifra — Plugins & middleware",
|
|
191
167
|
"slug": "plugins",
|
|
192
|
-
"code": "import { jwt, jwks, basicAuth } from \"@nifrajs/middleware\"\n\n// JWT (WebCrypto): an explicit algorithm allowlist is REQUIRED; alg:none and RSA/HMAC confusion are rejected.\nconst auth = jwt({ key: process.env.JWT_SECRET!, algorithms: [\"HS256\"], issuer: \"my-app\" })\
|
|
168
|
+
"code": "import { server } from \"@nifrajs/core\"\nimport { jwt, jwks, basicAuth } from \"@nifrajs/middleware\"\n\n// JWT (WebCrypto): an explicit algorithm allowlist is REQUIRED; alg:none and RSA/HMAC confusion are rejected.\nconst auth = jwt({ key: process.env.JWT_SECRET!, algorithms: [\"HS256\"], issuer: \"my-app\" })\nconst app = server()\n .use(auth) // 401s missing/invalid (optional:true lets them through)\n .get(\"/me\", (c) => auth.requireClaims(c.req)) // typed claims, or throws 401; auth.claims(req) is nullable\n// Asymmetric (rotating keys): key: jwks({ url: \"https://issuer/.well-known/jwks.json\" }) — https-only, cached.\n\n// HTTP Basic — static creds compared in CONSTANT TIME (SHA-256 + timing-safe), or a verify callback.\napp.use(basicAuth({ username: \"admin\", password: process.env.PASS!, realm: \"staging\" }))"
|
|
193
169
|
},
|
|
194
170
|
{
|
|
195
171
|
"name": "MIDDLEWARE",
|
|
196
172
|
"topic": "Nifra — Plugins & middleware",
|
|
197
173
|
"slug": "plugins",
|
|
198
|
-
"code": "import { cors, rateLimit, securityHeaders } from \"@nifrajs/middleware\"\n\n// Hardening middleware is a hook bundle (context-agnostic) — same app.use():\
|
|
174
|
+
"code": "import { server } from \"@nifrajs/core\"\nimport { cors, rateLimit, securityHeaders, MemoryStore } from \"@nifrajs/middleware\"\n\n// Hardening middleware is a hook bundle (context-agnostic) — same app.use():\nconst app = server()\n .use(securityHeaders())\n .use(cors({ origin: [\"https://app.example\"] }))\n // MemoryStore is dev/single-instance only — use a shared store (Redis, etc.) in production.\n .use(rateLimit({ store: new MemoryStore(), max: 100, windowMs: 60_000 }))"
|
|
199
175
|
},
|
|
200
176
|
{
|
|
201
177
|
"name": "OFFICIAL",
|
|
202
178
|
"topic": "Nifra — Plugins & middleware",
|
|
203
179
|
"slug": "plugins",
|
|
204
|
-
"code": "import { requestId, logger, etag } from \"@nifrajs/middleware\"\n\
|
|
205
|
-
},
|
|
206
|
-
{
|
|
207
|
-
"name": "OPS",
|
|
208
|
-
"topic": "Nifra — Plugins & middleware",
|
|
209
|
-
"slug": "plugins",
|
|
210
|
-
"code": "import { healthcheck, openapi } from \"@nifrajs/middleware\"\n\napp\n // Liveness + readiness. /ready runs each check concurrently → 200 (all pass) or 503.\n .use(healthcheck({ checks: { db: () => db.ping(), cache: () => redis.ping() } }))\n // GET /openapi.json from your routes (paths, methods, params); ui adds a Scalar page at /reference.\n .use(openapi({ info: { title: \"My API\", version: \"1.0.0\" }, ui: true }))"
|
|
180
|
+
"code": "import { server } from \"@nifrajs/core\"\nimport { requestId, logger, etag } from \"@nifrajs/middleware\"\n\nconst app = server()\n .use(requestId()) // reuse/generate x-request-id → c.requestId (typed) + response header\n .use(logger()) // one structured line/request: { method, path, status, ms }\n .use(etag()) // content-hash ETag on GET 200s → 304 on matching If-None-Match"
|
|
211
181
|
},
|
|
212
182
|
{
|
|
213
183
|
"name": "PERF",
|
|
214
184
|
"topic": "Nifra — Plugins & middleware",
|
|
215
185
|
"slug": "plugins",
|
|
216
|
-
"code": "import { compression, cacheControl } from \"@nifrajs/middleware\"\n\
|
|
217
|
-
},
|
|
218
|
-
{
|
|
219
|
-
"name": "PLUGIN",
|
|
220
|
-
"topic": "Nifra — Plugins & middleware",
|
|
221
|
-
"slug": "plugins",
|
|
222
|
-
"code": "import { definePlugin } from \"@nifrajs/core\"\n\n// A plugin is just (app) => app — call use/derive/decorate or register routes.\n// definePlugin adds a name so applying it twice (even transitively) is a no-op.\nexport const auth = definePlugin(\"auth\", (app) =>\n app.derive((c) => ({ user: verify(c.req) })), // adds c.user…\n)\n\napp\n .use(auth) // …threaded to every handler after this:\n .get(\"/me\", (c) => ({ id: c.user.id })) // c.user is fully typed\n\n// Inline plugins thread context too — no definePlugin needed for one-offs:\napp.use((a) => a.decorate(\"db\", db).derive((c) => ({ now: Date.now() })))"
|
|
186
|
+
"code": "import { server } from \"@nifrajs/core\"\nimport { compression, cacheControl } from \"@nifrajs/middleware\"\n\nconst app = server()\n .use(compression()) // gzip compressible responses (Accept-Encoding)\n .use(cacheControl(\"public, max-age=60\")) // Cache-Control on GET/HEAD 2xx (won't clobber)\n // …or a per-path policy — return undefined to leave a response untouched:\n .use(cacheControl((req) =>\n new URL(req.url).pathname.startsWith(\"/assets/\")\n ? \"public, max-age=31536000, immutable\"\n : undefined))"
|
|
223
187
|
},
|
|
224
188
|
{
|
|
225
189
|
"name": "RELIABILITY",
|
|
226
190
|
"topic": "Nifra — Plugins & middleware",
|
|
227
191
|
"slug": "plugins",
|
|
228
|
-
"code": "import { idempotency, MemoryIdempotencyStore } from \"@nifrajs/middleware\"\n\n// A retried POST with the same Idempotency-Key replays the first response instead of\n// re-running the side effect. Shared store in production (atomic claim); dev-only memory store.\napp.use(idempotency({ store: new MemoryIdempotencyStore() }))"
|
|
192
|
+
"code": "import { server } from \"@nifrajs/core\"\nimport { idempotency, MemoryIdempotencyStore } from \"@nifrajs/middleware\"\n\n// A retried POST with the same Idempotency-Key replays the first response instead of\n// re-running the side effect. Shared store in production (atomic claim); dev-only memory store.\nconst app = server()\napp.use(idempotency({ store: new MemoryIdempotencyStore() }))"
|
|
229
193
|
},
|
|
230
194
|
{
|
|
231
195
|
"name": "SHAPING",
|
|
232
196
|
"topic": "Nifra — Plugins & middleware",
|
|
233
197
|
"slug": "plugins",
|
|
234
|
-
"code": "import { methodOverride, trimTrailingSlash, language, timing, poweredBy, combine } from \"@nifrajs/middleware\"\n\
|
|
198
|
+
"code": "import { server } from \"@nifrajs/core\"\nimport { methodOverride, trimTrailingSlash, language, timing, poweredBy, combine } from \"@nifrajs/middleware\"\n\nconst app = server()\n .use(methodOverride()) // POST + X-HTTP-Method-Override → PUT/PATCH/DELETE (a real pre-routing request rewrite)\n .use(trimTrailingSlash()) // canonicalize URLs: 308 redirect (or rewrite), same-origin only, conservative methods\n .use(language({ supported: [\"en\", \"fr\"], defaultLanguage: \"en\" })) // Accept-Language → c.language + Content-Language\n .use(timing()) // Server-Timing header + typed c.timing marks/measures\n// poweredBy() is opt-in (nifra emits no X-Powered-By by default). combine(a, b, c) bundles several into one plugin."
|
|
235
199
|
},
|
|
236
200
|
{
|
|
237
201
|
"name": "BUILD",
|
|
@@ -239,12 +203,6 @@
|
|
|
239
203
|
"slug": "rendering",
|
|
240
204
|
"code": "// build.ts — buildClient, then prerender opted-in routes to static HTML.\nimport { buildClient, prerenderRoutes, cloudflarePagesRoutes } from \"@nifrajs/web/build\"\nimport { discoverRoutes } from \"@nifrajs/web/fs\"\n\nawait buildClient({ routesDir: \"./routes\", outDir: \"./dist\", clientModule: \"@nifrajs/web-react/client\" })\nconst { app } = await import(\"./server\")\nconst { prerendered } = await prerenderRoutes({\n app,\n routes: discoverRoutes(\"./routes\").routes,\n outDir: \"./dist\", // writes <path>/index.html + <path>/_data.json per prerendered route\n})\n\n// Hybrid deploy (Cloudflare Pages): serve prerendered HTML + _data.json from the CDN; everything\n// else falls through to the SSR worker.\nconst paths = prerendered.map((p) => p.path)\nBun.write(\"./dist/_routes.json\", JSON.stringify(cloudflarePagesRoutes({ prerendered: paths })))"
|
|
241
205
|
},
|
|
242
|
-
{
|
|
243
|
-
"name": "DRAFT",
|
|
244
|
-
"topic": "Nifra — Rendering: SSG & ISR",
|
|
245
|
-
"slug": "rendering",
|
|
246
|
-
"code": "// 1. A route you gate yourself flips draft mode on for the editor.\nimport { enableDraft, disableDraft } from \"@nifrajs/web\"\n\napp.get(\"/api/preview\", async (c) => {\n if (c.query.token !== env.PREVIEW_TOKEN) return new Response(\"nope\", { status: 401 })\n await enableDraft(c, env.DRAFT_SECRET) // signed, HttpOnly cookie\n return redirect(String(c.query.to ?? \"/\"))\n})\napp.get(\"/api/preview/exit\", (c) => (disableDraft(c), redirect(\"/\")))\n\n// 2. Loaders branch on ctx.draft to load unpublished content.\nexport async function loader({ api, draft }: LoaderArgs<typeof app>) {\n return { post: (await api.posts.get({ query: { slug, includeDrafts: draft } })).data }\n}\n\n// 3. Wire the SAME secret so loaders see ctx.draft + editors bypass the ISR cache.\ncreateWebApp({ adapter, manifest, clientEntry, api, draftSecret: env.DRAFT_SECRET })\nwithISR(app, { store, revalidate: 60, now: () => Date.now(), draftSecret: env.DRAFT_SECRET })"
|
|
247
|
-
},
|
|
248
206
|
{
|
|
249
207
|
"name": "FONTS",
|
|
250
208
|
"topic": "Nifra — Rendering: SSG & ISR",
|
|
@@ -255,31 +213,19 @@
|
|
|
255
213
|
"name": "ISR",
|
|
256
214
|
"topic": "Nifra — Rendering: SSG & ISR",
|
|
257
215
|
"slug": "rendering",
|
|
258
|
-
"code": "// server.ts — wrap the app with Incremental Static Regeneration.\nimport { createWebApp, withISR, MemoryCacheStore } from \"@nifrajs/web\"\n\nconst app = createWebApp({ adapter, manifest, clientEntry
|
|
259
|
-
},
|
|
260
|
-
{
|
|
261
|
-
"name": "KV",
|
|
262
|
-
"topic": "Nifra — Rendering: SSG & ISR",
|
|
263
|
-
"slug": "rendering",
|
|
264
|
-
"code": "// worker.ts — production uses a SHARED store so the cache + purges hold across instances.\nimport { withISR, KVCacheStore, revalidateEndpoint } from \"@nifrajs/web\"\n\nexport default {\n async fetch(req: Request, env: Env, ctx: ExecutionContext) {\n const store = new KVCacheStore(env.ISR_CACHE, { expirationTtl: 86_400 }) // Workers KV\n if (new URL(req.url).pathname === \"/__nifra/revalidate\") {\n return revalidateEndpoint({ store, secret: env.REVALIDATE_SECRET })(req)\n }\n const isr = withISR(app, { store, revalidate: 60, now: () => Date.now() })\n // waitUntil keeps the worker alive while a stale page regenerates behind the response.\n return isr(req, { env, waitUntil: (p) => ctx.waitUntil(p) })\n },\n}"
|
|
216
|
+
"code": "// server.ts — wrap the app with Incremental Static Regeneration.\nimport { createWebApp, withISR, MemoryCacheStore } from \"@nifrajs/web\"\nimport { reactAdapter } from \"@nifrajs/web-react\"\nimport { clientEntry, manifest } from \"./server-manifest\" // generated by buildServer\n\nconst app = createWebApp({ adapter: reactAdapter, manifest, clientEntry })\nconst store = new MemoryCacheStore() // dev / single-instance only\n\n// GET text/html responses are cached + served stale-while-revalidate. Default freshness 60s.\nconst isr = withISR(app, { store, revalidate: 60, now: () => Date.now() })\nBun.serve({ fetch: (req) => isr(req) })"
|
|
265
217
|
},
|
|
266
218
|
{
|
|
267
219
|
"name": "BOUNDED",
|
|
268
220
|
"topic": "Nifra — security & hardening",
|
|
269
221
|
"slug": "security",
|
|
270
|
-
"code": "import { server
|
|
222
|
+
"code": "import { server } from \"@nifrajs/core\"\n\nconst app = server()\n\n// A schema route is ALREADY bounded — the validated read enforces `maxBodyBytes`.\n// But a raw-body / file / BYO-validation route reads the body directly, which\n// `maxBodyBytes` does not cover. `c.boundedBody` caps that read:\napp.post(\"/import\", async (c) => {\n const bytes = await c.boundedBody(5 * 1024 * 1024) // cap THIS route at 5 MiB\n // Over-cap throws a flat 413; a malformed Content-Length a 400 — as control-flow\n // Responses (caught by the lifecycle like `throw redirect()`), so a handler can't\n // accidentally ignore the cap. The over-cap length is rejected BEFORE buffering;\n // a chunked / length-less body is aborted mid-stream once it crosses the cap.\n return { received: bytes.byteLength } // a returned object is serialized as JSON 200\n})\n\napp.post(\"/rpc\", async (c) => {\n const body = await c.boundedJson<{ method: string }>() // default: the server's maxBodyBytes\n // …bad JSON → 400. Then validate `body` with your schema before trusting it.\n return { method: body.method }\n})"
|
|
271
223
|
},
|
|
272
224
|
{
|
|
273
225
|
"name": "GATING",
|
|
274
226
|
"topic": "Nifra — security & hardening",
|
|
275
227
|
"slug": "security",
|
|
276
|
-
"code": "import { jwt, csrf, ipRestriction, bodyLimit } from \"@nifrajs/middleware\"\n\
|
|
277
|
-
},
|
|
278
|
-
{
|
|
279
|
-
"name": "IDEMPOTENCY",
|
|
280
|
-
"topic": "Nifra — security & hardening",
|
|
281
|
-
"slug": "security",
|
|
282
|
-
"code": "import { idempotency, MemoryIdempotencyStore } from \"@nifrajs/middleware\"\n\n// Dev / single-instance. In production use a SHARED store (Redis, etc.) with an atomic\n// claim — MemoryIdempotencyStore throws under NODE_ENV=production unless you opt in.\napp.use(idempotency({ store: new MemoryIdempotencyStore() }))\n\napp.post(\"/charge\", async (c) => {\n await chargeCard(/* … */) // the side effect\n return c.json({ ok: true, id })\n})\n\n// A client retrying POST /charge with the same `Idempotency-Key` header gets the FIRST\n// response replayed (`Idempotent-Replayed: true`) — the charge runs once. A concurrent\n// retry, while the first is still in flight, gets 409 { error: \"idempotency_in_progress\" }.\n// Transient 5xx are NOT cached (a failed call stays retryable)."
|
|
228
|
+
"code": "import { server } from \"@nifrajs/core\"\nimport { jwt, csrf, ipRestriction, bodyLimit } from \"@nifrajs/middleware\"\n\nconst app = server()\n // JWT: the algorithm allowlist is REQUIRED; alg:none and RSA/HMAC confusion are rejected; exp enforced.\n .use(jwt({ key: process.env.JWT_SECRET!, algorithms: [\"HS256\"], issuer: \"my-app\" }))\n // Signed double-submit CSRF (HMAC) + Origin/Referer check on unsafe methods. Secret must be >= 32 bytes.\n .use(csrf({ secret: process.env.CSRF_SECRET! }))\n // Allow/deny by IPv4/IPv6 + CIDR. FAILS CLOSED with no trusted client IP; X-Forwarded-For is ignored\n // unless trustedProxies > 0 (set it to the number of proxies you actually run in front of the app).\n .use(ipRestriction({ allow: [\"10.0.0.0/8\", \"::1\"], trustedProxies: 1 }))\n // Reject oversized bodies at the EDGE by Content-Length, before routing — fails closed (411) on a\n // length-less body. (The schema / c.boundedBody cap is the read-time guard; this is the cheap pre-filter.)\n .use(bodyLimit({ maxBytes: 1_000_000 }))"
|
|
283
229
|
},
|
|
284
230
|
{
|
|
285
231
|
"name": "LOGGING",
|
|
@@ -288,27 +234,39 @@
|
|
|
288
234
|
"code": "import { server, jsonLogger, commonSecretPatterns } from \"@nifrajs/core\"\n\n// Key-name redaction is always on; valuePatterns adds opt-in value + message scanning.\nconst app = server({\n logger: jsonLogger(undefined, { valuePatterns: commonSecretPatterns }),\n})\n\n// logger.error(\"auth failed for user@example.com with Bearer abc.def\")\n// → { ...,\"message\":\"auth failed for [REDACTED] with [REDACTED]\" }\n// Add your own: { valuePatterns: [...commonSecretPatterns, /\\bord_[a-z0-9]+/g] }"
|
|
289
235
|
},
|
|
290
236
|
{
|
|
291
|
-
"name": "
|
|
292
|
-
"topic": "Nifra —
|
|
293
|
-
"slug": "
|
|
294
|
-
"code": "import {
|
|
237
|
+
"name": "SSE",
|
|
238
|
+
"topic": "Nifra — Streaming",
|
|
239
|
+
"slug": "streaming",
|
|
240
|
+
"code": "import { server, sse } from \"@nifrajs/core\"\n\nconst app = server()\n// Your pub-sub of choice — subscribe returns an unsubscribe function.\ndeclare const notifications: { subscribe(on: (n: { id: string }) => void): () => void }\n\n// A live feed — push events until the client disconnects.\napp.get(\"/notifications\", (c) =>\n sse(c, (stream) => {\n const off = notifications.subscribe((n) =>\n stream.send({ event: \"notification\", id: n.id, data: JSON.stringify(n) }),\n )\n // Keep the connection open until the client leaves, then tear down.\n return new Promise<void>((resolve) =>\n stream.signal.addEventListener(\"abort\", () => { off(); resolve() }, { once: true }),\n )\n }, { keepAlive: 15_000 }),\n)"
|
|
295
241
|
},
|
|
296
242
|
{
|
|
297
|
-
"name": "
|
|
298
|
-
"topic": "Nifra —
|
|
299
|
-
"slug": "
|
|
300
|
-
"code": "
|
|
243
|
+
"name": "SERVER_ONLY_MARKER",
|
|
244
|
+
"topic": "Nifra — Troubleshooting",
|
|
245
|
+
"slug": "troubleshooting",
|
|
246
|
+
"code": "// secrets.ts — pure server logic: a secret constant, no `node:` import to catch.\nimport \"@nifrajs/web/server-only\" // ← fails the CLIENT build (loud, with the import chain)\nimport type { ServerOnly } from \"@nifrajs/web\" // ← type-level intent: this value is server-only\n\n// `ServerOnly<string>` is structurally `string` (the brand is an optional phantom field), so it\n// stays assignment-compatible — it documents intent without obstructing real use.\nexport const apiKey: ServerOnly<string> = process.env.SECRET_API_KEY!\n\n// If this module ever reaches a browser chunk, buildClient fails with:\n// server-only module reached the client bundle via routes/x.tsx → ./secrets.ts (marked server-only)\n// Reach it from a loader/action (server-only) instead, and the secret never ships to the client."
|
|
301
247
|
},
|
|
302
248
|
{
|
|
303
|
-
"name": "
|
|
304
|
-
"topic": "Nifra —
|
|
305
|
-
"slug": "
|
|
306
|
-
"code": "import {
|
|
249
|
+
"name": "CLIENT",
|
|
250
|
+
"topic": "Nifra — Types-first architecture",
|
|
251
|
+
"slug": "types-first",
|
|
252
|
+
"code": "import { client } from \"@nifrajs/client\"\nimport type { app } from \"./server\" // a TYPE import — server code never ships to the client\n\nconst api = client<typeof app>(\"https://api.example.com\")\n\nconst res = await api.users({ id: \"42\" }).get() // path + params autocomplete, no codegen\nif (res.ok) {\n res.data.name // typed from the route's response schema\n} else {\n res.error // client-call failures are returned, never thrown\n}"
|
|
307
253
|
},
|
|
308
254
|
{
|
|
309
|
-
"name": "
|
|
310
|
-
"topic": "Nifra —
|
|
311
|
-
"slug": "
|
|
312
|
-
"code": "import {
|
|
255
|
+
"name": "OPENAPI",
|
|
256
|
+
"topic": "Nifra — Types-first architecture",
|
|
257
|
+
"slug": "types-first",
|
|
258
|
+
"code": "import { server } from \"@nifrajs/core\"\nimport { openapi } from \"@nifrajs/middleware\"\nimport { GetUser } from \"./schema\" // the contract defined above\n\n// Generates an OpenAPI 3.1 document from your registered routes — lazily, on first request.\nexport const app = server()\n .use(openapi({ info: { title: \"My API\", version: \"1.0.0\" }, ui: true }))\n .get(\"/users/:id\", GetUser, (c) => ({ id: c.params.id, name: \"Ada\", role: \"admin\" as const }))\n\n// → GET /openapi.json (the spec, generated from your schemas)\n// → GET /reference (a Scalar API-reference page, because `ui: true`)"
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
"name": "ROUTE",
|
|
262
|
+
"topic": "Nifra — Types-first architecture",
|
|
263
|
+
"slug": "types-first",
|
|
264
|
+
"code": "import { server } from \"@nifrajs/core\"\nimport { GetUser } from \"./schema\"\n\nexport const app = server().get(\"/users/:id\", GetUser, (c) => {\n // c.params.id is typed `string` — parsed from the path and validated at the boundary.\n return { id: c.params.id, name: \"Ada\", role: \"admin\" as const }\n // ^ the return is checked against GetUser.response — a wrong shape is a tsc error.\n})"
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
"name": "SCHEMA",
|
|
268
|
+
"topic": "Nifra — Types-first architecture",
|
|
269
|
+
"slug": "types-first",
|
|
270
|
+
"code": "import { t } from \"@nifrajs/schema\"\n\n// One contract, defined once. Everything below is derived from it.\nexport const GetUser = {\n params: t.object({ id: t.string() }),\n response: t.object({\n id: t.string(),\n name: t.string(),\n role: t.union([t.literal(\"admin\"), t.literal(\"user\")]),\n }),\n}"
|
|
313
271
|
}
|
|
314
272
|
]
|