Server-side handler and platform adapters for ChatCops. Handles streaming responses, rate limiting, webhooks, and analytics.
npm install @chatcops/server @chatcops/coreimport express from 'express';
import { chatcopsMiddleware } from '@chatcops/server';
const app = express();
app.use(express.json());
app.all('/chat', chatcopsMiddleware({
provider: {
type: 'claude',
apiKey: process.env.ANTHROPIC_API_KEY,
},
systemPrompt: 'You are a helpful assistant for our website.',
allowedOrigins: ['*'],
}));
app.listen(3001);import { chatcopsVercelHandler } from '@chatcops/server';
const handler = chatcopsVercelHandler({
provider: { type: 'openai', apiKey: process.env.OPENAI_API_KEY },
systemPrompt: 'You are a helpful assistant.',
allowedOrigins: ['*'],
});
export const POST = (req: Request) => handler(req);
export const OPTIONS = (req: Request) => handler(req);
export const config = { runtime: 'edge' };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: ['*'],
});
return handler;
}
export default {
async fetch(request: Request, env: Env) {
return getHandler(env)(request);
},
};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)
conversationStore: new RedisConversationStore(redisClient),
// CORS origin (optional, default: '*')
allowedOrigins: ['*'],
});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.
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 }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.
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.',
conversationStore: new RedisConversationStore(redisClient),
});createChatHandler(config) returns handleChat(body, clientIp, options), an async generator of SSE data payloads. The built-in adapters use it; call it directly for other runtimes. Pass options.signal and abort it when the visitor disconnects: the provider request is cancelled, nothing more is yielded, and the partial reply isn't saved.
const abort = new AbortController();
const stream = new ReadableStream({
async start(controller) {
for await (const chunk of handler.handleChat(body, clientIp, { signal: abort.signal })) {
if (abort.signal.aborted) break;
controller.enqueue(new TextEncoder().encode(`data: ${chunk}\n\n`));
}
if (!abort.signal.aborted) controller.close();
},
cancel() {
abort.abort(); // the runtime cancels the response when the client goes away
},
});MIT