@diegoaltoworks/chatter 0.47.0 → 0.48.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 (118) hide show
  1. package/README.md +3 -4
  2. package/dist/auth/apikeys.d.ts.map +1 -1
  3. package/dist/auth/secretStrength.d.ts +8 -0
  4. package/dist/auth/secretStrength.d.ts.map +1 -0
  5. package/dist/bin/create-apikey.js +22 -3
  6. package/dist/bin/wa-pair.js +59 -11
  7. package/dist/channels/gates.d.ts +9 -0
  8. package/dist/channels/gates.d.ts.map +1 -1
  9. package/dist/channels/index.d.ts +1 -0
  10. package/dist/channels/index.d.ts.map +1 -1
  11. package/dist/channels/index.js +252 -8
  12. package/dist/channels/index.mjs +250 -8
  13. package/dist/channels/pipeline.d.ts +106 -0
  14. package/dist/channels/pipeline.d.ts.map +1 -0
  15. package/dist/channels/senders.d.ts +2 -1
  16. package/dist/channels/senders.d.ts.map +1 -1
  17. package/dist/channels/whatsapp/channel.d.ts +4 -0
  18. package/dist/channels/whatsapp/channel.d.ts.map +1 -1
  19. package/dist/channels/whatsapp/crypto.d.ts +17 -3
  20. package/dist/channels/whatsapp/crypto.d.ts.map +1 -1
  21. package/dist/channels/whatsapp/inbound.d.ts +34 -4
  22. package/dist/channels/whatsapp/inbound.d.ts.map +1 -1
  23. package/dist/channels/whatsapp/index.d.ts +2 -1
  24. package/dist/channels/whatsapp/index.d.ts.map +1 -1
  25. package/dist/channels/whatsapp/index.js +395 -159
  26. package/dist/channels/whatsapp/index.mjs +394 -160
  27. package/dist/channels/whatsapp/router.d.ts +107 -0
  28. package/dist/channels/whatsapp/router.d.ts.map +1 -0
  29. package/dist/core/answer.d.ts +5 -2
  30. package/dist/core/answer.d.ts.map +1 -1
  31. package/dist/core/logger.d.ts +23 -0
  32. package/dist/core/logger.d.ts.map +1 -0
  33. package/dist/core/messages.d.ts +8 -0
  34. package/dist/core/messages.d.ts.map +1 -1
  35. package/dist/core/pipeline.d.ts +2 -0
  36. package/dist/core/pipeline.d.ts.map +1 -1
  37. package/dist/core/retrieval.d.ts +4 -0
  38. package/dist/core/retrieval.d.ts.map +1 -1
  39. package/dist/flows/engine.d.ts +3 -0
  40. package/dist/flows/engine.d.ts.map +1 -1
  41. package/dist/flows/index.d.ts +1 -0
  42. package/dist/flows/index.d.ts.map +1 -1
  43. package/dist/flows/index.js +91 -25
  44. package/dist/flows/index.mjs +91 -26
  45. package/dist/flows/intent.d.ts +2 -1
  46. package/dist/flows/intent.d.ts.map +1 -1
  47. package/dist/flows/loader.d.ts +5 -3
  48. package/dist/flows/loader.d.ts.map +1 -1
  49. package/dist/flows/types.d.ts +9 -0
  50. package/dist/flows/types.d.ts.map +1 -1
  51. package/dist/history/index.d.ts +17 -0
  52. package/dist/history/index.d.ts.map +1 -0
  53. package/dist/history/index.js +100 -0
  54. package/dist/history/index.mjs +73 -0
  55. package/dist/history/tursoStore.d.ts +26 -0
  56. package/dist/history/tursoStore.d.ts.map +1 -0
  57. package/dist/history/types.d.ts +18 -0
  58. package/dist/history/types.d.ts.map +1 -0
  59. package/dist/index.d.ts +5 -2
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +1146 -32444
  62. package/dist/index.mjs +1082 -32410
  63. package/dist/mcp-server/cost-tracker.d.ts +11 -6
  64. package/dist/mcp-server/cost-tracker.d.ts.map +1 -1
  65. package/dist/mcp-server/logger.d.ts +5 -2
  66. package/dist/mcp-server/logger.d.ts.map +1 -1
  67. package/dist/mcp-server/sdk.d.ts +23 -0
  68. package/dist/mcp-server/sdk.d.ts.map +1 -0
  69. package/dist/mcp-server/tool-input.d.ts +12 -0
  70. package/dist/mcp-server/tool-input.d.ts.map +1 -0
  71. package/dist/mcp-server/types.d.ts +10 -3
  72. package/dist/mcp-server/types.d.ts.map +1 -1
  73. package/dist/mcp-server.d.ts +1 -2
  74. package/dist/mcp-server.d.ts.map +1 -1
  75. package/dist/mcp-server.js +192 -111
  76. package/dist/mcp-server.mjs +179 -98
  77. package/dist/middleware/auth.d.ts.map +1 -1
  78. package/dist/middleware/bodyLimit.d.ts +11 -0
  79. package/dist/middleware/bodyLimit.d.ts.map +1 -0
  80. package/dist/middleware/clientKey.d.ts +15 -0
  81. package/dist/middleware/clientKey.d.ts.map +1 -0
  82. package/dist/middleware/cors.d.ts.map +1 -1
  83. package/dist/middleware/jwt.d.ts +0 -3
  84. package/dist/middleware/jwt.d.ts.map +1 -1
  85. package/dist/middleware/originMatch.d.ts +27 -0
  86. package/dist/middleware/originMatch.d.ts.map +1 -0
  87. package/dist/middleware/ratelimit.d.ts +0 -6
  88. package/dist/middleware/ratelimit.d.ts.map +1 -1
  89. package/dist/middleware/referrer.d.ts +5 -1
  90. package/dist/middleware/referrer.d.ts.map +1 -1
  91. package/dist/middleware/securityHeaders.d.ts +20 -0
  92. package/dist/middleware/securityHeaders.d.ts.map +1 -0
  93. package/dist/middleware/session.d.ts +2 -1
  94. package/dist/middleware/session.d.ts.map +1 -1
  95. package/dist/middleware/windowBucket.d.ts +19 -0
  96. package/dist/middleware/windowBucket.d.ts.map +1 -0
  97. package/dist/personas/index.js +20 -1
  98. package/dist/personas/index.mjs +20 -1
  99. package/dist/personas/resolver.d.ts +3 -0
  100. package/dist/personas/resolver.d.ts.map +1 -1
  101. package/dist/routes/demo.d.ts.map +1 -1
  102. package/dist/routes/openai.d.ts.map +1 -1
  103. package/dist/routes/private.d.ts.map +1 -1
  104. package/dist/routes/public.d.ts.map +1 -1
  105. package/dist/scheduler/compose.d.ts +2 -1
  106. package/dist/scheduler/compose.d.ts.map +1 -1
  107. package/dist/scheduler/index.js +29 -9
  108. package/dist/scheduler/index.mjs +29 -9
  109. package/dist/scheduler/scheduler.d.ts +3 -0
  110. package/dist/scheduler/scheduler.d.ts.map +1 -1
  111. package/dist/scheduler/tick.d.ts +2 -0
  112. package/dist/scheduler/tick.d.ts.map +1 -1
  113. package/dist/server.d.ts.map +1 -1
  114. package/dist/server.js +678 -528
  115. package/dist/server.mjs +678 -528
  116. package/dist/types.d.ts +87 -0
  117. package/dist/types.d.ts.map +1 -1
  118. package/package.json +22 -7
package/dist/server.js CHANGED
@@ -41,12 +41,25 @@ var import_openai = __toESM(require("openai"));
41
41
  // src/auth/apikeys.ts
42
42
  var import_node_crypto = require("node:crypto");
43
43
  var import_jose = require("jose");
