@nifrajs/cli 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -68,7 +68,7 @@ routes/ from the current directory. Run from your project root.
68
68
  Port: \`dev\` and \`start\` share the default ${DEFAULT_DEV_PORT}. Override with \`--port <n>\` (alias \`-p\`) or the
69
69
  \`PORT\` env var (\`--port\` wins over \`PORT\`, which wins over the default).`;
70
70
  // Kept in lockstep with packages/cli/package.json by check:publish's version-consistency gate.
71
- const CLI_VERSION = "1.0.0";
71
+ const CLI_VERSION = "1.1.0";
72
72
  // A render adapter + nifra server are opaque to the CLI (it just forwards them); cast at the seam.
73
73
  const asAdapter = (v) => v;
74
74
  const asBunPlugins = (v) => v;
package/dist/mcp-http.js CHANGED
@@ -19,7 +19,7 @@ import { loadTypesCorpus } from "./types-search.js";
19
19
  // tool factory + the transport core from one entry: `import { respondMcpHttp, docsTools } from "@nifrajs/cli/mcp"`.
20
20
  export { docsTools } from "./mcp-docs-tools.js";
21
21
  // Kept in lockstep with packages/cli/package.json by check:publish's version-consistency gate.
22
- const VERSION = "1.0.0";
22
+ const VERSION = "1.1.0";
23
23
  const SERVER_INFO = { name: "nifra-docs", version: VERSION };
24
24
  const DOCS_HEALTH = "nifra docs MCP — POST JSON-RPC 2.0 here (methods: initialize, tools/list, tools/call). Tools: nifra_docs, nifra_example.";
25
25
  /** The two project-independent tools, reading the package's bundled corpus from disk (CLI use). */
