@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 +1 -1
- package/dist/mcp-http.js +1 -1
- package/docs/llms-full.txt +234 -1
- package/docs/types.json +476 -17
- package/package.json +7 -7
- package/src/cli.ts +1 -1
- package/src/mcp-http.ts +1 -1
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.
|
|
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.
|
|
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). */
|
package/docs/llms-full.txt
CHANGED
|
@@ -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
|
|