Skip to content

Releases: zeestack/dendra

v0.14.0 — mapError

Choose a tag to compare

@zeestack zeestack released this 29 Oct 02:55
5e4ca40

Found running dendra in production. No breaking changes.

  • .mapError() maps the app's own errors to DendraErrors in one place,
    instead of a try/catch in 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 as cause. New type
    ErrorMapper.

Reference: https://products.techfoundry.io/dendra/reference/router/#maperrorfn

v0.13.0 — hardening for 1.0

Choose a tag to compare

@zeestack zeestack released this 29 Oct 01:32
84e5e92

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, invokeStream and the Proc, AnyProc, CallSite,
    Transport and ProcedureKind types (import them from dendra/adapter);
    createMemoryStore, recallMemory, rememberFacts and formatMemoryBlock
    (from dendra/memory); toolSource and ToolSourceOptions (now
    lazyToolSource and LazyToolSourceOptions).
  • engines requires Node.js 22 or later.
  • An .ai() procedure whose input has no JSON Schema throws where it's defined,
    and maxBodyBytes caps 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 DendraError used to
    reach clients and the model with its own message, which can carry SQL,
    hostnames or credentials. It now arrives as INTERNAL: Internal error over
    HTTP, streams, WebSockets, job snapshots, the MCP bridge and tool calls, and
    the original is logged with console.error. After-hooks still get the
    original. If clients relied on such a message, throw a DendraError instead.
  • createToolExecutor only throws DendraErrors, 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.
  • createMcpToolSource throws a remote tool's error as a new BAD_GATEWAY (502)
    code, with the server's text.
  • lazyToolset throws when two tools sanitize to the same name (a.b and
    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, as s.* does: Zod 4 and ArkType, and Valibot through
    toStandardJsonSchema from @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, a z.date()) now throws
    where it's defined.
  • Per-tool MCP annotations. .ai({ annotations }) sets MCP's title,
    readOnlyHint, destructiveHint, idempotentHint and openWorldHint over
    the defaults dendra derives from .get() and confirm, so a POST that only
    reads, or a write that's safe to repeat, is described correctly
    (#59). inspectRouter shows
    each tool's annotations, so a test can assert them. New type
    ToolAnnotations.
  • createMcpRouter mounts an external MCP server's tools in your router as
    .ai() procedures ({ ...appRouter, gh: await createMcpRouter(client) }),
    so confirm (with createApprovals), your .use() guards, after-hooks and
    inspectRouter cover them like local tools
    (#58). Remote descriptions
    are cut to 1,024 characters there and in createMcpToolSource
    (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's signal, so a subscription can't hang an agent or grow without
    limit (#57). Tool executors
    take execute(name, args, { signal }); runAgent passes its signal, so
    aborting a run cancels the call in flight. mergeToolsets, ToolSource and
    createMcpBridge().callTool pass 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
    timeoutMs if it needs longer.
  • A stream that yields undefined sends null over NDJSON and WebSockets, as
    SSE already did. NDJSON used to write the line undefined, which made the
    client throw. A tool that returns undefined gives the MCP bridge and
    runAgent the text null instead of no text
    (#64).
  • maxBodyBytes caps 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) with 413 PAYLOAD_TOO_LARGE, while reading
    (#62). Raise maxBodyBytes
    if you accept larger JSON.
  • approvals.list(ctx) only returns actions in the caller's exact scope. When
    scope(ctx) returned undefined (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

Choose a tag to compare

@zeestack zeestack released this 28 Oct 14:46
abd6d61

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 DendraError failed across them, orPage missed core's
    NOT_FOUND, and .after() hooks reported a guard's error as INTERNAL.
  • CommonJS types. TypeScript projects compiling to CommonJS got the ESM
    types for every entry; they now get .d.cts files. Older
    moduleResolution: "node" setups resolve the subpaths too.
  • dendra/package.json is exported, and engines declares Node 18 or later.
  • followJob and useJob retry as soon as the browser comes back online,
    instead of waiting out the backoff.
  • The package is checked with publint and are-the-types-wrong in CI, and the
    example app is built and tested against each commit.

Dendra 0.11.0 — SSE streams and job status on the client

Choose a tag to compare

@zeestack zeestack released this 28 Oct 04:21
7aef93a
  • 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 from Accept, so no server change
    is needed. Errors still arrive as a typed DendraClientError, and stopping
    the loop still aborts the handler's signal. Idle streams send a : ping
    every 15 s (server option heartbeatMs). A .get() stream also works with a
    plain browser EventSource; close it on the done event.
  • Following a job in the browser. useJob(jobId, open) in dendra/react,
    and followJob(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, UNAUTHORIZED and other coded errors stop
    it and set error.
  • Streams through proxies. Stream responses send Cache-Control: no-cache
    and X-Accel-Buffering: no, and toNodeHandler flushes after each chunk so
    Express's compression() 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

Choose a tag to compare

@zeestack zeestack released this 28 Oct 01:16
ece285d
  • createServerCaller in dendra/next builds 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 timeoutMs deadlines and persisted onInterrupt: "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-argument stop() retains its
    Promise<void> signature and immediate cancellation, with a 30-second bound.

  • job.schedule(id, input, calendar) and jobs.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

Choose a tag to compare

@zeestack zeestack released this 23 Oct 02:04

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 a version that goes up on every change. Stores:
    createInMemoryJobStore() and createPgJobStore(pool).
  • Postgres pub/sub — createPgPubSub(pool) over LISTEN/NOTIFY: one
    listening connection per process, reconnect with backoff, one process's
    messages arrive in order, SQL triggers can publish with pg_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 one PubSubTransport away.
  • 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 clean close() 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) until reconnect(), and
    resubscribes open subscriptions.
  • Typed, validated input in middleware — .input() before .use() types and
    validates the guard's input.
  • Cancellation — every call takes { signal }; stream handlers get a signal
    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 }) turns NOT_FOUND into
    notFound() and UNAUTHORIZED into 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.
  • toNodeHandler aborted 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 to undefined.
  • Cancelling a pub/sub subscription that was never read leaked its listener (and
    its Redis subscription).

Breaking

  • Types only: createClient and createWsClient results are typed as what
    survives JSON (Jsonify<Out>): a Date is typed string, undefined and
    function properties are dropped. createCaller keeps the handler's real types,
    now as Caller<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

Choose a tag to compare

@zeestack zeestack released this 10 Oct 23:53

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

Choose a tag to compare

@zeestack zeestack released this 10 Oct 23:02

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) and dendra/react (useAssistant) helpers.
  • Zero runtime dependencies in the core.

Pre-1.0 and experimental — APIs may change before 1.0.