@chatcops/server 0.4.0 → 0.6.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 (77) hide show
  1. package/README.md +35 -7
  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 +3 -1
  7. package/dist/adapters/cloudflare.d.ts.map +1 -1
  8. package/dist/adapters/cloudflare.js +11 -58
  9. package/dist/adapters/cloudflare.js.map +1 -1
  10. package/dist/adapters/express.d.ts +6 -2
  11. package/dist/adapters/express.d.ts.map +1 -1
  12. package/dist/adapters/express.js +109 -26
  13. package/dist/adapters/express.js.map +1 -1
  14. package/dist/adapters/index.d.ts +1 -0
  15. package/dist/adapters/index.d.ts.map +1 -1
  16. package/dist/adapters/index.js +1 -0
  17. package/dist/adapters/index.js.map +1 -1
  18. package/dist/adapters/vercel.d.ts.map +1 -1
  19. package/dist/adapters/vercel.js +7 -60
  20. package/dist/adapters/vercel.js.map +1 -1
  21. package/dist/client-ip.d.ts +36 -0
  22. package/dist/client-ip.d.ts.map +1 -0
  23. package/dist/client-ip.js +62 -0
  24. package/dist/client-ip.js.map +1 -0
  25. package/dist/config.d.ts +110 -8
  26. package/dist/config.d.ts.map +1 -1
  27. package/dist/config.js +5 -1
  28. package/dist/config.js.map +1 -1
  29. package/dist/handler.d.ts +53 -2
  30. package/dist/handler.d.ts.map +1 -1
  31. package/dist/handler.js +410 -127
  32. package/dist/handler.js.map +1 -1
  33. package/dist/http.d.ts +66 -0
  34. package/dist/http.d.ts.map +1 -0
  35. package/dist/http.js +262 -0
  36. package/dist/http.js.map +1 -0
  37. package/dist/index.d.ts +4 -1
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +2 -1
  40. package/dist/index.js.map +1 -1
  41. package/dist/rate-limit.d.ts +5 -0
  42. package/dist/rate-limit.d.ts.map +1 -1
  43. package/dist/rate-limit.js +5 -0
  44. package/dist/rate-limit.js.map +1 -1
  45. package/dist/security/origin-policy.d.ts +19 -0
  46. package/dist/security/origin-policy.d.ts.map +1 -0
  47. package/dist/security/origin-policy.js +58 -0
  48. package/dist/security/origin-policy.js.map +1 -0
  49. package/dist/security/origin.d.ts +5 -0
  50. package/dist/security/origin.d.ts.map +1 -0
  51. package/dist/security/origin.js +47 -0
  52. package/dist/security/origin.js.map +1 -0
  53. package/dist/security/session-token.d.ts +65 -0
  54. package/dist/security/session-token.d.ts.map +1 -0
  55. package/dist/security/session-token.js +338 -0
  56. package/dist/security/session-token.js.map +1 -0
  57. package/dist/security/sessions.d.ts +49 -0
  58. package/dist/security/sessions.d.ts.map +1 -0
  59. package/dist/security/sessions.js +159 -0
  60. package/dist/security/sessions.js.map +1 -0
  61. package/dist/security/startup-checks.d.ts +44 -0
  62. package/dist/security/startup-checks.d.ts.map +1 -0
  63. package/dist/security/startup-checks.js +134 -0
  64. package/dist/security/startup-checks.js.map +1 -0
  65. package/dist/security/turnstile.d.ts +24 -0
  66. package/dist/security/turnstile.d.ts.map +1 -0
  67. package/dist/security/turnstile.js +93 -0
  68. package/dist/security/turnstile.js.map +1 -0
  69. package/dist/visitor-context.d.ts +20 -0
  70. package/dist/visitor-context.d.ts.map +1 -0
  71. package/dist/visitor-context.js +60 -0
  72. package/dist/visitor-context.js.map +1 -0
  73. package/dist/webhook.d.ts +48 -2
  74. package/dist/webhook.d.ts.map +1 -1
  75. package/dist/webhook.js +159 -31
  76. package/dist/webhook.js.map +1 -1
  77. 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
  }
