Skip to content

Signatures quoted from source. Wire: pinboard registry. Related: pinned (Python), pinboard. Source: packages/pinned/pinned-js.

Entry points

Import fromExports
@assistant-ui/pinnedpinned, pinnedWorker, upgradeResponse, Pinned, Cluster, mintAttestation, verifyAttestation, attestedCaller, Attestation, StateStore, memoryStateStore, FenceLost, ALARM_TRAY, parseRoute, invalidIdReason
@assistant-ui/pinned/nodeserve, envOptions, Serve, listenFetch, toRequest, writeResponse, writeRefusal

Runtime: Node 20+ (node:http, node:crypto, node:async_hooks, ws); no workerd or Durable Objects. Peer dependency: hono ^4.13.13. Pinning comes from pinboard placement leases.

pinned

const pinned: <A extends Pinned.App>(setup: Pinned.Setup<A>) => Pinned.Object<A>;
type Setup<A> = (ctx: Pinned.Context) => A | Promise<A>;
type App = { fetch: (request: Request, env: { pinned: Pinned.Call }) => Response | Promise<Response> };
  • setup runs on the first request for an id (activation) and returns the app serving that instance: a Hono app, or any { fetch }. A setup that returns anything else answers 500.
  • The app sees the path below /<namespace>/<id>, as under Hono's mount: /counter/a/increment matches .post("/increment"), the bare instance URL matches "/". Query, headers, body, and signal are the original request's.
  • c.env.pinned is a Pinned.Call: { ctx, attestation, epoch }; new Hono<Pinned.Env>() types it.
  • Lifecycle hooks register on ctx: ctx.onStop, ctx.onAlarm.
  • The namespace binds at hosting: hosts: { "/counter": Counter }. One object may host under several namespaces.
  • Errors follow Hono: an HTTPException answers its response, other Errors reach the app's onError (Hono's default answers 500 Internal Server Error). A thrown Response answers as-is.
  • Pinned.AppOf<typeof Counter> is the app type. hc<Pinned.AppOf<typeof Counter>>(instanceUrl) types a client from the routes in that type: Hono records routes in the type only when they are chained onto the returned app.
import { pinned } from "@assistant-ui/pinned";
import { Hono } from "hono";

export const Counter = pinned((ctx) => {
  let value = 0;
  ctx.onStop(() => console.log(`evicted ${ctx.id}`));
  const app = new Hono();
  app.post("/increment", (c) => c.json({ value: ++value }));
  app.get("/value", (c) => c.json({ value }));
  app.get("/peer", () => ctx.cluster.fetch(`/counter/${ctx.id}-peer/value`));
  return app;
});

pinnedWorker

const pinnedWorker: (opts: Pinned.Options) => Pinned.Worker;

Pinned.Options:

  • hosts: { [namespace: string]: Pinned.Object | Pinned.Host }: namespace (/ plus one path segment, ^/[^/\s]+$; anything else throws) to a pinned object, or { object, idleTimeoutMs (300000), maxInstances (10000), releasePlacementOnIdle (true), sweepIntervalMs (5000) }. A value not made by pinned throws; an empty map throws.
  • pinboardUrl, token, advertiseUrl: control-plane mode; pinboardUrl without token or advertiseUrl throws, as do token or advertiseUrl without pinboardUrl and an empty pinboardUrl. Without pinboardUrl the worker serves locally with attestation checks off: dev mode, which logs a DEV MODE warning and throws when NODE_ENV, ENVIRONMENT or APP_ENV is production/prod unless PINBOARD_ALLOW_DEV_IN_PRODUCTION=1.
  • hostname, env, envs: () => Record<string, string>, takeover: "wind_down" | "deny", epoch (integer ≥ 0): register fields; env and takeover without hostname throw.
  • clusterHost: "hostname" | "pinboard" (default "hostname"): how a cluster call through pinboard names its world. "pinboard" keeps the pinboard URL's Host and sends hostname in Pinboard-Hostname, for a pinboard behind a front door that routes by Host (serveMicrovm sets it). Any other value throws.
  • store?: StateStore: exposed to instances as ctx.store; the worker reads and writes only the alarm row.
  • heartbeatIntervalMs (3000), leaseBufferMs (5000), headroom: () => number (cgroup limit minus usage, floored by os.freemem(), in MB), log (console).

