@diegoaltoworks/chatter 0.45.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 (120) hide show
  1. package/README.md +19 -5
  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/core/serve-static.d.ts +38 -0
  40. package/dist/core/serve-static.d.ts.map +1 -0
  41. package/dist/flows/engine.d.ts +3 -0
  42. package/dist/flows/engine.d.ts.map +1 -1
  43. package/dist/flows/index.d.ts +1 -0
  44. package/dist/flows/index.d.ts.map +1 -1
  45. package/dist/flows/index.js +91 -25
  46. package/dist/flows/index.mjs +91 -26
  47. package/dist/flows/intent.d.ts +2 -1
  48. package/dist/flows/intent.d.ts.map +1 -1
  49. package/dist/flows/loader.d.ts +5 -3
  50. package/dist/flows/loader.d.ts.map +1 -1
  51. package/dist/flows/types.d.ts +9 -0
  52. package/dist/flows/types.d.ts.map +1 -1
  53. package/dist/history/index.d.ts +17 -0
  54. package/dist/history/index.d.ts.map +1 -0
  55. package/dist/history/index.js +100 -0
  56. package/dist/history/index.mjs +73 -0
  57. package/dist/history/tursoStore.d.ts +26 -0
  58. package/dist/history/tursoStore.d.ts.map +1 -0
  59. package/dist/history/types.d.ts +18 -0
  60. package/dist/history/types.d.ts.map +1 -0
  61. package/dist/index.d.ts +5 -2
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +1202 -32462
  64. package/dist/index.mjs +1118 -32414
  65. package/dist/mcp-server/cost-tracker.d.ts +11 -6
  66. package/dist/mcp-server/cost-tracker.d.ts.map +1 -1
  67. package/dist/mcp-server/logger.d.ts +5 -2
  68. package/dist/mcp-server/logger.d.ts.map +1 -1
  69. package/dist/mcp-server/sdk.d.ts +23 -0
  70. package/dist/mcp-server/sdk.d.ts.map +1 -0
  71. package/dist/mcp-server/tool-input.d.ts +12 -0
  72. package/dist/mcp-server/tool-input.d.ts.map +1 -0
  73. package/dist/mcp-server/types.d.ts +10 -3
  74. package/dist/mcp-server/types.d.ts.map +1 -1
  75. package/dist/mcp-server.d.ts +1 -2
  76. package/dist/mcp-server.d.ts.map +1 -1
  77. package/dist/mcp-server.js +192 -111
  78. package/dist/mcp-server.mjs +179 -98
  79. package/dist/middleware/auth.d.ts.map +1 -1
  80. package/dist/middleware/bodyLimit.d.ts +11 -0
  81. package/dist/middleware/bodyLimit.d.ts.map +1 -0
  82. package/dist/middleware/clientKey.d.ts +15 -0
  83. package/dist/middleware/clientKey.d.ts.map +1 -0
  84. package/dist/middleware/cors.d.ts.map +1 -1
  85. package/dist/middleware/jwt.d.ts +0 -3
  86. package/dist/middleware/jwt.d.ts.map +1 -1
  87. package/dist/middleware/originMatch.d.ts +27 -0
  88. package/dist/middleware/originMatch.d.ts.map +1 -0
  89. package/dist/middleware/ratelimit.d.ts +0 -6
  90. package/dist/middleware/ratelimit.d.ts.map +1 -1
  91. package/dist/middleware/referrer.d.ts +5 -1
  92. package/dist/middleware/referrer.d.ts.map +1 -1
  93. package/dist/middleware/securityHeaders.d.ts +20 -0
  94. package/dist/middleware/securityHeaders.d.ts.map +1 -0
  95. package/dist/middleware/session.d.ts +2 -1
  96. package/dist/middleware/session.d.ts.map +1 -1
  97. package/dist/middleware/windowBucket.d.ts +19 -0
  98. package/dist/middleware/windowBucket.d.ts.map +1 -0
  99. package/dist/personas/index.js +20 -1
  100. package/dist/personas/index.mjs +20 -1
  101. package/dist/personas/resolver.d.ts +3 -0
  102. package/dist/personas/resolver.d.ts.map +1 -1
  103. package/dist/routes/demo.d.ts.map +1 -1
  104. package/dist/routes/openai.d.ts.map +1 -1
  105. package/dist/routes/private.d.ts.map +1 -1
  106. package/dist/routes/public.d.ts.map +1 -1
  107. package/dist/scheduler/compose.d.ts +2 -1
  108. package/dist/scheduler/compose.d.ts.map +1 -1
  109. package/dist/scheduler/index.js +29 -9
  110. package/dist/scheduler/index.mjs +29 -9
  111. package/dist/scheduler/scheduler.d.ts +3 -0
  112. package/dist/scheduler/scheduler.d.ts.map +1 -1
  113. package/dist/scheduler/tick.d.ts +2 -0
  114. package/dist/scheduler/tick.d.ts.map +1 -1
  115. package/dist/server.d.ts.map +1 -1
  116. package/dist/server.js +726 -538
  117. package/dist/server.mjs +708 -527
  118. package/dist/types.d.ts +87 -0
  119. package/dist/types.d.ts.map +1 -1
  120. package/package.json +30 -9
package/dist/server.js CHANGED
@@ -33,20 +33,33 @@ __export(server_exports, {
33
33
  createServer: () => createServer
34
34
  });
35
35
  module.exports = __toCommonJS(server_exports);
36
+ var import_node_path4 = require("node:path");
36
37
  var import_client2 = require("@libsql/client");
37
38
  var import_hono5 = require("hono");
38
- var import_bun = require("hono/bun");
39
39
  var import_openai = __toESM(require("openai"));