44
+
45
+ // src/auth/secretStrength.ts
46
+ var MIN_SECRET_LENGTH = 16;
47
+ function assertStrongSecret(secret, label) {
48
+ if (!secret) {
49
+ throw new Error(`${label} is required and cannot be empty.`);
50
+ }
51
+ if (secret.length < MIN_SECRET_LENGTH) {
52
+ throw new Error(
53
+ `${label} is too weak: must be at least ${MIN_SECRET_LENGTH} characters (got ${secret.length}). Generate one with \`openssl rand -base64 32\`.`
54
+ );
55
+ }
56
+ }
57
+
58
+ // src/auth/apikeys.ts
44
59
  var ApiKeyManager = class {
45
60
  secretBytes;
46
61
  constructor(secret) {
47
- if (!secret) {
48
- throw new Error("API key secret is required");
49
- }
62
+ assertStrongSecret(secret, "API key secret (CHATTER_SECRET / config.auth.secret)");
50
63
  this.secretBytes = new TextEncoder().encode(secret);
51
64
  }
52
65
  /**
@@ -149,13 +162,256 @@ var ApiKeyManager = class {
149
162
  }
150
163
  };
151
164
 
165
+ // src/core/guardrails.ts
166
+ function detectLeakage(text) {
167
+ const flags = [
168
+ /system prompt/i,
169
+ /hidden instruction/i,
170
+ /here (are|is) (my|the) rules/i,
171
+ /BEGIN SYSTEM PROMPT/i,
172
+ /tool instructions/i
173
+ ];
174
+ return flags.some((rx) => rx.test(text));
175
+ }
176
+ function scrubOutput(text) {
177
+ return text.replace(/sk-[A-Za-z0-9]{15,}/g, "[REDACTED]").replace(/AIza[0-9A-Za-z_-]{30,}/g, "[REDACTED]");
178
+ }
179
+ function guardOutput(text) {
180
+ if (detectLeakage(text)) {
181
+ return "Sorry, I can't share internal instructions. How else can I help?";
182
+ }
183
+ return scrubOutput(text);
184
+ }
185
+
186
+ // src/core/llm.ts
187
+ var DEFAULT_MODEL = "gpt-4o";
188
+ async function completeOnce({
189
+ client,
190
+ system,
191
+ messages,
192
+ temperature = 0.2,
193
+ model = DEFAULT_MODEL
194
+ }) {
195
+ const res = await client.chat.completions.create({
196
+ model,
197
+ temperature,
198
+ messages: [{ role: "system", content: system }, ...messages]
199
+ });
200
+ const text = res.choices[0]?.message?.content ?? "";
201
+ return {
202
+ content: guardOutput(text),
203
+ usage: res.usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
204
+ };
205
+ }
206
+ async function* completeStream({
207
+ client,
208
+ system,
209
+ messages,
210
+ temperature = 0.2,
211
+ model = DEFAULT_MODEL
212
+ }) {
213
+ const stream5 = await client.chat.completions.create({
214
+ model,
215
+ temperature,
216
+ stream: true,
217
+ messages: [{ role: "system", content: system }, ...messages]
218
+ });
219
+ for await (const chunk2 of stream5) {
220
+ const delta = chunk2.choices[0]?.delta?.content ?? "";
221
+ if (delta) yield scrubOutput(delta);
222
+ }
223
+ }
224
+
225
+ // src/core/answer.ts
226
+ var noUsage = () => ({ prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 });
227
+ async function runAnswerFn(answerFn, { system, messages, mode, sender, conversationId }) {
228
+ const result = await answerFn({ system, messages, mode, sender, conversationId });
229
+ const raw = typeof result === "string" ? result : result?.content;
230
+ const usage = typeof result === "string" ? void 0 : result?.usage;
231
+ return {
232
+ content: guardOutput(typeof raw === "string" ? raw : ""),
233
+ usage: usage ?? noUsage()
234
+ };
235
+ }
236
+ async function answerOnce({
237
+ answerFn,
238
+ client,
239
+ system,
240
+ messages,
241
+ mode,
242
+ sender,
243
+ conversationId,
244
+ temperature,
245
+ model
246
+ }) {
247
+ if (answerFn) {
248
+ return runAnswerFn(answerFn, { system, messages, mode, sender, conversationId });
249
+ }
250
+ return completeOnce({ client, system, messages, temperature, model });
251
+ }
252
+ async function* answerStream({
253
+ answerFn,
254
+ client,
255
+ system,
256
+ messages,
257
+ mode,
258
+ sender,
259
+ conversationId,
260
+ temperature,
261
+ model
262
+ }) {
263
+ if (answerFn) {
264
+ const { content } = await runAnswerFn(answerFn, {
265
+ system,
266
+ messages,
267
+ mode,
268
+ sender,
269
+ conversationId
270
+ });
271
+ if (content) yield content;
272
+ return;
273
+ }
274
+ yield* completeStream({ client, system, messages, temperature, model });
275
+ }
276
+
277
+ // src/core/buckets.ts
278
+ function defaultBuckets(mode) {
279
+ return ["base", mode];
280
+ }
281
+ function isIdentified(sender) {
282
+ return typeof sender === "string" && sender.trim().length > 0;
283
+ }
284
+ function normalize(buckets) {
285
+ const out = [];
286
+ for (const raw of buckets) {
287
+ if (typeof raw !== "string") continue;
288
+ const bucket = raw.trim();
289
+ if (bucket && !out.includes(bucket)) out.push(bucket);
290
+ }
291
+ return out;
292
+ }
293
+ async function resolveBuckets({
294
+ mode,
295
+ sender,
296
+ bucketsFor
297
+ }) {
298
+ if (!bucketsFor) return void 0;
299
+ const identified = isIdentified(sender);
300
+ const requested = await bucketsFor(identified ? { mode, sender } : { mode });
301
+ if (!Array.isArray(requested)) return void 0;
302
+ const cleaned = normalize(requested);
303
+ if (identified) return cleaned;
304
+ const ceiling = new Set(defaultBuckets(mode));
305
+ return cleaned.filter((bucket) => ceiling.has(bucket));
306
+ }
307
+
308
+ // src/core/messages.ts
309
+ function isClientRole(role) {
310
+ return role === "user" || role === "assistant";
311
+ }
312
+ function lastUserMessage(messages) {
313
+ return [...messages].reverse().find((m) => m.role === "user");
314
+ }
315
+ function normalizeMessages(raw) {
316
+ if (!Array.isArray(raw) || raw.length === 0) return null;
317
+ const messages = [];
318
+ for (const entry of raw) {
319
+ if (!entry || typeof entry !== "object") return null;
320
+ const { role, content } = entry;
321
+ if (!isClientRole(role)) continue;
322
+ let text = null;
323
+ if (typeof content === "string") {
324
+ text = content;
325
+ } else if (Array.isArray(content)) {
326
+ text = content.filter(
327
+ (part) => !!part && typeof part === "object" && part.type === "text" && typeof part.text === "string"
328
+ ).map((part) => part.text).join("");
329
+ }
330
+ if (text === null) continue;
331
+ messages.push({ role, content: text });
332
+ }
333
+ return messages.length > 0 ? messages : null;
334
+ }
335
+ function normalizeChatBody(body) {
336
+ const source = body && typeof body === "object" ? body : {};
337
+ if (Array.isArray(source.messages)) {
338
+ if (source.messages.length === 0) {
339
+ return { ok: false, error: "messages array cannot be empty" };
340
+ }
341
+ const messages = normalizeMessages(source.messages);
342
+ if (!messages) {
343
+ return {
344
+ ok: false,
345
+ error: "messages must contain user/assistant messages with text content"
346
+ };
347
+ }
348
+ return { ok: true, messages };
349
+ }
350
+ const { message } = source;
351
+ if (message && typeof message !== "object") {
352
+ return { ok: true, messages: [{ role: "user", content: String(message) }] };
353
+ }
354
+ return { ok: false, error: "either 'message' or 'messages' required" };
355
+ }
356
+
357
+ // src/core/pipeline.ts
358
+ var MODE_SETTINGS = {
359
+ public: { topK: 6, label: "Context" },
360
+ private: { topK: 8, label: "Internal Context" }
361
+ };
362
+ async function prepareChat({
363
+ store,
364
+ prompts,
365
+ mode,
366
+ messages,
367
+ personaLayer,
368
+ channelHint,
369
+ buckets
370
+ }) {
371
+ const lastUserMsg = lastUserMessage(messages);
372
+ if (!lastUserMsg) {
373
+ throw new Error("no user message found in conversation");
374
+ }
375
+ const { topK, label } = MODE_SETTINGS[mode];
376
+ const ctx = await store.query(lastUserMsg.content, topK, buckets ?? defaultBuckets(mode));
377
+ const persona = personaLayer?.trim() || (mode === "private" ? prompts.privatePersona : prompts.publicPersona);
378
+ const hint = channelHint?.trim();
379
+ const system = [
380
+ prompts.baseSystemRules,
381
+ persona,
382
+ ...hint ? [hint] : [],
383
+ `${label}:
384
+ ${ctx.join("\n\n")}`
385
+ ].join("\n\n");
386
+ return { system, messages, context: ctx };
387
+ }
388
+
389
+ // src/core/logger.ts
390
+ var LEVEL_ORDER = { debug: 0, info: 1, warn: 2, error: 3 };
391
+ function createConsoleLogger(level = "info") {
392
+ const threshold = LEVEL_ORDER[level];
393
+ const write = (at) => (...args) => {
394
+ if (LEVEL_ORDER[at] < threshold) return;
395
+ console.error(`[${at}]`, ...args);
396
+ };
397
+ return {
398
+ debug: write("debug"),
399
+ info: write("info"),
400
+ warn: write("warn"),
401
+ error: write("error")
402
+ };
403
+ }
404
+ function resolveLogger(logger, level) {
405
+ return logger ?? createConsoleLogger(level);
406
+ }
407
+
152
408
  // src/channels/senders.ts
153
- function createSenderRegistry() {
409
+ function createSenderRegistry(logger = createConsoleLogger()) {
154
410
  const senders = /* @__PURE__ */ new Map();
155
411
  return {
156
412
  register(name, sender) {
157
413
  if (senders.has(name)) {
158
- console.warn(`Channel sender "${name}" is already registered; overwriting.`);
414
+ logger.warn(`Channel sender "${name}" is already registered; overwriting.`);
159
415
  }
160
416
  senders.set(name, sender);
161
417
  },
@@ -172,7 +428,7 @@ function createSenderRegistry() {
172
428
  await sender.sendText(chatId, text);
173
429
  return true;
174
430
  } catch (error) {
175
- console.warn(`Channel sender "${name}" sendText failed:`, error);
431
+ logger.warn(`Channel sender "${name}" sendText failed:`, error);
176
432
  return false;
177
433
  }
178
434
  },
@@ -183,7 +439,7 @@ function createSenderRegistry() {
183
439
  await sender.sendVoice(chatId, payload);
184
440
  return true;
185
441
  } catch (error) {
186
- console.warn(`Channel sender "${name}" sendVoice failed:`, error);
442
+ logger.warn(`Channel sender "${name}" sendVoice failed:`, error);
187
443
  return false;
188
444
  }
189
445
  },
@@ -194,7 +450,7 @@ function createSenderRegistry() {
194
450
  await sender.sendMedia(chatId, payload);
195
451
  return true;
196
452
  } catch (error) {
197
- console.warn(`Channel sender "${name}" sendMedia failed:`, error);
453
+ logger.warn(`Channel sender "${name}" sendMedia failed:`, error);
198
454
  return false;
199
455
  }
200
456
  }
@@ -293,6 +549,7 @@ var VectorStore = class _VectorStore {
293
549
  authToken: options.databaseAuthToken
294
550
  });
295
551
  this.knowledgeDir = options.knowledgeDir || "./config/knowledge";
552
+ this.logger = options.logger ?? createConsoleLogger();
296
553
  }
297
554
  client;
298
555
  /**
@@ -302,9 +559,10 @@ var VectorStore = class _VectorStore {
302
559
  */
303
560
  db;
304
561
  knowledgeDir;
562
+ logger;
305
563
  // On boot: ingest new chunks and embed only missing ones.
