Skip to content

Latest commit

 

History

History

server

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 

README.md

@chatcops/server

Server-side handler and platform adapters for ChatCops. Handles streaming responses, rate limiting, webhooks, and analytics.

Install

npm install @chatcops/server @chatcops/core

Quick Start

Express

import express from 'express';
import { chatcopsMiddleware } from '@chatcops/server';

const app = express();
app.use(express.json());

// app.use, so /chat/session, where the widget gets its session token, reaches it too.
app.use('/chat', chatcopsMiddleware({
  provider: {
    type: 'claude',
    apiKey: process.env.ANTHROPIC_API_KEY,
  },
  systemPrompt: 'You are a helpful assistant for our website.',
  allowedOrigins: ['*'],
  sessionSecrets: [process.env.CHATCOPS_SESSION_SECRET], // openssl rand -base64 32
}));

app.listen(3001);

Vercel Edge

import { chatcopsVercelHandler } from '@chatcops/server';

const handler = chatcopsVercelHandler({
  provider: { type: 'openai', apiKey: process.env.OPENAI_API_KEY },
  systemPrompt: 'You are a helpful assistant.',
  allowedOrigins: ['*'],
  sessionSecrets: [process.env.CHATCOPS_SESSION_SECRET],
});

export const POST = (req: Request) => handler(req);
export const OPTIONS = (req: Request) => handler(req);
export const config = { runtime: 'edge' };

Export the same handler from the /session route next to it (app/api/chat/session/route.ts for a chat at /api/chat), where the widget gets its session token.

Cloudflare Workers

import { chatcopsCloudflareHandler } from '@chatcops/server';

let handler: ReturnType<typeof chatcopsCloudflareHandler> | undefined;
function getHandler(env: Env) {
  handler ??= chatcopsCloudflareHandler({
    provider: { type: 'claude', apiKey: env.ANTHROPIC_API_KEY },
    systemPrompt: 'You are a helpful assistant.',
    allowedOrigins: ['*'],
    sessionSecrets: [env.CHATCOPS_SESSION_SECRET],
  });
  return handler;
}

export default {
  async fetch(request: Request, env: Env) {
    return getHandler(env)(request);
  },
};

Configuration

import { createChatHandler } from '@chatcops/server';
import { LeadCaptureTool, FAQKnowledgeSource } from '@chatcops/core';

const handler = createChatHandler({
  // AI provider (required)
  provider: {
    type: 'claude',          // 'claude' | 'openai' | 'gemini'
    apiKey: 'sk-...',
    model: 'claude-haiku-5-5',
  },

  // System prompt (required)
  systemPrompt: 'You are a helpful support assistant.',

  // Tools (optional)
  tools: [
    new LeadCaptureTool((lead) => {
      console.log('New lead:', lead.email);
    }),
  ],

  // Knowledge base (optional)
  knowledge: [
    new FAQKnowledgeSource([
      { question: 'What do you do?', answer: 'We build software.' },
    ]),
  ],

  // Rate limiting (optional, default: 30 req/60s)
  rateLimit: { maxRequests: 10, windowMs: 60_000 },

  // Webhooks (optional)
  webhooks: [
    { url: 'https://hooks.example.com/leads', events: ['lead:captured'] },
  ],

  // Analytics (optional)
  analytics: true,

  // Conversation storage (optional, default: in-memory)
  storage: { conversations: new RedisConversationStore(redisClient) },

  // Session tokens: required unless allowAnonymous: true
  sessionSecrets: [process.env.CHATCOPS_SESSION_SECRET],

  // CORS origin (optional, default: '*')
  allowedOrigins: ['*'],
});

SSE Stream Format

All adapters produce identical Server-Sent Events:

data: {"content":"Hello"}
data: {"content":" there!"}
data: {"done":true}
data: [DONE]

On error:

data: {"error":"rate_limit","retryAfter":42}
data: [DONE]

Error codes: invalid_request, rate_limit, provider_error, internal_error.

Rate Limiting

In-memory, per-IP. Suitable for single-instance deployments.

import { RateLimiter } from '@chatcops/server';

const limiter = new RateLimiter({ maxRequests: 30, windowMs: 60_000 });
const result = limiter.check('127.0.0.1');
// { allowed: true } or { allowed: false, retryAfter: 42 }

Webhooks

HMAC-SHA256 signed, with retries (up to 3 attempts).

import { WebhookDispatcher } from '@chatcops/server';

const dispatcher = new WebhookDispatcher([
  { url: 'https://hooks.example.com/chat', events: ['*'], secret: 'my-secret' },
]);

await dispatcher.dispatch('lead:captured', { email: 'user@example.com' });

Each delivery carries X-ChatCops-Signature: t=<seconds>,v1=<hmac>, signed over the raw body. Check it on the receiving side with verifyWebhook:

import { verifyWebhook } from '@chatcops/server';

const valid = await verifyWebhook({ body: rawBody, signature: request.headers.get('X-ChatCops-Signature'), secret: 'my-secret' });

The body is { id, type, created, data }; id stays the same across retries. 429 and 5xx responses are retried, redirects aren't followed, and URLs must be https on a public host unless the webhook sets allowPrivateNetwork: true. See the server setup docs for the full format.

Custom Conversation Store

By default, conversations are held in memory (MemoryStore). You can persist conversations across server restarts or share them across instances by implementing ConversationStore from @chatcops/core:

import type { ConversationStore, Conversation } from '@chatcops/core';

class RedisConversationStore implements ConversationStore {
  constructor(private redis: RedisClient) {}

  async get(id: string): Promise<Conversation | null> {
    const data = await this.redis.get(`conversation:${id}`);
    return data ? JSON.parse(data) : null;
  }

  async save(conversation: Conversation): Promise<void> {
    await this.redis.set(`conversation:${conversation.id}`, JSON.stringify(conversation));
  }

  async delete(id: string): Promise<void> {
    await this.redis.del(`conversation:${id}`);
  }
}

Pass the store when initializing the handler:

const handler = createChatHandler({
  provider: { type: 'claude', apiKey: process.env.ANTHROPIC_API_KEY },
  systemPrompt: 'You are a helpful assistant.',
  storage: { conversations: new RedisConversationStore(redisClient) },
  allowAnonymous: true,
});

Custom integrations

For a runtime with the Fetch API's Request and Response (Next.js, Hono, Bun, Deno, Workers), pass requests to chatcopsVercelHandler(config): it's a plain (Request) => Promise<Response>. For Node's req and res (node:http, Fastify, Koa), turn the request into a Request and stream the Response back. The docs have the code: Next.js, Hono, Bun and Deno. Route both the chat path and <chat path>/session to it, since the widget gets its session token there.

The handler does CORS, allowedOrigins, body limits, pings, session tokens and the 0.5 format for old widgets, so bridging to it is better than calling createChatHandler and writing the HTTP side yourself.

License

MIT