40
40
 
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;
@@ -427,266 +676,140 @@ var VectorStore = class _VectorStore {
427
676
  const res = await this.db.execute({
428
677
  sql: `SELECT c.id, c.text, e.embedding
429
678
  FROM chunks c
430
- JOIN embeddings e ON e.id = c.id
431
- WHERE c.bucket IN (${placeholders})`,
432
- args: allowed
433
- });
434
- const scored = [];
435
- for (const row of res.rows) {
436
- const emb = JSON.parse(String(row.embedding));
437
- const s = _VectorStore.cosine(qv, emb);
438
- scored.push({ s, text: String(row.text) });
439
- }
440
- scored.sort((a, b) => b.s - a.s);
441
- return scored.slice(0, k).map((r) => r.text);
442
- }
443
- };
444
-
445
- // src/core/widgets.ts
446
- var import_node_fs3 = require("node:fs");
447
- var import_node_path3 = require("node:path");
448
- var import_node_url = require("node:url");
449
- var import_meta = {};
450
- function resolveStatic(configStaticDir) {
451
- if (configStaticDir && (0, import_node_fs3.existsSync)(configStaticDir)) {
452
- return { staticDir: configStaticDir };
453
- }
454
- try {
455
- if (import_meta.url) {
456
- const moduleDir = (0, import_node_path3.dirname)((0, import_node_url.fileURLToPath)(import_meta.url));
457
- const staticDir = (0, import_node_path3.join)(moduleDir, "static");
458
- if ((0, import_node_fs3.existsSync)(staticDir)) return { staticDir };
459
- }
460
- } catch {
461
- }
462
- try {
463
- if (typeof __dirname !== "undefined") {
464
- const staticDir = (0, import_node_path3.join)(__dirname, "static");
465
- if ((0, import_node_fs3.existsSync)(staticDir)) return { staticDir };
466
- }
467
- } catch {
468
- }
469
- return { staticDir: null };
470
- }
471
-
472
- // src/middleware/cors.ts
473
- var ALLOWED_HEADERS = "content-type, x-api-key, authorization";
474
- var ALLOWED_METHODS = "POST, GET, OPTIONS";
475
- var cors = (allowedOrigins) => async (c, next) => {
476
- const requestOrigin = c.req.header("origin");
477
- const allowAll = !allowedOrigins || allowedOrigins.length === 0 || allowedOrigins.includes("*");
478
- const originHeader = resolveOrigin(requestOrigin, allowedOrigins, allowAll);
479
- if (c.req.method === "OPTIONS") {
480
- return new Response(null, {
481
- status: 204,
482
- headers: {
483
- "access-control-allow-origin": originHeader,
484
- "access-control-allow-headers": ALLOWED_HEADERS,
485
- "access-control-allow-methods": ALLOWED_METHODS,
486
- ...allowAll ? {} : { vary: "Origin" }
487
- }
488
- });
489
- }
490
- c.header("access-control-allow-origin", originHeader);
491
- c.header("access-control-allow-headers", ALLOWED_HEADERS);
492
- c.header("access-control-allow-methods", ALLOWED_METHODS);
493
- if (!allowAll) {
494
- c.header("vary", "Origin");
495
- }
496
- await next();
497
- };
498
- function resolveOrigin(requestOrigin, allowedOrigins, allowAll) {
499
- if (allowAll) return "*";
500
- if (requestOrigin && allowedOrigins.includes(requestOrigin)) {
501
- return requestOrigin;
502
- }
503
- return allowedOrigins[0];
504
- }
505
-
506
- // src/routes/demo.ts
507
- var import_hono = require("hono");
508
- var import_streaming = require("hono/streaming");
509
-
510
- // src/core/guardrails.ts
511
- function detectLeakage(text) {
512
- const flags = [
513
- /system prompt/i,
514
- /hidden instruction/i,
515
- /here (are|is) (my|the) rules/i,
516
- /BEGIN SYSTEM PROMPT/i,
517
- /tool instructions/i
518
- ];
519
- return flags.some((rx) => rx.test(text));
520
- }
521
- function scrubOutput(text) {
522
- return text.replace(/sk-[A-Za-z0-9]{15,}/g, "[REDACTED]").replace(/AIza[0-9A-Za-z_-]{30,}/g, "[REDACTED]");
523
- }
524
- function guardOutput(text) {
525
- if (detectLeakage(text)) {
526
- return "Sorry, I can't share internal instructions. How else can I help?";
527
- }
528
- return scrubOutput(text);
529
- }
530
-
531
- // src/core/llm.ts
532
- var DEFAULT_MODEL = "gpt-4o";
533
- async function completeOnce({
534
- client,
535
- system,
536
- messages,
537
- temperature = 0.2,
538
- model = DEFAULT_MODEL
539
- }) {
540
- const res = await client.chat.completions.create({
541
- model,
542
- temperature,
543
- messages: [{ role: "system", content: system }, ...messages]
544
- });
545
- const text = res.choices[0]?.message?.content ?? "";
546
- return {
547
- content: guardOutput(text),
548
- usage: res.usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
549
- };
550
- }
551
- async function* completeStream({
552
- client,
553
- system,
554
- messages,
555
- temperature = 0.2,
556
- model = DEFAULT_MODEL
557
- }) {
558
- const stream5 = await client.chat.completions.create({
559
- model,
560
- temperature,
561
- stream: true,
562
- messages: [{ role: "system", content: system }, ...messages]
563
- });
564
- for await (const chunk2 of stream5) {
565
- const delta = chunk2.choices[0]?.delta?.content ?? "";
566
- if (delta) yield scrubOutput(delta);
567
- }
568
- }
569
-
570
- // src/core/answer.ts
571
- var noUsage = () => ({ prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 });
572
- async function runAnswerFn(answerFn, { system, messages, mode, sender }) {
573
- const result = await answerFn({ system, messages, mode, sender });
574
- const raw = typeof result === "string" ? result : result?.content;
575
- const usage = typeof result === "string" ? void 0 : result?.usage;
576
- return {
577
- content: guardOutput(typeof raw === "string" ? raw : ""),
578
- usage: usage ?? noUsage()
579
- };
580
- }
581
- async function answerOnce({
582
- answerFn,
583
- client,
584
- system,
585
- messages,
586
- mode,
587
- sender,
588
- temperature,
589
- model
590
- }) {
591
- if (answerFn) {
592
- return runAnswerFn(answerFn, { system, messages, mode, sender });
593
- }
594
- return completeOnce({ client, system, messages, temperature, model });
595
- }
596
- async function* answerStream({
597
- answerFn,
598
- client,
599
- system,
600
- messages,
601
- mode,
602
- sender,
603
- temperature,
604
- model
605
- }) {
606
- if (answerFn) {
607
- const { content } = await runAnswerFn(answerFn, { system, messages, mode, sender });
608
- if (content) yield content;
609
- return;
610
- }
611
- yield* completeStream({ client, system, messages, temperature, model });
612
- }
613
-
614
- // src/core/buckets.ts
615
- function defaultBuckets(mode) {
616
- return ["base", mode];
617
- }
618
- function isIdentified(sender) {
619
- return typeof sender === "string" && sender.trim().length > 0;
620
- }
621
- function normalize(buckets) {
622
- const out = [];
623
- for (const raw of buckets) {
624
- if (typeof raw !== "string") continue;
625
- const bucket = raw.trim();
626
- if (bucket && !out.includes(bucket)) out.push(bucket);
627
- }
628
- return out;
629
- }
630
- async function resolveBuckets({
631
- mode,
632
- sender,
633
- bucketsFor
634
- }) {
635
- if (!bucketsFor) return void 0;
636
- const identified = isIdentified(sender);
637
- const requested = await bucketsFor(identified ? { mode, sender } : { mode });
638
- if (!Array.isArray(requested)) return void 0;
639
- const cleaned = normalize(requested);
640
- if (identified) return cleaned;
641
- const ceiling = new Set(defaultBuckets(mode));
642
- return cleaned.filter((bucket) => ceiling.has(bucket));
643
- }
679
+ JOIN embeddings e ON e.id = c.id
680
+ WHERE c.bucket IN (${placeholders})`,
681
+ args: allowed
682
+ });
683
+ const scored = [];
684
+ for (const row of res.rows) {
685
+ const emb = JSON.parse(String(row.embedding));
686
+ const s = _VectorStore.cosine(qv, emb);
687
+ scored.push({ s, text: String(row.text) });
688
+ }
689
+ scored.sort((a, b) => b.s - a.s);
690
+ return scored.slice(0, k).map((r) => r.text);
691
+ }
692
+ };
644
693
 
645
- // src/core/messages.ts
646
- function isClientRole(role) {
647
- return role === "user" || role === "assistant";
694
+ // src/core/serve-static.ts
695
+ var STATIC_ADAPTERS = {
696
+ bun: "hono/bun",
697
+ node: "@hono/node-server/serve-static"
698
+ };
699
+ function detectRuntime() {
700
+ return typeof globalThis.Bun === "undefined" ? "node" : "bun";
648
701
  }
649
- function normalizeMessages(raw) {
650
- if (!Array.isArray(raw) || raw.length === 0) return null;
651
- const messages = [];
652
- for (const entry of raw) {
653
- if (!entry || typeof entry !== "object") return null;
654
- const { role, content } = entry;
655
- if (!isClientRole(role)) continue;
656
- let text = null;
657
- if (typeof content === "string") {
658
- text = content;
659
- } else if (Array.isArray(content)) {
660
- text = content.filter(
661
- (part) => !!part && typeof part === "object" && part.type === "text" && typeof part.text === "string"
662
- ).map((part) => part.text).join("");
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
+ );
708
+ }
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);
663
718
  }
664
- if (text === null) continue;
665
- messages.push({ role, content: text });
666
- }
667
- return messages.length > 0 ? messages : null;
719
+ );
720
+ cache.set(runtime, loading);
721
+ return loading;
668
722
  }
669
- function normalizeChatBody(body) {
670
- const source = body && typeof body === "object" ? body : {};
671
- if (Array.isArray(source.messages)) {
672
- if (source.messages.length === 0) {
673
- 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 };
674
738
  }
675
- const messages = normalizeMessages(source.messages);
676
- if (!messages) {
677
- return {
678
- ok: false,
679
- error: "messages must contain user/assistant messages with text content"
680
- };
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 };
681
745
  }
682
- return { ok: true, messages };
746
+ } catch {
683
747
  }
684
- const { message } = source;
685
- if (message && typeof message !== "object") {
686
- 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
+ });
687
770
  }
688
- 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";
689
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
+ };
808
+ }
809
+
810
+ // src/routes/demo.ts
811
+ var import_hono = require("hono");
812
+ var import_streaming = require("hono/streaming");
690
813
 
691
814
  // src/core/session.ts
692
815
  var import_node_crypto2 = require("node:crypto");
@@ -739,26 +862,94 @@ function getActiveSessions() {
739
862
  return sessions.size;
740
863
  }
741
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
+
742
941
  // src/routes/demo.ts
743
- function checkDemoOrigin(allowedOrigins) {
942
+ function checkDemoOrigin(allowedOrigins, allowLocalhost) {
744
943
  const allowAll = allowedOrigins.length === 0 || allowedOrigins.includes("*");
944
+ const matches = allowLocalhost ? matchesAllowedOriginOrLocalhost : matchesAllowedOrigin;
745
945
  return async (c, next) => {
746
- const origin = c.req.header("origin");
747
- const referer = c.req.header("referer");
748
- const host = c.req.header("host") || "";
749
- if (host.startsWith("localhost:") || host.startsWith("127.0.0.1:")) {
750
- return next();
751
- }
752
946
  if (allowAll) {
753
947
  return next();
754
948
  }
755
- if (origin) {
756
- const allowed = allowedOrigins.some((allowed2) => origin.startsWith(allowed2));
757
- if (allowed) return next();
758
- }
759
- if (referer) {
760
- const allowed = allowedOrigins.some((allowed2) => referer.startsWith(allowed2));
761
- 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();
762
953
  }
763
954
  return c.json(
764
955
  {
@@ -768,22 +959,12 @@ function checkDemoOrigin(allowedOrigins) {
768
959
  );
769
960
  };
770
961
  }
771
- function limitSessionCreation() {
772
- const buckets = /* @__PURE__ */ new Map();
773
- const WINDOW_MS = 36e5;
962
+ function limitSessionCreation(trustProxy) {
963
+ const limiter = createWindowBucketLimiter(36e5);
774
964
  const LIMIT = 10;
775
965
  return async (c, next) => {
776
- const ip = c.req.header("x-forwarded-for")?.split(",")[0].trim() || "unknown-ip";
777
- const key = `session:${ip}`;
778
- const now = Date.now();
779
- const b = buckets.get(key) ?? { count: 0, windowStart: now };
780
- if (now - b.windowStart >= WINDOW_MS) {
781
- b.count = 0;
782
- b.windowStart = now;
783
- }
784
- b.count += 1;
785
- buckets.set(key, b);
786
- if (b.count > LIMIT) {
966
+ const key = `session:${clientRateLimitKey(c, trustProxy)}`;
967
+ if (!limiter.hit(key, LIMIT)) {
787
968
  return c.json(
788
969
  {
789
970
  error: "Session key limit exceeded (10 per hour). Please wait or use your own API key."
@@ -794,22 +975,12 @@ function limitSessionCreation() {
794
975
  await next();
795
976
  };
796
977
  }
797
- function limitDemo() {
798
- const buckets = /* @__PURE__ */ new Map();
799
- const WINDOW_MS = 6e4;
978
+ function limitDemo(trustProxy) {
979
+ const limiter = createWindowBucketLimiter(6e4);
800
980
  const LIMIT = 10;
801
981
  return async (c, next) => {
802
- const ip = c.req.header("x-forwarded-for")?.split(",")[0].trim() || "unknown-ip";
803
- const key = `demo:${ip}`;
804
- const now = Date.now();
805
- const b = buckets.get(key) ?? { count: 0, windowStart: now };
806
- if (now - b.windowStart >= WINDOW_MS) {
807
- b.count = 0;
808
- b.windowStart = now;
809
- }
810
- b.count += 1;
811
- buckets.set(key, b);
812
- if (b.count > LIMIT) {
982
+ const key = `demo:${clientRateLimitKey(c, trustProxy)}`;
983
+ if (!limiter.hit(key, LIMIT)) {
813
984
  return c.json(
814
985
  {
815
986
  error: "Demo rate limit exceeded (10 requests/minute). Please wait or use your own API key."
@@ -821,82 +992,91 @@ function limitDemo() {
821
992
  };
822
993
  }
823
994
  function demoRoutes(deps) {
824
- const { client, store, config, prompts } = deps;
995
+ const { client, store, config, prompts, logger } = deps;
825
996
  const app = new import_hono.Hono();
826
997
  const demoOrigins = config.server?.allowedOrigins ?? [];
827
- app.get("/api/demo/session", checkDemoOrigin(demoOrigins), limitSessionCreation(), (c) => {
828
- const session = createSession({
829
- maxRequests: 20,
830
- ttl: 3600
831
- // 1 hour
832
- });
833
- console.log(
834
- `[Demo] Created session: ${session.key.slice(0, 20)}... (${session.maxRequests} requests, ${3600}s TTL)`
835
- );
836
- return c.json({
837
- key: session.key,
838
- expiresIn: 3600,
839
- maxRequests: session.maxRequests,
840
- message: "Use this temporary key in the x-api-key header"
841
- });
842
- });
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
+ );
843
1021
  app.get("/api/demo/stats", (c) => {
844
1022
  return c.json({
845
1023
  activeSessions: getActiveSessions()
846
1024
  });
847
1025
  });
848
- app.post("/api/demo/chat", limitDemo(), async (c) => {
849
- try {
850
- const body = await c.req.json().catch(() => ({}));
851
- const normalized = normalizeChatBody(body);
852
- if (!normalized.ok) {
853
- return c.json({ error: normalized.error }, 400);
854
- }
855
- const { messages } = normalized;
856
- const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
857
- if (!lastUserMsg) {
858
- return c.json({ error: "no user message found in conversation" }, 400);
859
- }
860
- const buckets = await resolveBuckets({ mode: "public", bucketsFor: config.bucketsFor }) ?? defaultBuckets("public");
861
- const ctx = await store.query(lastUserMsg.content, 6, buckets);
862
- const system = [
863
- prompts.baseSystemRules,
864
- prompts.publicPersona,
865
- `Context:
866
- ${ctx.join("\n\n")}`
867
- ].join("\n\n");
868
- const url = new URL(c.req.url);
869
- const wantsStream3 = url.searchParams.get("stream") === "1";
870
- if (wantsStream3) {
871
- return (0, import_streaming.stream)(c, async (s) => {
872
- for await (const delta of answerStream({
873
- answerFn: config.answerFn,
874
- client,
875
- system,
876
- messages,
877
- mode: "public"
878
- })) {
879
- 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 })}
880
1058
 
881
1059
  `);
