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