@chatcops/server 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/README.md +4 -4
  2. package/dist/adapters/ai-sdk.d.ts +12 -0
  3. package/dist/adapters/ai-sdk.d.ts.map +1 -0
  4. package/dist/adapters/ai-sdk.js +27 -0
  5. package/dist/adapters/ai-sdk.js.map +1 -0
  6. package/dist/adapters/cloudflare.d.ts.map +1 -1
  7. package/dist/adapters/cloudflare.js +9 -83
  8. package/dist/adapters/cloudflare.js.map +1 -1
  9. package/dist/adapters/express.d.ts +6 -2
  10. package/dist/adapters/express.d.ts.map +1 -1
  11. package/dist/adapters/express.js +102 -30
  12. package/dist/adapters/express.js.map +1 -1
  13. package/dist/adapters/index.d.ts +1 -0
  14. package/dist/adapters/index.d.ts.map +1 -1
  15. package/dist/adapters/index.js +1 -0
  16. package/dist/adapters/index.js.map +1 -1
  17. package/dist/adapters/vercel.d.ts.map +1 -1
  18. package/dist/adapters/vercel.js +7 -74
  19. package/dist/adapters/vercel.js.map +1 -1
  20. package/dist/client-ip.d.ts +37 -0
  21. package/dist/client-ip.d.ts.map +1 -0
  22. package/dist/client-ip.js +57 -0
  23. package/dist/client-ip.js.map +1 -0
  24. package/dist/config.d.ts +136 -8
  25. package/dist/config.d.ts.map +1 -1
  26. package/dist/config.js +15 -1
  27. package/dist/config.js.map +1 -1
  28. package/dist/examples/cloudflare-worker.js +1 -1
  29. package/dist/examples/cloudflare-worker.js.map +1 -1
  30. package/dist/handler.d.ts +52 -1
  31. package/dist/handler.d.ts.map +1 -1
  32. package/dist/handler.js +417 -136
  33. package/dist/handler.js.map +1 -1
  34. package/dist/http.d.ts +69 -0
  35. package/dist/http.d.ts.map +1 -0
  36. package/dist/http.js +262 -0
  37. package/dist/http.js.map +1 -0
  38. package/dist/index.d.ts +4 -0
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +1 -0
  41. package/dist/index.js.map +1 -1
  42. package/dist/knowledge.d.ts +18 -0
  43. package/dist/knowledge.d.ts.map +1 -0
  44. package/dist/knowledge.js +32 -0
  45. package/dist/knowledge.js.map +1 -0
  46. package/dist/rate-limit.d.ts +5 -0
  47. package/dist/rate-limit.d.ts.map +1 -1
  48. package/dist/rate-limit.js +5 -0
  49. package/dist/rate-limit.js.map +1 -1
  50. package/dist/security/origin-policy.d.ts +19 -0
  51. package/dist/security/origin-policy.d.ts.map +1 -0
  52. package/dist/security/origin-policy.js +60 -0
  53. package/dist/security/origin-policy.js.map +1 -0
  54. package/dist/security/origin.d.ts +5 -0
  55. package/dist/security/origin.d.ts.map +1 -0
  56. package/dist/security/origin.js +47 -0
  57. package/dist/security/origin.js.map +1 -0
  58. package/dist/security/session-token.d.ts +65 -0
  59. package/dist/security/session-token.d.ts.map +1 -0
  60. package/dist/security/session-token.js +338 -0
  61. package/dist/security/session-token.js.map +1 -0
  62. package/dist/security/sessions.d.ts +72 -0
  63. package/dist/security/sessions.d.ts.map +1 -0
  64. package/dist/security/sessions.js +216 -0
  65. package/dist/security/sessions.js.map +1 -0
  66. package/dist/security/startup-checks.d.ts +45 -0
  67. package/dist/security/startup-checks.d.ts.map +1 -0
  68. package/dist/security/startup-checks.js +124 -0
  69. package/dist/security/startup-checks.js.map +1 -0
  70. package/dist/security/turnstile.d.ts +24 -0
  71. package/dist/security/turnstile.d.ts.map +1 -0
  72. package/dist/security/turnstile.js +93 -0
  73. package/dist/security/turnstile.js.map +1 -0
  74. package/dist/visitor-context.d.ts.map +1 -1
  75. package/dist/visitor-context.js +7 -0
  76. package/dist/visitor-context.js.map +1 -1
  77. package/dist/webhook.d.ts +3 -2
  78. package/dist/webhook.d.ts.map +1 -1
  79. package/dist/webhook.js +5 -2
  80. package/dist/webhook.js.map +1 -1
  81. package/package.json +22 -5
