@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.