@@ -4411,6 +4411,61 @@ one cheap read instead of the whole corpus. For the wider framework: the repo's
4411
4411
  [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
4412
4412
  done-gate, or `nifra mcp` to give the agent live project tools.
4413
4413
 
4414
+ ## @nifrajs/cache
4415
+
4416
+ Typed KV cache for nifra — **TTL**, **stale-while-revalidate**, **tag invalidation**, and **single-flight
4417
+ stampede protection** on a **pluggable store** (in-memory by default; bring CF KV / Redis for a shared
4418
+ cache). **Dependency-free**; runs on Bun/Node/Deno/Workers.
4419
+
4420
+ ```ts
4421
+ import { createCache } from "@nifrajs/cache"
4422
+
4423
+ const cache = createCache({ defaultTtlMs: 30_000 })
4424
+
4425
+ // Cache-aside in a loader — one DB hit per key per TTL, even under a stampede:
4426
+ export async function loader({ params }) {
4427
+ const user = await cache.wrap(
4428
+ `user:${params.id}`,
4429
+ () => db.users.find(params.id),
4430
+ { ttlMs: 30_000, swrMs: 60_000, tags: [`user:${params.id}`] },
4431
+ )
4432
+ return { user }
4433
+ }
4434
+
4435
+ // On write, drop everything tagged for that user:
4436
+ await cache.invalidateTag(`user:${id}`)
4437
+ ```
4438
+
4439
+ ## Semantics
4440
+
4441
+ - **`wrap(key, loader, opts)`** — returns the cached value, or runs `loader`, stores, and returns it.
4442
+ - **Fresh** (`now < staleAt`): the cached value, no loader call.
4443
+ - **Stale-but-live** (`staleAt ≤ now < expiresAt`, i.e. within `swrMs`): the stale value is returned
4444
+ **immediately** while a **background** refresh runs (deduped). Latency stays flat; data self-heals.
4445
+ - **Miss/expired**: awaits `loader`. Concurrent misses for the same key share **one** call (no stampede).
4446
+ - A throwing `loader` is **not** cached (and in the background path goes to `onError`, never rejects the caller).
4447
+ - **`set` / `get` / `has` / `delete`**, **`invalidateTag(tag)`**, **`clear()`**.
4448
+ - TTL: `ttlMs` (→ stale) + optional `swrMs` (→ served-stale window) + optional `tags`.
4449
+
4450
+ ## Stores
4451
+
4452
+ The default `MemoryCache` is in-process with lazy expiry, a tag index, and an optional LRU cap
4453
+ (`new MemoryCache({ maxEntries: 10_000 })`). For a cache shared across instances, implement `CacheStore`
4454
+ (`get` / `set` / `delete` / `invalidateTag` / `clear`) over CF KV, Redis, etc.:
4455
+
4456
+ ```ts
4457
+ const cache = createCache({ store: new RedisCacheStore(redis) })
4458
+ ```
4459
+
4460
+ On **Cloudflare Workers** the in-memory cache is per-isolate and short-lived — back it with **CF KV** (or
4461
+ the Cache API) via a `CacheStore` for anything that should survive across requests/instances.
4462
+
4463
+ ## API
4464
+
4465
+ - `createCache(options?)` → `Cache` — `{ store?, defaultTtlMs?, now?, onError? }`.
4466
+ - `cache.wrap(key, loader, { ttlMs?, swrMs?, tags? })` · `get` · `has` · `set` · `delete` · `invalidateTag` · `clear`.
4467
+ - `MemoryCache({ maxEntries?, now? })` — the default store; implement `CacheStore` for your own.
4468
+
4414
4469
  ## @nifrajs/cli
4415
4470
 
4416
4471
  The `nifra` CLI — zero-config dev/build/start for a nifra app, plus the agent surfaces (`nifra context`,
@@ -4942,6 +4997,89 @@ one cheap read instead of the whole corpus. For the wider framework: the repo's
4942
4997
  [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
4943
4998
  done-gate, or `nifra mcp` to give the agent live project tools.
4944
4999
 
5000
+ ## @nifrajs/jobs
5001
+
5002
+ Typed background jobs for nifra — enqueue work off the request path, run it with **retries + exponential
5003
+ backoff + dead-lettering** on a **pluggable store**. The async companion to [`@nifrajs/cron`](../cron)
5004
+ (cron schedules; jobs does the deferred work — email, webhooks, image processing). **Dependency-free.**
5005
+
5006
+ ```ts
5007
+ import { createQueue } from "@nifrajs/jobs"
5008
+ import { t } from "@nifrajs/schema"
5009
+
5010
+ const q = createQueue()
5011
+
5012
+ const email = q.define("send-email", {
5013
+ input: t.object({ to: t.string(), subject: t.string() }), // validated at enqueue (the trust boundary)
5014
+ retries: { attempts: 5 }, // 5 tries, exponential backoff, then dead-letter
5015
+ async handler({ to, subject }, ctx) {
5016
+ await send(to, subject) // ctx = { id, name, attempt }
5017
+ },
5018
+ })
5019
+
5020
+ // In a route handler — enqueue and return immediately:
5021
+ await email.enqueue({ to: "a@b.com", subject: "Welcome" })
5022
+ await email.enqueue({ to: "b@b.com", subject: "Later" }, { delayMs: 60_000 }) // run in 1 min
5023
+
5024
+ // Start the worker on a long-running server (Bun/Node/Deno):
5025
+ const worker = q.start({ concurrency: 4 })
5026
+ // graceful shutdown: await worker.stop()
5027
+ ```
5028
+
5029
+ `enqueue` is typed against the handler's payload, and `input` (any [Standard Schema](https://standardschema.dev)
5030
+ validator — `@nifrajs/schema`'s `t`, Zod, Valibot, …) validates it before it's stored, so a bad payload
5031
+ fails at the call site, not three retries later.
5032
+
5033
+ ## Retries
5034
+
5035
+ Per-job: `retries: number` (attempts) or `{ attempts, backoff }`. A handler that throws is routed to
5036
+ `onError`, then retried with the backoff delay; after the last attempt it's **dead-lettered**, not lost.
5037
+
5038
+ ```ts
5039
+ import { exponentialBackoff, fixedBackoff } from "@nifrajs/jobs"
5040
+
5041
+ q.define("flaky", { retries: { attempts: 3, backoff: fixedBackoff(5_000) }, handler })
5042
+ createQueue({ backoff: exponentialBackoff({ baseMs: 500, maxMs: 60_000, jitter: 0.2 }) }) // queue default
5043
+ ```
5044
+
5045
+ ## Stores
5046
+
5047
+ The default `MemoryJobStore` is single-process — correct for dev and a single server, but not durable and
5048
+ not multi-worker. Implement the `JobStore` interface (`enqueue` / `lease` / `complete` / `retry` /
5049
+ `deadLetter` / `counts`) over Redis/Postgres for durability or horizontal workers:
5050
+
5051
+ ```ts
5052
+ const q = createQueue({ store: new RedisJobStore(redis) })
5053
+ ```
5054
+
5055
+ Leasing is at-least-once: a leased job is hidden for `leaseMs`; a worker that dies mid-job releases it
5056
+ back automatically. Make handlers **idempotent**.
5057
+
5058
+ ## Cloudflare Workers
5059
+
5060
+ Workers has no long-lived process, so don't call `start()`. Back the queue with a durable store and a
5061
+ [CF Queue](https://developers.cloudflare.com/queues/), enqueue via the producer binding, and drain from
5062
+ the consumer:
5063
+
5064
+ ```ts
5065
+ export default {
5066
+ async queue(_batch, env) {
5067
+ const q = createQueue({ store: new D1JobStore(env.DB) })
5068
+ await q.process() // one round; the platform schedules invocations
5069
+ },
5070
+ }
5071
+ ```
5072
+
5073
+ ## API
5074
+
5075
+ - `createQueue(options?)` → `Queue` — `{ store?, onError?, now?, defaultAttempts?, backoff? }`.
5076
+ - `queue.define(name, { handler, input?, retries? })` → typed `JobHandle` with `.enqueue(payload, { delayMs? | runAt? })`.
5077
+ - `queue.enqueue(name, payload, options?)` — enqueue by name.
5078
+ - `queue.start({ concurrency?, pollIntervalMs?, leaseMs? })` → `Worker` (`.stop()` drains gracefully).
5079
+ - `queue.process()` — run one poll round (for Workers / custom drivers). `queue.drain()` — process until empty.
5080
+ - `queue.counts()` → `{ pending, active, dead }`. `queue.store` — the underlying store.
5081
+ - Stores: `MemoryJobStore`. Backoff: `exponentialBackoff`, `fixedBackoff`, `noBackoff`.
5082
+
4945
5083
  ## @nifrajs/middleware
4946
5084
 
4947
5085
  Composable, dependency-light middleware for [nifra](../../README.md) — CORS, security headers, body
@@ -5258,6 +5396,85 @@ one cheap read instead of the whole corpus. For the wider framework: the repo's
5258
5396
  [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
5259
5397
  done-gate, or `nifra mcp` to give the agent live project tools.
5260
5398
 
5399
+ ## @nifrajs/storage
5400
+
5401
+ Blob storage for nifra — one `StorageAdapter` interface, several adapters. The **persistence half of
5402
+ [`@nifrajs/uploads`](../uploads)**: `uploads` decides what's allowed (MIME, size, signed URLs, EXIF
5403
+ stripping); `storage` puts the bytes somewhere. **Dependency-free.**
5404
+
5405
+ ```ts
5406
+ import { FileStorage } from "@nifrajs/storage"
5407
+
5408
+ const storage = new FileStorage("./uploads")
5409
+ await storage.put("avatars/u1.png", bytes, { contentType: "image/png" })
5410
+
5411
+ const object = await storage.get("avatars/u1.png") // { body, size, contentType?, metadata? } | null
5412
+ await storage.delete("avatars/u1.png")
5413
+ const keys = await storage.list({ prefix: "avatars/" })
5414
+ ```
5415
+
5416
+ ## Adapters
5417
+
5418
+ | Adapter | Use | Notes |
5419
+ |---|---|---|
5420
+ | `MemoryStorage` | dev / tests | not durable, not shared across instances |
5421
+ | `FileStorage(root)` | a long-running server (Bun/Node/Deno) | bytes-only: `contentType` inferred from the extension, custom metadata not persisted |
5422
+ | `R2Storage(env.BUCKET)` | the edge (Cloudflare R2) | round-trips `contentType` + metadata; binding typed structurally (no `@cloudflare/workers-types`) |
5423
+
5424
+ Implement `StorageAdapter` (`put` / `get` / `delete` / `exists` / `list`) for **S3 / GCS / anything else** —
5425
+ the same five methods, and your routes don't change.
5426
+
5427
+ ## Key safety
5428
+
5429
+ Keys are POSIX-ish relative paths (`avatars/u1.png`). **Every** adapter rejects unsafe keys — absolute
5430
+ paths, `..` traversal, NUL bytes, backslashes — via `assertSafeKey`, so a `FileStorage` key can never
5431
+ escape its root and a key valid in one adapter is valid in all.
5432
+
5433
+ ```ts
5434
+ import { assertSafeKey, StorageKeyError } from "@nifrajs/storage"
5435
+ ```
5436
+
5437
+ ## API
5438
+
5439
+ - `new MemoryStorage()` · `new FileStorage(root)` · `new R2Storage(bucket)`.
5440
+ - `put(key, data, { contentType?, metadata? })` — `data` is `Uint8Array | ArrayBuffer | string`.
5441
+ - `get(key)` → `{ body, size, contentType?, metadata? } | null` · `delete(key)` · `exists(key)` · `list({ prefix?, limit? })`.
5442
+ - `assertSafeKey(key)` / `StorageKeyError` · `toBytes(data)`.
5443
+
5444
+ ## @nifrajs/testing
5445
+
5446
+ Testing helpers for nifra apps. [`@nifrajs/client`](../client)'s **`testClient`** is already the typed,
5447
+ no-network in-process request client (nifra's supertest / `inject`). This package adds what it doesn't: a
5448
+ **cookie jar** and a **cookie-persisting `testSession`**, so a login → authenticated-request flow tests as
5449
+ easily as a single request.
5450
+
5451
+ ```ts
5452
+ import { testSession } from "@nifrajs/testing"
5453
+ import { app } from "../src/app"
5454
+
5455
+ const { client, cookies } = testSession<typeof app>(app)
5456
+
5457
+ await client.auth.login.post({ email, password }) // Set-Cookie captured into the jar
5458
+ const me = await client.me.get() // Cookie sent automatically
5459
+ expect(me.ok && me.data.id).toBeDefined()
5460
+ expect(cookies.get("sid")).toBeDefined() // inspect the jar
5461
+
5462
+ await client.auth.logout.post() // a Max-Age=0 Set-Cookie clears it
5463
+ expect(cookies.get("sid")).toBeUndefined()
5464
+ ```
5465
+
5466
+ Same in-process client as `testClient` — the app's own `fetch`, no server/port/network, the full real
5467
+ lifecycle (validation, middleware, contracts, auth) and end-to-end types from `App`. The only addition is
5468
+ that every call carries and captures cookies via a shared jar.
5469
+
5470
+ ## API
5471
+
5472
+ - `testSession<App>(app, { origin?, cookies? })` → `{ client: Treaty<App>, cookies: CookieJar }`.
5473
+ - `cookieJar()` → `CookieJar` — `header()` · `applyTo(headers)` · `store(response)` · `set` · `get` · `clear` · `size`.
5474
+ Honours removal (`Max-Age=0` / past `Expires`); other cookie attributes are ignored (in-process, same-origin).
5475
+
5476
+ For a **stateless** request (no cookies), use `testClient` from `@nifrajs/client` directly.
5477
+
5261
5478
  ## @nifrajs/uploads
5262
5479
 
5263
5480
  File-upload hardening for nifra — magic-byte MIME detection, size + type validation, short-TTL signed download URLs, and EXIF/metadata stripping. Dependency-free, edge-safe.
@@ -5590,6 +5807,10 @@ done-gate, or `nifra mcp` to give the agent live project tools.
5590
5807
 
5591
5808
  `betterAuth`, `BetterAuthLike`, `BetterAuthOptions`, `getSession`, `requireSession`, `RequireSessionOptions`, `SessionOf`
5592
5809
 
5810
+ ## @nifrajs/cache
5811
+
5812
+ `Cache`, `CacheOptions`, `CacheStore`, `createCache`, `MemoryCache`, `MemoryCacheOptions`, `SetOptions`, `StoredEntry`, `WrapOptions`
5813
+
5593
5814
  ## @nifrajs/client
5594
5815
 
5595
5816
  `ActionArgs`, `ActionData`, `ApiError`, `ApiProxy`, `client`, `ClientOptions`, `FetchFn`, `inProcessClient`, `Jsonify`, `LoaderArgs`, `LoaderData`, `RegistryOf`, `Result`, `testClient`, `Treaty`, `TreatyFromRegistry`
@@ -5626,6 +5847,10 @@ done-gate, or `nifra mcp` to give the agent live project tools.
5626
5847
 
5627
5848
  `batch`, `BindableElement`, `BindableRoot`, `bindScope`, `computed`, `effect`, `island`, `IslandContext`, `IslandHost`, `IslandScope`, `IslandSetup`, `islandState`, `mountIslands`, `signal`, `Signal`
5628
5849
 
5850
+ ## @nifrajs/jobs
5851
+
5852
+ `Backoff`, `createQueue`, `EnqueueOptions`, `exponentialBackoff`, `ExponentialOptions`, `fixedBackoff`, `JobContext`, `JobCounts`, `JobDefinition`, `JobError`, `JobHandle`, `JobHandler`, `JobStore`, `JobValidationError`, `MemoryJobStore`, `noBackoff`, `Queue`, `QueueOptions`, `RetryPolicy`, `StandardResult`, `StandardSchemaV1`, `StoredJob`, `Worker`, `WorkerOptions`
5853
+
5629
5854
  ## @nifrajs/mcp
5630
5855
 
5631
5856
  `bridgeScript`, `createMcpServer`, `CreateMcpServerOptions`, `defineMcpTool`, `DefineMcpToolOptions`, `defineMcpWidget`, `DefineMcpWidgetOptions`, `McpAppBridge`, `McpHttpOptions`, `McpServer`, `McpToolHandlerResult`, `McpUiIntent`, `McpWidget`, `respondMcpHttp`, `uiResourceMeta`, `widgetDocument`
@@ -5648,7 +5873,15 @@ done-gate, or `nifra mcp` to give the agent live project tools.
5648
5873
 
5649
5874
  ## @nifrajs/schema
5650
5875
 
5651
- `fromTypeBox`, `NifraSchema`, `OpenAPIDocument`, `OpenAPIInfo`, `registerFormat`, `t`, `toOpenAPI`
5876
+ `decodeCursor`, `encodeCursor`, `fromTypeBox`, `NifraSchema`, `OpenAPIDocument`, `OpenAPIInfo`, `Page`, `paginate`, `registerFormat`, `t`, `toOpenAPI`
5877
+
5878
+ ## @nifrajs/storage
5879
+
5880
+ `assertSafeKey`, `FileStorage`, `ListOptions`, `MemoryStorage`, `PutOptions`, `R2BucketLike`, `R2ObjectLike`, `R2Storage`, `StorageAdapter`, `StorageData`, `StorageKeyError`, `StorageObject`, `toBytes`
5881
+
5882
+ ## @nifrajs/testing
5883
+
5884
+ `AppLike`, `cookieJar`, `CookieJar`, `testSession`, `TestSession`, `TestSessionOptions`
5652
5885
 
5653
5886
  ## @nifrajs/uploads
5654
5887