Authentication
How human clients and agents authenticate to a chat thread.
| Principal | Credential |
|---|---|
| Human client (browser) | a capability token, minted by your server |
| Agent (server process) | the App Secret, used directly |
| Observer | a 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:
| Claim | Value |
|---|---|
aud | pingerchips:v3 |
socket_id, client_id | exactly what you passed |
capabilities | the list you requested |
app_id, app_key | your app |
iat, nbf, exp | exp = now + 300 s |
jti | unique 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 eventschat:publish:user_message:{threadId}—send,edit,regeneratechat:cancel_own:{threadId}— cancel runs it ownschat: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.