Emitting events
Every event goes through POST /v1/events, authenticated as one of two principal
types. Which one you use changes what the server derives for you and what
provenance the event gets — see Trust & render-tokens.
App vs. subject principal
- Subject principal (end user)
- App / publisher principal (S2S)
The caller authenticates with their own SSO-JWT. actor is unconditionally the
authenticated subject — nothing in the request body can override it.
import { CNSClient } from "@amphoze/cns-client";
const client = new CNSClient({
baseUrl: "https://cns.example.com",
getToken: () => getEndUserToken(),
});
const event = await client.emit("order.shipped", {
target: { subject_ids: ["sub_carrier_ops"] },
data: { order_id: "ord_123" },
});
provenance defaults to client-asserted. It escalates to backend-verified only
if the request carries a verified render-token
in metadata.render_token.
The caller authenticates with a service-JWT it minted itself (see
Apps & credentials). The server additionally
checks (tenant_id, app_id, event_type) against the app's allow-list, stamps
source.app, and always sets provenance: backend-verified — an authenticated,
authorized backend calling CNS directly is the server anchor.
import { CNSServer } from "@amphoze/cns-server";
const server = new CNSServer({
baseUrl: "https://cns.example.com",
getServiceToken: () => mintOrCacheServiceToken(), // see the caution below
});
const event = await server.emit("invoice.paid", {
target: { subject_ids: ["sub_finance_team"] },
data: { invoice_id: "inv_789", amount_cents: 250000 },
});
An app principal may additionally set actor_subject_id — asserting which of the
tenant's subjects performed the action, since an app backend has no end-user JWT of
its own to derive an actor from:
{ "event_type": "invoice.paid", "actor_subject_id": "sub_alice", "data": { "...": "..." } }
actor_subject_id is ignored entirely for a subject-principal caller — a browser
client can never spoof a different actor by setting it.
:::caution Minting the service-JWT itself
getServiceToken above must already return a valid, signed EdDSA service-JWT
({app, tenant, key_ver, iss: "cns-service-auth", iat, exp}). The TypeScript SDK
does not ship a helper that mints one — only the Go core does
(internal/core/trust.ServiceMinter). Until a TS/Python minting helper exists, a
non-Go backend needs to hand-roll this JWT using the private key its secrets adapter
materialized. This is a real, verified gap — see
SDK Reference → TS Server.
:::
Equivalent curl
curl -s -X POST "$CNS_API/v1/events" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"event_type": "order.shipped",
"target": { "subject_ids": ["sub_carrier_ops"] },
"data": { "order_id": "ord_123" }
}'
202 Accepted with the fully server-stamped envelope on success. See
API Reference → POST /v1/events for every
field, status code, and the exact client-suppliable subset.
Ordering fields are yours to manage
session_id and seq are client-suppliable, and the ordering guarantees in
Events & the envelope only hold if you manage
them consistently: pick one session_id per SDK-instance lifetime (CNSClient
defaults to a fresh UUID per instance) and increment seq for every event that
instance emits, starting from 0. CNSClient.emit() does this bookkeeping for you
automatically; CNSServer.emit() (app/publisher principal) does not manage a
session/seq for you — pass your own if ordering matters for that event stream, or
omit both and accept ingest_time-only ordering.