306
564
  async build() {
307
- console.log("\u{1F504} Building knowledge base...");
565
+ this.logger.info("\u{1F504} Building knowledge base...");
308
566
  await this.db.execute(`
309
567
  CREATE TABLE IF NOT EXISTS chunks (id TEXT PRIMARY KEY, bucket TEXT NOT NULL, source TEXT NOT NULL, text TEXT NOT NULL);
310
568
  `);
@@ -313,7 +571,7 @@ var VectorStore = class _VectorStore {
313
571
  `);
314
572
  await this.db.execute("CREATE INDEX IF NOT EXISTS idx_chunks_bucket ON chunks(bucket);");
315
573
  const docs = loadKnowledge(this.knowledgeDir);
316
- console.log(`\u{1F4DA} Loaded ${docs.length} knowledge documents`);
574
+ this.logger.info(`\u{1F4DA} Loaded ${docs.length} knowledge documents`);
317
575
  const rows = [];
318
576
  for (const d of docs) {
319
577
  for (const part of chunk(d.text)) {
@@ -321,21 +579,19 @@ var VectorStore = class _VectorStore {
321
579
  rows.push({ id, bucket: d.bucket, source: d.source, text: part });
322
580
  }
323
581
  }
324
- console.log(`\u{1F4E6} Created ${rows.length} chunks from knowledge documents`);
582
+ this.logger.info(`\u{1F4E6} Created ${rows.length} chunks from knowledge documents`);
325
583
  await this.cleanupStaleChunks(rows.map((r) => r.id));
326
- const tx = await this.db.transaction("write");
327
- try {
328
- for (const r of rows) {
329
- await tx.execute({
584
+ const UPSERT_BATCH = 500;
585
+ for (let i = 0; i < rows.length; i += UPSERT_BATCH) {
586
+ const batch = rows.slice(i, i + UPSERT_BATCH);
587
+ await this.db.batch(
588
+ batch.map((r) => ({
330
589
  sql: `INSERT INTO chunks(id,bucket,source,text) VALUES(?,?,?,?)
331
590
  ON CONFLICT(id) DO NOTHING`,
332
591
  args: [r.id, r.bucket, r.source, r.text]
333
- });
334
- }
335
- await tx.commit();
336
- } catch (e) {
337
- await tx.rollback();
338
- throw e;
592
+ })),
593
+ "write"
594
+ );
339
595
  }
340
596
  const ids = rows.map((r) => r.id);
341
597
  const missing = [];
@@ -350,32 +606,25 @@ var VectorStore = class _VectorStore {
350
606
  for (const row of res.rows) missing.push(String(row.id));
351
607
  }
352
608
  if (missing.length === 0) {
353
- console.log("\u2705 No new chunks to embed - knowledge base is up to date");
609
+ this.logger.info("\u2705 No new chunks to embed - knowledge base is up to date");
354
610
  return;
355
611
  }
356
- console.log(`\u{1F52E} Embedding ${missing.length} new/updated chunks with ${EMB_MODEL}...`);
612
+ this.logger.info(`\u{1F52E} Embedding ${missing.length} new/updated chunks with ${EMB_MODEL}...`);
357
613
  const textById = new Map(rows.map((r) => [r.id, r.text]));
358
614
  const BATCH = 96;
359
615
  for (let i = 0; i < missing.length; i += BATCH) {
360
616
  const batchIds = missing.slice(i, i + BATCH);
361
617
  const inputs = batchIds.map((id) => textById.get(id) || "");
362
618
  const emb = await this.client.embeddings.create({ model: EMB_MODEL, input: inputs });
363
- const tx2 = await this.db.transaction("write");
364
- try {
365
- emb.data.forEach((d, idx) => {
366
- const id = batchIds[idx];
367
- tx2.execute({
368
- sql: "INSERT INTO embeddings(id,model,embedding) VALUES(?,?,?)",
369
- args: [id, EMB_MODEL, JSON.stringify(d.embedding)]
370
- });
371
- });
372
- await tx2.commit();
373
- } catch (e) {
374
- await tx2.rollback();
375
- throw e;
376
- }
619
+ await this.db.batch(
620
+ emb.data.map((d, idx) => ({
621
+ sql: "INSERT INTO embeddings(id,model,embedding) VALUES(?,?,?)",
622
+ args: [batchIds[idx], EMB_MODEL, JSON.stringify(d.embedding)]
623
+ })),
624
+ "write"
625
+ );
377
626
  }
378
- console.log(`\u2705 Successfully embedded ${missing.length} new chunks`);
627
+ this.logger.info(`\u2705 Successfully embedded ${missing.length} new chunks`);
379
628
  }
380
629
  // Remove chunks from database that no longer exist in markdown files
381
630
  async cleanupStaleChunks(currentIds) {
@@ -386,7 +635,7 @@ var VectorStore = class _VectorStore {
386
635
  if (staleIds.length === 0) {
387
636
  return;
388
637
  }
389
- console.log(`\u{1F9F9} Cleaning up ${staleIds.length} stale chunks...`);
638
+ this.logger.info(`\u{1F9F9} Cleaning up ${staleIds.length} stale chunks...`);
390
639
  const BATCH_SIZE = 500;
391
640
  for (let i = 0; i < staleIds.length; i += BATCH_SIZE) {
392
641
  const batch = staleIds.slice(i, i + BATCH_SIZE);
@@ -396,7 +645,7 @@ var VectorStore = class _VectorStore {
396
645
  args: batch
397
646
  });
398
647
  }
399
- console.log(`\u2713 Cleaned up ${staleIds.length} stale chunks and their embeddings`);
648
+ this.logger.info(`\u2713 Cleaned up ${staleIds.length} stale chunks and their embeddings`);
400
649
  }
401
650
  static cosine(a, b) {
402
651
  let dot = 0;
@@ -446,278 +695,122 @@ var VectorStore = class _VectorStore {
446
695
  var STATIC_ADAPTERS = {
447
696
  bun: "hono/bun",
448
697
  node: "@hono/node-server/serve-static"
449
- };
450
- function detectRuntime() {
451
- return typeof globalThis.Bun === "undefined" ? "node" : "bun";
452
- }
453
- function wrapMissingAdapterError(runtime, cause) {
454
- const specifier = STATIC_ADAPTERS[runtime];
455
- return new Error(
456
- `Serving static files under ${runtime} requires '${specifier}', which could not be loaded. ` + (runtime === "node" ? "Install the optional peer dependency with `npm install @hono/node-server` (or bun/pnpm/yarn), or run Chatter headless (`features.headless: true`) if it should serve no static assets." : "Upgrade 'hono' to a version that ships the Bun adapter."),
457
- { cause }
458
- );
459
- }
460
- var cache = /* @__PURE__ */ new Map();
461
- function loadServeStatic(runtime = detectRuntime()) {
462
- const cached = cache.get(runtime);
463
- if (cached) return cached;
464
- const loading = (runtime === "bun" ? import("hono/bun") : import("@hono/node-server/serve-static")).then(
465
- (module2) => module2.serveStatic,
466
- (error) => {
467
- cache.delete(runtime);
468
- throw wrapMissingAdapterError(runtime, error);
469
- }
470
- );
471
- cache.set(runtime, loading);
472
- return loading;
473
- }
474
-
475
- // src/core/widgets.ts
476
- var import_node_fs3 = require("node:fs");
477
- var import_node_path3 = require("node:path");
478
- var import_node_url = require("node:url");
479
- var import_meta = {};
480
- function resolveStatic(configStaticDir) {
481
- if (configStaticDir && (0, import_node_fs3.existsSync)(configStaticDir)) {
482
- return { staticDir: configStaticDir };
483
- }
484
- try {
485
- if (import_meta.url) {
486
- const moduleDir = (0, import_node_path3.dirname)((0, import_node_url.fileURLToPath)(import_meta.url));
487
- const staticDir = (0, import_node_path3.join)(moduleDir, "static");
488
- if ((0, import_node_fs3.existsSync)(staticDir)) return { staticDir };
489
- }
490
- } catch {
491
- }
492
- try {
493
- if (typeof __dirname !== "undefined") {
494
- const staticDir = (0, import_node_path3.join)(__dirname, "static");
495
- if ((0, import_node_fs3.existsSync)(staticDir)) return { staticDir };
496
- }
497
- } catch {
498
- }
499
- return { staticDir: null };
500
- }
501
-
502
- // src/middleware/cors.ts
503
- var ALLOWED_HEADERS = "content-type, x-api-key, authorization";
504
- var ALLOWED_METHODS = "POST, GET, OPTIONS";
505
- var cors = (allowedOrigins) => async (c, next) => {
506
- const requestOrigin = c.req.header("origin");
507
- const allowAll = !allowedOrigins || allowedOrigins.length === 0 || allowedOrigins.includes("*");
508
- const originHeader = resolveOrigin(requestOrigin, allowedOrigins, allowAll);
509
- if (c.req.method === "OPTIONS") {
510
- return new Response(null, {
511
- status: 204,
512
- headers: {
513
- "access-control-allow-origin": originHeader,
514
- "access-control-allow-headers": ALLOWED_HEADERS,
515
- "access-control-allow-methods": ALLOWED_METHODS,
516
- ...allowAll ? {} : { vary: "Origin" }
517
- }
518
- });
519
- }
520
- c.header("access-control-allow-origin", originHeader);
521
- c.header("access-control-allow-headers", ALLOWED_HEADERS);
522
- c.header("access-control-allow-methods", ALLOWED_METHODS);
523
- if (!allowAll) {
524
- c.header("vary", "Origin");
525
- }
526
- await next();
527
- };
528
- function resolveOrigin(requestOrigin, allowedOrigins, allowAll) {
529
- if (allowAll) return "*";
530
- if (requestOrigin && allowedOrigins.includes(requestOrigin)) {
531
- return requestOrigin;
532
- }
533
- return allowedOrigins[0];
534
- }
535
-
536
- // src/routes/demo.ts
537
- var import_hono = require("hono");
538
- var import_streaming = require("hono/streaming");
539
-
540
- // src/core/guardrails.ts
541
- function detectLeakage(text) {
542
- const flags = [
543
- /system prompt/i,
544
- /hidden instruction/i,
545
- /here (are|is) (my|the) rules/i,
546
- /BEGIN SYSTEM PROMPT/i,
547
- /tool instructions/i
548
- ];
549
- return flags.some((rx) => rx.test(text));
550
- }
551
- function scrubOutput(text) {
552
- return text.replace(/sk-[A-Za-z0-9]{15,}/g, "[REDACTED]").replace(/AIza[0-9A-Za-z_-]{30,}/g, "[REDACTED]");
553
- }
554
- function guardOutput(text) {
555
- if (detectLeakage(text)) {
556
- return "Sorry, I can't share internal instructions. How else can I help?";
557
- }
558
- return scrubOutput(text);
559
- }
560
-
561
- // src/core/llm.ts
562
- var DEFAULT_MODEL = "gpt-4o";
563
- async function completeOnce({
564
- client,
565
- system,
566
- messages,
567
- temperature = 0.2,
568
- model = DEFAULT_MODEL
569
- }) {
570
- const res = await client.chat.completions.create({
571
- model,
572
- temperature,
573
- messages: [{ role: "system", content: system }, ...messages]
574
- });
575
- const text = res.choices[0]?.message?.content ?? "";
576
- return {
577
- content: guardOutput(text),
578
- usage: res.usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
579
- };
580
- }
581
- async function* completeStream({
582
- client,
583
- system,
584
- messages,
585
- temperature = 0.2,
586
- model = DEFAULT_MODEL
587
- }) {
588
- const stream5 = await client.chat.completions.create({
589
- model,
590
- temperature,
591
- stream: true,
592
- messages: [{ role: "system", content: system }, ...messages]
593
- });
594
- for await (const chunk2 of stream5) {
595
- const delta = chunk2.choices[0]?.delta?.content ?? "";
596
- if (delta) yield scrubOutput(delta);
597
- }
598
- }
599
-
600
- // src/core/answer.ts
601
- var noUsage = () => ({ prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 });
602
- async function runAnswerFn(answerFn, { system, messages, mode, sender }) {
603
- const result = await answerFn({ system, messages, mode, sender });
604
- const raw = typeof result === "string" ? result : result?.content;
605
- const usage = typeof result === "string" ? void 0 : result?.usage;
606
- return {
607
- content: guardOutput(typeof raw === "string" ? raw : ""),
608
- usage: usage ?? noUsage()
609
- };
610
- }
611
- async function answerOnce({
612
- answerFn,
613
- client,
614
- system,
615
- messages,
616
- mode,
617
- sender,
618
- temperature,
619
- model
620
- }) {
621
- if (answerFn) {
622
- return runAnswerFn(answerFn, { system, messages, mode, sender });
623
- }
624
- return completeOnce({ client, system, messages, temperature, model });
625
- }
626
- async function* answerStream({
627
- answerFn,
628
- client,
629
- system,
630
- messages,
631
- mode,
632
- sender,
633
- temperature,
634
- model
635
- }) {
636
- if (answerFn) {
637
- const { content } = await runAnswerFn(answerFn, { system, messages, mode, sender });
638
- if (content) yield content;
639
- return;
640
- }
641
- yield* completeStream({ client, system, messages, temperature, model });
642
- }
643
-
644
- // src/core/buckets.ts
645
- function defaultBuckets(mode) {
646
- return ["base", mode];
647
- }
648
- function isIdentified(sender) {
649
- return typeof sender === "string" && sender.trim().length > 0;
650
- }
651
- function normalize(buckets) {
652
- const out = [];
653
- for (const raw of buckets) {
654
- if (typeof raw !== "string") continue;
655
- const bucket = raw.trim();
656
- if (bucket && !out.includes(bucket)) out.push(bucket);
657
- }
658
- return out;
659
- }
660
- async function resolveBuckets({
661
- mode,
662
- sender,
663
- bucketsFor
664
- }) {
665
- if (!bucketsFor) return void 0;
666
- const identified = isIdentified(sender);
667
- const requested = await bucketsFor(identified ? { mode, sender } : { mode });
668
- if (!Array.isArray(requested)) return void 0;
669
- const cleaned = normalize(requested);
670
- if (identified) return cleaned;
671
- const ceiling = new Set(defaultBuckets(mode));
672
- return cleaned.filter((bucket) => ceiling.has(bucket));
698
+ };
699
+ function detectRuntime() {
700
+ return typeof globalThis.Bun === "undefined" ? "node" : "bun";
673
701
  }
674
-
675
- // src/core/messages.ts
676
- function isClientRole(role) {
677
- return role === "user" || role === "assistant";
702
+ function wrapMissingAdapterError(runtime, cause) {
703
+ const specifier = STATIC_ADAPTERS[runtime];
704
+ return new Error(
705
+ `Serving static files under ${runtime} requires '${specifier}', which could not be loaded. ` + (runtime === "node" ? "Install the optional peer dependency with `npm install @hono/node-server` (or bun/pnpm/yarn), or run Chatter headless (`features.headless: true`) if it should serve no static assets." : "Upgrade 'hono' to a version that ships the Bun adapter."),
706
+ { cause }
707
+ );
678
708
  }
679
- function normalizeMessages(raw) {
680
- if (!Array.isArray(raw) || raw.length === 0) return null;
681
- const messages = [];
682
- for (const entry of raw) {
683
- if (!entry || typeof entry !== "object") return null;
684
- const { role, content } = entry;
685
- if (!isClientRole(role)) continue;
686
- let text = null;
687
- if (typeof content === "string") {
688
- text = content;
689
- } else if (Array.isArray(content)) {
690
- text = content.filter(
691
- (part) => !!part && typeof part === "object" && part.type === "text" && typeof part.text === "string"
692
- ).map((part) => part.text).join("");
709
+ var cache = /* @__PURE__ */ new Map();
710
+ function loadServeStatic(runtime = detectRuntime()) {
711
+ const cached = cache.get(runtime);
712
+ if (cached) return cached;
713
+ const loading = (runtime === "bun" ? import("hono/bun") : import("@hono/node-server/serve-static")).then(
714
+ (module2) => module2.serveStatic,
715
+ (error) => {
716
+ cache.delete(runtime);
717
+ throw wrapMissingAdapterError(runtime, error);
693
718
  }
694
- if (text === null) continue;
695
- messages.push({ role, content: text });
696
- }
697
- return messages.length > 0 ? messages : null;
719
+ );
720
+ cache.set(runtime, loading);
721
+ return loading;
698
722
  }
699
- function normalizeChatBody(body) {
700
- const source = body && typeof body === "object" ? body : {};
701
- if (Array.isArray(source.messages)) {
702
- if (source.messages.length === 0) {
703
- return { ok: false, error: "messages array cannot be empty" };
723
+
724
+ // src/core/widgets.ts
725
+ var import_node_fs3 = require("node:fs");
726
+ var import_node_path3 = require("node:path");
727
+ var import_node_url = require("node:url");
728
+ var import_meta = {};
729
+ function resolveStatic(configStaticDir) {
730
+ if (configStaticDir && (0, import_node_fs3.existsSync)(configStaticDir)) {
731
+ return { staticDir: configStaticDir };
732
+ }
733
+ try {
734
+ if (import_meta.url) {
735
+ const moduleDir = (0, import_node_path3.dirname)((0, import_node_url.fileURLToPath)(import_meta.url));
736
+ const staticDir = (0, import_node_path3.join)(moduleDir, "static");
737
+ if ((0, import_node_fs3.existsSync)(staticDir)) return { staticDir };
704
738
  }
705
- const messages = normalizeMessages(source.messages);
706
- if (!messages) {
707
- return {
708
- ok: false,
709
- error: "messages must contain user/assistant messages with text content"
710
- };
739
+ } catch {
740
+ }
741
+ try {
742
+ if (typeof __dirname !== "undefined") {
743
+ const staticDir = (0, import_node_path3.join)(__dirname, "static");
744
+ if ((0, import_node_fs3.existsSync)(staticDir)) return { staticDir };
711
745
  }
712
- return { ok: true, messages };
746
+ } catch {
713
747
  }
714
- const { message } = source;
715
- if (message && typeof message !== "object") {
716
- return { ok: true, messages: [{ role: "user", content: String(message) }] };
748
+ return { staticDir: null };
749
+ }
750
+
751
+ // src/middleware/cors.ts
752
+ var ALLOWED_HEADERS = "content-type, x-api-key, authorization, x-conversation-id";
753
+ var ALLOWED_METHODS = "POST, GET, OPTIONS";
754
+ var EXPOSED_HEADERS = "x-conversation-id";
755
+ var cors = (allowedOrigins) => async (c, next) => {
756
+ const requestOrigin = c.req.header("origin");
757
+ const allowAll = !allowedOrigins || allowedOrigins.length === 0 || allowedOrigins.includes("*");
758
+ const originHeader = resolveOrigin(requestOrigin, allowedOrigins, allowAll);
759
+ if (c.req.method === "OPTIONS") {
760
+ return new Response(null, {
761
+ status: 204,
762
+ headers: {
763
+ "access-control-allow-origin": originHeader,
764
+ "access-control-allow-headers": ALLOWED_HEADERS,
765
+ "access-control-allow-methods": ALLOWED_METHODS,
766
+ "access-control-expose-headers": EXPOSED_HEADERS,
767
+ ...allowAll ? {} : { vary: "Origin" }
768
+ }
769
+ });
717
770
  }
718
- return { ok: false, error: "either 'message' or 'messages' required" };
771
+ c.header("access-control-allow-origin", originHeader);
772
+ c.header("access-control-allow-headers", ALLOWED_HEADERS);
773
+ c.header("access-control-allow-methods", ALLOWED_METHODS);
774
+ c.header("access-control-expose-headers", EXPOSED_HEADERS);
775
+ if (!allowAll) {
776
+ c.header("vary", "Origin");
777
+ }
778
+ await next();
779
+ };
780
+ function resolveOrigin(requestOrigin, allowedOrigins, allowAll) {
781
+ if (allowAll) return "*";
782
+ if (requestOrigin && allowedOrigins.includes(requestOrigin)) {
783
+ return requestOrigin;
784
+ }
785
+ return allowedOrigins[0];
786
+ }
787
+
788
+ // src/middleware/securityHeaders.ts
789
+ var DEFAULT_CSP = "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https:; frame-ancestors 'self'";
790
+ var DEFAULT_HSTS = "max-age=15552000; includeSubDomains";
791
+ function isHttps(c, trustProxy) {
792
+ if (new URL(c.req.url).protocol === "https:") return true;
793
+ if (!trustProxy) return false;
794
+ return c.req.header("x-forwarded-proto")?.toLowerCase() === "https";
795
+ }
796
+ function securityHeaders(config) {
797
+ const csp = config.server?.contentSecurityPolicy || DEFAULT_CSP;
798
+ const hsts = config.server?.strictTransportSecurity ?? DEFAULT_HSTS;
799
+ const trustProxy = config.rateLimit?.trustProxy ?? true;
800
+ return async (c, next) => {
801
+ await next();
802
+ c.header("content-security-policy", csp);
803
+ c.header("x-content-type-options", "nosniff");
804
+ if (hsts && (isHttps(c, trustProxy) || process.env.NODE_ENV === "production")) {
805
+ c.header("strict-transport-security", hsts);
806
+ }
807
+ };
719
808
  }
720
809
 
810
+ // src/routes/demo.ts
811
+ var import_hono = require("hono");
812
+ var import_streaming = require("hono/streaming");
813
+
721
814
  // src/core/session.ts
722
815
  var import_node_crypto2 = require("node:crypto");
723
816
  var sessions = /* @__PURE__ */ new Map();
@@ -769,26 +862,94 @@ function getActiveSessions() {
769
862
  return sessions.size;
770
863
  }
771
864
 
865
+ // src/middleware/bodyLimit.ts
866
+ var import_body_limit = require("hono/body-limit");
867
+ var DEFAULT_MAX_REQUEST_BYTES = 262144;
868
+ function chatBodyLimit(maxSize = DEFAULT_MAX_REQUEST_BYTES, toError = (message) => ({ error: message })) {
869
+ return (0, import_body_limit.bodyLimit)({
870
+ maxSize,
871
+ onError: (c) => c.json(toError("Request body too large"), 413)
872
+ });
873
+ }
874
+
875
+ // src/middleware/clientKey.ts
876
+ function clientRateLimitKey(c, trustProxy) {
877
+ if (!trustProxy) return "shared";
878
+ const header = c.req.header("x-forwarded-for");
879
+ if (!header) return "unknown-ip";
880
+ return header.split(",")[0].trim() || "unknown-ip";
881
+ }
882
+
883
+ // src/middleware/originMatch.ts
884
+ function matchesAllowedOrigin(origin, referer, allowedOrigins) {
885
+ return allowedOrigins.some((allowed) => {
886
+ if (origin === allowed) return true;
887
+ if (referer) {
888
+ if (referer === allowed) return true;
889
+ if (referer.startsWith(`${allowed}/`)) return true;
890
+ if (referer.startsWith(`${allowed}?`)) return true;
891
+ }
892
+ return false;
893
+ });
894
+ }
895
+ function isLocalhost(value) {
896
+ if (!value) return false;
897
+ try {
898
+ const url = new URL(value);
899
+ if (url.protocol !== "http:" && url.protocol !== "https:") return false;
900
+ return url.hostname === "localhost" || url.hostname === "127.0.0.1";
901
+ } catch {
902
+ return false;
903
+ }
904
+ }
905
+ function matchesAllowedOriginOrLocalhost(origin, referer, allowedOrigins) {
906
+ if (isLocalhost(origin) || isLocalhost(referer)) return true;
907
+ return matchesAllowedOrigin(origin, referer, allowedOrigins);
908
+ }
909
+
910
+ // src/middleware/windowBucket.ts
911
+ function createWindowBucketLimiter(windowMs, now = Date.now) {
912
+ const buckets = /* @__PURE__ */ new Map();
913
+ let lastSweep = now();
914
+ function sweep(current) {
915
+ if (current - lastSweep < windowMs) return;
916
+ lastSweep = current;
917
+ for (const [key, bucket] of buckets) {
918
+ if (current - bucket.windowStart >= windowMs) buckets.delete(key);
919
+ }
920
+ }
921
+ return {
922
+ /** Records one hit for `key` and reports whether it's still within `limit`. */
923
+ hit(key, limit) {
924
+ const current = now();
925
+ sweep(current);
926
+ const bucket = buckets.get(key);
927
+ if (!bucket || current - bucket.windowStart >= windowMs) {
928
+ buckets.set(key, { count: 1, windowStart: current });
929
+ return 1 <= limit;
930
+ }
931
+ bucket.count += 1;
932
+ return bucket.count <= limit;
933
+ },
934
+ /** Current map size — for tests asserting eviction actually happened. */
935
+ size() {
936
+ return buckets.size;
937
+ }
938
+ };
939
+ }
940
+
772
941
  // src/routes/demo.ts
773
- function checkDemoOrigin(allowedOrigins) {
942
+ function checkDemoOrigin(allowedOrigins, allowLocalhost) {
774
943
  const allowAll = allowedOrigins.length === 0 || allowedOrigins.includes("*");
944
+ const matches = allowLocalhost ? matchesAllowedOriginOrLocalhost : matchesAllowedOrigin;
775
945
  return async (c, next) => {
776
- const origin = c.req.header("origin");
777
- const referer = c.req.header("referer");
778
- const host = c.req.header("host") || "";
779
- if (host.startsWith("localhost:") || host.startsWith("127.0.0.1:")) {
780
- return next();
781
- }
782
946
  if (allowAll) {
783
947
  return next();
784
948
  }
785
- if (origin) {
786
- const allowed = allowedOrigins.some((allowed2) => origin.startsWith(allowed2));
787
- if (allowed) return next();
788
- }
789
- if (referer) {
790
- const allowed = allowedOrigins.some((allowed2) => referer.startsWith(allowed2));
791
- if (allowed) return next();
949
+ const origin = c.req.header("origin");
950
+ const referer = c.req.header("referer");
951
+ if (matches(origin, referer, allowedOrigins)) {
952
+ return next();
792
953
  }
793
954
  return c.json(
794
955
  {
@@ -798,22 +959,12 @@ function checkDemoOrigin(allowedOrigins) {
798
959
  );
799
960
  };
800
961
  }
801
- function limitSessionCreation() {
802
- const buckets = /* @__PURE__ */ new Map();
803
- const WINDOW_MS = 36e5;
962
+ function limitSessionCreation(trustProxy) {
963
+ const limiter = createWindowBucketLimiter(36e5);
804
964
  const LIMIT = 10;
805
965
  return async (c, next) => {
806
- const ip = c.req.header("x-forwarded-for")?.split(",")[0].trim() || "unknown-ip";
807
- const key = `session:${ip}`;
808
- const now = Date.now();
809
- const b = buckets.get(key) ?? { count: 0, windowStart: now };
810
- if (now - b.windowStart >= WINDOW_MS) {
811
- b.count = 0;
812
- b.windowStart = now;
813
- }
814
- b.count += 1;
815
- buckets.set(key, b);
816
- if (b.count > LIMIT) {
966
+ const key = `session:${clientRateLimitKey(c, trustProxy)}`;
967
+ if (!limiter.hit(key, LIMIT)) {
817
968
  return c.json(
818
969
  {
819
970
  error: "Session key limit exceeded (10 per hour). Please wait or use your own API key."
@@ -824,22 +975,12 @@ function limitSessionCreation() {
824
975
  await next();
825
976
  };
826
977
  }
827
- function limitDemo() {
828
- const buckets = /* @__PURE__ */ new Map();
829
- const WINDOW_MS = 6e4;
978
+ function limitDemo(trustProxy) {
979
+ const limiter = createWindowBucketLimiter(6e4);
830
980
  const LIMIT = 10;
831
981
  return async (c, next) => {
832
- const ip = c.req.header("x-forwarded-for")?.split(",")[0].trim() || "unknown-ip";
833
- const key = `demo:${ip}`;
834
- const now = Date.now();
835
- const b = buckets.get(key) ?? { count: 0, windowStart: now };
836
- if (now - b.windowStart >= WINDOW_MS) {
837
- b.count = 0;
838
- b.windowStart = now;
839
- }
840
- b.count += 1;
841
- buckets.set(key, b);
842
- if (b.count > LIMIT) {
982
+ const key = `demo:${clientRateLimitKey(c, trustProxy)}`;
983
+ if (!limiter.hit(key, LIMIT)) {
843
984
  return c.json(
844
985
  {
845
986
  error: "Demo rate limit exceeded (10 requests/minute). Please wait or use your own API key."
@@ -851,82 +992,91 @@ function limitDemo() {
851
992
  };
852
993
  }
853
994
  function demoRoutes(deps) {
854
- const { client, store, config, prompts } = deps;
995
+ const { client, store, config, prompts, logger } = deps;
855
996
  const app = new import_hono.Hono();
856
997
  const demoOrigins = config.server?.allowedOrigins ?? [];
857
- app.get("/api/demo/session", checkDemoOrigin(demoOrigins), limitSessionCreation(), (c) => {
858
- const session = createSession({
859
- maxRequests: 20,
860
- ttl: 3600
861
- // 1 hour
862
- });
863
- console.log(
864
- `[Demo] Created session: ${session.key.slice(0, 20)}... (${session.maxRequests} requests, ${3600}s TTL)`
865
- );
866
- return c.json({
867
- key: session.key,
868
- expiresIn: 3600,
869
- maxRequests: session.maxRequests,
870
- message: "Use this temporary key in the x-api-key header"
871
- });
872
- });
998
+ const allowLocalhostDemo = config.server?.allowLocalhostDemo ?? false;
999
+ const trustProxy = config.rateLimit?.trustProxy ?? true;
1000
+ app.get(
1001
+ "/api/demo/session",
1002
+ checkDemoOrigin(demoOrigins, allowLocalhostDemo),
1003
+ limitSessionCreation(trustProxy),
1004
+ (c) => {
1005
+ const session = createSession({
1006
+ maxRequests: 20,
1007
+ ttl: 3600
1008
+ // 1 hour
1009
+ });
1010
+ logger.debug(
1011
+ `[Demo] Created session: ${session.key.slice(0, 20)}... (${session.maxRequests} requests, ${3600}s TTL)`
1012
+ );
1013
+ return c.json({
1014
+ key: session.key,
1015
+ expiresIn: 3600,
1016
+ maxRequests: session.maxRequests,
1017
+ message: "Use this temporary key in the x-api-key header"
1018
+ });
1019
+ }
1020
+ );
873
1021
  app.get("/api/demo/stats", (c) => {
874
1022
  return c.json({
875
1023
  activeSessions: getActiveSessions()
876
1024
  });
877
1025
  });
878
- app.post("/api/demo/chat", limitDemo(), async (c) => {
879
- try {
880
- const body = await c.req.json().catch(() => ({}));
881
- const normalized = normalizeChatBody(body);
882
- if (!normalized.ok) {
883
- return c.json({ error: normalized.error }, 400);
884
- }
885
- const { messages } = normalized;
886
- const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
887
- if (!lastUserMsg) {
888
- return c.json({ error: "no user message found in conversation" }, 400);
889
- }
890
- const buckets = await resolveBuckets({ mode: "public", bucketsFor: config.bucketsFor }) ?? defaultBuckets("public");
891
- const ctx = await store.query(lastUserMsg.content, 6, buckets);
892
- const system = [
893
- prompts.baseSystemRules,
894
- prompts.publicPersona,
895
- `Context:
896
- ${ctx.join("\n\n")}`
897
- ].join("\n\n");
898
- const url = new URL(c.req.url);
899
- const wantsStream3 = url.searchParams.get("stream") === "1";
900
- if (wantsStream3) {
901
- return (0, import_streaming.stream)(c, async (s) => {
902
- for await (const delta of answerStream({
903
- answerFn: config.answerFn,
904
- client,
905
- system,
906
- messages,
907
- mode: "public"
908
- })) {
909
- await s.write(`data: ${JSON.stringify({ delta })}
1026
+ app.post(
1027
+ "/api/demo/chat",
1028
+ chatBodyLimit(config.server?.maxRequestBytes),
1029
+ // Reject oversized bodies first
1030
+ limitDemo(trustProxy),
1031
+ async (c) => {
1032
+ try {
1033
+ const body = await c.req.json().catch(() => ({}));
1034
+ const normalized = normalizeChatBody(body);
1035
+ if (!normalized.ok) {
1036
+ return c.json({ error: normalized.error }, 400);
1037
+ }
1038
+ const { messages } = normalized;
1039
+ if (!lastUserMessage(messages)) {
1040
+ return c.json({ error: "no user message found in conversation" }, 400);
1041
+ }
1042
+ const buckets = await resolveBuckets({ mode: "public", bucketsFor: config.bucketsFor });
1043
+ const { system } = await prepareChat({ store, prompts, mode: "public", messages, buckets });
1044
+ const model = config.openai.model || DEFAULT_MODEL;
1045
+ const url = new URL(c.req.url);
1046
+ const wantsStream3 = url.searchParams.get("stream") === "1";
1047
+ if (wantsStream3) {
1048
+ return (0, import_streaming.stream)(c, async (s) => {
1049
+ for await (const delta of answerStream({
1050
+ answerFn: config.answerFn,
1051
+ client,
1052
+ system,
1053
+ messages,
1054
+ mode: "public",
1055
+ model
1056
+ })) {
1057
+ await s.write(`data: ${JSON.stringify({ delta })}
910
1058
 
911
1059
  `);
912
- }
913
- await s.write("event: end\ndata: {}\n\n");
1060
+ }
1061
+ await s.write("event: end\ndata: {}\n\n");
1062
+ });
1063
+ }
1064
+ const out = await answerOnce({
1065
+ answerFn: config.answerFn,
1066
+ client,
1067
+ system,
1068
+ messages,
1069
+ mode: "public",
1070
+ model
914
1071
  });
1072
+ return c.json({ reply: out.content });
1073
+ } catch (error) {
1074
+ const err = error;
1075
+ logger.error("Demo chat error:", err);
1076
+ return c.json({ error: err.message || "Internal error" }, 500);
915
1077
  }
916
- const out = await answerOnce({
917
- answerFn: config.answerFn,
918
- client,
919
- system,
920
- messages,
921
- mode: "public"
922
- });
923
- return c.json({ reply: out.content });
924
- } catch (error) {
925
- const err = error;
926
- console.error("Demo chat error:", err);
927
- return c.json({ error: err.message || "Internal error" }, 500);
928
1078
  }
929
- });
1079
+ );
930
1080
  return app;