882
- }
883
- 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
884
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);
885
1077
  }
886
- const out = await answerOnce({
887
- answerFn: config.answerFn,
888
- client,
889
- system,
890
- messages,
891
- mode: "public"
892
- });
893
- return c.json({ reply: out.content });
894
- } catch (error) {
895
- const err = error;
896
- console.error("Demo chat error:", err);
897
- return c.json({ error: err.message || "Internal error" }, 500);
898
1078
  }
899
- });
1079
+ );
900
1080
  return app;
901
1081
  }
902
1082
 
@@ -904,38 +1084,6 @@ ${ctx.join("\n\n")}`
904
1084
  var import_hono2 = require("hono");
905
1085
  var import_streaming2 = require("hono/streaming");
906
1086
 
907
- // src/core/pipeline.ts
908
- var MODE_SETTINGS = {
909
- public: { topK: 6, label: "Context" },
910
- private: { topK: 8, label: "Internal Context" }
911
- };
912
- async function prepareChat({
913
- store,
914
- prompts,
915
- mode,
916
- messages,
917
- personaLayer,
918
- channelHint,
919
- buckets
920
- }) {
921
- const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
922
- if (!lastUserMsg) {
923
- throw new Error("no user message found in conversation");
924
- }
925
- const { topK, label } = MODE_SETTINGS[mode];
926
- const ctx = await store.query(lastUserMsg.content, topK, buckets ?? defaultBuckets(mode));
927
- const persona = personaLayer?.trim() || (mode === "private" ? prompts.privatePersona : prompts.publicPersona);
928
- const hint = channelHint?.trim();
929
- const system = [
930
- prompts.baseSystemRules,
931
- persona,
932
- ...hint ? [hint] : [],
933
- `${label}:
934
- ${ctx.join("\n\n")}`
935
- ].join("\n\n");
936
- return { system, messages };
937
- }
938
-
939
1087
  // src/middleware/jwt.ts
940
1088
  var import_jose2 = require("jose");
941
1089
  function jwtSubject(c) {
@@ -962,7 +1110,7 @@ function createJWTMiddleware(config) {
962
1110
  }
963
1111
  throw new Error("JWT configuration missing (JWKS URL or PUBLIC KEY PEM).");
964
1112
  }
965
- return async function requirePrivateJWT2(c, next) {
1113
+ return async function requirePrivateJWT(c, next) {
966
1114
  const hdr = c.req.header("authorization") ?? "";
967
1115
  if (!hdr.startsWith("Bearer ")) return c.json({ error: "Unauthorized" }, 401);
968
1116
  const token = hdr.slice("Bearer ".length);
@@ -975,41 +1123,24 @@ function createJWTMiddleware(config) {
975
1123
  }
976
1124
  };
977
1125
  }
978
- var requirePrivateJWT = createJWTMiddleware({});
979
1126
 
980
1127
  // src/middleware/ratelimit.ts
981
- var DEMO_KEYS = ["chatter-api-key-here"];
982
- function getKeyFromIP(c) {
983
- const h = c.req.header("x-forwarded-for");
984
- if (h) return h.split(",")[0].trim();
985
- return "unknown-ip";
986
- }
987
1128
  function createRateLimiter(config) {
988
- const buckets = /* @__PURE__ */ new Map();
989
- const WINDOW_MS = 6e4;
1129
+ const limiter = createWindowBucketLimiter(6e4);
990
1130
  const publicLimit = config.rateLimit?.public || 60;
991
1131
  const privateLimit = config.rateLimit?.private || 120;
992
- function allow(key, limit) {
993
- const now = Date.now();
994
- const b = buckets.get(key) ?? { count: 0, windowStart: now };
995
- if (now - b.windowStart >= WINDOW_MS) {
996
- b.count = 0;
997
- b.windowStart = now;
998
- }
999
- b.count += 1;
1000
- buckets.set(key, b);
1001
- return b.count <= limit;
1002
- }
1132
+ const trustProxy = config.rateLimit?.trustProxy ?? true;
1133
+ const demoApiKeys = config.rateLimit?.demoApiKeys ?? [];
1003
1134
  return {
1004
1135
  // Public: key is IP only
1005
1136
  // Demo keys get 10x stricter rate limits
1006
1137
  limitPublic() {
1007
1138
  return async (c, next) => {
1008
1139
  const apiKey = c.req.header("x-api-key");
1009
- const isDemoKey = apiKey ? DEMO_KEYS.includes(apiKey) : false;
1010
- const key = `pub:${getKeyFromIP(c)}`;
1140
+ const isDemoKey = apiKey ? demoApiKeys.includes(apiKey) : false;
1141
+ const key = `pub:${clientRateLimitKey(c, trustProxy)}`;
1011
1142
  const limit = isDemoKey ? Math.floor(publicLimit / 10) : publicLimit;
1012
- if (!allow(key, limit)) {
1143
+ if (!limiter.hit(key, limit)) {
1013
1144
  return c.json(
1014
1145
  {
1015
1146
  error: isDemoKey ? "Demo rate limit exceeded. Please use your own API key for higher limits." : "Rate limit exceeded"
@@ -1025,28 +1156,47 @@ function createRateLimiter(config) {
1025
1156
  return async (c, next) => {
1026
1157
  const sub = c.jwtSub;
1027
1158
  const key = `private:${sub ?? "no-sub"}`;
1028
- if (!allow(key, privateLimit)) return c.json({ error: "Rate limit" }, 429);
1159
+ if (!limiter.hit(key, privateLimit)) return c.json({ error: "Rate limit" }, 429);
1029
1160
  await next();
1030
1161
  };
1031
1162
  }
1032
1163
  };
1033
1164
  }
1034
- var defaultLimiter = createRateLimiter({});
1035
- var limitPublic = defaultLimiter.limitPublic;
1036
- var limitPrivate = defaultLimiter.limitPrivate;
1037
1165
 
1038
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
+ }
1039
1186
  function errorJson(c, status, message) {
1040
1187
  return c.json(
1041
1188
  { error: { message, type: status === 401 ? "authentication_error" : "invalid_request_error" } },
1042
1189
  status
1043
1190
  );
1044
1191
  }
1192
+ function oversizedBodyError(message) {
1193
+ return { error: { message, type: "invalid_request_error" } };
1194
+ }
1045
1195
  function openaiRoutes(deps) {
1046
1196
  const { client, store, config, prompts, apiKeyManager } = deps;
1047
1197
  const app = new import_hono2.Hono();
1048
- const { limitPublic: limitPublic2, limitPrivate: limitPrivate2 } = createRateLimiter(config);
1049
- const requirePrivateJWT2 = createJWTMiddleware(config);
1198
+ const { limitPublic, limitPrivate } = createRateLimiter(config);
1199
+ const requirePrivateJWT = createJWTMiddleware(config);
1050
1200
  const enablePublic = config.features?.enablePublicChat !== false;
1051
1201
  const enablePrivate = config.features?.enablePrivateChat !== false;
1052
1202
  const serverModel = config.openai.model || DEFAULT_MODEL;
@@ -1061,7 +1211,10 @@ function openaiRoutes(deps) {
1061
1211
  }
1062
1212
  try {
1063
1213
  const result = await apiKeyManager.verify(key);
1064
- if (result.valid) return await next();
1214
+ if (result.valid) {
1215
+ c.apiKeySub = payloadSub(result.payload);
1216
+ return await next();
1217
+ }
1065
1218
  } catch {
1066
1219
  }
1067
1220
  return errorJson(c, 401, "Invalid API key");
@@ -1088,6 +1241,9 @@ function openaiRoutes(deps) {
1088
1241
  } catch {
1089
1242
  return errorJson(c, 400, "Conversation must contain at least one user message");
1090
1243
  }
1244
+ const sender = jwtSubject(c) ?? apiKeySubject(c);
1245
+ const conversationId = clientConversationId(c, body) ?? crypto.randomUUID();
1246
+ c.header("x-conversation-id", conversationId);
1091
1247
  const id = `chatcmpl-${crypto.randomUUID()}`;
1092
1248
  const created = Math.floor(Date.now() / 1e3);
1093
1249
  if (wantsStream3 === true) {
@@ -1110,6 +1266,8 @@ function openaiRoutes(deps) {
1110
1266
  system,
1111
1267
  messages,
1112
1268
  mode,
1269
+ sender,
1270
+ conversationId,
1113
1271
  temperature,
1114
1272
  model: serverModel
1115
1273
  })) {
@@ -1125,6 +1283,8 @@ function openaiRoutes(deps) {
1125
1283
  system,
1126
1284
  messages,
1127
1285
  mode,
1286
+ sender,
1287
+ conversationId,
1128
1288
  temperature,
1129
1289
  model: serverModel
1130
1290
  });
@@ -1140,13 +1300,15 @@ function openaiRoutes(deps) {
1140
1300
  });
1141
1301
  };
1142
1302
  if (enablePublic) {
1303
+ app.use("/v1/*", chatBodyLimit(config.server?.maxRequestBytes, oversizedBodyError));
1143
1304
  app.use("/v1/*", requireApiKey);
1144
- app.use("/v1/*", limitPublic2());
1305
+ app.use("/v1/*", limitPublic());
1145
1306
  app.post("/v1/chat/completions", handleChatCompletions("public"));
1146
1307
  }
1147
1308
  if (enablePrivate) {
1148
- app.use("/api/private/v1/*", requirePrivateJWT2);
1149
- 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());
1150
1312
  app.post("/api/private/v1/chat/completions", handleChatCompletions("private"));
1151
1313
  }
1152
1314
  return app;
@@ -1164,10 +1326,11 @@ function wantsStream(c) {
1164
1326
  function privateRoutes(deps) {
1165
1327
  const { client, store, config, prompts } = deps;
1166
1328
  const app = new import_hono3.Hono();
1167
- const requirePrivateJWT2 = createJWTMiddleware(config);
1168
- const { limitPrivate: limitPrivate2 } = createRateLimiter(config);
1169
- app.use("/api/private/*", requirePrivateJWT2);
1170
- 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());
1171
1334
  app.post("/api/private/chat", async (c) => {
1172
1335
  const body = await c.req.json().catch(() => ({}));
1173
1336
  const normalized = normalizeChatBody(body);
@@ -1175,9 +1338,10 @@ function privateRoutes(deps) {
1175
1338
  return c.json({ error: normalized.error }, 400);
1176
1339
  }
1177
1340
  const { messages } = normalized;
1341
+ const sender = jwtSubject(c);
1178
1342
  const buckets = await resolveBuckets({
1179
1343
  mode: "private",
1180
- sender: jwtSubject(c),
1344
+ sender,
1181
1345
  bucketsFor: config.bucketsFor
1182
1346
  });
1183
1347
  let system;
@@ -1196,6 +1360,7 @@ function privateRoutes(deps) {
1196
1360
  system,
1197
1361
  messages,
1198
1362
  mode: "private",
1363
+ sender,
1199
1364
  model
1200
1365
  })) {
1201
1366
  await s.write(`data: ${JSON.stringify({ delta })}