package/README.md CHANGED
@@ -25,7 +25,7 @@ app.all('/chat', chatcopsMiddleware({
25
25
  apiKey: process.env.ANTHROPIC_API_KEY,
26
26
  },
27
27
  systemPrompt: 'You are a helpful assistant for our website.',
28
- cors: '*',
28
+ allowedOrigins: ['*'],
29
29
  }));
30
30
 
31
31
  app.listen(3001);
@@ -39,7 +39,7 @@ import { chatcopsVercelHandler } from '@chatcops/server';
39
39
  const handler = chatcopsVercelHandler({
40
40
  provider: { type: 'openai', apiKey: process.env.OPENAI_API_KEY },
41
41
  systemPrompt: 'You are a helpful assistant.',
42
- cors: '*',
42
+ allowedOrigins: ['*'],
43
43
  });
44
44
 
45
45
  export const POST = (req: Request) => handler(req);
@@ -57,7 +57,7 @@ function getHandler(env: Env) {
57
57
  handler ??= chatcopsCloudflareHandler({
58
58
  provider: { type: 'claude', apiKey: env.ANTHROPIC_API_KEY },
59
59
  systemPrompt: 'You are a helpful assistant.',
60
- cors: '*',
60
+ allowedOrigins: ['*'],
61
61
  });
62
62
  return handler;
63
63
  }
@@ -115,7 +115,7 @@ const handler = createChatHandler({
115
115
  conversationStore: new RedisConversationStore(redisClient),
116
116
 
117
117
  // CORS origin (optional, default: '*')
118
- cors: '*',
118
+ allowedOrigins: ['*'],
119
119
  });