931
1081
  }
932
1082
 
@@ -934,38 +1084,6 @@ ${ctx.join("\n\n")}`
934
1084
  var import_hono2 = require("hono");
935
1085
  var import_streaming2 = require("hono/streaming");
936
1086
 
937
- // src/core/pipeline.ts
938
- var MODE_SETTINGS = {
939
- public: { topK: 6, label: "Context" },
940
- private: { topK: 8, label: "Internal Context" }
941
- };
942
- async function prepareChat({
943
- store,
944
- prompts,
945
- mode,
946
- messages,
947
- personaLayer,
948
- channelHint,
949
- buckets
950
- }) {
951
- const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
952
- if (!lastUserMsg) {
953
- throw new Error("no user message found in conversation");
954
- }
955
- const { topK, label } = MODE_SETTINGS[mode];
956
- const ctx = await store.query(lastUserMsg.content, topK, buckets ?? defaultBuckets(mode));
957
- const persona = personaLayer?.trim() || (mode === "private" ? prompts.privatePersona : prompts.publicPersona);
958
- const hint = channelHint?.trim();
959
- const system = [
960
- prompts.baseSystemRules,
961
- persona,
962
- ...hint ? [hint] : [],
963
- `${label}:
964
- ${ctx.join("\n\n")}`
965
- ].join("\n\n");
966
- return { system, messages };
967
- }
968
-
969
1087
  // src/middleware/jwt.ts
970
1088
  var import_jose2 = require("jose");
971
1089
  function jwtSubject(c) {
@@ -992,7 +1110,7 @@ function createJWTMiddleware(config) {
992
1110
  }
993
1111
  throw new Error("JWT configuration missing (JWKS URL or PUBLIC KEY PEM).");
994
1112
  }
995
- return async function requirePrivateJWT2(c, next) {
1113
+ return async function requirePrivateJWT(c, next) {
996
1114
  const hdr = c.req.header("authorization") ?? "";
997
1115
  if (!hdr.startsWith("Bearer ")) return c.json({ error: "Unauthorized" }, 401);
998
1116
  const token = hdr.slice("Bearer ".length);
@@ -1005,41 +1123,24 @@ function createJWTMiddleware(config) {
1005
1123
  }
1006
1124
  };
1007
1125
  }
1008
- var requirePrivateJWT = createJWTMiddleware({});
1009
1126
 
1010
1127
  // src/middleware/ratelimit.ts
1011
- var DEMO_KEYS = ["chatter-api-key-here"];
1012
- function getKeyFromIP(c) {
1013
- const h = c.req.header("x-forwarded-for");
1014
- if (h) return h.split(",")[0].trim();
1015
- return "unknown-ip";
1016
- }
1017
1128
  function createRateLimiter(config) {
1018
- const buckets = /* @__PURE__ */ new Map();
1019
- const WINDOW_MS = 6e4;
1129
+ const limiter = createWindowBucketLimiter(6e4);
1020
1130
  const publicLimit = config.rateLimit?.public || 60;
1021
1131
  const privateLimit = config.rateLimit?.private || 120;
1022
- function allow(key, limit) {
1023
- const now = Date.now();
1024
- const b = buckets.get(key) ?? { count: 0, windowStart: now };
1025
- if (now - b.windowStart >= WINDOW_MS) {
1026
- b.count = 0;
1027
- b.windowStart = now;
1028
- }
1029
- b.count += 1;
1030
- buckets.set(key, b);
1031
- return b.count <= limit;
1032
- }
1132
+ const trustProxy = config.rateLimit?.trustProxy ?? true;
1133
+ const demoApiKeys = config.rateLimit?.demoApiKeys ?? [];
1033
1134
  return {
1034
1135
  // Public: key is IP only
1035
1136
  // Demo keys get 10x stricter rate limits
1036
1137
  limitPublic() {
1037
1138
  return async (c, next) => {
1038
1139
  const apiKey = c.req.header("x-api-key");
1039
- const isDemoKey = apiKey ? DEMO_KEYS.includes(apiKey) : false;
1040
- const key = `pub:${getKeyFromIP(c)}`;
1140
+ const isDemoKey = apiKey ? demoApiKeys.includes(apiKey) : false;
1141
+ const key = `pub:${clientRateLimitKey(c, trustProxy)}`;
1041
1142
  const limit = isDemoKey ? Math.floor(publicLimit / 10) : publicLimit;
1042
- if (!allow(key, limit)) {
1143
+ if (!limiter.hit(key, limit)) {
1043
1144
  return c.json(
1044
1145
  {
1045
1146
  error: isDemoKey ? "Demo rate limit exceeded. Please use your own API key for higher limits." : "Rate limit exceeded"
@@ -1055,28 +1156,47 @@ function createRateLimiter(config) {
1055
1156
  return async (c, next) => {
1056
1157
  const sub = c.jwtSub;
1057
1158
  const key = `private:${sub ?? "no-sub"}`;
1058
- if (!allow(key, privateLimit)) return c.json({ error: "Rate limit" }, 429);
1159
+ if (!limiter.hit(key, privateLimit)) return c.json({ error: "Rate limit" }, 429);
1059
1160
  await next();
1060
1161
  };
1061
1162
  }
1062
1163
  };
1063
1164
  }
1064
- var defaultLimiter = createRateLimiter({});
1065
- var limitPublic = defaultLimiter.limitPublic;
1066
- var limitPrivate = defaultLimiter.limitPrivate;
1067
1165
 
1068
1166
  // src/routes/openai.ts
1167
+ function apiKeySubject(c) {
1168
+ const sub = c.apiKeySub;
1169
+ return sub && sub.trim().length > 0 ? sub : void 0;
1170
+ }
1171
+ function payloadSub(payload) {
1172
+ const sub = payload?.sub;
1173
+ return typeof sub === "string" ? sub : void 0;
1174
+ }
1175
+ var CONVERSATION_ID_PATTERN = /^[A-Za-z0-9_.:-]{1,200}$/;
1176
+ function sanitizeConversationId(value) {
1177
+ const trimmed = value?.trim();
1178
+ return trimmed && CONVERSATION_ID_PATTERN.test(trimmed) ? trimmed : void 0;
1179
+ }
1180
+ function clientConversationId(c, body) {
1181
+ const header = sanitizeConversationId(c.req.header("x-conversation-id"));
1182
+ if (header) return header;
1183
+ const fromBody = body?.conversation_id;
1184
+ return sanitizeConversationId(typeof fromBody === "string" ? fromBody : void 0);
1185
+ }
1069
1186
  function errorJson(c, status, message) {
1070
1187
  return c.json(
1071
1188
  { error: { message, type: status === 401 ? "authentication_error" : "invalid_request_error" } },
1072
1189
  status
1073
1190
  );
1074
1191
  }
1192
+ function oversizedBodyError(message) {
1193
+ return { error: { message, type: "invalid_request_error" } };
1194
+ }
1075
1195
  function openaiRoutes(deps) {
1076
1196
  const { client, store, config, prompts, apiKeyManager } = deps;
1077
1197
  const app = new import_hono2.Hono();
1078
- const { limitPublic: limitPublic2, limitPrivate: limitPrivate2 } = createRateLimiter(config);
1079
- const requirePrivateJWT2 = createJWTMiddleware(config);
1198
+ const { limitPublic, limitPrivate } = createRateLimiter(config);
1199
+ const requirePrivateJWT = createJWTMiddleware(config);
1080
1200
  const enablePublic = config.features?.enablePublicChat !== false;
1081
1201
  const enablePrivate = config.features?.enablePrivateChat !== false;
1082
1202
  const serverModel = config.openai.model || DEFAULT_MODEL;
@@ -1091,7 +1211,10 @@ function openaiRoutes(deps) {
1091
1211
  }
1092
1212
  try {
1093
1213
  const result = await apiKeyManager.verify(key);
1094
- if (result.valid) return await next();
1214
+ if (result.valid) {
1215
+ c.apiKeySub = payloadSub(result.payload);
1216
+ return await next();
1217
+ }
1095
1218
  } catch {
1096
1219
  }
1097
1220
  return errorJson(c, 401, "Invalid API key");
@@ -1118,6 +1241,9 @@ function openaiRoutes(deps) {
1118
1241
  } catch {
1119
1242
  return errorJson(c, 400, "Conversation must contain at least one user message");
1120
1243
  }
1244
+ const sender = jwtSubject(c) ?? apiKeySubject(c);
1245
+ const conversationId = clientConversationId(c, body) ?? crypto.randomUUID();
1246
+ c.header("x-conversation-id", conversationId);
1121
1247
  const id = `chatcmpl-${crypto.randomUUID()}`;
1122
1248
  const created = Math.floor(Date.now() / 1e3);
1123
1249
  if (wantsStream3 === true) {
@@ -1140,6 +1266,8 @@ function openaiRoutes(deps) {
1140
1266
  system,
1141
1267
  messages,
1142
1268
  mode,
1269
+ sender,
1270
+ conversationId,
1143
1271
  temperature,
1144
1272
  model: serverModel
1145
1273
  })) {
@@ -1155,6 +1283,8 @@ function openaiRoutes(deps) {
1155
1283
  system,
1156
1284
  messages,
1157
1285
  mode,
1286
+ sender,
1287
+ conversationId,
1158
1288
  temperature,
1159
1289
  model: serverModel
1160
1290
  });
@@ -1170,13 +1300,15 @@ function openaiRoutes(deps) {
1170
1300
  });
1171
1301
  };
1172
1302
  if (enablePublic) {
1303
+ app.use("/v1/*", chatBodyLimit(config.server?.maxRequestBytes, oversizedBodyError));
1173
1304
  app.use("/v1/*", requireApiKey);
1174
- app.use("/v1/*", limitPublic2());
1305
+ app.use("/v1/*", limitPublic());
1175
1306
  app.post("/v1/chat/completions", handleChatCompletions("public"));
1176
1307
  }
1177
1308
  if (enablePrivate) {
1178
- app.use("/api/private/v1/*", requirePrivateJWT2);
1179
- app.use("/api/private/v1/*", limitPrivate2());
1309
+ app.use("/api/private/v1/*", chatBodyLimit(config.server?.maxRequestBytes, oversizedBodyError));
1310
+ app.use("/api/private/v1/*", requirePrivateJWT);
1311
+ app.use("/api/private/v1/*", limitPrivate());
1180
1312
  app.post("/api/private/v1/chat/completions", handleChatCompletions("private"));
1181
1313
  }
1182
1314
  return app;
@@ -1194,10 +1326,11 @@ function wantsStream(c) {
1194
1326
  function privateRoutes(deps) {
1195
1327
  const { client, store, config, prompts } = deps;
1196
1328
  const app = new import_hono3.Hono();
1197
- const requirePrivateJWT2 = createJWTMiddleware(config);
1198
- const { limitPrivate: limitPrivate2 } = createRateLimiter(config);
1199
- app.use("/api/private/*", requirePrivateJWT2);
1200
- app.use("/api/private/*", limitPrivate2());
1329
+ const requirePrivateJWT = createJWTMiddleware(config);
1330
+ const { limitPrivate } = createRateLimiter(config);
1331
+ app.use("/api/private/*", chatBodyLimit(config.server?.maxRequestBytes));
1332
+ app.use("/api/private/*", requirePrivateJWT);
1333
+ app.use("/api/private/*", limitPrivate());
1201
1334
  app.post("/api/private/chat", async (c) => {
1202
1335
  const body = await c.req.json().catch(() => ({}));
1203
1336
  const normalized = normalizeChatBody(body);
@@ -1205,9 +1338,10 @@ function privateRoutes(deps) {
1205
1338
  return c.json({ error: normalized.error }, 400);
1206
1339
  }
1207
1340
  const { messages } = normalized;
1341
+ const sender = jwtSubject(c);
1208
1342
  const buckets = await resolveBuckets({
1209
1343
  mode: "private",
1210
- sender: jwtSubject(c),
1344
+ sender,
1211
1345
  bucketsFor: config.bucketsFor
1212
1346
  });
1213
1347
  let system;
@@ -1226,6 +1360,7 @@ function privateRoutes(deps) {
1226
1360
  system,
1227
1361
  messages,
1228
1362
  mode: "private",
1363
+ sender,
1229
1364
  model
1230
1365
  })) {
1231
1366
  await s.write(`data: ${JSON.stringify({ delta })}