@@ -80,7 +80,7 @@ const handler = createChatHandler({
80
80
  provider: {
81
81
  type: 'claude', // 'claude' | 'openai' | 'gemini'
82
82
  apiKey: 'sk-...',
83
- model: 'claude-haiku-4-5-20251001',
83
+ model: 'claude-haiku-5-5',
84
84
  },
85
85
 
86
86
  // System prompt (required)
@@ -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
 
@@ -153,7 +153,7 @@ const result = limiter.check('127.0.0.1');
153
153
 
154
154
  ## Webhooks
155
155
 
156
- HMAC-SHA256 signed, with exponential backoff retries (up to 3 attempts).
156
+ HMAC-SHA256 signed, with retries (up to 3 attempts).
157
157
 
158
158
  ```typescript
159
159
  import { WebhookDispatcher } from '@chatcops/server';
@@ -165,7 +165,15 @@ const dispatcher = new WebhookDispatcher([
165
165
  await dispatcher.dispatch('lead:captured', { email: 'user@example.com' });
166
166
  ```
167
167
 
168
- Signature sent via `X-ChatCops-Signature` header.
168
+ Each delivery carries `X-ChatCops-Signature: t=<seconds>,v1=<hmac>`, signed over the raw body. Check it on the receiving side with `verifyWebhook`:
169
+
170
+ ```typescript
171
+ import { verifyWebhook } from '@chatcops/server';
172
+
173
+ const valid = await verifyWebhook({ body: rawBody, signature: request.headers.get('X-ChatCops-Signature'), secret: 'my-secret' });
174
+ ```
175
+
176
+ 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.
169
177
 
170
178
  ## Custom Conversation Store
171
179
 
@@ -202,6 +210,26 @@ const handler = createChatHandler({
202
210
  });
203
211
  ```
204
212
 
213
+ ## Custom integrations
214
+
215
+ `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.
216
+
217
+ ```typescript
218
+ const abort = new AbortController();
219
+ const stream = new ReadableStream({
220
+ async start(controller) {
221
+ for await (const chunk of handler.handleChat(body, clientIp, { signal: abort.signal })) {
222
+ if (abort.signal.aborted) break;
223
+ controller.enqueue(new TextEncoder().encode(`data: ${chunk}\n\n`));
224
+ }
225
+ if (!abort.signal.aborted) controller.close();
226
+ },
227
+ cancel() {
228
+ abort.abort(); // the runtime cancels the response when the client goes away
229
+ },
230
+ });
231
+ ```
232
+
205
233
  ## License
206
234
 
207
235
  MIT
@@ -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) : fetchClientIp(request, config.clientIp)),
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,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,OAAO,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;YAC7H,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,3 +1,5 @@
1
1
  import type { ChatCopsServerConfig } from '../config.js';
2
- export declare function chatcopsCloudflareHandler(config: ChatCopsServerConfig): (request: Request) => Promise<Response>;
2
+ export declare function chatcopsCloudflareHandler(config: ChatCopsServerConfig): (request: Request, _env?: unknown, ctx?: {
3
+ waitUntil?(promise: Promise<unknown>): void;
4
+ }) => Promise<Response>;
3
5
  //# sourceMappingURL=cloudflare.d.ts.map
