@nifrajs/cli 0.1.0-alpha.1

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 (95) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +99 -0
  3. package/dist/check.d.ts +100 -0
  4. package/dist/check.d.ts.map +1 -0
  5. package/dist/check.js +690 -0
  6. package/dist/check.js.map +1 -0
  7. package/dist/cli.d.ts +3 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +208 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/docs-search.d.ts +30 -0
  12. package/dist/docs-search.d.ts.map +1 -0
  13. package/dist/docs-search.js +94 -0
  14. package/dist/docs-search.js.map +1 -0
  15. package/dist/doctor.d.ts +52 -0
  16. package/dist/doctor.d.ts.map +1 -0
  17. package/dist/doctor.js +291 -0
  18. package/dist/doctor.js.map +1 -0
  19. package/dist/examples.d.ts +30 -0
  20. package/dist/examples.d.ts.map +1 -0
  21. package/dist/examples.js +70 -0
  22. package/dist/examples.js.map +1 -0
  23. package/dist/introspect.d.ts +64 -0
  24. package/dist/introspect.d.ts.map +1 -0
  25. package/dist/introspect.js +243 -0
  26. package/dist/introspect.js.map +1 -0
  27. package/dist/load.d.ts +51 -0
  28. package/dist/load.d.ts.map +1 -0
  29. package/dist/load.js +70 -0
  30. package/dist/load.js.map +1 -0
  31. package/dist/mcp-docs-tools.d.ts +13 -0
  32. package/dist/mcp-docs-tools.d.ts.map +1 -0
  33. package/dist/mcp-docs-tools.js +63 -0
  34. package/dist/mcp-docs-tools.js.map +1 -0
  35. package/dist/mcp-http.d.ts +43 -0
  36. package/dist/mcp-http.d.ts.map +1 -0
  37. package/dist/mcp-http.js +136 -0
  38. package/dist/mcp-http.js.map +1 -0
  39. package/dist/mcp-protocol.d.ts +97 -0
  40. package/dist/mcp-protocol.d.ts.map +1 -0
  41. package/dist/mcp-protocol.js +248 -0
  42. package/dist/mcp-protocol.js.map +1 -0
  43. package/dist/mcp-render.d.ts +17 -0
  44. package/dist/mcp-render.d.ts.map +1 -0
  45. package/dist/mcp-render.js +73 -0
  46. package/dist/mcp-render.js.map +1 -0
  47. package/dist/mcp-run.d.ts +24 -0
  48. package/dist/mcp-run.d.ts.map +1 -0
  49. package/dist/mcp-run.js +142 -0
  50. package/dist/mcp-run.js.map +1 -0
  51. package/dist/mcp-ws.d.ts +26 -0
  52. package/dist/mcp-ws.d.ts.map +1 -0
  53. package/dist/mcp-ws.js +258 -0
  54. package/dist/mcp-ws.js.map +1 -0
  55. package/dist/mcp.d.ts +42 -0
  56. package/dist/mcp.d.ts.map +1 -0
  57. package/dist/mcp.js +712 -0
  58. package/dist/mcp.js.map +1 -0
  59. package/dist/openapi-tool.d.ts +6 -0
  60. package/dist/openapi-tool.d.ts.map +1 -0
  61. package/dist/openapi-tool.js +48 -0
  62. package/dist/openapi-tool.js.map +1 -0
  63. package/dist/scaffold.d.ts +35 -0
  64. package/dist/scaffold.d.ts.map +1 -0
  65. package/dist/scaffold.js +140 -0
  66. package/dist/scaffold.js.map +1 -0
  67. package/dist/search-terms.d.ts +17 -0
  68. package/dist/search-terms.d.ts.map +1 -0
  69. package/dist/search-terms.js +82 -0
  70. package/dist/search-terms.js.map +1 -0
  71. package/dist/test-tool.d.ts +29 -0
  72. package/dist/test-tool.d.ts.map +1 -0
  73. package/dist/test-tool.js +150 -0
  74. package/dist/test-tool.js.map +1 -0
  75. package/docs/examples.json +314 -0
  76. package/docs/llms-full.txt +4325 -0
  77. package/package.json +58 -0
  78. package/src/check.ts +790 -0
  79. package/src/cli.ts +237 -0
  80. package/src/docs-search.ts +111 -0
  81. package/src/doctor.ts +349 -0
  82. package/src/examples.ts +89 -0
  83. package/src/introspect.ts +284 -0
  84. package/src/load.ts +129 -0
  85. package/src/mcp-docs-tools.ts +72 -0
  86. package/src/mcp-http.ts +155 -0
  87. package/src/mcp-protocol.ts +326 -0
  88. package/src/mcp-render.ts +78 -0
  89. package/src/mcp-run.ts +153 -0
  90. package/src/mcp-ws.ts +328 -0
  91. package/src/mcp.ts +863 -0
  92. package/src/openapi-tool.ts +51 -0
  93. package/src/scaffold.ts +164 -0
  94. package/src/search-terms.ts +89 -0
  95. package/src/test-tool.ts +181 -0
