ccs-chat-client
@central-code-station/ccs-chat-client is the server-side SDK for your backend. Your backend applies your business rules about who may talk to whom, then uses it to create conversations, manage members, archive them and send messages on behalf of users. It runs on Node.js 20 or later and authenticates with a backend client (client_credentials).
It works only in its own tenant and tenant application, and it holds a client secret, so never use it in a browser. The signed-in user’s side uses ccs-chat-user-client and ccs-chat-notifications-client.
Install
Section titled “Install”The package is private on GitHub Packages. Point the scope at it in your .npmrc, then install. See SDKs overview for how to get access.
@central-code-station:registry=https://npm.pkg.github.com//npm.pkg.github.com/:_authToken=${GITHUB_PACKAGES_TOKEN}npm install @central-code-station/ccs-chat-clientGITHUB_PACKAGES_TOKEN is a GitHub token with read:packages that can read the organization’s packages. Do not commit a real token. The package has no runtime dependencies and uses the global fetch and Buffer.
Construct
Section titled “Construct”Create one ChatClient per process and reuse it. It fetches its token lazily, caches it and renews it 60 seconds before it expires. The library reads no environment variables itself; these names are the suggested convention.
import { ChatClient } from '@central-code-station/ccs-chat-client';
// One ChatClient per process: it fetches its token lazily, caches it and renews it 60 s before exp.export const chat = new ChatClient({ issuerUrl: process.env.CCS_ISSUER_URL!, clientId: process.env.CCS_CHAT_SYNC_CLIENT_ID!, clientSecret: process.env.CCS_CHAT_SYNC_CLIENT_SECRET!, chatApiUrl: process.env.CHAT_API_URL!, resource: process.env.CHAT_API_RESOURCE!, tenantId: process.env.CCS_TENANT_ID!, applicationId: process.env.CCS_APPLICATION_ID!, // scope: ['chat:conversations.manage', 'chat:messages.send'], // default: 'chat:sync'});Your backend client needs the scopes for what you call. A client never gets messages.delete, administration or directory search, whatever its scopes. See Register the backend client and Scopes.
Apply your own rule first, then call Chat. DIRECT conversations dedupe on their two members, so creating one is safe to repeat, and from a machine client it reopens the pair’s archived conversation.
import { chat } from './sdk-chat-client-construct';
declare function isCoachOf(coachId: string, clientId: string): Promise<boolean>;
export async function openCoachChat(coachId: string, clientId: string): Promise<string> { if (!(await isCoachOf(coachId, clientId))) throw new Error('forbidden'); // your rule first
// DIRECT conversations dedupe on their two members, so this is safe to repeat. const conversation = await chat.conversations.create({ type: 'DIRECT', member_user_ids: [coachId, clientId], actor_user_id: coachId, }); return conversation.conversation_id;}
export async function runMorningSquad(): Promise<void> { const group = await chat.conversations.create({ type: 'GROUP', name: 'Morning squad', member_user_ids: ['coach-ana', 'client-john'], // every id must be an eligible tenant member actor_user_id: 'coach-ana', // recorded as creator and OWNER });
await chat.messages.send(group.conversation_id, { body: 'Welcome!', actor_user_id: 'coach-ana' }); await chat.messages.send(group.conversation_id, { body: 'Session moved to 9:00', message_type: 'SYSTEM' });
const page = await chat.messages.list(group.conversation_id, { limit: 50 }); // newest first const older = await chat.messages.list(group.conversation_id, { before: page.next_cursor ?? undefined }); console.log(page.items.length, older.items.length);
await chat.channels.create({ name: 'announcements', visibility: 'PUBLIC', member_user_ids: ['coach-ana'] }); await chat.conversations.archive(group.conversation_id); // read-only from now on}Each method links to the REST operation it calls. Types are the API’s snake_case wire format. See the generated reference for every type.
| Resource | Method | Calls | Notes |
|---|---|---|---|
conversations |
create(input) |
Create conversation | DIRECT (exactly two members, deduped), GROUP or CHANNEL. |
get(id) |
Get conversation | ||
list({ type?, before?, limit? }) |
List conversations | Pages with next_cursor. |
|
update(id, patch) |
Update conversation | DIRECT accepts only status. |
|
archive(id) / reactivate(id) |
Update conversation | Sets status to ARCHIVED or ACTIVE. |
|
listMembers(id, options?) |
List members | ||
addMember(id, input) / removeMember(id, userId) |
Add member, Remove member | GROUP and CHANNEL. | |
channels |
create(input) / list(options?) |
Conversations create and list | Wrappers with type: 'CHANNEL'. |
messages |
send(conversationId, input) |
Send a message | On behalf of actor_user_id, or a SYSTEM message. |
list(conversationId, options?) |
List messages | Newest first, deleted ones as tombstones. | |
users |
lookup(query) |
Look up users | Minimum 2 characters. |
Reactions, read state and typing are user-only, so they live in the user client. A page holds 50 items by default and 100 items at most.
Configuration reference
Section titled “Configuration reference”ChatClientOptions:
| Field | Required | Description |
|---|---|---|
issuerUrl |
yes | The Identity Server’s bare issuer origin, such as https://auth.centralcodestation.com. No /api. |
clientId, clientSecret |
yes | Your backend client. Keep the secret on the server. |
chatApiUrl |
yes | The Chat API base URL, such as https://chat.centralcodestation.com/api/v1. |
resource |
yes | The Chat API’s resource identifier, such as https://chat.centralcodestation.com/api. Not the audience chat-api: that is invalid_target. |
tenantId, applicationId |
yes | The client’s own tenant and tenant application. |
scope |
no | Default 'chat:sync'. A scope, or an array of granular scopes, each in the client’s grant. |
fetch |
no | Override fetch for tests or runtimes without one. |
Errors
Section titled “Errors”Every failure is a ChatApiError with status, code (the API’s error string) and body. Subclasses: ChatUnauthorizedError (401), ChatForbiddenError (403), ChatNotFoundError (404), ChatUnprocessableError (422) and ChatRateLimitedError (429, with retryAfterSeconds). A failed token request, for example invalid_client or invalid_scope, surfaces the same way.
import { ChatApiError, ChatRateLimitedError, ChatUnprocessableError } from '@central-code-station/ccs-chat-client';import { chat } from './sdk-chat-client-construct';
export async function addClientToSquad(conversationId: string, userId: string): Promise<void> { try { await chat.conversations.addMember(conversationId, { user_id: userId }); } catch (error) { if (error instanceof ChatUnprocessableError && error.code === 'target_user_not_eligible') { return; // not an active tenant member with access to the application } if (error instanceof ChatRateLimitedError) { console.warn(`Retry in ${error.retryAfterSeconds ?? 1} s`); return; } if (error instanceof ChatApiError) console.error(error.status, error.code, error.message); throw error; }}Codes you will meet most are target_user_not_eligible, conversation_archived, client_context_mismatch and rate_limited. Machine clients are limited to 600 requests per 60 s per client. See Handle errors and retries and Troubleshooting.
Module formats
Section titled “Module formats”The package ships ES modules and CommonJS with type declarations for each. import loads dist/esm, require() loads dist/cjs. It works from ESM and CommonJS projects, with any bundler, and with the node16, nodenext, bundler and legacy node module resolution. Load it one way per app: the two builds are separate copies, so instanceof checks do not cross them.
Reference and changelog
Section titled “Reference and changelog”- Generated API reference
- Changelog
- Upgrading from 2.x: Migrate ccs-chat-client 2 to 3