@@ -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,IActC,SAAS,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CAmDnE"}
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,63 +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
- }
14
- return async function handler(request) {
15
- // Handle CORS preflight
16
- if (request.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 (request.method !== 'POST') {
28
- return errorResponse(405, 'Method not allowed');
29
- }
30
- const clientIp = request.headers.get('cf-connecting-ip') ?? 'unknown';
31
- let body;
32
- try {
33
- body = await request.json();
34
- }
35
- catch {
36
- return errorResponse(400, 'Invalid JSON body');
37
- }
38
- const { readable, writable } = new TransformStream();
39
- const writer = writable.getWriter();
40
- const encoder = new TextEncoder();
41
- (async () => {
42
- try {
43
- for await (const chunk of handleChat(body, clientIp)) {
44
- await writer.write(encoder.encode(`data: ${chunk}\n\n`));
45
- }
46
- await writer.write(encoder.encode('data: [DONE]\n\n'));
47
- }
48
- catch {
49
- await writer.write(encoder.encode(`data: ${JSON.stringify({ error: 'internal_error' })}\n\n`));
50
- }
51
- finally {
52
- await writer.close();
53
- }
54
- })();
55
- return new Response(readable, {
56
- headers: {
57
- 'Content-Type': 'text/event-stream',
58
- 'Cache-Control': 'no-cache',
59
- 'Access-Control-Allow-Origin': corsOrigin,
60
- },
6
+ const chat = createChatHandler(config);
7
+ const origins = originPolicy(config);
8
+ // Workers call fetch(request, env, ctx); ctx.waitUntil keeps lead webhooks
9
+ // running after a visitor disconnects.
10
+ return function handler(request, _env, ctx) {
11
+ return fetchChatResponse(chat, origins, request, {
12
+ clientIp: (req) => cloudflareClientIp(req, config.clientIp),
13
+ ...(ctx?.waitUntil ? { waitUntil: (promise) => ctx.waitUntil?.(promise) } : {}),
61
14
  });
62
15
  };
63
16
  }
@@ -1 +1 @@
1
- {"version":3,"file":"cloudflare.js","sourceRoot":"","sources":["../../src/adapters/cloudflare.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAGlD,MAAM,UAAU,yBAAyB,CAAC,MAA4B;IACpE,MAAM,EAAE,UAAU,EAAE,GAAG,iBAAiB,CAAC,MAAM,CAAC,CAAC;IACjD,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,IAAI,GAAG,CAAC;IAEtC,SAAS,aAAa,CAAC,MAAc,EAAE,KAAa;QAClD,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE;YAC7C,MAAM;YACN,OAAO,EAAE;gBACP,cAAc,EAAE,kBAAkB;gBAClC,6BAA6B,EAAE,UAAU;aAC1C;SACF,CAAC,CAAC;IACL,CAAC;IAED,OAAO,KAAK,UAAU,OAAO,CAAC,OAAgB;QAC5C,wBAAwB;QACxB,IAAI,OAAO,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;YACjC,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE;gBACxB,MAAM,EAAE,GAAG;gBACX,OAAO,EAAE;oBACP,6BAA6B,EAAE,UAAU;oBACzC,8BAA8B,EAAE,eAAe;oBAC/C,8BAA8B,EAAE,cAAc;oBAC9C,wBAAwB,EAAE,OAAO;iBAClC;aACF,CAAC,CAAC;QACL,CAAC;QAED,IAAI,OAAO,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;YAC9B,OAAO,aAAa,CAAC,GAAG,EAAE,oBAAoB,CAAC,CAAC;QAClD,CAAC;QAED,MAAM,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,IAAI,SAAS,CAAC;QACtE,IAAI,IAAa,CAAC;QAClB,IAAI,CAAC;YACH,IAAI,GAAG,MAAM,OAAO,CAAC,IAAI,EAAE,CAAC;QAC9B,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,aAAa,CAAC,GAAG,EAAE,mBAAmB,CAAC,CAAC;QACjD,CAAC;QAED,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,IAAI,eAAe,EAAE,CAAC;QACrD,MAAM,MAAM,GAAG,QAAQ,CAAC,SAAS,EAAE,CAAC;QACpC,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC;QAElC,CAAC,KAAK,IAAI,EAAE;YACV,IAAI,CAAC;gBACH,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,UAAU,CAAC,IAAI,EAAE,QAAQ,CAAC,EAAE,CAAC;oBACrD,MAAM,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,KAAK,MAAM,CAAC,CAAC,CAAC;gBAC3D,CAAC;gBACD,MAAM,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,kBAAkB,CAAC,CAAC,CAAC;YACzD,CAAC;YAAC,MAAM,CAAC;gBACP,MAAM,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;YACjG,CAAC;oBAAS,CAAC;gBACT,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;YACvB,CAAC;QACH,CAAC,CAAC,EAAE,CAAC;QAEL,OAAO,IAAI,QAAQ,CAAC,QAAQ,EAAE;YAC5B,OAAO,EAAE;gBACP,cAAc,EAAE,mBAAmB;gBACnC,eAAe,EAAE,UAAU;gBAC3B,6BAA6B,EAAE,UAAU;aAC1C;SACF,CAAC,CAAC;IACL,CAAC,CAAC;AACJ,CAAC"}
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,CAAC;YAC3D,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,mBAkCtF;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,mBAuEvF;AAaD;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,oBAAoB,IAIrD,KAAK,OAAO,EAAE,KAAK,QAAQ,mBA8B1C"}
@@ -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,49 +13,128 @@ 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
+ sendError(405, 'Method not allowed');
30
37
  return;
31
38
  }
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);
37
- try {
38
- for await (const chunk of handleChat(req.body, clientIp)) {
39
- res.write(`data: ${chunk}\n\n`);
39
+ if (!origins.allows(origin)) {
40
+ sendError(403, 'origin_not_allowed');
41
+ return;
42
+ }
43
+ // `app.use('/api/chat', chatcopsMiddleware(...))` sends /api/chat/session here too.
44
+ if (isSessionPath(req.path ?? '')) {
45
+ setHeaders(origins.headers(origin));
46
+ if (!chat.startSession) {
47
+ res.status(404).json(errorBody('invalid_request', { message: 'Session tokens are off. Set `sessionSecrets` to turn them on.' }));
48
+ return;
40
49
  }
41
- res.write('data: [DONE]\n\n');
50
+ const reply = await chat.startSession(req.body, origin, expressClientIp(req));
51
+ res.status(reply.status).json(reply.body);
52
+ return;
42
53
  }
43
- catch (err) {
44
- res.write(`data: ${JSON.stringify({ error: 'internal_error' })}\n\n`);
54
+ const clientIp = expressClientIp(req);
55
+ // Stop the reply when the visitor goes away before it's done.
56
+ const abort = new AbortController();
57
+ res.on('close', () => {
58
+ if (!res.writableFinished)
59
+ abort.abort();
60
+ });
61
+ const protocolHeader = req.headers?.[PROTOCOL_HEADER];
62
+ const authorization = req.headers?.authorization;
63
+ const reply = await chatReply(chat, req.body, clientIp, {
64
+ signal: abort.signal,
65
+ protocolHeader: typeof protocolHeader === 'string' ? protocolHeader : null,
66
+ authorization: typeof authorization === 'string' ? authorization : null,
67
+ origin,
68
+ });
69
+ if (reply.kind === 'json') {
70
+ setHeaders({ ...origins.headers(origin), ...reply.headers });
71
+ res.status(reply.status).json(reply.body);
72
+ return;
73
+ }
74
+ setHeaders({ ...reply.headers, ...origins.headers(origin) });
75
+ try {
76
+ for await (const frame of reply.frames) {
77
+ if (abort.signal.aborted)
78
+ break;
79
+ res.write(frame);
80
+ }
45
81
  }
46
82
  finally {
47
83
  res.end();
48
84
  }
49
85
  };
50
86
  }
87
+ function constantTimeEqual(a, b) {
88
+ if (a.length !== b.length) {
89
+ return false;
90
+ }
91
+ let mismatch = 0;
92
+ for (let i = 0; i < a.length; i++) {
93
+ mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
94
+ }
95
+ return mismatch === 0;
96
+ }
97
+ /**
98
+ * Analytics stats as JSON. With a storage that has an event sink, they cover
99
+ * every instance, for `?from=` and `?to=` (milliseconds) or the last 30 days.
100
+ */
51
101
  export function chatcopsAnalyticsHandler(config) {
52
- const { getAnalytics } = getOrCreateChatHandler(config);
53
- return (_req, res) => {
54
- res.json(getAnalytics());
102
+ const { analyticsStats } = getOrCreateChatHandler(config);
103
+ let warned = false;
104
+ return async (req, res) => {
105
+ if (config.analyticsToken) {
106
+ const auth = req.headers?.authorization;
107
+ const token = typeof auth === 'string' && auth.startsWith('Bearer ') ? auth.slice(7) : undefined;
108
+ if (!token || !constantTimeEqual(token, config.analyticsToken)) {
109
+ res.setHeader('WWW-Authenticate', 'Bearer');
110
+ res.status(401).json({ error: 'unauthorized' });
111
+ return;
112
+ }
113
+ }
114
+ else if (!warned) {
115
+ warned = true;
116
+ console.warn('[chatcops] The analytics endpoint is public and anyone can read your stats. Set `analyticsToken` in your ChatCopsServerConfig to require a bearer token.');
117
+ }
118
+ const from = timeParam(req.query?.from);
119
+ const to = timeParam(req.query?.to);
120
+ if (from === null || to === null) {
121
+ res.status(400).json({ error: 'invalid_range' });
122
+ return;
123
+ }
124
+ try {
125
+ res.json(await analyticsStats({ from, to }));
126
+ }
127
+ catch (err) {
128
+ console.error('[chatcops] Reading analytics failed:', err instanceof Error ? err.message : String(err));
129
+ res.status(500).json({ error: 'internal_error' });
130
+ }
55
131
  };
56
132
  }
133
+ /** A `from` or `to` query value: unset, a time in milliseconds, or `null` when it isn't one. */
134
+ function timeParam(value) {
135
+ if (value === undefined)
136
+ return undefined;
137
+ const time = typeof value === 'string' && value.trim() !== '' ? Number(value) : Number.NaN;
138
+ return Number.isFinite(time) ? time : null;
139
+ }
57
140
  //# 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,CAmD/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,65 +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
- const stream = new ReadableStream({
40
- async start(controller) {
41
- try {
42
- for await (const chunk of handleChat(body, clientIp)) {
43
- controller.enqueue(encoder.encode(`data: ${chunk}\n\n`));
44
- }
45
- controller.enqueue(encoder.encode('data: [DONE]\n\n'));
46
- }
47
- catch {
48
- controller.enqueue(encoder.encode(`data: ${JSON.stringify({ error: 'internal_error' })}\n\n`));
49
- }
50
- finally {
51
- controller.close();
52
- }
53
- },
54
- });
55
- return new Response(stream, {
56
- headers: {
57
- 'Content-Type': 'text/event-stream',
58
- 'Cache-Control': 'no-cache',
59
- 'Connection': 'keep-alive',
60
- 'Access-Control-Allow-Origin': corsOrigin,
61
- },
62
- });
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) });
63
10
  };