@@ -1235,7 +1370,15 @@ function privateRoutes(deps) {
1235
1370
  await s.write("event: end\ndata: {}\n\n");
1236
1371
  });
1237
1372
  }
1238
- const out = await answerOnce({ answerFn, client, system, messages, mode: "private", model });
1373
+ const out = await answerOnce({
1374
+ answerFn,
1375
+ client,
1376
+ system,
1377
+ messages,
1378
+ mode: "private",
1379
+ sender,
1380
+ model
1381
+ });
1239
1382
  return c.json({ reply: out.content });
1240
1383
  });
1241
1384
  return app;
@@ -1247,64 +1390,55 @@ var import_streaming4 = require("hono/streaming");
1247
1390
 
1248
1391
  // src/middleware/auth.ts
1249
1392
  function createAuthMiddleware(deps) {
1250
- const { apiKeyManager } = deps;
1393
+ const { apiKeyManager, logger } = deps;
1251
1394
  return async function requirePublicKey(c, next) {
1252
1395
  const hdr = c.req.header("x-api-key");
1253
1396
  if (hdr?.startsWith("session_")) {
1254
- console.log(`[Auth] Session key allowed: ${hdr.slice(0, 20)}...`);
1397
+ logger.debug(`[Auth] Session key allowed: ${hdr.slice(0, 20)}...`);
1255
1398
  await next();
1256
1399
  return;
1257
1400
  }
1258
1401
  if (!apiKeyManager) {
1259
- console.log("[Auth] No API key manager configured");
1402
+ logger.debug("[Auth] No API key manager configured");
1260
1403
  return c.json({ error: "Unauthorized - API key authentication not configured" }, 401);
1261
1404
  }
1262
1405
  if (!hdr) {
1263
- console.log("[Auth] No API key provided");
1406
+ logger.debug("[Auth] No API key provided");
1264
1407
  return c.json({ error: "Unauthorized - Missing x-api-key header" }, 401);
1265
1408
  }
1266
1409
  try {
1267
1410
  const result = await apiKeyManager.verify(hdr);
1268
1411
  if (result.valid) {
1269
1412
  const payload = result.payload;
1270
- console.log(`[Auth] JWT API key authorized: ${payload.name || payload.sub || "unknown"}`);
1413
+ logger.debug(`[Auth] JWT API key authorized: ${payload.name || payload.sub || "unknown"}`);
1271
1414
  await next();
1272
1415
  return;
1273
1416
  }
1274
- console.log(`[Auth] JWT verification failed: ${result.error}`);
1417
+ logger.debug(`[Auth] JWT verification failed: ${result.error}`);
1275
1418
  } catch (error) {
1276
- console.log(`[Auth] JWT verification error: ${error}`);
1419
+ logger.warn(`[Auth] JWT verification error: ${error}`);
1277
1420
  }
1278
- console.log(`[Auth] Invalid API key: ${hdr?.slice(0, 20) || "none"}`);
1421
+ logger.debug(`[Auth] Invalid API key: ${hdr?.slice(0, 20) || "none"}`);
1279
1422
  return c.json({ error: "Unauthorized" }, 401);
1280
1423
  };
1281
1424
  }