Pinned.Worker:

  • fetch(request: Request): Promise<Response>: routes /_pinned/challenge, /_pinned/stats, and /<namespace>/<id>/<rest>.
  • app: Hono: the same routes (/_pinned/*, /<namespace>/*) as a Hono app, for userApp.route("/", worker.app) next to the user's routes. Serve that app with listenFetch(userApp.fetch, { port }) (@assistant-ui/pinned/node) so WebSocket upgrades complete, then worker.start().
  • start(): starts the idle sweeps and the registration loop.
  • drain(deadlineMs: number | null): Promise<void>: refuses new activations, evicts idle instances now, aborts the rest at the deadline (Infinity never aborts), resolves once every instance closed; null revokes.
  • close(): Promise<void>: closes every instance (releasing placements), deregisters, stops heartbeats.
  • ready (registered and inside the lease), superseded, draining, drained, lease.healthy, lease.expired, lease.flaps, registration.state.{sessionId, reregistrations, draining}, secret.

Request handling, in order:

  • /_pinned/* requires Pinboard-Proxy-Secret or a valid attestation, else 403 {"detail":"invalid proxy secret"}; /_pinned/challenge?challenge= answers {"value": ...} for the current pair, else 404 {"detail":"unknown challenge"}.
  • Instance routes in control-plane mode require a valid Pinboard-Attestation (audiences: the session id and each host's local audience), else 403 {"detail":"invalid attestation"}.
  • Invalid ids answer 400 with the same messages as Python (id must be non-empty, id must be at most 256 characters, id must contain only [A-Za-z0-9._~-]); a non-digit Pinboard-Epoch answers 400 {"detail":"malformed placement epoch"}.
  • 503 refusals carry Retry-After: 1: lease expired; not serving, stale placement epoch; retry, released placement epoch; retry, host draining; retry, instance capacity reached; retry, instance past its wind-down deadline; retry, instance closing; retry. A refused activation while the lease is unhealthy adds Pinned-Refusal: lease-unhealthy.
  • In control-plane mode with a store, POST /<namespace>/<id>/_wake is pinboard's durable wake: it activates the instance at the Pinboard-Epoch and answers 200 {"ok": true} once it is resident, with the same 503 refusals as requests; other methods answer 405. Without a store the path reaches the instance.
  • In control-plane mode, POST /<namespace>/<id>/_alarm is pinboard's alarm fire: it needs Pinboard-Proxy-Secret (403 {"detail":"invalid proxy secret"}) and runs the instance's due alarm at the Pinboard-Epoch, answering 200 {"fired": boolean}, or 202 while a run is in progress. A store fence loss answers 409; a throwing handler answers 500.
  • Exceptions that escape the app (a thrown non-Error, a throwing onError, a failed setup) are logged and answer 500 {"detail":"Internal Server Error"}.

Registration mirrors the Python RegistrationClient: fresh challenge pair per send, fixed-rate loop anchored at send time, 503 Retry-After deferral and jittered exponential backoff, duplicates quit before the lease renews, messages applied in order with contiguous-prefix acks, draining echo applied as a control-plane drain, 409 reregister re-registers at once with prevSessionId, 409 superseded aborts every instance and ends the loop. Releases batch through POST /registry/release with epoch as a decimal string; release, tag, and deregister retry three times.

upgradeResponse

const upgradeResponse: (upgrade: Pinned.Upgrade) => Response;
type Upgrade = { protocol?: string; handler: (socket: WebSocket) => void };
  • Returned from a route handler for a request carrying Upgrade: websocket; serve completes the handshake with ws (accepting protocol when the client offered it, no subprotocol otherwise) and hands the open ws.WebSocket to handler.
  • Attestation, id validation, epoch fencing, and 503 refusals run for upgrades exactly as for requests; a non-upgrade Response to an upgrade request is written as a plain HTTP response (status, headers, body) and the socket closed, so a Pinned-Refusal rides back to the pinboard for re-placement.
  • An accepted socket keeps the instance in flight until it closes: idle eviction and drain wait for it, the drain deadline aborts the instance (signal, then the onStop handlers) and closes sockets still open with 1001 instance closed.
  • Returned to a request without an upgrade, serve answers 500.
  • Middleware must leave the response untouched: one that copies it (c.header() after next(), c.res = …) loses the upgrade, and the request answers 500. Hono gives the same rule for its own WebSocket helper.
app.get("/ws", () =>
  upgradeResponse({
    protocol: "echo.v1",
    handler: (ws) => ws.on("message", (data) => ws.send(String(data))),
  }),
);

Pinned.Context

type Context = {
  id: string;
  ns: string;
  signal: AbortSignal;
  cluster: Cluster;
  store: StateStore | null;
  readonly epoch: number | null;
  readonly windingDown: boolean;
  abort: () => void;
  onStop: (handler: () => void | Promise<void>) => void;
  onAlarm: (handler: () => void | Promise<void>) => void;
  waitUntil: (promise: Promise<unknown>) => void;
  windDown: (deadlineMs?: number) => void;
  tag: (tag: string) => Promise<void>;
  alarm: { get: () => Promise<number | null>; set: (at: Date | number | null) => Promise<void> };
};
type Cluster = { fetch: (path: string, init?: RequestInit) => Promise<Response> };
  • onStop(handler) runs handler on eviction, after open work settled; handlers run last-registered first, a throw is logged.
  • signal aborts on eviction: idle timeout, wind-down deadline, drain deadline, duplicate quit, supersession, abort(), close().
  • tag(tag) stamps the owner tag on the instance's placement through POST /registry/tag at the admitted epoch, for GET /registry/overview?tag=, and stamps it again when a later request admits a newer epoch; a no-op without a control plane, throws before any request was admitted or when the pinboard stays unreachable.
  • A response whose body is still streaming, or an accepted WebSocket, keeps the instance in flight until the body ends, the client cancels it, or the socket closes; idle eviction and drain wait for it, the drain deadline aborts it through signal.
  • waitUntil(promise) keeps the instance in flight until promise settles: idle eviction and drain wait for it. Teardown aborts signal, waits for every pending waitUntil, then runs the onStop handlers. A rejection is logged. A non-promise throws. Pending ones count as tasks in /_pinned/stats. Python's equivalent is create_task.
  • cluster.fetch("/<namespace>/<id>/<rest>"): a target namespace attached to this worker with the id resident (or no pinboard) dispatches in process; otherwise the call goes through pinboardUrl with Pinboard-From, Pinboard-Session, Pinboard-Session-Secret, Pinboard-Subrequest-Depth, and Host set to hostname (or the pinboard host; with clusterHost: "pinboard", the pinboard host plus Pinboard-Hostname). TLS always names the pinboard URL's host. Inbound Pinboard-* and Host headers are dropped. Depth past 50 throws.
  • The target sees c.env.pinned.attestation as { kind: "instance", ns, id, depth }.

Alarms

  • One alarm per instance. alarm.set(at) writes the ALARM_TRAY row (("pinned", "alarm"), unmarked, doc: { at, nonce }: epoch ms rounded up, and a fresh id per write) through store at the instance's epoch (0 before any admitted request) and resolves once it commits; a later set replaces it, set(null) deletes it. An invalid Date or a non-finite number throws; without a store, set and get throw.
  • alarm.get() reads the stored time; null when none, or while its handler runs unless the handler set a new one.
  • ctx.onAlarm(handler) sets the handler, once per instance (a second call throws), usually in setup. Once due, it runs; a due alarm without one throws. The row is deleted after it resolves, unless a set replaced it; a throw or a crash leaves the row, and the alarm fires again. Delivery is at-least-once: handlers must be idempotent. Retries and backoff are the handler's: catch and set a later time.
  • Runs are serialized per instance and keep it in flight, like a request. A run reads the row and later clears it, taking turns with set writes; the clear deletes only the row whose nonce the run fired.
  • Control-plane mode: a resident instance's alarm fires from its owner's timer at the due time, while the worker is inside its lease. Eviction drops the timer; activation re-arms it from the store. Pinboard's sweeper fires the rest through _alarm (sweep), activating an evicted id on any live worker, within one sweepIntervalMs. A request below the admitted epoch answers 503 stale placement epoch; retry; an epoch the store superseded answers 409; a run already in progress answers 202 {"fired": false, "running": true}. Both paths land on the owner's serialized runs, and each run re-reads the row, so an alarm the timer committed finds nothing due for the sweep.
  • Local mode: a process timer fires due alarms, activating evicted ids. A failed run fires again after the namespace's sweepIntervalMs. After a restart, a stored alarm re-arms on its id's next activation.
const Reminder = pinned((ctx) => {
  ctx.onAlarm(() => ctx.cluster.fetch(`/inbox/${ctx.id}/remind`, { method: "POST" }));
  const app = new Hono();
  app.post("/in-a-minute", async (c) => {
    await ctx.alarm.set(Date.now() + 60_000);
    return c.json({ ok: true });
  });
  return app;
});

Pinned types

  • Pinned.Object<A>: { readonly setup: Pinned.Setup<A> }, what pinned returns. Pinned.AppOf<O>: its app type.
  • Pinned.Host: { object: Pinned.Object; idleTimeoutMs?; maxInstances?; releasePlacementOnIdle?; sweepIntervalMs? }.
  • Pinned.App: { fetch: (request: Request, env: Pinned.Env["Bindings"]) => Response | Promise<Response> }; a Hono app is one.
  • Pinned.Env: { Bindings: { pinned: Pinned.Call } }, the Hono env of an instance app.
  • Pinned.Alarm: { get: () => Promise<number | null>; set: (at: Date | number | null) => Promise<void> }, the type of ctx.alarm.
  • Pinned.Call: { ctx: Pinned.Context; attestation: Attestation.Instance | Attestation.Account | null; epoch: number | null }; epoch is the placement epoch the request was admitted at, null without a control plane.
  • Pinned.Upgrade: { protocol?: string; handler: (socket: WebSocket) => void }, the argument of upgradeResponse.

Attestation

const mintAttestation: (secret: string, claims: { aud: string; now: number; caller?: string; depth?: number }) => string;
const verifyAttestation: (token: string, opts: { secret: string; audiences: Iterable<string>; now: number }) => Attestation.Claims | null;
const attestedCaller: (claims: Attestation.Claims | null) => Attestation.Instance | Attestation.Account | null;
  • Compact JWS, header {"alg":"HS256","typ":"pinboard+jwt"}, exp = iat + 30, ±30 s clock skew, constant-time MAC check; byte-identical to pinned.pinboard.attestation (now in seconds).
  • Attestation.Instance: { kind: "instance"; ns; id; depth }. Attestation.Account: { kind: "account"; sub; claims; scopes }.

StateStore

type StateStore = {
  setTray: (ns, id, trayNs, trayId, marked: boolean, doc, epoch: number) => Promise<void>;
  deleteTray: (ns, id, trayNs, trayId, epoch) => Promise<void>;
  getTray: (ns, id, trayNs, trayId, epoch) => Promise<{ marked: boolean; doc } | null>;
  list: (ns, id, epoch, filters?: { trayNs?: string; marked?: boolean }) => Promise<StateStore.Row[]>;
};
const memoryStateStore: () => StateStore;
class FenceLost extends Error { key: string; epoch: number }

An epoch below the stored instance epoch throws FenceLost; at or above advances it. No tray runtime ships in this package: the store is a seam for instance code, plus the alarm row ALARM_TRAY = { trayNs: "pinned", trayId: "alarm" }. Pinboard's DurabilityStore.listDue must read the rows this store commits. ctx.store rejects setTray and deleteTray in the reserved "pinned" tray namespace.

serve

const serve: (options: Serve.Options) => Promise<{ worker: Pinned.Worker; server: http.Server; port: number; close: () => Promise<void> }>;
const envOptions: (env: Record<string, string | undefined>) => Omit<Serve.Options, "hosts">;
  • Serve.Options = Pinned.Options & { port?: number; host?: string }; port defaults to 0 (ephemeral).
  • Options default from envOptions(process.env); explicit options override it. envOptions reads PORT, PINBOARD_URL, PINBOARD_TOKEN, ADVERTISE_URL, PINNED_HOSTNAME; a non-integer PORT throws.
  • serve binds first, then worker.start(); close() closes the worker, terminates sockets still open, then the server.
  • node:http upgrade events go through worker.fetch like requests; an upgradeResponse completes the handshake, any other Response is written by writeRefusal(socket, response).
import { serve } from "@assistant-ui/pinned/node";

const { port } = await serve({ hosts: { "/counter": Counter } });