120
120
  ```
121
121
 
@@ -0,0 +1,12 @@
1
+ import type { ChatCopsServerConfig } from '../config.js';
2
+ /**
3
+ * A Fetch API handler that `useChat` from the AI SDK can talk to: it reads
4
+ * the AI SDK's request and answers in its UI message stream. Use it as a
5
+ * Next.js route handler or anywhere that takes `(request) => Response`.
6
+ *
7
+ * The server keeps the conversation (keyed by the chat `id`) and reads only
8
+ * the last user message, unlike AI SDK routes that take the whole history
9
+ * from the client.
10
+ */
11
+ export declare function chatcopsAiSdkHandler(config: ChatCopsServerConfig): (req: Request) => Promise<Response>;
12
+ //# sourceMappingURL=ai-sdk.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ai-sdk.d.ts","sourceRoot":"","sources":["../../src/adapters/ai-sdk.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAEzD;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,oBAAoB,IAIvC,KAAK,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CAMzD"}
@@ -0,0 +1,27 @@
1
+ import { cloudflareClientIp, fetchClientIp } from '../client-ip.js';
2
+ import { createChatHandler } from '../handler.js';
3
+ import { fetchChatResponse } from '../http.js';
4
+ import { originPolicy } from '../security/origin-policy.js';
5
+ /**
6
+ * A Fetch API handler that `useChat` from the AI SDK can talk to: it reads
7
+ * the AI SDK's request and answers in its UI message stream. Use it as a
8
+ * Next.js route handler or anywhere that takes `(request) => Response`.
9
+ *
10
+ * The server keeps the conversation (keyed by the chat `id`) and reads only
11
+ * the last user message, unlike AI SDK routes that take the whole history
12
+ * from the client.
13
+ */
14
+ export function chatcopsAiSdkHandler(config) {
15
+ const chat = createChatHandler(config);
16
+ const origins = originPolicy(config);
17
+ return function handler(req) {
18
+ return fetchChatResponse(chat, origins, req, {
19
+ clientIp: (request) => (onWorkers() ? cloudflareClientIp(request, config.clientIp, chat.logger) : fetchClientIp(request, config.clientIp, chat.logger)),
20
+ aiSdk: true,
21
+ });
22
+ };
23
+ }
24
+ function onWorkers() {
25
+ return globalThis.navigator?.userAgent === 'Cloudflare-Workers';
26
+ }
27
+ //# sourceMappingURL=ai-sdk.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ai-sdk.js","sourceRoot":"","sources":["../../src/adapters/ai-sdk.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACpE,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAG5D;;;;;;;;GAQG;AACH,MAAM,UAAU,oBAAoB,CAAC,MAA4B;IAC/D,MAAM,IAAI,GAAG,iBAAiB,CAAC,MAAM,CAAC,CAAC;IACvC,MAAM,OAAO,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;IAErC,OAAO,SAAS,OAAO,CAAC,GAAY;QAClC,OAAO,iBAAiB,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE;YAC3C,QAAQ,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,kBAAkB,CAAC,OAAO,EAAE,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,OAAO,EAAE,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;YACvJ,KAAK,EAAE,IAAI;SACZ,CAAC,CAAC;IACL,CAAC,CAAC;AACJ,CAAC;AAED,SAAS,SAAS;IAChB,OAAQ,UAAqD,CAAC,SAAS,EAAE,SAAS,KAAK,oBAAoB,CAAC;AAC9G,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"cloudflare.d.ts","sourceRoot":"","sources":["../../src/adapters/cloudflare.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAEzD,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,oBAAoB,IAiBlE,SAAS,OAAO,EAChB,OAAO,OAAO,EACd,MAAM;IAAE,SAAS,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAAA;CAAE,KACpD,OAAO,CAAC,QAAQ,CAAC,CAwErB"}
1
+ {"version":3,"file":"cloudflare.d.ts","sourceRoot":"","sources":["../../src/adapters/cloudflare.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAEzD,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,oBAAoB,IAOlE,SAAS,OAAO,EAChB,OAAO,OAAO,EACd,MAAM;IAAE,SAAS,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAAA;CAAE,KACpD,OAAO,CAAC,QAAQ,CAAC,CAMrB"}
@@ -1,90 +1,16 @@
1
+ import { cloudflareClientIp } from '../client-ip.js';
1
2
  import { createChatHandler } from '../handler.js';
3
+ import { fetchChatResponse } from '../http.js';
4
+ import { originPolicy } from '../security/origin-policy.js';
2
5
  export function chatcopsCloudflareHandler(config) {
3
- const { handleChat } = createChatHandler(config);
4
- const corsOrigin = config.cors ?? '*';
5
- function errorResponse(status, error) {
6
- return new Response(JSON.stringify({ error }), {
7
- status,
8
- headers: {
9
- 'Content-Type': 'application/json',
10
- 'Access-Control-Allow-Origin': corsOrigin,
11
- },
12
- });
13
- }
6
+ const chat = createChatHandler(config);
7
+ const origins = originPolicy(config);
14
8
  // Workers call fetch(request, env, ctx); ctx.waitUntil keeps lead webhooks
15
9
  // running after a visitor disconnects.
16
- return async function handler(request, _env, ctx) {
17
- // Handle CORS preflight
18
- if (request.method === 'OPTIONS') {
19
- return new Response(null, {
20
- status: 204,
21
- headers: {
22
- 'Access-Control-Allow-Origin': corsOrigin,
23
- 'Access-Control-Allow-Methods': 'POST, OPTIONS',
24
- 'Access-Control-Allow-Headers': 'Content-Type',
25
- 'Access-Control-Max-Age': '86400',
26
- },
27
- });
28
- }
29
- if (request.method !== 'POST') {
30
- return errorResponse(405, 'Method not allowed');
31
- }
32
- const clientIp = request.headers.get('cf-connecting-ip') ?? 'unknown';
33
- let body;
34
- try {
35
- body = await request.json();
36
- }
37
- catch {
38
- return errorResponse(400, 'Invalid JSON body');
39
- }
40
- const { readable, writable } = new TransformStream();
41
- const writer = writable.getWriter();
42
- const encoder = new TextEncoder();
43
- // When the visitor disconnects the runtime cancels the response, which errors
44
- // the writer right away; abort then, without waiting for the next write.
45
- const abort = new AbortController();
46
- writer.closed.catch(() => abort.abort());
47
- const send = async (text) => {
48
- if (abort.signal.aborted)
49
- return false;
50
- try {
51
- await writer.write(encoder.encode(text));
52
- return true;
53
- }
54
- catch {
55
- abort.abort();
56
- return false;
57
- }
58
- };
59
- const pump = (async () => {
60
- try {
61
- for await (const chunk of handleChat(body, clientIp, { signal: abort.signal })) {
62
- if (!(await send(`data: ${chunk}\n\n`)))
63
- break;
64
- }
65
- await send('data: [DONE]\n\n');
66
- }
67
- catch (err) {
68
- if (!abort.signal.aborted)
69
- console.error('[chatcops] Unhandled error while handling chat:', err);
70
- await send(`data: ${JSON.stringify({ error: 'internal_error' })}\n\n`);
71
- }
72
- finally {
73
- if (abort.signal.aborted) {
74
- await writer.abort().catch(() => { });
75
- }
76
- else {
77
- await writer.close().catch(() => { });
78
- }
79
- }
80
- })();
81
- ctx?.waitUntil?.(pump);
82
- return new Response(readable, {
83
- headers: {
84
- 'Content-Type': 'text/event-stream',
85
- 'Cache-Control': 'no-cache',
86
- 'Access-Control-Allow-Origin': corsOrigin,
87
- },
10
+ return function handler(request, _env, ctx) {
11
+ return fetchChatResponse(chat, origins, request, {
12
+ clientIp: (req) => cloudflareClientIp(req, config.clientIp, chat.logger),
13
+ ...(ctx?.waitUntil ? { waitUntil: (promise) => ctx.waitUntil?.(promise) } : {}),
88
14
  });
89
15
  };
90
16
  }
@@ -1 +1 @@
1
- {"version":3,"file":"cloudflare.js","sourceRoot":"","sources":["../../src/adapters/cloudflare.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cloudflare.js","sourceRoot":"","sources":["../../src/adapters/cloudflare.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAG5D,MAAM,UAAU,yBAAyB,CAAC,MAA4B;IACpE,MAAM,IAAI,GAAG,iBAAiB,CAAC,MAAM,CAAC,CAAC;IACvC,MAAM,OAAO,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;IAErC,2EAA2E;IAC3E,uCAAuC;IACvC,OAAO,SAAS,OAAO,CACrB,OAAgB,EAChB,IAAc,EACd,GAAqD;QAErD,OAAO,iBAAiB,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE;YAC/C,QAAQ,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,kBAAkB,CAAC,GAAG,EAAE,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC;YACxE,GAAG,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,OAAyB,EAAE,EAAE,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAClG,CAAC,CAAC;IACL,CAAC,CAAC;AACJ,CAAC"}
@@ -1,5 +1,9 @@
1
1
  import type { Request, Response, NextFunction } from 'express';
2
2
  import type { ChatCopsServerConfig } from '../config.js';
3
- export declare function chatcopsMiddleware(config: ChatCopsServerConfig): (req: Request, res: Response, next: NextFunction) => Promise<void>;
4
- export declare function chatcopsAnalyticsHandler(config: ChatCopsServerConfig): (_req: Request, res: Response) => void;
3
+ export declare function chatcopsMiddleware(config: ChatCopsServerConfig): (req: Request, res: Response, _next: NextFunction) => Promise<void>;
4
+ /**
5
+ * Analytics stats as JSON. With a storage that has an event sink, they cover
6
+ * every instance, for `?from=` and `?to=` (milliseconds) or the last 30 days.
7
+ */
8
+ export declare function chatcopsAnalyticsHandler(config: ChatCopsServerConfig): (req: Request, res: Response) => Promise<void>;
5
9
  //# sourceMappingURL=express.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"express.d.ts","sourceRoot":"","sources":["../../src/adapters/express.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE/D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAezD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,oBAAoB,IASvB,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,mBA4CtF;AAED,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,oBAAoB,IAE3D,MAAM,OAAO,EAAE,KAAK,QAAQ,UAGrC"}
1
+ {"version":3,"file":"express.d.ts","sourceRoot":"","sources":["../../src/adapters/express.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAK/D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAezD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,oBAAoB,IAIvB,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,OAAO,YAAY,mBAwEvF;AAaD;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,oBAAoB,IAIrD,KAAK,OAAO,EAAE,KAAK,QAAQ,mBA+B1C"}
@@ -1,4 +1,8 @@
1
+ import { errorBody } from '@chatcops/core';
2
+ import { expressClientIp } from '../client-ip.js';
1
3
  import { createChatHandler } from '../handler.js';
4
+ import { PROTOCOL_HEADER, chatReply, isSessionPath } from '../http.js';
5
+ import { originPolicy } from '../security/origin-policy.js';
2
6
  const chatHandlers = new WeakMap();
3
7
  function getOrCreateChatHandler(config) {
4
8
  let handler = chatHandlers.get(config);
@@ -9,50 +13,71 @@ function getOrCreateChatHandler(config) {
9
13
  return handler;
10
14
  }
11
15
  export function chatcopsMiddleware(config) {
12
- const { handleChat } = getOrCreateChatHandler(config);
13
- const corsOrigin = config.cors ?? '*';
14
- function sendError(res, status, error) {
15
- res.setHeader('Access-Control-Allow-Origin', corsOrigin);
16
- res.status(status).json({ error });
17
- }
18
- return async function chatcopsHandler(req, res, next) {
16
+ const chat = getOrCreateChatHandler(config);
17
+ const origins = originPolicy(config);
18
+ return async function chatcopsHandler(req, res, _next) {
19
+ const header = req.headers?.origin;
20
+ const origin = typeof header === 'string' ? header : null;
21
+ const setHeaders = (headers) => {
22
+ for (const [name, value] of Object.entries(headers))
23
+ res.setHeader(name, value);
24
+ };
25
+ const sendError = (status, error) => {
26
+ setHeaders(origins.headers(origin));
27
+ res.status(status).json({ error });
28
+ };
19
29
  // Handle CORS preflight
20
30
  if (req.method === 'OPTIONS') {
21
- res.setHeader('Access-Control-Allow-Origin', corsOrigin);
22
- res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
23
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
24
- res.setHeader('Access-Control-Max-Age', '86400');
31
+ setHeaders(origins.preflightHeaders(origin));
25
32
  res.status(204).end();
26
33
  return;
27
34
  }
28
35
  if (req.method !== 'POST') {
29
- sendError(res, 405, 'Method not allowed');
36
+ res.setHeader('Allow', 'POST, OPTIONS');
37
+ sendError(405, 'Method not allowed');
38
+ return;
39
+ }
40
+ if (!origins.allows(origin)) {
41
+ sendError(403, 'origin_not_allowed');
30
42
  return;
31
43
  }
32
- const clientIp = req.ip ?? req.socket.remoteAddress ?? 'unknown';
33
- res.setHeader('Content-Type', 'text/event-stream');
34
- res.setHeader('Cache-Control', 'no-cache');
35
- res.setHeader('Connection', 'keep-alive');
36
- res.setHeader('Access-Control-Allow-Origin', corsOrigin);
44
+ // `app.use('/api/chat', chatcopsMiddleware(...))` sends /api/chat/session here too.
45
+ if (isSessionPath(req.path ?? '')) {
46
+ setHeaders(origins.headers(origin));
47
+ if (!chat.startSession) {
48
+ res.status(404).json(errorBody('invalid_request', { message: 'Session tokens are off. Set `sessionSecrets` to turn them on.' }));
49
+ return;
50
+ }
51
+ const reply = await chat.startSession(req.body, origin, expressClientIp(req, chat.logger));
52
+ res.status(reply.status).json(reply.body);
53
+ return;
54
+ }
55
+ const clientIp = expressClientIp(req, chat.logger);
37
56
  // Stop the reply when the visitor goes away before it's done.
38
57
  const abort = new AbortController();
39
58
  res.on('close', () => {
40
59
  if (!res.writableFinished)
41
60
  abort.abort();
42
61
  });
62
+ const protocolHeader = req.headers?.[PROTOCOL_HEADER];
63
+ const authorization = req.headers?.authorization;
64
+ const reply = await chatReply(chat, req.body, clientIp, {
65
+ signal: abort.signal,
66
+ protocolHeader: typeof protocolHeader === 'string' ? protocolHeader : null,
67
+ authorization: typeof authorization === 'string' ? authorization : null,
68
+ origin,
69
+ });
70
+ if (reply.kind === 'json') {
71
+ setHeaders({ ...origins.headers(origin), ...reply.headers });
72
+ res.status(reply.status).json(reply.body);
73
+ return;
74
+ }
75
+ setHeaders({ ...reply.headers, ...origins.headers(origin) });
43
76
  try {
44
- for await (const chunk of handleChat(req.body, clientIp, { signal: abort.signal })) {
77
+ for await (const frame of reply.frames) {
45
78
  if (abort.signal.aborted)
46
79
  break;
47
- res.write(`data: ${chunk}\n\n`);
48
- }
49
- if (!abort.signal.aborted)
50
- res.write('data: [DONE]\n\n');
51
- }
52
- catch (err) {
53
- if (!abort.signal.aborted) {
54
- console.error('[chatcops] Unhandled error while handling chat:', err);
55
- res.write(`data: ${JSON.stringify({ error: 'internal_error' })}\n\n`);
80
+ res.write(frame);
56
81
  }
57
82
  }
58
83
  finally {
@@ -60,10 +85,57 @@ export function chatcopsMiddleware(config) {
60
85
  }
61
86
  };
62
87
  }
88
+ function constantTimeEqual(a, b) {
89
+ if (a.length !== b.length) {
90
+ return false;
91
+ }
92
+ let mismatch = 0;
93
+ for (let i = 0; i < a.length; i++) {
94
+ mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
95
+ }
96
+ return mismatch === 0;
97
+ }
98
+ /**
99
+ * Analytics stats as JSON. With a storage that has an event sink, they cover
100
+ * every instance, for `?from=` and `?to=` (milliseconds) or the last 30 days.
101
+ */
63
102
  export function chatcopsAnalyticsHandler(config) {
64
- const { getAnalytics } = getOrCreateChatHandler(config);
65
- return (_req, res) => {
66
- res.json(getAnalytics());
103
+ const { analyticsStats, logger } = getOrCreateChatHandler(config);
104
+ let warned = false;
105
+ return async (req, res) => {
106
+ if (config.analyticsToken) {
107
+ const auth = req.headers?.authorization;
108
+ const token = typeof auth === 'string' && auth.startsWith('Bearer ') ? auth.slice(7) : undefined;
109
+ if (!token || !constantTimeEqual(token, config.analyticsToken)) {
110
+ res.setHeader('WWW-Authenticate', 'Bearer');
111
+ res.status(401).json({ error: 'unauthorized' });
112
+ return;
113
+ }
114
+ }
115
+ else if (!warned) {
116
+ warned = true;
117
+ logger.warn('analytics_public', 'The analytics endpoint is public and anyone can read your stats. Set `analyticsToken` in your ChatCopsServerConfig to require a bearer token.');
118
+ }
119
+ const from = timeParam(req.query?.from);
120
+ const to = timeParam(req.query?.to);
121
+ if (from === null || to === null) {
122
+ res.status(400).json({ error: 'invalid_range' });
123
+ return;
124
+ }
125
+ try {
126
+ res.json(await analyticsStats({ from, to }));
127
+ }
128
+ catch (err) {
129
+ logger.error('analytics_read_failed', `Reading analytics failed: ${err instanceof Error ? err.message : String(err)}`);
130
+ res.status(500).json({ error: 'internal_error' });
131
+ }
67
132
  };
68
133
  }
134
+ /** A `from` or `to` query value: unset, a time in milliseconds, or `null` when it isn't one. */
135
+ function timeParam(value) {
136
+ if (value === undefined)
137
+ return undefined;
138
+ const time = typeof value === 'string' && value.trim() !== '' ? Number(value) : Number.NaN;
139
+ return Number.isFinite(time) ? time : null;
140
+ }
69
141
  //# sourceMappingURL=express.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"express.js","sourceRoot":"","sources":["../../src/adapters/express.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"express.js","sourceRoot":"","sources":["../../src/adapters/express.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  export { chatcopsMiddleware, chatcopsAnalyticsHandler } from './express.js';
2
2
  export { chatcopsVercelHandler } from './vercel.js';
3
3
  export { chatcopsCloudflareHandler } from './cloudflare.js';
4
+ export { chatcopsAiSdkHandler } from './ai-sdk.js';
4
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/adapters/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AAC5E,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAAE,yBAAyB,EAAE,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/adapters/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AAC5E,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAAE,yBAAyB,EAAE,MAAM,iBAAiB,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC"}
@@ -1,4 +1,5 @@
1
1
  export { chatcopsMiddleware, chatcopsAnalyticsHandler } from './express.js';
2
2
  export { chatcopsVercelHandler } from './vercel.js';
3
3
  export { chatcopsCloudflareHandler } from './cloudflare.js';
4
+ export { chatcopsAiSdkHandler } from './ai-sdk.js';
4
5
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/adapters/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AAC5E,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAAE,yBAAyB,EAAE,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/adapters/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AAC5E,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAAE,yBAAyB,EAAE,MAAM,iBAAiB,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"vercel.d.ts","sourceRoot":"","sources":["../../src/adapters/vercel.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAEzD,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,oBAAoB,IAclC,KAAK,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CA6D/D"}
1
+ {"version":3,"file":"vercel.d.ts","sourceRoot":"","sources":["../../src/adapters/vercel.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAEzD,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,oBAAoB,IAIxC,KAAK,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CAGzD"}
@@ -1,79 +1,12 @@
1
+ import { fetchClientIp } from '../client-ip.js';
1
2
  import { createChatHandler } from '../handler.js';
3
+ import { fetchChatResponse } from '../http.js';
4
+ import { originPolicy } from '../security/origin-policy.js';
2
5
  export function chatcopsVercelHandler(config) {
3
- const { handleChat } = createChatHandler(config);
4
- const corsOrigin = config.cors ?? '*';
5
- function errorResponse(status, error) {
6
- return new Response(JSON.stringify({ error }), {
7
- status,
8
- headers: {
9
- 'Content-Type': 'application/json',
10
- 'Access-Control-Allow-Origin': corsOrigin,
11
- },
12
- });
13
- }
14
- return async function handler(req) {
15
- // Handle CORS preflight
16
- if (req.method === 'OPTIONS') {
17
- return new Response(null, {
18
- status: 204,
19
- headers: {
20
- 'Access-Control-Allow-Origin': corsOrigin,
21
- 'Access-Control-Allow-Methods': 'POST, OPTIONS',
22
- 'Access-Control-Allow-Headers': 'Content-Type',
23
- 'Access-Control-Max-Age': '86400',
24
- },
25
- });
26
- }
27
- if (req.method !== 'POST') {
28
- return errorResponse(405, 'Method not allowed');
29
- }
30
- const clientIp = req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? 'unknown';
31
- let body;
32
- try {
33
- body = await req.json();
34
- }
35
- catch {
36
- return errorResponse(400, 'Invalid JSON body');
37
- }
38
- const encoder = new TextEncoder();
39
- // The runtime cancels the response stream when the visitor disconnects.
40
- const abort = new AbortController();
41
- const stream = new ReadableStream({
42
- async start(controller) {
43
- const send = (text) => {
44
- if (!abort.signal.aborted)
45
- controller.enqueue(encoder.encode(text));
46
- };
47
- try {
48
- for await (const chunk of handleChat(body, clientIp, { signal: abort.signal })) {
49
- if (abort.signal.aborted)
50
- break;
51
- send(`data: ${chunk}\n\n`);
52
- }
53
- send('data: [DONE]\n\n');
54
- }
55
- catch (err) {
56
- if (!abort.signal.aborted)
57
- console.error('[chatcops] Unhandled error while handling chat:', err);
58
- send(`data: ${JSON.stringify({ error: 'internal_error' })}\n\n`);
59
- }
60
- finally {
61
- if (!abort.signal.aborted)
62
- controller.close();
63
- }
64
- },
65
- cancel() {
66
- abort.abort();
67
- },
68
- });
69
- return new Response(stream, {
70
- headers: {
71
- 'Content-Type': 'text/event-stream',
72
- 'Cache-Control': 'no-cache',
73
- 'Connection': 'keep-alive',
74
- 'Access-Control-Allow-Origin': corsOrigin,
75
- },
76
- });
6
+ const chat = createChatHandler(config);
7
+ const origins = originPolicy(config);
8
+ return function handler(req) {
9
+ return fetchChatResponse(chat, origins, req, { clientIp: (request) => fetchClientIp(request, config.clientIp, chat.logger) });
77
10
  };
78
11
  }
79
12
  //# sourceMappingURL=vercel.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"vercel.js","sourceRoot":"","sources":["../../src/adapters/vercel.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"vercel.js","sourceRoot":"","sources":["../../src/adapters/vercel.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAG5D,MAAM,UAAU,qBAAqB,CAAC,MAA4B;IAChE,MAAM,IAAI,GAAG,iBAAiB,CAAC,MAAM,CAAC,CAAC;IACvC,MAAM,OAAO,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;IAErC,OAAO,SAAS,OAAO,CAAC,GAAY;QAClC,OAAO,iBAAiB,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,EAAE,QAAQ,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,aAAa,CAAC,OAAO,EAAE,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAChI,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,37 @@
1
+ import { type Logger } from '@chatcops/core';
2
+ /**
3
+ * The visitor's IP, for per-IP rate limits and budgets. Only headers a
4
+ * platform sets itself are read, never ones a visitor can send. When the IP
5
+ * can't be told, per-IP limits are skipped for that request (with one
6
+ * warning) instead of every visitor sharing one bucket.
7
+ */
8
+ /** Reads the visitor's IP from a request, for hosts the adapter doesn't know. */
9
+ export type ClientIpResolver = (request: Request) => string | null | undefined;
10
+ export declare function cleanIp(value: string | null | undefined): string | undefined;
11
+ /** Vercel overwrites these headers, so a visitor can't set them there. */
12
+ export declare function onVercel(): boolean;
13
+ export declare function warnOnce(key: string, message: string, logger?: Logger): void;
14
+ /** For tests. */
15
+ export declare function resetClientIpWarnings(): void;
16
+ /** The fetch handler for Vercel and other Fetch API runtimes. */
17
+ export declare function fetchClientIp(request: Request, resolver: ClientIpResolver | undefined, logger?: Logger): string | undefined;
18
+ /** Cloudflare sets CF-Connecting-IP on every request that reaches a Worker. */
19
+ export declare function cloudflareClientIp(request: Request, resolver: ClientIpResolver | undefined, logger?: Logger): string | undefined;
20
+ interface ExpressLikeRequest {
21
+ ip?: string;
22
+ socket?: {
23
+ remoteAddress?: string;
24
+ };
25
+ headers?: Record<string, string | string[] | undefined>;
26
+ app?: {
27
+ get?(setting: string): unknown;
28
+ };
29
+ }
30
+ /**
31
+ * Express's `req.ip` follows its `trust proxy` setting. Behind a proxy with
32
+ * that setting off, every visitor gets the proxy's address and shares one
33
+ * rate limit and budget, so that case gets a warning.
34
+ */
35
+ export declare function expressClientIp(req: ExpressLikeRequest, logger?: Logger): string | undefined;
36
+ export {};
37
+ //# sourceMappingURL=client-ip.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-ip.d.ts","sourceRoot":"","sources":["../src/client-ip.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAC5D;;;;;GAKG;AAEH,iFAAiF;AACjF,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;AAQ/E,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAG5E;AAED,0EAA0E;AAC1E,wBAAgB,QAAQ,IAAI,OAAO,CAElC;AAID,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAE,MAAwB,GAAG,IAAI,CAI7F;AAED,iBAAiB;AACjB,wBAAgB,qBAAqB,IAAI,IAAI,CAE5C;AAED,iEAAiE;AACjE,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,gBAAgB,GAAG,SAAS,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAY3H;AAED,+EAA+E;AAC/E,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,gBAAgB,GAAG,SAAS,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAOhI;AAED,UAAU,kBAAkB;IAC1B,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACpC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;IACxD,GAAG,CAAC,EAAE;QAAE,GAAG,CAAC,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC;CAC1C;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,kBAAkB,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAS5F"}