@@ -1205,7 +1370,15 @@ function privateRoutes(deps) {
1205
1370
  await s.write("event: end\ndata: {}\n\n");
1206
1371
  });
1207
1372
  }
1208
- 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
+ });
1209
1382
  return c.json({ reply: out.content });
1210
1383
  });
1211
1384
  return app;
@@ -1217,64 +1390,55 @@ var import_streaming4 = require("hono/streaming");
1217
1390
 
1218
1391
  // src/middleware/auth.ts
1219
1392
  function createAuthMiddleware(deps) {
1220
- const { apiKeyManager } = deps;
1393
+ const { apiKeyManager, logger } = deps;
1221
1394
  return async function requirePublicKey(c, next) {
1222
1395
  const hdr = c.req.header("x-api-key");
1223
1396
  if (hdr?.startsWith("session_")) {
1224
- console.log(`[Auth] Session key allowed: ${hdr.slice(0, 20)}...`);
1397
+ logger.debug(`[Auth] Session key allowed: ${hdr.slice(0, 20)}...`);
1225
1398
  await next();
1226
1399
  return;
1227
1400
  }
1228
1401
  if (!apiKeyManager) {
1229
- console.log("[Auth] No API key manager configured");
1402
+ logger.debug("[Auth] No API key manager configured");
1230
1403
  return c.json({ error: "Unauthorized - API key authentication not configured" }, 401);
1231
1404
  }
1232
1405
  if (!hdr) {
1233
- console.log("[Auth] No API key provided");
1406
+ logger.debug("[Auth] No API key provided");
1234
1407
  return c.json({ error: "Unauthorized - Missing x-api-key header" }, 401);
1235
1408
  }
1236
1409
  try {
1237
1410
  const result = await apiKeyManager.verify(hdr);
1238
1411
  if (result.valid) {
1239
1412
  const payload = result.payload;
1240
- 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"}`);
1241
1414
  await next();
1242
1415
  return;
1243
1416
  }
1244
- console.log(`[Auth] JWT verification failed: ${result.error}`);
1417
+ logger.debug(`[Auth] JWT verification failed: ${result.error}`);
1245
1418
  } catch (error) {
1246
- console.log(`[Auth] JWT verification error: ${error}`);
1419
+ logger.warn(`[Auth] JWT verification error: ${error}`);
1247
1420
  }
1248
- console.log(`[Auth] Invalid API key: ${hdr?.slice(0, 20) || "none"}`);
1421
+ logger.debug(`[Auth] Invalid API key: ${hdr?.slice(0, 20) || "none"}`);
1249
1422
  return c.json({ error: "Unauthorized" }, 401);
1250
1423
  };
1251
1424
  }