64
11
  }
65
12
  //# sourceMappingURL=vercel.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"vercel.js","sourceRoot":"","sources":["../../src/adapters/vercel.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAGlD,MAAM,UAAU,qBAAqB,CAAC,MAA4B;IAChE,MAAM,EAAE,UAAU,EAAE,GAAG,iBAAiB,CAAC,MAAM,CAAC,CAAC;IACjD,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,IAAI,GAAG,CAAC;IAEtC,SAAS,aAAa,CAAC,MAAc,EAAE,KAAa;QAClD,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE;YAC7C,MAAM;YACN,OAAO,EAAE;gBACP,cAAc,EAAE,kBAAkB;gBAClC,6BAA6B,EAAE,UAAU;aAC1C;SACF,CAAC,CAAC;IACL,CAAC;IAED,OAAO,KAAK,UAAU,OAAO,CAAC,GAAY;QACxC,wBAAwB;QACxB,IAAI,GAAG,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;YAC7B,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE;gBACxB,MAAM,EAAE,GAAG;gBACX,OAAO,EAAE;oBACP,6BAA6B,EAAE,UAAU;oBACzC,8BAA8B,EAAE,eAAe;oBAC/C,8BAA8B,EAAE,cAAc;oBAC9C,wBAAwB,EAAE,OAAO;iBAClC;aACF,CAAC,CAAC;QACL,CAAC;QAED,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;YAC1B,OAAO,aAAa,CAAC,GAAG,EAAE,oBAAoB,CAAC,CAAC;QAClD,CAAC;QAED,MAAM,QAAQ,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,SAAS,CAAC;QACxF,IAAI,IAAa,CAAC;QAClB,IAAI,CAAC;YACH,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;QAC1B,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,aAAa,CAAC,GAAG,EAAE,mBAAmB,CAAC,CAAC;QACjD,CAAC;QAED,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC;QAClC,MAAM,MAAM,GAAG,IAAI,cAAc,CAAC;YAChC,KAAK,CAAC,KAAK,CAAC,UAAU;gBACpB,IAAI,CAAC;oBACH,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,UAAU,CAAC,IAAI,EAAE,QAAQ,CAAC,EAAE,CAAC;wBACrD,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,KAAK,MAAM,CAAC,CAAC,CAAC;oBAC3D,CAAC;oBACD,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,kBAAkB,CAAC,CAAC,CAAC;gBACzD,CAAC;gBAAC,MAAM,CAAC;oBACP,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;gBACjG,CAAC;wBAAS,CAAC;oBACT,UAAU,CAAC,KAAK,EAAE,CAAC;gBACrB,CAAC;YACH,CAAC;SACF,CAAC,CAAC;QAEH,OAAO,IAAI,QAAQ,CAAC,MAAM,EAAE;YAC1B,OAAO,EAAE;gBACP,cAAc,EAAE,mBAAmB;gBACnC,eAAe,EAAE,UAAU;gBAC3B,YAAY,EAAE,YAAY;gBAC1B,6BAA6B,EAAE,UAAU;aAC1C;SACF,CAAC,CAAC;IACL,CAAC,CAAC;AACJ,CAAC"}
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,CAAC,EAAE,CAAC,CAAC;IACnH,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The visitor's IP, for per-IP rate limits and budgets. Only headers a
3
+ * platform sets itself are read, never ones a visitor can send. When the IP
4
+ * can't be told, per-IP limits are skipped for that request (with one
5
+ * warning) instead of every visitor sharing one bucket.
6
+ */
7
+ /** Reads the visitor's IP from a request, for hosts the adapter doesn't know. */
8
+ export type ClientIpResolver = (request: Request) => string | null | undefined;
9
+ export declare function cleanIp(value: string | null | undefined): string | undefined;
10
+ /** Vercel overwrites these headers, so a visitor can't set them there. */
11
+ export declare function onVercel(): boolean;
12
+ export declare function warnOnce(key: string, message: string): void;
13
+ /** For tests. */
14
+ export declare function resetClientIpWarnings(): void;
15
+ /** The fetch handler for Vercel and other Fetch API runtimes. */
16
+ export declare function fetchClientIp(request: Request, resolver: ClientIpResolver | undefined): string | undefined;
17
+ /** Cloudflare sets CF-Connecting-IP on every request that reaches a Worker. */
18
+ export declare function cloudflareClientIp(request: Request, resolver: ClientIpResolver | undefined): string | undefined;
19
+ interface ExpressLikeRequest {
20
+ ip?: string;
21
+ socket?: {
22
+ remoteAddress?: string;
23
+ };
24
+ headers?: Record<string, string | string[] | undefined>;
25
+ app?: {
26
+ get?(setting: string): unknown;
27
+ };
28
+ }
29
+ /**
30
+ * Express's `req.ip` follows its `trust proxy` setting. Behind a proxy with
31
+ * that setting off, every visitor gets the proxy's address and shares one
32
+ * rate limit and budget, so that case gets a warning.
33
+ */
34
+ export declare function expressClientIp(req: ExpressLikeRequest): string | undefined;
35
+ export {};
36
+ //# 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;;;;;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,GAAG,IAAI,CAI3D;AAED,iBAAiB;AACjB,wBAAgB,qBAAqB,IAAI,IAAI,CAE5C;AAED,iEAAiE;AACjE,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,gBAAgB,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAW1G;AAED,+EAA+E;AAC/E,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,gBAAgB,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAO/G;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,GAAG,MAAM,GAAG,SAAS,CAQ3E"}