Repository navigation
Releases: zeestack/dendra
Release list
v0.14.0 — mapError
Found running dendra in production. No breaking changes.
.mapError()maps the app's own errors toDendraErrors in one place,
instead of atry/catchin every handler
(#77, found running dendra in
production). It covers every guard and handler on every transport, after-hooks
see the mapped code, and the original is kept ascause. New type
ErrorMapper.
Reference: https://products.techfoundry.io/dendra/reference/router/#maperrorfn
v0.13.0 — hardening for 1.0
From the 1.0 milestone: the AI/MCP security review and the bugs the API
reference turned up. Requires Node.js 22.
Breaking (the 1.0 upgrade guide covers each):
- The deprecated aliases from 0.12 are removed from
dendra:resolveProc,
invokeUnary,invokeStreamand theProc,AnyProc,CallSite,
TransportandProcedureKindtypes (import them fromdendra/adapter);
createMemoryStore,recallMemory,rememberFactsandformatMemoryBlock
(fromdendra/memory);toolSourceandToolSourceOptions(now
lazyToolSourceandLazyToolSourceOptions). enginesrequires Node.js 22 or later.- An
.ai()procedure whose input has no JSON Schema throws where it's defined,
andmaxBodyBytescaps JSON bodies (both below).
From the AI/MCP security review
(#42,
docs/security-review-ai-mcp.md).
- Unexpected errors are masked. An error that isn't a
DendraErrorused to
reach clients and the model with its own message, which can carry SQL,
hostnames or credentials. It now arrives asINTERNAL: Internal errorover
HTTP, streams, WebSockets, job snapshots, the MCP bridge and tool calls, and
the original is logged withconsole.error. After-hooks still get the
original. If clients relied on such a message, throw aDendraErrorinstead. createToolExecutoronly throwsDendraErrors, so a hand-written agent loop
can pass the message to the model. A tool without.ai()answers
NOT_FOUND, like a missing one, so the model can't probe for procedures.createMcpToolSourcethrows a remote tool's error as a newBAD_GATEWAY(502)
code, with the server's text.lazyToolsetthrows when two tools sanitize to the same name (a.band
a_b), which used to route calls to the wrong one.- Zod, ArkType and Valibot schemas reach OpenAPI and AI tools. Libraries
that implement Standard JSON Schema publish their JSON Schema to OpenAPI and
tool definitions, ass.*does: Zod 4 and ArkType, and Valibot through
toStandardJsonSchemafrom@valibot/to-json-schema
(#65). Before, a tool with a
Zod input showed the model no parameters. Breaking: an.ai()procedure
whose input has no JSON Schema (Zod 3, plain Valibot, az.date()) now throws
where it's defined. - Per-tool MCP annotations.
.ai({ annotations })sets MCP'stitle,
readOnlyHint,destructiveHint,idempotentHintandopenWorldHintover
the defaults dendra derives from.get()andconfirm, so a POST that only
reads, or a write that's safe to repeat, is described correctly
(#59).inspectRoutershows
each tool'sannotations, so a test can assert them. New type
ToolAnnotations. createMcpRoutermounts an external MCP server's tools in your router as
.ai()procedures ({ ...appRouter, gh: await createMcpRouter(client) }),
soconfirm(withcreateApprovals), your.use()guards, after-hooks and
inspectRoutercover them like local tools
(#58). Remote descriptions
are cut to 1,024 characters there and increateMcpToolSource
(maxDescriptionLength). The AI reference has a new section on treating
external servers as untrusted.- Streaming tools are bounded and cancellable. A
.stream()procedure
exposed with.ai()stops collecting after 1,000 chunks or 30 s, set per tool
with.ai({ maxItems, timeoutMs }). It answers with what arrived and aborts
the handler'ssignal, so a subscription can't hang an agent or grow without
limit (#57). Tool executors
takeexecute(name, args, { signal });runAgentpasses itssignal, so
aborting a run cancels the call in flight.mergeToolsets,ToolSourceand
createMcpBridge().callToolpass the signal through.createMcpToolSource
stops waiting but can't cancel the remote call. Behaviour change: a
streaming tool that ran longer than 30 s used to wait for its end; raise
timeoutMsif it needs longer. - A stream that yields
undefinedsendsnullover NDJSON and WebSockets, as
SSE already did. NDJSON used to write the lineundefined, which made the
client throw. A tool that returnsundefinedgives the MCP bridge and
runAgentthe textnullinstead of no text
(#64). maxBodyBytescaps JSON bodies too. RPC, batch and REST JSON bodies were
read whole with no limit; now, like uploads, they're refused past the cap
(default 10 MB) with413 PAYLOAD_TOO_LARGE, while reading
(#62). RaisemaxBodyBytes
if you accept larger JSON.approvals.list(ctx)only returns actions in the caller's exact scope. When
scope(ctx)returnedundefined(say, a user with no tenant), it used to
return every tenant's pending actions
(#63).- The example app's stdio MCP server only calls tools it listed and leaves out
confirm tools; its agent route builds ctx from the request instead of a user
id in the body.
Full docs: https://products.techfoundry.io/dendra/ · Upgrade guide: https://products.techfoundry.io/dendra/upgrading/
v0.12.1 — packaging fixes
Upgrade from 0.12.0 if you load dendra with require() or use TypeScript with
CommonJS. No API changes.
- CommonJS shares one copy of core. Each
require()subpath
(dendra/next,dendra/adapter, …) used to bundle its own copy, so
instanceof DendraErrorfailed across them,orPagemissed core's
NOT_FOUND, and.after()hooks reported a guard's error asINTERNAL. - CommonJS types. TypeScript projects compiling to CommonJS got the ESM
types for every entry; they now get.d.ctsfiles. Older
moduleResolution: "node"setups resolve the subpaths too. dendra/package.jsonis exported, andenginesdeclares Node 18 or later.followJobanduseJobretry as soon as the browser comes back online,
instead of waiting out the backoff.- The package is checked with
publintandare-the-types-wrongin CI, and the
example app is built and tested against each commit.
Dendra 0.11.0 — SSE streams and job status on the client
- Server-Sent Events.
createClient(url, { streamFormat: "sse" })streams
.stream()procedures as SSE instead of NDJSON, for proxies and CDNs that
buffer NDJSON. The server picks the format fromAccept, so no server change
is needed. Errors still arrive as a typedDendraClientError, and stopping
the loop still aborts the handler'ssignal. Idle streams send a: ping
every 15 s (server optionheartbeatMs). A.get()stream also works with a
plain browserEventSource; close it on thedoneevent. - Following a job in the browser.
useJob(jobId, open)indendra/react,
andfollowJob(open)in core, follow a background job until it succeeds or
fails, with no polling. A dropped connection reopens after a backoff and picks
up the current state;NOT_FOUND,UNAUTHORIZEDand other coded errors stop
it and seterror. - Streams through proxies. Stream responses send
Cache-Control: no-cache
andX-Accel-Buffering: no, andtoNodeHandlerflushes after each chunk so
Express'scompression()doesn't hold them back. The README's new "Deploying
streams" section covers nginx, Caddy, compression, idle timeouts and
WebSockets behind a proxy.
Behaviour change: if a stream fails before it starts (for example,
createContext throws), the client now throws the error. Before, the loop
received the error object as an ordinary value.
No upgrade steps are needed.
Dendra 0.10.0
-
createServerCallerindendra/nextbuilds request context from Next.js
headers and reuses it within a server render. -
inspectRouter, named middleware and typed.meta()make router policy
assertions possible without reading procedure internals. -
Per-attempt
timeoutMsdeadlines and persistedonInterrupt: "fail"prevent
automatic replay of interrupted work. Late results cannot overwrite terminal
failures; handlers that ignore cancellation keep their local concurrency slot. -
jobs.stop({ graceMs, timeoutMs })provides bounded shutdown and reports
unfinished work or settlement errors. No-argumentstop()retains its
Promise<void>signature and immediate cancellation, with a 30-second bound. -
job.schedule(id, input, calendar)andjobs.unschedule(id)support durable
daily/weekly UTC schedules. Atomic occurrence insertion and cursor advancement
prevent duplicate occurrences across workers. Missed runs coalesce into one
catch-up, not an unbounded backlog. -
Both built-in stores support the new policies. Custom stores explicitly
advertise capabilities; unsupported features fail clearly.
Upgrading: run the new Postgres store.schema as owner before deploying
workers. It adds columns to existing job tables, a schedules table and its
revision sequence; grant the runtime role DML and sequence USAGE. Stop 0.9
workers before enabling new policies, or use distinct queues during transition.
Choose retries: 0, onInterrupt: "fail" for paid work that must not replay.
Cancellation is cooperative and cannot undo external side effects.
Daily/weekly schedules are UTC only; arbitrary cron expressions and timezones
are outside this release. Existing retry defaults remain unchanged.
v0.9.0 — real-time and long-running work
This release also carries everything planned for 0.8, which was never published
on its own. Upgrading from 0.7.x: read Breaking below; it's types plus one
change to chained .input().
New
- Background jobs —
createJobs({ store, pubsub, context }),
jobs.define(name, { input, run, key, retries, backoff, concurrency }), and
route.….background(job, map?), which validates, runs the guards, enqueues and
returns{ jobId }. Retries with backoff, one queued-or-running job per key, a
per-name concurrency cap, leases so a crashed worker's job is retried, and
job.watch(jobId, { signal })to stream status: each state once, in order,
with aversionthat goes up on every change. Stores:
createInMemoryJobStore()andcreatePgJobStore(pool). - Postgres pub/sub —
createPgPubSub(pool)overLISTEN/NOTIFY: one
listening connection per process, reconnect with backoff, one process's
messages arrive in order, SQL triggers can publish withpg_notify. Messages
must be under 8000 bytes. - Pluggable pub/sub —
createPubSub(transport)holds the shared subscription
logic; in-memory, Redis and Postgres are transports over it, and another broker
is onePubSubTransportaway. attachWsServer— WebSocket server with safe defaults: required Origin
allowlist,createContext(Request)once per connection before the socket
opens, 1 MB messages, 1 MB unsent backlog, 100 subscriptions per connection,
30 s heartbeat,revalidate,closeWhere, and a cleanclose()for shutdown.createWsConnection— a browser connection that reconnects (0.5 s doubling
to 10 s, with jitter), retries at once when the browser comes back online,
reports its status, stops on 4401 (signed out) untilreconnect(), and
resubscribes open subscriptions.- Typed, validated input in middleware —
.input()before.use()types and
validates the guard'sinput. - Cancellation — every call takes
{ signal }; stream handlers get asignal
that aborts when the client disconnects, cancels or unsubscribes. - Batching that doesn't stall — batch results stream back as each call
settles; the URL names the calls (__batch?p=a,b);{ batch: false }sends a
call alone. Old clients and servers still work with new ones. dendra/next—orPage(call, { signIn })turnsNOT_FOUNDinto
notFound()andUNAUTHORIZEDinto a sign-in redirect in Server Components.createWsSession(router, send, ctx, { maxSubscriptions }).
Fixed
- Client calls are real Promises (
.catch,.finally,instanceof Promise),
still lazy and still iterable for streams. toNodeHandleraborted every request as soon as its body was read.- A batch the server rejects as a whole now rejects every call in it; they used
to resolve toundefined. - Cancelling a pub/sub subscription that was never read leaked its listener (and
its Redis subscription).
Breaking
- Types only:
createClientandcreateWsClientresults are typed as what
survives JSON (Jsonify<Out>): aDateis typedstring,undefinedand
function properties are dropped.createCallerkeeps the handler's real types,
now asCaller<R>;Client<R>is the over-the-wire type. - Chained
.input(): a later.input()now extends the earlier one instead of
replacing it (objects only; the earlier schema wins a shared key), and both
validate the call.
Upgrading: parse date strings from client results instead of casting them to
Date; type createCaller results as Caller<R>; check any procedure that calls
.input() twice. If you hand-rolled a WebSocket server or a reconnecting browser
socket, attachWsServer and createWsConnection replace them.
Install: npm i dendra@0.9.0 · Full notes: https://products.techfoundry.io/dendra/releases
v0.1.1
Amazon Bedrock support
runAgent's client type is now structural (AnthropicClientLike — only needs messages.stream), so Claude on Amazon Bedrock drops in with no cast:
import { AnthropicBedrock } from "@anthropic-ai/bedrock-sdk";
yield* runAgent({ client: new AnthropicBedrock({ awsRegion: "us-east-1" }),
model: "anthropic.claude-3-5-sonnet-20241022-v2:0", tools, execute, prompt });Also: README images now use absolute URLs so the npm/GitHub Packages pages render the hero and diagrams.
📦 npm i dendra@0.1.1
v0.1.0
First public release of dendra — a typed, decorator-free, codegen-free RPC/REST framework for Next.js and any Web-standard runtime, where every procedure can also be an LLM tool and an MCP server.
📦 npm: https://www.npmjs.com/package/dendra · npm i dendra
Highlights
- One definition → typed client (no codegen), REST + OpenAPI, streaming, in-process caller, LLM tools, and an MCP server.
- Middleware (
.use()), typed.output(), request batching, class controllers. - In-memory and Redis pub/sub; WebSocket transport.
- AI/MCP both directions: expose your procedures as tools/MCP, and consume external MCP servers (
createMcpToolSource+mergeToolsets). - Optional
dendra/anthropic(runAgent) anddendra/react(useAssistant) helpers. - Zero runtime dependencies in the core.
Pre-1.0 and experimental — APIs may change before 1.0.