1282
1425
 
1283
1426
  // src/middleware/referrer.ts
1284
- var DEMO_KEYS2 = ["chatter-api-key-here"];
1285
- function requireReferrer(allowedOrigins) {
1427
+ function requireReferrer(allowedOrigins, demoApiKeys = [], logger = createConsoleLogger()) {
1286
1428
  return async (c, next) => {
1287
1429
  const apiKey = c.req.header("x-api-key");
1288
- const isDemoKey = apiKey && DEMO_KEYS2.includes(apiKey);
1430
+ const isDemoKey = apiKey ? demoApiKeys.includes(apiKey) : false;
1289
1431
  const isSessionKey = apiKey?.startsWith("session_");
1290
1432
  if (isDemoKey || isSessionKey) {
1291
1433
  const referer = c.req.header("referer");
1292
1434
  const origin = c.req.header("origin");
1293
- console.log(
1435
+ logger.debug(
1294
1436
  `[Referrer] Checking ${isSessionKey ? "session" : "demo"} key: ${apiKey?.slice(0, 20)}...`
1295
1437
  );
1296
- console.log(`[Referrer] Origin: ${origin || "none"}, Referer: ${referer || "none"}`);
1297
- const isAllowed = allowedOrigins.some((allowed) => {
1298
- if (origin === allowed) return true;
1299
- if (referer) {
1300
- if (referer === allowed) return true;
1301
- if (referer.startsWith(`${allowed}/`)) return true;
1302
- if (referer.startsWith(`${allowed}?`)) return true;
1303
- }
1304
- return false;
1305
- });
1438
+ logger.debug(`[Referrer] Origin: ${origin || "none"}, Referer: ${referer || "none"}`);
1439
+ const isAllowed = matchesAllowedOrigin(origin, referer, allowedOrigins);
1306
1440
  if (!isAllowed) {
1307
- console.log(`[Referrer] BLOCKED - not from allowed origins: ${allowedOrigins.join(", ")}`);
1441
+ logger.debug(`[Referrer] BLOCKED - not from allowed origins: ${allowedOrigins.join(", ")}`);
1308
1442
  return c.json(
1309
1443
  {
1310
1444
  error: "Demo API keys only work on official demo pages. Please visit our website or use your own API key."
@@ -1312,29 +1446,29 @@ function requireReferrer(allowedOrigins) {
1312
1446
  403
1313
1447
  );
1314
1448
  }
1315
- console.log("[Referrer] Allowed - origin matches");
1449
+ logger.debug("[Referrer] Allowed - origin matches");
1316
1450
  }
1317
1451
  await next();
1318
1452
  };
1319
1453
  }
1320
1454
 
1321
1455
  // src/middleware/session.ts
1322
- function validateSessionKey() {
1456
+ function validateSessionKey(logger = createConsoleLogger()) {
1323
1457
  return async (c, next) => {
1324
1458
  const apiKey = c.req.header("x-api-key");
1325
1459
  if (apiKey?.startsWith("session_")) {
1326
- console.log(`[Session] Validating key: ${apiKey.slice(0, 20)}...`);
1460
+ logger.debug(`[Session] Validating key: ${apiKey.slice(0, 20)}...`);
1327
1461
  const info = getSessionInfo(apiKey);
1328
1462
  if (info) {
1329
- console.log(
1463
+ logger.debug(
1330
1464
  `[Session] Key found - requests: ${info.requests}/${info.maxRequests}, expires: ${new Date(info.expiresAt).toISOString()}`
1331
1465
  );
1332
1466
  } else {
1333
- console.log("[Session] Key not found in session store");
1467
+ logger.debug("[Session] Key not found in session store");
1334
1468
  }
1335
1469
  const isValid = validateSession(apiKey);
1336
1470
  if (!isValid) {
1337
- console.log("[Session] REJECTED - expired or quota exceeded");
1471
+ logger.debug("[Session] REJECTED - expired or quota exceeded");
1338
1472
  return c.json(
1339
1473
  {
1340
1474
  error: "Session expired or quota exceeded. Please refresh the page to get a new session."
@@ -1342,7 +1476,7 @@ function validateSessionKey() {
1342
1476
  401
1343
1477
  );
1344
1478
  }
1345
- console.log("[Session] Valid - request count incremented");
1479
+ logger.debug("[Session] Valid - request count incremented");
1346
1480
  }
1347
1481
  await next();
1348
1482
  };
@@ -1356,15 +1490,19 @@ function wantsStream2(c) {
1356
1490
  return acc.includes("text/event-stream");
1357
1491
  }
1358
1492
  function publicRoutes(deps) {
1359
- const { client, store, config, prompts } = deps;
1493
+ const { client, store, config, prompts, logger } = deps;
1360
1494
  const app = new import_hono4.Hono();
1361
1495
  const requirePublicKey = createAuthMiddleware(deps);
1362
- const { limitPublic: limitPublic2 } = createRateLimiter(config);
1496
+ const { limitPublic } = createRateLimiter(config);
1363
1497
  const referrerOrigins = config.server?.allowedOrigins?.length ? config.server.allowedOrigins.filter((o) => o !== "*") : [config.bot.publicUrl, "http://localhost:8181", "http://127.0.0.1:8181"];
1364
- app.use("/api/public/*", validateSessionKey());
1498
+ app.use("/api/public/*", chatBodyLimit(config.server?.maxRequestBytes));
1499
+ app.use("/api/public/*", validateSessionKey(logger));
1365
1500
  app.use("/api/public/*", requirePublicKey);
1366
- app.use("/api/public/*", requireReferrer(referrerOrigins));
1367
- app.use("/api/public/*", limitPublic2());
1501
+ app.use(
1502
+ "/api/public/*",
1503
+ requireReferrer(referrerOrigins, config.rateLimit?.demoApiKeys ?? [], logger)
1504
+ );
1505
+ app.use("/api/public/*", limitPublic());
1368
1506
  app.post("/api/public/chat", async (c) => {
1369
1507
  const body = await c.req.json().catch(() => ({}));
1370
1508
  const normalized = normalizeChatBody(body);
@@ -1406,7 +1544,8 @@ function publicRoutes(deps) {
1406
1544
 
1407
1545
  // src/server.ts
1408
1546
  async function createServer(config) {
1409
- console.log(`\u{1F680} Starting ${config.bot.name}...`);
1547
+ const logger = resolveLogger(config.logger, config.logLevel);
1548
+ logger.info(`\u{1F680} Starting ${config.bot.name}...`);
1410
1549
  const knowledgeDir = config.knowledgeDir || "./config/knowledge";
1411
1550
  const promptsDir = config.promptsDir || "./config/prompts";
1412
1551
  const publicDir = config.publicDir || "./public";
@@ -1425,24 +1564,26 @@ async function createServer(config) {
1425
1564
  });
1426
1565
  const store = new VectorStore(client, {
1427
1566
  databaseClient: db,
1428
- knowledgeDir
1567
+ knowledgeDir,
1568
+ logger
1429
1569
  });
1430
1570
  await store.build();
1431
- console.log("\u2705 Knowledge base ready");
1571
+ logger.info("\u2705 Knowledge base ready");
1432
1572
  const prompts = new PromptLoader(promptsDir, config.bot);
1433
1573
  let apiKeyManager;
1434
- const secret = process.env.CHATTER_SECRET || config.auth?.secret;
1574
+ const secret = config.auth?.secret || process.env.CHATTER_SECRET;
1435
1575
  if (secret) {
1436
1576
  apiKeyManager = new ApiKeyManager(secret);
1437
- console.log("\u2705 API key manager initialized");
1577
+ logger.info("\u2705 API key manager initialized");
1438
1578
  } else if (config.auth?.apiKeyManager) {
1439
1579
  apiKeyManager = config.auth.apiKeyManager;
1440
- console.log("\u2705 Custom API key manager configured");
1580
+ logger.info("\u2705 Custom API key manager configured");
1441
1581
  }
1442
1582
  const app = new import_hono5.Hono();
1443
1583
  if (config.server?.cors !== false) {
1444
1584
  app.use("*", cors(config.server?.allowedOrigins));
1445
1585
  }
1586
+ app.use("*", securityHeaders(config));
1446
1587
  app.get("/healthz", (c) => c.text("ok"));
1447
1588
  app.get(
1448
1589
  "/config",
@@ -1460,7 +1601,7 @@ async function createServer(config) {
1460
1601
  try {
1461
1602
  serveStatic = await loadServeStatic();
1462
1603
  } catch (error) {
1463
- console.error(`\u274C Static assets disabled: ${error.message}`);
1604
+ logger.error(`\u274C Static assets disabled: ${error.message}`);
1464
1605
  }
1465
1606
  }
1466
1607
  if (serveStatic && staticDir) {
@@ -1510,8 +1651,17 @@ async function createServer(config) {
1510
1651
  app.get("/demo/chatbot", serveStatic({ path: `${publicDir}/demo-chatbot.html` }));
1511
1652
  app.get("/demo/react", serveStatic({ path: `${publicDir}/react-demo.html` }));
1512
1653
  }
1513
- const senders = createSenderRegistry();
1514
- const deps = { client, store, db, config, prompts, apiKeyManager, senders };
1654
+ const senders = createSenderRegistry(logger);
1655
+ const deps = {
1656
+ client,
1657
+ store,
1658
+ db,
1659
+ config,
1660
+ prompts,
1661
+ apiKeyManager,
1662
+ senders,
1663
+ logger
1664
+ };
1515
1665
  if (enablePublic) {
1516
1666
  app.route("/", publicRoutes(deps));
1517
1667
  }
@@ -1533,13 +1683,13 @@ async function createServer(config) {
1533
1683
  try {
1534
1684
  await channel.start(deps);
1535
1685
  started.push(channel);
1536
- console.log(`\u2705 Channel "${channel.name}" started`);
1686
+ logger.info(`\u2705 Channel "${channel.name}" started`);
1537
1687
  } catch (error) {
1538
- console.error(`\u274C Channel "${channel.name}" failed to start:`, error);
1688
+ logger.error(`\u274C Channel "${channel.name}" failed to start:`, error);
1539
1689
  }
1540
1690
  }
1541
1691
  }
1542
- console.log(`\u2705 ${config.bot.name} server ready`);
1692
+ logger.info(`\u2705 ${config.bot.name} server ready`);
1543
1693
  return Object.assign(app, {
1544
1694
  // Scoped to this call's started channels, not global — two servers in
1545
1695
  // one process each stop only their own. A channel's `stop()` throwing
@@ -1551,7 +1701,7 @@ async function createServer(config) {
1551
1701
  try {
1552
1702
  await channel.stop?.();
1553
1703
  } catch (error) {
1554
- console.error(`Channel "${channel.name}" failed to stop:`, error);
1704
+ logger.error(`Channel "${channel.name}" failed to stop:`, error);
1555
1705
  }
1556
1706
  })
1557
1707
  );