@@ -0,0 +1,314 @@
1
+ [
2
+ {
3
+ "name": "CLIENT",
4
+ "topic": "Nifra — API & typed client",
5
+ "slug": "api",
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
+ },
8
+ {
9
+ "name": "CONTRACT",
10
+ "topic": "Nifra — API & typed client",
11
+ "slug": "api",
12
+ "code": "import { defineContract, implement } from \"@nifrajs/core\"\nimport { z } from \"zod\"\n\n// 1. Declare the contract — methods, paths, and input schemas, no handlers.\n// Share this object between server and (optionally) other services.\nexport const contract = defineContract({\n listUsers: { method: \"GET\", path: \"/users\" },\n getUser: { method: \"GET\", path: \"/users/:id\" },\n createUser:{ method: \"POST\", path: \"/users\", body: z.object({ name: z.string() }) },\n search: { method: \"GET\", path: \"/search\", query: z.object({ page: z.string() }) },\n})\n\n// 2. Implement it — handlers are checked against the contract (path params, body, query all typed).\nexport const app = implement(contract, {\n listUsers: () => users.all(),\n getUser: (c) => users.find(c.params.id),\n createUser:(c) => users.create(c.body.name),\n search: (c) => ({ page: c.query.page }),\n})"
13
+ },
14
+ {
15
+ "name": "INLINE",
16
+ "topic": "Nifra — API & typed client",
17
+ "slug": "api",
18
+ "code": "import { server } from \"@nifrajs/core\"\nimport { z } from \"zod\" // any Standard Schema works: zod, valibot, arktype…\n\nexport const app = server()\n .get(\"/users/:id\", (c) => ({ id: c.params.id })) // c.params is typed from the path\n .post(\"/users\", { body: z.object({ name: z.string().min(1) }) }, // body validated at the boundary\n (c) => ({ created: c.body.name })) // c.body is the validated type\n .get(\"/search\", { query: z.object({ page: z.string() }) }, // query validated too\n (c) => ({ page: c.query.page }))\n .listen(3000)"
19
+ },
20
+ {
21
+ "name": "BETTERAUTH",
22
+ "topic": "Nifra — Auth & sessions",
23
+ "slug": "auth",
24
+ "code": "// auth.ts — your configured Better Auth instance (database, providers, …):\nimport { betterAuth as createBetterAuth } from \"better-auth\"\nexport const auth = createBetterAuth({ database: db, emailAndPassword: { enabled: true } })\n\n// server.ts — ONE use() mounts every Better Auth endpoint at /api/auth/*:\nimport { betterAuth, getSession, requireSession } from \"@nifrajs/better-auth\"\nconst app = server()\n .use(betterAuth(auth)) // sign-in/up/out, OAuth, 2FA, session…\n .get(\"/me\", async (c) => (await requireSession(auth, c.req)).user) // typed; 401 when signed out\n\n// In a loader/action, read the session from the raw Request:\nexport async function loader({ request }) {\n const session = await getSession(auth, request) // { user, session } | null — typed\n const { user } = await requireSession(auth, request, { redirectTo: \"/login\" }) // or guard it\n return { user }\n}"
25
+ },
26
+ {
27
+ "name": "GUARD",
28
+ "topic": "Nifra — Auth & sessions",
29
+ "slug": "auth",
30
+ "code": "// A protected route's loader — reads the session and redirects when absent.\nimport { requireUser } from \"@nifrajs/auth\" // browser-safe; OK to import in a route module\n\nexport async function loader({ request, api }) {\n const sessions = api // the manager injected via createWebApp's `api`\n const session = await sessions.read(request)\n // requireUser throws a 302 to /login when there's no session; nifra returns the thrown Response.\n const userId = requireUser(session, \"userId\", { redirectTo: \"/login\" })\n return { userId }\n}"
31
+ },
32
+ {
33
+ "name": "SETUP",
34
+ "topic": "Nifra — Auth & sessions",
35
+ "slug": "auth",
36
+ "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, KVSessionStore } from \"@nifrajs/auth\"\n\nlet manager\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}))"
37
+ },
38
+ {
39
+ "name": "FRAMEWORK",
40
+ "topic": "Nifra — CLI",
41
+ "slug": "cli",
42
+ "code": "// framework.ts — the one place an app names its framework. The CLI reads these:\nimport react from \"@vitejs/plugin-react\"\nimport { reactAdapter } from \"@nifrajs/web-react\"\n\nexport const adapter = reactAdapter\nexport const clientModule = \"@nifrajs/web-react/client\"\nexport const vitePlugins = [react()] // dev HMR (Fast Refresh)\n// Vue/Svelte/Solid also export:\n// clientPlugins = [vueBunPlugin(\"dom\")] // compile routes for the client build\n// serverPlugins = [vueBunPlugin(\"ssr\")] // compile routes for SSR (nifra start registers them)\n// conditions = [\"solid\"] // Solid: resolve solid-js to its source\n// define = { __VUE_OPTIONS_API__: \"true\", ... } // Vue feature flags"
43
+ },
44
+ {
45
+ "name": "CONTENT",
46
+ "topic": "Nifra — Loaders & actions",
47
+ "slug": "data",
48
+ "code": "// content.config.ts — a typed, validated collection over a folder of Markdown.\nimport { defineCollection } from \"@nifrajs/content/fs\"\nimport { t } from \"@nifrajs/schema\"\n\nexport const blog = defineCollection({\n dir: \"content/blog\",\n schema: t.object({ title: t.string(), date: t.string(), draft: t.boolean() }),\n})\n\n// a loader — typed + validated entries, no manual fs/frontmatter parsing:\nexport async function loader() {\n const posts = (await blog.all()).filter((p) => !p.frontmatter.draft)\n return { posts: posts.sort((a, b) => b.frontmatter.date.localeCompare(a.frontmatter.date)) }\n}\n// posts[0].frontmatter is { title; date; draft } (typed); posts[0].html is the rendered Markdown"
49
+ },
50
+ {
51
+ "name": "DECORATE",
52
+ "topic": "Nifra — Databases",
53
+ "slug": "database",
54
+ "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
+ },
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
+ {
69
+ "name": "SQLITE",
70
+ "topic": "Nifra — Databases",
71
+ "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)"
73
+ },
74
+ {
75
+ "name": "CF",
76
+ "topic": "Nifra — Deployment",
77
+ "slug": "deployment",
78
+ "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
+ },
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
+ {
99
+ "name": "AUTOMATED",
100
+ "topic": "Nifra — Fonts",
101
+ "slug": "fonts",
102
+ "code": "// fonts.build.ts — run once at build time (a prebuild step). It hits the network.\nimport { loadGoogleFont } from \"@nifrajs/web/fonts\"\n\nconst inter = await loadGoogleFont(\n { family: \"Inter\", weights: [400, 700], subsets: [\"latin\"], sizeAdjust: \"107%\" },\n { outDir: \"public/fonts\", publicPath: \"/fonts\" },\n)\n\nawait Bun.write(\"app/fonts.css\", inter.css) // the @font-face stylesheet\nawait Bun.write(\"app/fonts.preloads.json\", JSON.stringify(inter.preloads))"
103
+ },
104
+ {
105
+ "name": "MANUAL",
106
+ "topic": "Nifra — Fonts",
107
+ "slug": "fonts",
108
+ "code": "// fonts.css — you dropped inter.woff2 into public/fonts/ yourself\nimport { fontFace } from \"@nifrajs/web\"\n\nexport default fontFace({\n family: \"Inter\",\n src: [{ url: \"/fonts/inter.woff2\" }], // format() inferred from the extension\n weight: \"100 900\", // a variable-font range\n display: \"swap\", // the default — paints fallback text instantly\n sizeAdjust: \"107%\", // metric override → no fallback→web-font shift\n})"
109
+ },
110
+ {
111
+ "name": "PRELOAD",
112
+ "topic": "Nifra — Fonts",
113
+ "slug": "fonts",
114
+ "code": "// a root layout's meta — becomes <link rel=\"preload\" as=\"font\" crossorigin> in <head>\nimport { fontPreload } from \"@nifrajs/web\"\n\nexport const meta = { link: [fontPreload({ href: \"/fonts/inter.woff2\" })] }"
115
+ },
116
+ {
117
+ "name": "SWAP",
118
+ "topic": "Nifra — Frameworks",
119
+ "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 api,\n})"
121
+ },
122
+ {
123
+ "name": "NEGOTIATE",
124
+ "topic": "Nifra — i18n",
125
+ "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})"
133
+ },
134
+ {
135
+ "name": "DIMENSIONS",
136
+ "topic": "Nifra — images",
137
+ "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.\nimageDimensions(new Uint8Array(headerBytes))"
139
+ },
140
+ {
141
+ "name": "LOADERS",
142
+ "topic": "Nifra — images",
143
+ "slug": "images",
144
+ "code": "import type { ImageLoader } from \"@nifrajs/image\"\nimport { cloudflareLoader, identityLoader } from \"@nifrajs/image\"\n\n// Built-in: Cloudflare Images. `base` prepends an origin to bare paths.\nconst cf = cloudflareLoader({ base: \"https://assets.example.com\" })\n\n// The default when you pass no loader: no transform (still CLS-safe + lazy,\n// just no responsive variants).\nidentityLoader({ src: \"/a.png\", width: 800 }) // → \"/a.png\"\n\n// Any CDN is a pure (src, width, quality?) → URL builder:\nconst imgix: ImageLoader = ({ src, width, quality }) =>\n `https://my.imgix.net${src}?w=${width}&auto=format${quality ? `&q=${quality}` : \"\"}`"
145
+ },
146
+ {
147
+ "name": "SIGNING",
148
+ "topic": "Nifra — images",
149
+ "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: env.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: env.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: env.IMAGE_SECRET,\n expiresIn: 300, // seconds → adds &exp=\n})"
151
+ },
152
+ {
153
+ "name": "CLIENT",
154
+ "topic": "Nifra — Getting started",
155
+ "slug": "index",
156
+ "code": "import { client } from \"@nifrajs/client\"\nimport type { app } from \"./server\"\n\n// The client infers the server's types — no codegen. Never throws: { data, error }.\nconst api = client<typeof app>(\"http://localhost:3000\")\nconst { data, error } = await api.users({ id: \"7\" }).get()\n// ^? { id: string } | undefined"
157
+ },
158
+ {
159
+ "name": "HELLO",
160
+ "topic": "Nifra — Getting started",
161
+ "slug": "index",
162
+ "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
+ },
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
+ {
177
+ "name": "CACHING",
178
+ "topic": "Nifra — Plugins & middleware",
179
+ "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)"
181
+ },
182
+ {
183
+ "name": "IDENTITY",
184
+ "topic": "Nifra — Plugins & middleware",
185
+ "slug": "plugins",
186
+ "code": "import { defineIdentityPlugin, server } from \"@nifrajs/core\"\n\n// A plugin that registers routes/hooks but adds NO context type is a type-IDENTITY plugin.\n// defineIdentityPlugin keeps app.use()'s return type EXACTLY the caller's server, so routes added\n// after .use() keep their types — and the typed client derived from them stays intact.\nexport const audit = defineIdentityPlugin(\"audit\", (app) =>\n app.onResponse((res) => res),\n)\n\n// /a AND /b stay fully typed across the .use():\nconst api = server().get(\"/a\", () => ({ a: 1 })).use(audit).get(\"/b\", () => ({ b: 2 }))"
187
+ },
188
+ {
189
+ "name": "JWT_BASIC",
190
+ "topic": "Nifra — Plugins & middleware",
191
+ "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\" })\napp\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
+ },
194
+ {
195
+ "name": "MIDDLEWARE",
196
+ "topic": "Nifra — Plugins & middleware",
197
+ "slug": "plugins",
198
+ "code": "import { cors, rateLimit, securityHeaders } from \"@nifrajs/middleware\"\n\n// Hardening middleware is a hook bundle (context-agnostic) — same app.use():\napp\n .use(securityHeaders())\n .use(cors({ origins: [\"https://app.example\"] }))\n .use(rateLimit({ limit: 100, windowMs: 60_000 }))"
199
+ },
200
+ {
201
+ "name": "OFFICIAL",
202
+ "topic": "Nifra — Plugins & middleware",
203
+ "slug": "plugins",
204
+ "code": "import { requestId, logger, etag } from \"@nifrajs/middleware\"\n\napp\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"
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 }))"
211
+ },
212
+ {
213
+ "name": "PERF",
214
+ "topic": "Nifra — Plugins & middleware",
215
+ "slug": "plugins",
216
+ "code": "import { compression, cacheControl } from \"@nifrajs/middleware\"\n\napp\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))"
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() })))"
223
+ },
224
+ {
225
+ "name": "RELIABILITY",
226
+ "topic": "Nifra — Plugins & middleware",
227
+ "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() }))"
229
+ },
230
+ {
231
+ "name": "SHAPING",
232
+ "topic": "Nifra — Plugins & middleware",
233
+ "slug": "plugins",
234
+ "code": "import { methodOverride, trimTrailingSlash, language, timing, poweredBy, combine } from \"@nifrajs/middleware\"\n\napp\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
+ },
236
+ {
237
+ "name": "BUILD",
238
+ "topic": "Nifra — Rendering: SSG & ISR",
239
+ "slug": "rendering",
240
+ "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
+ },
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
+ {
249
+ "name": "FONTS",
250
+ "topic": "Nifra — Rendering: SSG & ISR",
251
+ "slug": "rendering",
252
+ "code": "// fonts.css — a CLS-safe @font-face for a self-hosted font (the pipeline bundles + hashes it).\nimport { fontFace } from \"@nifrajs/web\"\nexport default fontFace({\n family: \"Inter\",\n src: [{ url: \"/fonts/inter-var.woff2\" }], // self-host it — never hotlink a CDN\n weight: \"100 900\", // variable font\n sizeAdjust: \"100.06%\", ascentOverride: \"90%\", // optional: stop fallback->web-font layout shift\n})\n\n// a root layout — preload the file so it downloads WITH the document (not after CSS parse):\nimport { fontPreload } from \"@nifrajs/web\"\nexport const meta = { link: [fontPreload({ href: \"/fonts/inter-var.woff2\" })] }"
253
+ },
254
+ {
255
+ "name": "ISR",
256
+ "topic": "Nifra — Rendering: SSG & ISR",
257
+ "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, api })\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) })"
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}"
265
+ },
266
+ {
267
+ "name": "BOUNDED",
268
+ "topic": "Nifra — security & hardening",
269
+ "slug": "security",
270
+ "code": "import { server, t } 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 c.json({ received: bytes.byteLength })\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})"
271
+ },
272
+ {
273
+ "name": "GATING",
274
+ "topic": "Nifra — security & hardening",
275
+ "slug": "security",
276
+ "code": "import { jwt, csrf, ipRestriction, bodyLimit } from \"@nifrajs/middleware\"\n\napp\n // JWT: the algorithm allowlist is REQUIRED; alg:none and RSA/HMAC confusion are rejected; exp enforced.\n .use(jwt({ key: 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: 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 }))"
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)."
283
+ },
284
+ {
285
+ "name": "LOGGING",
286
+ "topic": "Nifra — security & hardening",
287
+ "slug": "security",
288
+ "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
+ },
290
+ {
291
+ "name": "STRIP",
292
+ "topic": "Nifra — security & hardening",
293
+ "slug": "security",
294
+ "code": "import { stripImageMetadata } from \"@nifrajs/uploads\"\nimport { bunImageBackend } from \"@nifrajs/image/backends\"\n\n// Drop EXIF/GPS by re-encoding through any @nifrajs/image backend. @nifrajs/uploads keeps\n// ZERO dependency on @nifrajs/image — the backend is passed in (structural type), so this\n// also works with sharpImageBackend(sharp) on Node or wasmImageBackend(...) on the edge.\nconst clean = await stripImageMetadata(result.bytes, bunImageBackend())"
295
+ },
296
+ {
297
+ "name": "UPLOADS",
298
+ "topic": "Nifra — security & hardening",
299
+ "slug": "security",
300
+ "code": "import { validateUpload, signDownloadUrl } from \"@nifrajs/uploads\"\n\napp.post(\"/avatar\", async (c) => {\n const form = await c.req.formData()\n const file = form.get(\"file\")\n if (!(file instanceof Blob)) return c.json({ ok: false, error: \"no_file\" }, 400)\n\n // Size cap + REAL type by magic bytes — a .exe renamed .png (or a spoofed\n // Content-Type) is caught, because the bytes win. An oversized Blob is rejected\n // by its .size BEFORE it's buffered into memory.\n const result = await validateUpload(file, {\n maxBytes: 2_000_000,\n accept: [\"image/png\", \"image/jpeg\"], // exact, or \"image/*\"\n })\n if (!result.ok) return c.json({ ok: false, error: result.reason }, 400)\n // reason: \"too_large\" | \"empty\" | \"unrecognized\" | \"type_not_allowed\"\n\n await save(result.bytes, `${id}.${result.ext}`) // result.mime / .ext are trustworthy\n\n // Hand back a short-TTL, tamper-evident URL (HMAC over path + expiry):\n const url = await signDownloadUrl(`/files/${id}`, env.FILE_SECRET, { expiresInSeconds: 300 })\n return c.json({ ok: true, url })\n})"
301
+ },
302
+ {
303
+ "name": "WEBHOOK",
304
+ "topic": "Nifra — security & hardening",
305
+ "slug": "security",
306
+ "code": "import { verifyWebhook } from \"@nifrajs/core\"\n\napp.post(\"/webhooks/stripe\", async (c) => {\n // Reads the raw body BOUNDED (DoS guard), verifies the HMAC CONSTANT-TIME, and only\n // then returns the payload. Never JSON.parse a webhook before the signature checks out.\n const r = await verifyWebhook(c.req, env.STRIPE_WEBHOOK_SECRET, { provider: \"stripe\" })\n if (!r.ok) return c.json({ ok: false, error: r.reason }, 400)\n // reason: \"missing_signature\" | \"invalid_signature\" | \"timestamp_out_of_tolerance\"\n // | \"malformed_signature\" | \"payload_too_large\" | \"invalid_content_length\"\n\n const event = StripeEvent.parse(JSON.parse(r.payload)) // validate at the trust boundary\n // …handle event… (pair with idempotency below so a redelivery doesn't double-process)\n return c.json({ ok: true })\n})\n\n// GitHub (sha256=…hex), or any provider via the generic preset:\nawait verifyWebhook(c.req, env.GH_SECRET, { provider: \"github\" })\nawait verifyWebhook(c.req, [next, current], { // an array accepts either during a rotation\n header: \"x-signature\", encoding: \"base64\", prefix: \"v1=\",\n})"
307
+ },
308
+ {
309
+ "name": "SSE",
310
+ "topic": "Nifra — Streaming",
311
+ "slug": "streaming",
312
+ "code": "import { sse } from \"@nifrajs/core\"\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)"
313
+ }
314
+ ]