Pingerchips LogoPingerchips
Chat / SessionsCookbook

Authentication

How human clients and agents authenticate to a chat thread.

PrincipalCredential
Human client (browser)a capability token, minted by your server
Agent (server process)the App Secret, used directly
Observera token carrying only chat:subscribe:{threadId}

Chat uses the same unified auth model as channels and Durable Objects — the capability grammar just has a chat product.


Human clients — capability tokens

Your backend calls POST /api/auth (with X-App-Key + X-App-Secret headers) and gets back a signed JWT. The browser presents it when joining the thread channel.

The flow

Browser                Your server              Pingerchips
  │  join thread-xyz        │                        │
  ├───────────────────────► │                        │
  │                         │  POST /api/auth        │
  │                         │  X-App-Key / X-App-Secret
  │                         ├──────────────────────► │
  │                         │  { auth: "<jwt>" }     │
  │                         │ ◄──────────────────────┤
  │  { auth: "<jwt>" }      │                        │
  │ ◄───────────────────────┤                        │
  │  WS join { auth: jwt }                           │
  ├────────────────────────────────────────────────► │
  │                                    verifies jwt  │

Request

POST /api/auth
X-App-Key: {appKey}
X-App-Secret: {appSecret}
Content-Type: application/json

{
  "socket_id": "abc123.def456",
  "client_id": "user-42",
  "capabilities": [
    "chat:subscribe:550e8400-e29b-41d4-a716-446655440000",
    "chat:publish:user_message:550e8400-e29b-41d4-a716-446655440000"
  ]
}

Response

{ "auth": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." }

What's in the token

The JWT is HS256, signed with your App Secret, aud pingerchips:v3, and expires in 5 minutes. Its claims bind it to one socket and one client:

ClaimValue
audpingerchips:v3
socket_id, client_idexactly what you passed
capabilitiesthe list you requested
app_id, app_keyyour app
iat, nbf, expexp = now + 300 s
jtiunique token id

The server rejects the join if the token's socket_id doesn't match the connection, if it's expired, if the signature is wrong, or if the token carries no chat:subscribe capability for the thread being joined.

Default capabilities

authenticateChat without a 4th argument requests the full client set for the thread:

  • chat:subscribe:{threadId} — receive all thread events
  • chat:publish:user_message:{threadId}send, edit, regenerate
  • chat:cancel_own:{threadId} — cancel runs it owns
  • chat:tool_approval:{threadId} — approve/reject suspended tool calls

Narrow it for observers (['subscribe']) or read-only embeds.


Server-side helper

pingerchips-js-server wraps the HTTP call:

import PingerchipsServer from 'pingerchips-js-server';

const pc = new PingerchipsServer(APP_KEY, APP_SECRET);

app.post('/chat/auth', async (req, res) => {
  const { socket_id, thread_id, client_id } = req.body;
  if (!(await canAccess(req.user, thread_id))) {
    return res.status(403).json({ error: 'Forbidden' });
  }
  // full client set
  const token = await pc.authenticateChat(socket_id, thread_id, client_id);

  // or narrowed — 4th arg is chat verbs, short or fully-qualified
  // await pc.authenticateChat(socket_id, thread_id, client_id, ['subscribe']);

  res.json(token); // { auth: "<jwt>" }
});

The browser client calls this endpoint automatically when you set authEndpoint on PingerchipsChat, POSTing { socket_id, thread_id, client_id }.


Agents

Agents skip the HTTP endpoint and the capability token. They join with the App Secret directly:

import { AgentSession } from 'pingerchips-js-server/agent-session.js';

const agent = new AgentSession(APP_KEY, APP_SECRET);
await agent.connect(threadId, 'my-bot');

An agent join grants: subscribe, publish:run_lifecycle, publish:token, publish:message, publish:cancel_any. This path is unchanged by the unified auth model.

The App Secret must never reach the browser. Agent code runs only on your server.


No-auth mode

If your app has enable_user_authentication off (App Settings), clients can join a thread without a token — with the default client capability set and a client_id from the join payload. Use this only for prototypes or fully public threads.

On this page