Skip to content

TypeScript/JavaScript client

The official TypeScript/JavaScript client lives in clients/js. Unlike the read-only Go client, it speaks the WebSocket mutation protocol: chat, claims, releases, board reads, presence, and receipts. It runs unchanged in the browser and in Node 20+ (both expose a global WebSocket) and has no runtime dependencies.

Install

npm install @anulum/synapse-channel

Connect and coordinate

import { SynapseClient, MessageType } from "@anulum/synapse-channel";

const client = new SynapseClient({
  uri: "ws://127.0.0.1:8876",
  name: "SYNAPSE-CHANNEL/web-agent",
  token: process.env.SYNAPSE_TOKEN,
});

client.on(MessageType.Chat, (m) => console.log(`${m.sender}: ${m.payload}`));
await client.connect();

client.chat("hello", { target: "all" });
client.claim("synapse-channel:web", ["src/web/**"]);
client.requestBoard();
client.release("synapse-channel:web");
client.close();

claim(taskId, paths, pathIdentity?) also accepts the version-1 ClaimScopeIdentity exported by the package. This is an additive transport type for bridges that already used the Python Git/filesystem resolver; the dependency-free browser client deliberately does not inspect a local repository or invent canonical values. When present, its canonical worktree_path is also sent as the ordinary worktree field so the hub validates the full display scope. Omit the third argument when no trusted local resolver is available. The hub then retains legacy literal-path comparison.

connect() opens the socket, sends the registration heartbeat (with the token when one is configured), and resolves once the hub returns its welcome; it rejects if the hub closes the socket before welcoming the identity, if no welcome arrives within readyTimeoutMs, or if close() is called first.

Each connect() is one socket generation. isReady is true only while that socket is open and welcomed: a hub close, a transport error, a welcome timeout or close() all leave the client not ready, stop the heartbeat and detach the socket, so the same instance can connect() again and receives a fresh welcome. Callbacks from a superseded socket are ignored. A connect() while a socket is already open or pending rejects; close() it first.

Scope and boundaries

For release recovery, call prepareRelease(taskId, epoch?, idemKey?) with a unique key before sending. Retain the prepared epoch and canonical semantic SHA-256 digest: sorted ASCII JSON keys, compact separators, non-ASCII characters escaped as \uXXXX, excluding timestamp, client_timestamp, auth, and signature. Send with release(taskId, prepared.epoch, idemKey). That call proves only a send. Preparation and confirmation use the hub's Python whitespace rules for task ids, including NEL while preserving BOM, and Unicode fingerprints use ASCII JSON escapes. The prepared epoch is typed as an optional number for TS callers.

requestReleaseConfirmation(taskId, operationId, requestDigest, requestId) reads the original result without another mutation. Match the hub sender, your target and request id; accept only release_confirmation.status === "confirmed" with the original task, owner, key, digest and valid receipt. Unknown and ordinary legacy snapshots remain uncertain. Confirmation is historical and does not free a newer claim. The wire contract and manual CLI workflow define the same recovery semantics as Python.

The optional scoped attachment API uses wire version four. Configure signRegistration with a bound identity signer and signAttachment with a per-message signer, then call attachment(MessageType.AttachmentBegin, fields) and subscribe to MessageType.AttachmentResult. The client refuses these requests without a version-four welcome or an attachment signer. The caller supplies the credential-backed signing callbacks; the dependency-free client does not manage private keys.

The client implements the agent-side envelope and the connection lifecycle: registration, keepalive heartbeats, typed send helpers, and inbound dispatch by MessageType. It does not run the hub, does not enforce ACLs, and does not verify per-message authentication — those are hub-side. On a secured or ACL-enforcing hub, supply the connect token; namespace authorisation still depends on the hub binding the sender, so use a token (and per-message auth) on an exposed hub.

It is a separate npm package and does not ship inside the Python synapse-channel distribution.