1252
1425
 
1253
1426
  // src/middleware/referrer.ts
1254
- var DEMO_KEYS2 = ["chatter-api-key-here"];
1255
- function requireReferrer(allowedOrigins) {
1427
+ function requireReferrer(allowedOrigins, demoApiKeys = [], logger = createConsoleLogger()) {
1256
1428
  return async (c, next) => {
1257
1429
  const apiKey = c.req.header("x-api-key");
1258
- const isDemoKey = apiKey && DEMO_KEYS2.includes(apiKey);
1430
+ const isDemoKey = apiKey ? demoApiKeys.includes(apiKey) : false;
1259
1431
  const isSessionKey = apiKey?.startsWith("session_");
1260
1432
  if (isDemoKey || isSessionKey) {
1261
1433
  const referer = c.req.header("referer");
1262
1434
  const origin = c.req.header("origin");
1263
- console.log(
1435
+ logger.debug(
1264
1436
  `[Referrer] Checking ${isSessionKey ? "session" : "demo"} key: ${apiKey?.slice(0, 20)}...`
1265
1437
  );
1266
- console.log(`[Referrer] Origin: ${origin || "none"}, Referer: ${referer || "none"}`);
1267
- const isAllowed = allowedOrigins.some((allowed) => {
1268
- if (origin === allowed) return true;
1269
- if (referer) {
1270
- if (referer === allowed) return true;
1271
- if (referer.startsWith(`${allowed}/`)) return true;
1272
- if (referer.startsWith(`${allowed}?`)) return true;
1273
- }
1274
- return false;
1275
- });
1438
+ logger.debug(`[Referrer] Origin: ${origin || "none"}, Referer: ${referer || "none"}`);
1439
+ const isAllowed = matchesAllowedOrigin(origin, referer, allowedOrigins);
1276
1440
  if (!isAllowed) {
1277
- console.log(`[Referrer] BLOCKED - not from allowed origins: ${allowedOrigins.join(", ")}`);
1441
+ logger.debug(`[Referrer] BLOCKED - not from allowed origins: ${allowedOrigins.join(", ")}`);
1278
1442
  return c.json(
1279
1443
  {
1280
1444
  error: "Demo API keys only work on official demo pages. Please visit our website or use your own API key."
@@ -1282,29 +1446,29 @@ function requireReferrer(allowedOrigins) {
1282
1446
  403
1283
1447
  );
1284
1448
  }
1285
- console.log("[Referrer] Allowed - origin matches");
1449
+ logger.debug("[Referrer] Allowed - origin matches");
1286
1450
  }
1287
1451
  await next();
1288
1452
  };
1289
1453
  }
1290
1454
 
1291
1455
  // src/middleware/session.ts
1292
- function validateSessionKey() {
1456
+ function validateSessionKey(logger = createConsoleLogger()) {
1293
1457
  return async (c, next) => {
1294
1458
  const apiKey = c.req.header("x-api-key");
1295
1459
  if (apiKey?.startsWith("session_")) {
1296
- console.log(`[Session] Validating key: ${apiKey.slice(0, 20)}...`);
1460
+ logger.debug(`[Session] Validating key: ${apiKey.slice(0, 20)}...`);
1297
1461
  const info = getSessionInfo(apiKey);
1298
1462
  if (info) {
1299
- console.log(
1463
+ logger.debug(
1300
1464
  `[Session] Key found - requests: ${info.requests}/${info.maxRequests}, expires: ${new Date(info.expiresAt).toISOString()}`
1301
1465
  );
1302
1466
  } else {
1303
- console.log("[Session] Key not found in session store");
1467
+ logger.debug("[Session] Key not found in session store");
1304
1468
  }
1305
1469
  const isValid = validateSession(apiKey);
1306
1470
  if (!isValid) {
1307
- console.log("[Session] REJECTED - expired or quota exceeded");
1471
+ logger.debug("[Session] REJECTED - expired or quota exceeded");
1308
1472
  return c.json(
1309
1473
  {
1310
1474
  error: "Session expired or quota exceeded. Please refresh the page to get a new session."
@@ -1312,7 +1476,7 @@ function validateSessionKey() {
1312
1476
  401
1313
1477
  );
1314
1478
  }
1315
- console.log("[Session] Valid - request count incremented");
1479
+ logger.debug("[Session] Valid - request count incremented");
1316
1480
  }
1317
1481
  await next();
1318
1482
  };
@@ -1326,15 +1490,19 @@ function wantsStream2(c) {
1326
1490
  return acc.includes("text/event-stream");
1327
1491
  }
1328
1492
  function publicRoutes(deps) {
1329
- const { client, store, config, prompts } = deps;
1493
+ const { client, store, config, prompts, logger } = deps;
1330
1494
  const app = new import_hono4.Hono();
1331
1495
  const requirePublicKey = createAuthMiddleware(deps);
1332
- const { limitPublic: limitPublic2 } = createRateLimiter(config);
1496
+ const { limitPublic } = createRateLimiter(config);
1333
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"];
1334
- app.use("/api/public/*", validateSessionKey());
1498
+ app.use("/api/public/*", chatBodyLimit(config.server?.maxRequestBytes));
1499
+ app.use("/api/public/*", validateSessionKey(logger));
1335
1500
  app.use("/api/public/*", requirePublicKey);
1336
- app.use("/api/public/*", requireReferrer(referrerOrigins));
1337
- 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());
1338
1506
  app.post("/api/public/chat", async (c) => {
1339
1507
  const body = await c.req.json().catch(() => ({}));
1340
1508
  const normalized = normalizeChatBody(body);
@@ -1376,7 +1544,8 @@ function publicRoutes(deps) {
1376
1544
 
1377
1545
  // src/server.ts
1378
1546
  async function createServer(config) {
1379
- 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}...`);
1380
1549
  const knowledgeDir = config.knowledgeDir || "./config/knowledge";
1381
1550
  const promptsDir = config.promptsDir || "./config/prompts";
1382
1551
  const publicDir = config.publicDir || "./public";
@@ -1395,24 +1564,26 @@ async function createServer(config) {
1395
1564
  });
1396
1565
  const store = new VectorStore(client, {
1397
1566
  databaseClient: db,
1398
- knowledgeDir
1567
+ knowledgeDir,
1568
+ logger
1399
1569
  });
1400
1570
  await store.build();
1401
- console.log("\u2705 Knowledge base ready");
1571
+ logger.info("\u2705 Knowledge base ready");
1402
1572
  const prompts = new PromptLoader(promptsDir, config.bot);
1403
1573
  let apiKeyManager;
1404
- const secret = process.env.CHATTER_SECRET || config.auth?.secret;
1574
+ const secret = config.auth?.secret || process.env.CHATTER_SECRET;
1405
1575
  if (secret) {
1406
1576
  apiKeyManager = new ApiKeyManager(secret);
1407
- console.log("\u2705 API key manager initialized");
1577
+ logger.info("\u2705 API key manager initialized");
1408
1578
  } else if (config.auth?.apiKeyManager) {
1409
1579
  apiKeyManager = config.auth.apiKeyManager;
1410
- console.log("\u2705 Custom API key manager configured");
1580
+ logger.info("\u2705 Custom API key manager configured");
1411
1581
  }
1412
1582
  const app = new import_hono5.Hono();
1413
1583
  if (config.server?.cors !== false) {
1414
1584
  app.use("*", cors(config.server?.allowedOrigins));
1415
1585
  }
1586
+ app.use("*", securityHeaders(config));
1416
1587
  app.get("/healthz", (c) => c.text("ok"));
1417
1588
  app.get(
1418
1589
  "/config",
@@ -1425,55 +1596,72 @@ async function createServer(config) {
1425
1596
  chat: config.chat || {}
1426
1597
  })
1427
1598
  );
1428
- if (!headless && staticDir) {
1429
- const relativePath = require("node:path").relative(process.cwd(), staticDir);
1430
- app.get("/chatter.js", (0, import_bun.serveStatic)({ path: `${relativePath}/chatter.js` }));
1431
- app.get("/chatter.css", (0, import_bun.serveStatic)({ path: `${relativePath}/chatter.css` }));
1599
+ let serveStatic = null;
1600
+ if (!headless && (staticDir || publicDir)) {
1601
+ try {
1602
+ serveStatic = await loadServeStatic();
1603
+ } catch (error) {
1604
+ logger.error(`\u274C Static assets disabled: ${error.message}`);
1605
+ }
1432
1606
  }
1433
- if (!headless && publicDir) {
1434
- app.get("/", (0, import_bun.serveStatic)({ path: `${publicDir}/index.html` }));
1435
- app.get("/chat", (0, import_bun.serveStatic)({ path: `${publicDir}/chat.html` }));
1436
- app.get("/private", (0, import_bun.serveStatic)({ path: `${publicDir}/private.html` }));
1437
- app.get("/vanilla/chat", (0, import_bun.serveStatic)({ path: `${publicDir}/vanilla/chat.html` }));
1607
+ if (serveStatic && staticDir) {
1608
+ const relativePath = (0, import_node_path4.relative)(process.cwd(), staticDir);
1609
+ app.get("/chatter.js", serveStatic({ path: `${relativePath}/chatter.js` }));
1610
+ app.get("/chatter.css", serveStatic({ path: `${relativePath}/chatter.css` }));
1611
+ }
1612
+ if (serveStatic && publicDir) {
1613
+ app.get("/", serveStatic({ path: `${publicDir}/index.html` }));
1614
+ app.get("/chat", serveStatic({ path: `${publicDir}/chat.html` }));
1615
+ app.get("/private", serveStatic({ path: `${publicDir}/private.html` }));
1616
+ app.get("/vanilla/chat", serveStatic({ path: `${publicDir}/vanilla/chat.html` }));
1438
1617
  app.get(
1439
1618
  "/demo/widget-button-public",
1440
- (0, import_bun.serveStatic)({ path: `${publicDir}/demo/widget-button-public.html` })
1619
+ serveStatic({ path: `${publicDir}/demo/widget-button-public.html` })
1441
1620
  );
1442
1621
  app.get(
1443
1622
  "/demo/widget-button-private",
1444
- (0, import_bun.serveStatic)({ path: `${publicDir}/demo/widget-button-private.html` })
1623
+ serveStatic({ path: `${publicDir}/demo/widget-button-private.html` })
1445
1624
  );
1446
1625
  app.get(
1447
1626
  "/demo/widget-inline-public",
1448
- (0, import_bun.serveStatic)({ path: `${publicDir}/demo/widget-inline-public.html` })
1627
+ serveStatic({ path: `${publicDir}/demo/widget-inline-public.html` })
1449
1628
  );
1450
1629
  app.get(
1451
1630
  "/demo/widget-inline-private",
1452
- (0, import_bun.serveStatic)({ path: `${publicDir}/demo/widget-inline-private.html` })
1631
+ serveStatic({ path: `${publicDir}/demo/widget-inline-private.html` })
1453
1632
  );
1454
1633
  app.get(
1455
1634
  "/demo/react-button-public",
1456
- (0, import_bun.serveStatic)({ path: `${publicDir}/demo/react-button-public.html` })
1635
+ serveStatic({ path: `${publicDir}/demo/react-button-public.html` })
1457
1636
  );
1458
1637
  app.get(
1459
1638
  "/demo/react-button-private",
1460
- (0, import_bun.serveStatic)({ path: `${publicDir}/demo/react-button-private.html` })
1639
+ serveStatic({ path: `${publicDir}/demo/react-button-private.html` })
1461
1640
  );
1462
1641
  app.get(
1463
1642
  "/demo/react-inline-public",
1464
- (0, import_bun.serveStatic)({ path: `${publicDir}/demo/react-inline-public.html` })
1643
+ serveStatic({ path: `${publicDir}/demo/react-inline-public.html` })
1465
1644
  );
1466
1645
  app.get(
1467
1646
  "/demo/react-inline-private",
1468
- (0, import_bun.serveStatic)({ path: `${publicDir}/demo/react-inline-private.html` })
1647
+ serveStatic({ path: `${publicDir}/demo/react-inline-private.html` })
1469
1648
  );
1470
- app.get("/demo/public", (0, import_bun.serveStatic)({ path: `${publicDir}/demo-public.html` }));
1471
- app.get("/demo/private", (0, import_bun.serveStatic)({ path: `${publicDir}/demo-private.html` }));
1472
- app.get("/demo/chatbot", (0, import_bun.serveStatic)({ path: `${publicDir}/demo-chatbot.html` }));
1473
- app.get("/demo/react", (0, import_bun.serveStatic)({ path: `${publicDir}/react-demo.html` }));
1649
+ app.get("/demo/public", serveStatic({ path: `${publicDir}/demo-public.html` }));
1650
+ app.get("/demo/private", serveStatic({ path: `${publicDir}/demo-private.html` }));
1651
+ app.get("/demo/chatbot", serveStatic({ path: `${publicDir}/demo-chatbot.html` }));
1652
+ app.get("/demo/react", serveStatic({ path: `${publicDir}/react-demo.html` }));
1474
1653
  }
1475
- const senders = createSenderRegistry();
1476
- 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
+ };
1477
1665
  if (enablePublic) {
1478
1666
  app.route("/", publicRoutes(deps));
1479
1667
  }
@@ -1495,13 +1683,13 @@ async function createServer(config) {
1495
1683
  try {
1496
1684
  await channel.start(deps);
1497
1685
  started.push(channel);
1498
- console.log(`\u2705 Channel "${channel.name}" started`);
1686
+ logger.info(`\u2705 Channel "${channel.name}" started`);
1499
1687
  } catch (error) {
1500
- console.error(`\u274C Channel "${channel.name}" failed to start:`, error);
1688
+ logger.error(`\u274C Channel "${channel.name}" failed to start:`, error);
1501
1689
  }
1502
1690
  }
1503
1691
  }
1504
- console.log(`\u2705 ${config.bot.name} server ready`);
1692
+ logger.info(`\u2705 ${config.bot.name} server ready`);
1505
1693
  return Object.assign(app, {
1506
1694
  // Scoped to this call's started channels, not global — two servers in
1507
1695
  // one process each stop only their own. A channel's `stop()` throwing
@@ -1513,7 +1701,7 @@ async function createServer(config) {
1513
1701
  try {
1514
1702
  await channel.stop?.();
1515
1703
  } catch (error) {
1516
- console.error(`Channel "${channel.name}" failed to stop:`, error);
1704
+ logger.error(`Channel "${channel.name}" failed to stop:`, error);
1517
1705
  }
1518
1706
  })
1519
1707
  );