Skip to main content

@amphoze/cns-client

The frontend SDK: emit as the signed-in end user, redeem render-tokens, and (optionally) subscribe to realtime updates. Re-exports everything from @amphoze/cns-core as well.

Construction

import { CNSClient } from "@amphoze/cns-client";

const client = new CNSClient({
baseUrl: "https://cns.example.com",
getToken: () => getEndUserToken(), // Bearer JWT supplier, sync or async
sessionId: undefined, // defaults to crypto.randomUUID()
fetchImpl: undefined, // override for non-global fetch
realtime: { // omit for emit-only usage
url: "wss://realtime.example.com/connection/websocket",
getToken: undefined, // defaults to the client's own getToken
wsFactory: undefined,
minBackoffMs: 500,
maxBackoffMs: 15000,
},
});

sessionId defaults to a fresh UUID per CNSClient instance — one SDK-instance lifetime is one ordering session.

emit(eventType, input?)

emit(eventType: string, input?: Omit<EmitEventInput, "event_type" | "session_id" | "seq">): Promise<CNSEvent>

POSTs to /v1/events as the authenticated end user. The client automatically fills session_id (from construction), a self-incrementing seq (starting at 0), event_time (now), and monotonic_offset_ms (from performance.now() when available) — you only ever supply target, data, and metadata. actor/ tenant_id are always server-derived; this SDK never sets them. See Emitting events.

captureProof(docId, source)

captureProof(docId: string, source: Response | Headers | Record<string, string>): void

Pure bookkeeping, no network call: records that source (typically a fetch Response) was received while loading docId, so a later markViewed(docId) can recover an X-Notify-Proof header carrier from it.

markViewed(docId, opts?)

markViewed(docId: string, opts?: { data?: Record<string, unknown>; carriers?: CarrierSources }): Promise<CNSEvent>

Emits a viewed event, auto-reading a render-token from whichever carrier the serving backend embedded — priority order: an explicit override, a header captured via captureProof, a JSON _proof field, a DOM <meta name="notify-proof"> tag, then window.__notifyProof__. It never makes a network call to fetch a token — if nothing carries one, the event still emits (as client-asserted; it simply carries no escalation proof).

subscribe(channel, cb, opts?) / connectionState

subscribe(channel: string, cb: (event: CNSEvent) => void, opts?: { sinceSeq?: number }): () => void
connectionState: "connecting" | "open" | "reconnecting" | "closed"

Requires realtime options at construction (throws otherwise). Opens a WebSocket directly to the realtime-adapter URL you configured — reconnects with exponential backoff, and folds sinceSeq into the subscribe request for replay.

:::caution Read this before reaching for subscribe() This does not call GET /v1/stream — it's an entirely separate transport (a direct WebSocket to whatever realtime.url you configured, e.g. Centrifugo). Its wire protocol is an explicitly unconfirmed placeholder (the source's own comment calls it "a minimal, reasonable JSON command/reply shape," not a confirmed contract) — the design spec lists the real Centrifugo channel-naming/authz-binding contract as an unresolved open item. connectionState defaults to "closed" when no realtime options were given. See Realtime & streaming for the full picture, including the separate, already-working GET /v1/stream SSE endpoint this method does not use. :::

What's not here

No method reads GET /v1/feed — see the caution in Guides → Subscribing. No method mints a service-JWT — that's an app/publisher (backend) concern; see TS Server.