@diegoaltoworks/chatter 0.44.0 → 0.45.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 (149) hide show
  1. package/dist/client/index.js +615 -0
  2. package/dist/client/index.mjs +592 -0
  3. package/dist/core/session.d.ts +1 -0
  4. package/dist/core/session.d.ts.map +1 -1
  5. package/dist/index.js +2 -1
  6. package/dist/index.mjs +2 -1
  7. package/dist/server.js +1526 -0
  8. package/dist/server.mjs +1497 -0
  9. package/package.json +9 -5
  10. package/dist/auth/apikeys.test.d.ts +0 -2
  11. package/dist/auth/apikeys.test.d.ts.map +0 -1
  12. package/dist/channels/gates.test.d.ts +0 -2
  13. package/dist/channels/gates.test.d.ts.map +0 -1
  14. package/dist/channels/senders.test.d.ts +0 -2
  15. package/dist/channels/senders.test.d.ts.map +0 -1
  16. package/dist/channels/whatsapp/authState.test.d.ts +0 -2
  17. package/dist/channels/whatsapp/authState.test.d.ts.map +0 -1
  18. package/dist/channels/whatsapp/baileys.test.d.ts +0 -2
  19. package/dist/channels/whatsapp/baileys.test.d.ts.map +0 -1
  20. package/dist/channels/whatsapp/channel.test.d.ts +0 -2
  21. package/dist/channels/whatsapp/channel.test.d.ts.map +0 -1
  22. package/dist/channels/whatsapp/crypto.test.d.ts +0 -2
  23. package/dist/channels/whatsapp/crypto.test.d.ts.map +0 -1
  24. package/dist/channels/whatsapp/images.test.d.ts +0 -2
  25. package/dist/channels/whatsapp/images.test.d.ts.map +0 -1
  26. package/dist/channels/whatsapp/inbound.test.d.ts +0 -2
  27. package/dist/channels/whatsapp/inbound.test.d.ts.map +0 -1
  28. package/dist/channels/whatsapp/lease.test.d.ts +0 -2
  29. package/dist/channels/whatsapp/lease.test.d.ts.map +0 -1
  30. package/dist/channels/whatsapp/pairing.test.d.ts +0 -2
  31. package/dist/channels/whatsapp/pairing.test.d.ts.map +0 -1
  32. package/dist/channels/whatsapp/qr.test.d.ts +0 -2
  33. package/dist/channels/whatsapp/qr.test.d.ts.map +0 -1
  34. package/dist/client/__tests__/markdown.test.d.ts +0 -5
  35. package/dist/client/__tests__/markdown.test.d.ts.map +0 -1
  36. package/dist/client/__tests__/mobile-chat.test.d.ts +0 -6
  37. package/dist/client/__tests__/mobile-chat.test.d.ts.map +0 -1
  38. package/dist/client/examples/node.d.ts +0 -5
  39. package/dist/client/examples/node.d.ts.map +0 -1
  40. package/dist/core/answer.test.d.ts +0 -8
  41. package/dist/core/answer.test.d.ts.map +0 -1
  42. package/dist/core/buckets.test.d.ts +0 -10
  43. package/dist/core/buckets.test.d.ts.map +0 -1
  44. package/dist/core/guardrails.test.d.ts +0 -2
  45. package/dist/core/guardrails.test.d.ts.map +0 -1
  46. package/dist/core/loaders.test.d.ts +0 -2
  47. package/dist/core/loaders.test.d.ts.map +0 -1
  48. package/dist/core/messages.test.d.ts +0 -8
  49. package/dist/core/messages.test.d.ts.map +0 -1
  50. package/dist/core/pipeline.test.d.ts +0 -8
  51. package/dist/core/pipeline.test.d.ts.map +0 -1
  52. package/dist/core/prompts.test.d.ts +0 -2
  53. package/dist/core/prompts.test.d.ts.map +0 -1
  54. package/dist/core/retrieval.test.d.ts +0 -8
  55. package/dist/core/retrieval.test.d.ts.map +0 -1
  56. package/dist/core/session.test.d.ts +0 -2
  57. package/dist/core/session.test.d.ts.map +0 -1
  58. package/dist/core/widgets.test.d.ts +0 -2
  59. package/dist/core/widgets.test.d.ts.map +0 -1
  60. package/dist/flows/__fixtures__/broken/handler.d.ts +0 -3
  61. package/dist/flows/__fixtures__/broken/handler.d.ts.map +0 -1
  62. package/dist/flows/__fixtures__/greeting/handler.d.ts +0 -3
  63. package/dist/flows/__fixtures__/greeting/handler.d.ts.map +0 -1
  64. package/dist/flows/__fixtures__/greeting/prefill.d.ts +0 -3
  65. package/dist/flows/__fixtures__/greeting/prefill.d.ts.map +0 -1
  66. package/dist/flows/engine.test.d.ts +0 -9
  67. package/dist/flows/engine.test.d.ts.map +0 -1
  68. package/dist/flows/intent.test.d.ts +0 -6
  69. package/dist/flows/intent.test.d.ts.map +0 -1
  70. package/dist/flows/loader.test.d.ts +0 -8
  71. package/dist/flows/loader.test.d.ts.map +0 -1
  72. package/dist/flows/params.test.d.ts +0 -2
  73. package/dist/flows/params.test.d.ts.map +0 -1
  74. package/dist/flows/registry.test.d.ts +0 -2
  75. package/dist/flows/registry.test.d.ts.map +0 -1
  76. package/dist/flows/session.test.d.ts +0 -7
  77. package/dist/flows/session.test.d.ts.map +0 -1
  78. package/dist/flows/utils.test.d.ts +0 -2
  79. package/dist/flows/utils.test.d.ts.map +0 -1
  80. package/dist/images/cache.test.d.ts +0 -2
  81. package/dist/images/cache.test.d.ts.map +0 -1
  82. package/dist/images/cacheStore.test.d.ts +0 -8
  83. package/dist/images/cacheStore.test.d.ts.map +0 -1
  84. package/dist/images/captionComposer.test.d.ts +0 -2
  85. package/dist/images/captionComposer.test.d.ts.map +0 -1
  86. package/dist/images/cloudinaryClient.test.d.ts +0 -2
  87. package/dist/images/cloudinaryClient.test.d.ts.map +0 -1
  88. package/dist/images/cloudinaryConfig.test.d.ts +0 -2
  89. package/dist/images/cloudinaryConfig.test.d.ts.map +0 -1
  90. package/dist/images/cloudinarySignature.test.d.ts +0 -2
  91. package/dist/images/cloudinarySignature.test.d.ts.map +0 -1
  92. package/dist/images/config.test.d.ts +0 -2
  93. package/dist/images/config.test.d.ts.map +0 -1
  94. package/dist/images/generator.test.d.ts +0 -2
  95. package/dist/images/generator.test.d.ts.map +0 -1
  96. package/dist/images/openaiClient.test.d.ts +0 -2
  97. package/dist/images/openaiClient.test.d.ts.map +0 -1
  98. package/dist/images/uploader.test.d.ts +0 -2
  99. package/dist/images/uploader.test.d.ts.map +0 -1
  100. package/dist/mcp-server/conversation-id.test.d.ts +0 -2
  101. package/dist/mcp-server/conversation-id.test.d.ts.map +0 -1
  102. package/dist/mcp-server/cost-tracker.test.d.ts +0 -2
  103. package/dist/mcp-server/cost-tracker.test.d.ts.map +0 -1
  104. package/dist/mcp-server/logger.test.d.ts +0 -2
  105. package/dist/mcp-server/logger.test.d.ts.map +0 -1
  106. package/dist/mcp-server/rate-limiter.test.d.ts +0 -2
  107. package/dist/mcp-server/rate-limiter.test.d.ts.map +0 -1
  108. package/dist/middleware/auth.test.d.ts +0 -2
  109. package/dist/middleware/auth.test.d.ts.map +0 -1
  110. package/dist/middleware/cors.test.d.ts +0 -2
  111. package/dist/middleware/cors.test.d.ts.map +0 -1
  112. package/dist/middleware/jwt.test.d.ts +0 -2
  113. package/dist/middleware/jwt.test.d.ts.map +0 -1
  114. package/dist/middleware/ratelimit.test.d.ts +0 -2
  115. package/dist/middleware/ratelimit.test.d.ts.map +0 -1
  116. package/dist/middleware/referrer.test.d.ts +0 -2
  117. package/dist/middleware/referrer.test.d.ts.map +0 -1
  118. package/dist/middleware/session.test.d.ts +0 -2
  119. package/dist/middleware/session.test.d.ts.map +0 -1
  120. package/dist/personas/greeter.test.d.ts +0 -2
  121. package/dist/personas/greeter.test.d.ts.map +0 -1
  122. package/dist/personas/resolver.test.d.ts +0 -2
  123. package/dist/personas/resolver.test.d.ts.map +0 -1
  124. package/dist/personas/timeContext.test.d.ts +0 -2
  125. package/dist/personas/timeContext.test.d.ts.map +0 -1
  126. package/dist/routes/chat.test.d.ts +0 -9
  127. package/dist/routes/chat.test.d.ts.map +0 -1
  128. package/dist/routes/openai.test.d.ts +0 -8
  129. package/dist/routes/openai.test.d.ts.map +0 -1
  130. package/dist/scheduler/claimStore.test.d.ts +0 -2
  131. package/dist/scheduler/claimStore.test.d.ts.map +0 -1
  132. package/dist/scheduler/compose.test.d.ts +0 -2
  133. package/dist/scheduler/compose.test.d.ts.map +0 -1
  134. package/dist/scheduler/deliver.test.d.ts +0 -2
  135. package/dist/scheduler/deliver.test.d.ts.map +0 -1
  136. package/dist/scheduler/dueDrops.test.d.ts +0 -2
  137. package/dist/scheduler/dueDrops.test.d.ts.map +0 -1
  138. package/dist/scheduler/scheduler.test.d.ts +0 -2
  139. package/dist/scheduler/scheduler.test.d.ts.map +0 -1
  140. package/dist/scheduler/tick.test.d.ts +0 -2
  141. package/dist/scheduler/tick.test.d.ts.map +0 -1
  142. package/dist/server.test.d.ts +0 -9
  143. package/dist/server.test.d.ts.map +0 -1
  144. package/dist/shortener.test.d.ts +0 -2
  145. package/dist/shortener.test.d.ts.map +0 -1
  146. package/dist/usage/limits.test.d.ts +0 -10
  147. package/dist/usage/limits.test.d.ts.map +0 -1
  148. package/dist/usage/tursoStore.test.d.ts +0 -9
  149. package/dist/usage/tursoStore.test.d.ts.map +0 -1
@@ -0,0 +1,1497 @@
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
+ // src/server.ts
9
+ import { createClient as createClient2 } from "@libsql/client";
10
+ import { Hono as Hono5 } from "hono";
11
+ import { serveStatic } from "hono/bun";
12
+ import OpenAI from "openai";
13
+
14
+ // src/auth/apikeys.ts
15
+ import { randomUUID } from "node:crypto";
16
+ import { jwtVerify, SignJWT } from "jose";
17
+ var ApiKeyManager = class {
18
+ secretBytes;
19
+ constructor(secret) {
20
+ if (!secret) {
21
+ throw new Error("API key secret is required");
22
+ }
23
+ this.secretBytes = new TextEncoder().encode(secret);
24
+ }
25
+ /**
26
+ * Create a new API key
27
+ *
28
+ * @param options - API key options
29
+ * @returns JWT token string
30
+ */
31
+ async create(options = {}) {
32
+ const keyId = randomUUID();
33
+ const keyName = options.name || "api-key";
34
+ const expiresIn = options.expiresIn || "365d";
35
+ const expirationMs = this.parseExpiration(expiresIn);
36
+ const expiresAt = new Date(Date.now() + expirationMs);
37
+ const jwt = await new SignJWT({
38
+ name: keyName,
39
+ type: "api_key",
40
+ ...options.claims
41
+ }).setProtectedHeader({ alg: "HS256" }).setSubject(keyId).setIssuedAt().setExpirationTime(expiresAt).sign(this.secretBytes);
42
+ return jwt;
43
+ }
44
+ /**
45
+ * Verify an API key
46
+ *
47
+ * @param token - JWT token to verify
48
+ * @returns Verification result with payload if valid
49
+ */
50
+ async verify(token) {
51
+ try {
52
+ const { payload } = await jwtVerify(token, this.secretBytes);
53
+ if (payload.type !== "api_key") {
54
+ return {
55
+ valid: false,
56
+ error: "Token is not an API key"
57
+ };
58
+ }
59
+ return {
60
+ valid: true,
61
+ payload
62
+ };
63
+ } catch (error) {
64
+ return {
65
+ valid: false,
66
+ error: error instanceof Error ? error.message : "Verification failed"
67
+ };
68
+ }
69
+ }
70
+ /**
71
+ * Parse expiration string to milliseconds
72
+ *
73
+ * @param expiresIn - Time string (e.g., "1y", "30d", "12h")
74
+ * @returns Milliseconds
75
+ */
76
+ parseExpiration(expiresIn) {
77
+ const match = expiresIn.match(/^(\d+)([smhdMy])$/);
78
+ if (!match) {
79
+ throw new Error(
80
+ `Invalid expiration format: ${expiresIn}. Supported: s (seconds), m (minutes), h (hours), d (days), M (months), y (years)`
81
+ );
82
+ }
83
+ const value = Number.parseInt(match[1], 10);
84
+ const unit = match[2];
85
+ switch (unit) {
86
+ case "s":
87
+ return value * 1e3;
88
+ case "m":
89
+ return value * 60 * 1e3;
90
+ case "h":
91
+ return value * 60 * 60 * 1e3;
92
+ case "d":
93
+ return value * 24 * 60 * 60 * 1e3;
94
+ case "M":
95
+ return value * 30 * 24 * 60 * 60 * 1e3;
96
+ // Approximate 30 days
97
+ case "y":
98
+ return value * 365 * 24 * 60 * 60 * 1e3;
99
+ // Approximate 365 days
100
+ default:
101
+ throw new Error(`Unknown time unit: ${unit}`);
102
+ }
103
+ }
104
+ /**
105
+ * Get information about a token without verifying signature
106
+ * Useful for debugging
107
+ *
108
+ * @param token - JWT token
109
+ * @returns Decoded payload (unverified)
110
+ */
111
+ decode(token) {
112
+ try {
113
+ const parts = token.split(".");
114
+ if (parts.length !== 3) {
115
+ return null;
116
+ }
117
+ const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf-8"));
118
+ return payload;
119
+ } catch {
120
+ return null;
121
+ }
122
+ }
123
+ };
124
+
125
+ // src/channels/senders.ts
126
+ function createSenderRegistry() {
127
+ const senders = /* @__PURE__ */ new Map();
128
+ return {
129
+ register(name, sender) {
130
+ if (senders.has(name)) {
131
+ console.warn(`Channel sender "${name}" is already registered; overwriting.`);
132
+ }
133
+ senders.set(name, sender);
134
+ },
135
+ unregister(name) {
136
+ senders.delete(name);
137
+ },
138
+ available(name) {
139
+ return senders.has(name);
140
+ },
141
+ async sendText(name, chatId, text) {
142
+ const sender = senders.get(name);
143
+ if (!sender) return false;
144
+ try {
145
+ await sender.sendText(chatId, text);
146
+ return true;
147
+ } catch (error) {
148
+ console.warn(`Channel sender "${name}" sendText failed:`, error);
149
+ return false;
150
+ }
151
+ },
152
+ async sendVoice(name, chatId, payload) {
153
+ const sender = senders.get(name);
154
+ if (!sender?.sendVoice) return false;
155
+ try {
156
+ await sender.sendVoice(chatId, payload);
157
+ return true;
158
+ } catch (error) {
159
+ console.warn(`Channel sender "${name}" sendVoice failed:`, error);
160
+ return false;
161
+ }
162
+ },
163
+ async sendMedia(name, chatId, payload) {
164
+ const sender = senders.get(name);
165
+ if (!sender?.sendMedia) return false;
166
+ try {
167
+ await sender.sendMedia(chatId, payload);
168
+ return true;
169
+ } catch (error) {
170
+ console.warn(`Channel sender "${name}" sendMedia failed:`, error);
171
+ return false;
172
+ }
173
+ }
174
+ };
175
+ }
176
+
177
+ // src/core/prompts.ts
178
+ import { readFileSync } from "node:fs";
179
+ import { join } from "node:path";
180
+ var PromptLoader = class {
181
+ constructor(promptsDir, bot) {
182
+ this.promptsDir = promptsDir;
183
+ this.bot = bot;
184
+ }
185
+ promptsDir;
186
+ bot;
187
+ /**
188
+ * Load a prompt template and interpolate variables
189
+ */
190
+ load(filename) {
191
+ const path = join(this.promptsDir, filename);
192
+ const template = readFileSync(path, "utf-8");
193
+ const personFirstName = this.bot.personName.split(" ")[0];
194
+ return template.replace(/\{\{botName\}\}/g, this.bot.name).replace(/\{\{personName\}\}/g, this.bot.personName).replace(/\{\{personFirstName\}\}/g, personFirstName);
195
+ }
196
+ /** Base system rules prompt */
197
+ get baseSystemRules() {
198
+ return this.load("base.txt");
199
+ }
200
+ /** Public persona prompt */
201
+ get publicPersona() {
202
+ return this.load("public.txt");
203
+ }
204
+ /** Private persona prompt */
205
+ get privatePersona() {
206
+ return this.load("private.txt");
207
+ }
208
+ };
209
+
210
+ // src/core/retrieval.ts
211
+ import { createClient } from "@libsql/client";
212
+
213
+ // src/core/loaders.ts
214
+ import { readdirSync, readFileSync as readFileSync2, statSync } from "node:fs";
215
+ import { join as join2 } from "node:path";
216
+ function walk(dir) {
217
+ const items = [];
218
+ try {
219
+ for (const e of readdirSync(dir)) {
220
+ const p = join2(dir, e);
221
+ const st = statSync(p);
222
+ if (st.isDirectory()) items.push(...walk(p));
223
+ else if (p.endsWith(".md")) items.push(p);
224
+ }
225
+ } catch {
226
+ }
227
+ return items;
228
+ }
229
+ function loadKnowledge(root = "config/knowledge") {
230
+ const docs = [];
231
+ for (const bucket of ["base", "public", "private"]) {
232
+ const dir = join2(root, bucket);
233
+ for (const f of walk(dir)) {
234
+ docs.push({ id: f, text: readFileSync2(f, "utf8"), source: f, bucket });
235
+ }
236
+ }
237
+ return docs;
238
+ }
239
+
240
+ // src/core/retrieval.ts
241
+ var EMB_MODEL = "text-embedding-3-large";
242
+ function chunk(text, max = 900) {
243
+ const out = [];
244
+ let buf = "";
245
+ for (const line of text.replace(/\r/g, "").split("\n")) {
246
+ if (`${buf}
247
+ ${line}`.length > max) {
248
+ out.push(buf.trim());
249
+ buf = line;
250
+ } else buf += `
251
+ ${line}`;
252
+ }
253
+ if (buf.trim()) out.push(buf.trim());
254
+ return out;
255
+ }
256
+ async function sha256(input) {
257
+ const data = new TextEncoder().encode(input);
258
+ const hash = await crypto.subtle.digest("SHA-256", data);
259
+ return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
260
+ }
261
+ var VectorStore = class _VectorStore {
262
+ constructor(client, options) {
263
+ this.client = client;
264
+ this.db = options.databaseClient ? options.databaseClient : createClient({
265
+ url: options.databaseUrl,
266
+ authToken: options.databaseAuthToken
267
+ });
268
+ this.knowledgeDir = options.knowledgeDir || "./config/knowledge";
269
+ }
270
+ client;
271
+ /**
272
+ * The libsql client backing this store. When a client was injected this is
273
+ * that same instance, so callers holding it (e.g. `ServerDependencies.db`)
274
+ * and the store share one connection.
275
+ */
276
+ db;
277
+ knowledgeDir;
278
+ // On boot: ingest new chunks and embed only missing ones.
279
+ async build() {
280
+ console.log("\u{1F504} Building knowledge base...");
281
+ await this.db.execute(`
282
+ CREATE TABLE IF NOT EXISTS chunks (id TEXT PRIMARY KEY, bucket TEXT NOT NULL, source TEXT NOT NULL, text TEXT NOT NULL);
283
+ `);
284
+ await this.db.execute(`
285
+ CREATE TABLE IF NOT EXISTS embeddings (id TEXT PRIMARY KEY, model TEXT NOT NULL, embedding BLOB NOT NULL);
286
+ `);
287
+ await this.db.execute("CREATE INDEX IF NOT EXISTS idx_chunks_bucket ON chunks(bucket);");
288
+ const docs = loadKnowledge(this.knowledgeDir);
289
+ console.log(`\u{1F4DA} Loaded ${docs.length} knowledge documents`);
290
+ const rows = [];
291
+ for (const d of docs) {
292
+ for (const part of chunk(d.text)) {
293
+ const id = await sha256(`${d.bucket}|${d.source}|${part}`);
294
+ rows.push({ id, bucket: d.bucket, source: d.source, text: part });
295
+ }
296
+ }
297
+ console.log(`\u{1F4E6} Created ${rows.length} chunks from knowledge documents`);
298
+ 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({
303
+ sql: `INSERT INTO chunks(id,bucket,source,text) VALUES(?,?,?,?)
304
+ ON CONFLICT(id) DO NOTHING`,
305
+ 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;
312
+ }
313
+ const ids = rows.map((r) => r.id);
314
+ const missing = [];
315
+ for (let i = 0; i < ids.length; i += 500) {
316
+ const batch = ids.slice(i, i + 500);
317
+ const placeholders = batch.map(() => "?").join(",");
318
+ const res = await this.db.execute({
319
+ sql: `SELECT id FROM chunks WHERE id IN (${placeholders})
320
+ EXCEPT SELECT id FROM embeddings`,
321
+ args: batch
322
+ });
323
+ for (const row of res.rows) missing.push(String(row.id));
324
+ }
325
+ if (missing.length === 0) {
326
+ console.log("\u2705 No new chunks to embed - knowledge base is up to date");
327
+ return;
328
+ }
329
+ console.log(`\u{1F52E} Embedding ${missing.length} new/updated chunks with ${EMB_MODEL}...`);
330
+ const textById = new Map(rows.map((r) => [r.id, r.text]));
331
+ const BATCH = 96;
332
+ for (let i = 0; i < missing.length; i += BATCH) {
333
+ const batchIds = missing.slice(i, i + BATCH);
334
+ const inputs = batchIds.map((id) => textById.get(id) || "");
335
+ 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
+ }
350
+ }
351
+ console.log(`\u2705 Successfully embedded ${missing.length} new chunks`);
352
+ }
353
+ // Remove chunks from database that no longer exist in markdown files
354
+ async cleanupStaleChunks(currentIds) {
355
+ const result = await this.db.execute("SELECT id FROM chunks");
356
+ const dbIds = result.rows.map((row) => String(row.id));
357
+ const currentIdSet = new Set(currentIds);
358
+ const staleIds = dbIds.filter((id) => !currentIdSet.has(id));
359
+ if (staleIds.length === 0) {
360
+ return;
361
+ }
362
+ console.log(`\u{1F9F9} Cleaning up ${staleIds.length} stale chunks...`);
363
+ const BATCH_SIZE = 500;
364
+ for (let i = 0; i < staleIds.length; i += BATCH_SIZE) {
365
+ const batch = staleIds.slice(i, i + BATCH_SIZE);
366
+ const placeholders = batch.map(() => "?").join(",");
367
+ await this.db.execute({
368
+ sql: `DELETE FROM chunks WHERE id IN (${placeholders})`,
369
+ args: batch
370
+ });
371
+ }
372
+ console.log(`\u2713 Cleaned up ${staleIds.length} stale chunks and their embeddings`);
373
+ }
374
+ static cosine(a, b) {
375
+ let dot = 0;
376
+ let na = 0;
377
+ let nb = 0;
378
+ for (let i = 0; i < a.length; i++) {
379
+ dot += a[i] * b[i];
380
+ na += a[i] * a[i];
381
+ nb += b[i] * b[i];
382
+ }
383
+ return dot / (Math.sqrt(na) * Math.sqrt(nb));
384
+ }
385
+ /**
386
+ * Retrieve top-k across allowed buckets; compute similarity in app (simple
387
+ * & portable).
388
+ *
389
+ * `allowed` is a bucket-name list rather than the {@link Bucket} union: the
390
+ * `chunks` table constrains nothing, so a deployment that ingests its own
391
+ * buckets can query them (`build` only writes and prunes the three the
392
+ * knowledge loader walks). Names are bound as query parameters, never
393
+ * interpolated. An empty list retrieves nothing, and short-circuits before
394
+ * the embedding call.
395
+ */
396
+ async query(q, k = 6, allowed = ["base"]) {
397
+ if (allowed.length === 0) return [];
398
+ const qv = (await this.client.embeddings.create({ model: EMB_MODEL, input: [q] })).data[0].embedding;
399
+ const placeholders = allowed.map(() => "?").join(",");
400
+ const res = await this.db.execute({
401
+ 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
+ }
616
+
617
+ // src/core/messages.ts
618
+ function isClientRole(role) {
619
+ return role === "user" || role === "assistant";
620
+ }
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("");
635
+ }
636
+ if (text === null) continue;
637
+ messages.push({ role, content: text });
638
+ }
639
+ return messages.length > 0 ? messages : null;
640
+ }
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" };
646
+ }
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
+ };
653
+ }
654
+ return { ok: true, messages };
655
+ }
656
+ const { message } = source;
657
+ if (message && typeof message !== "object") {
658
+ return { ok: true, messages: [{ role: "user", content: String(message) }] };
659
+ }
660
+ return { ok: false, error: "either 'message' or 'messages' required" };
661
+ }
662
+
663
+ // src/core/session.ts
664
+ import { randomUUID as randomUUID2 } from "node:crypto";
665
+ var sessions = /* @__PURE__ */ new Map();
666
+ var sessionSweeper = setInterval(
667
+ () => {
668
+ const now = Date.now();
669
+ for (const [key, data] of Array.from(sessions.entries())) {
670
+ if (now > data.expiresAt) {
671
+ sessions.delete(key);
672
+ }
673
+ }
674
+ },
675
+ 5 * 60 * 1e3
676
+ );
677
+ sessionSweeper.unref?.();
678
+ function createSession(options = {}) {
679
+ const { maxRequests = 20, ttl = 3600, metadata } = options;
680
+ const key = `session_${randomUUID2()}`;
681
+ const now = Date.now();
682
+ const session = {
683
+ key,
684
+ created: now,
685
+ requests: 0,
686
+ maxRequests,
687
+ expiresAt: now + ttl * 1e3,
688
+ ...metadata && { metadata }
689
+ };
690
+ sessions.set(key, session);
691
+ return session;
692
+ }
693
+ function validateSession(key) {
694
+ const session = sessions.get(key);
695
+ if (!session) return false;
696
+ const now = Date.now();
697
+ if (now > session.expiresAt) {
698
+ sessions.delete(key);
699
+ return false;
700
+ }
701
+ if (session.requests >= session.maxRequests) {
702
+ return false;
703
+ }
704
+ session.requests++;
705
+ return true;
706
+ }
707
+ function getSessionInfo(key) {
708
+ return sessions.get(key) || null;
709
+ }
710
+ function getActiveSessions() {
711
+ return sessions.size;
712
+ }
713
+
714
+ // src/routes/demo.ts
715
+ function checkDemoOrigin(allowedOrigins) {
716
+ const allowAll = allowedOrigins.length === 0 || allowedOrigins.includes("*");
717
+ 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
+ if (allowAll) {
725
+ return next();
726
+ }
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();
734
+ }
735
+ return c.json(
736
+ {
737
+ error: "Demo access restricted to authorized domains"
738
+ },
739
+ 403
740
+ );
741
+ };
742
+ }
743
+ function limitSessionCreation() {
744
+ const buckets = /* @__PURE__ */ new Map();
745
+ const WINDOW_MS = 36e5;
746
+ const LIMIT = 10;
747
+ 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) {
759
+ return c.json(
760
+ {
761
+ error: "Session key limit exceeded (10 per hour). Please wait or use your own API key."
762
+ },
763
+ 429
764
+ );
765
+ }
766
+ await next();
767
+ };
768
+ }
769
+ function limitDemo() {
770
+ const buckets = /* @__PURE__ */ new Map();
771
+ const WINDOW_MS = 6e4;
772
+ const LIMIT = 10;
773
+ 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) {
785
+ return c.json(
786
+ {
787
+ error: "Demo rate limit exceeded (10 requests/minute). Please wait or use your own API key."
788
+ },
789
+ 429
790
+ );
791
+ }
792
+ await next();
793
+ };
794
+ }
795
+ function demoRoutes(deps) {
796
+ const { client, store, config, prompts } = deps;
797
+ const app = new Hono();
798
+ 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
+ });
815
+ app.get("/api/demo/stats", (c) => {
816
+ return c.json({
817
+ activeSessions: getActiveSessions()
818
+ });
819
+ });
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 })}
852
+
853
+ `);
854
+ }
855
+ await s.write("event: end\ndata: {}\n\n");
856
+ });
857
+ }
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
+ }
871
+ });
872
+ return app;
873
+ }
874
+
875
+ // src/routes/openai.ts
876
+ import { Hono as Hono2 } from "hono";
877
+ import { stream as stream2 } from "hono/streaming";
878
+
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
+ // src/middleware/jwt.ts
912
+ import { createRemoteJWKSet, importSPKI, jwtVerify as jwtVerify2 } from "jose";
913
+ function jwtSubject(c) {
914
+ const sub = c.jwtSub;
915
+ return sub && sub.trim().length > 0 ? sub : void 0;
916
+ }
917
+ function createJWTMiddleware(config) {
918
+ let jwks = null;
919
+ let spkiKey = null;
920
+ const jwtConfig = config.auth?.jwt;
921
+ async function verify(token) {
922
+ const opts = {};
923
+ if (jwtConfig?.issuer) opts.issuer = jwtConfig.issuer;
924
+ if (jwtConfig?.audience) opts.audience = jwtConfig.audience;
925
+ if (jwtConfig?.jwksUrl) {
926
+ jwks = jwks ?? createRemoteJWKSet(new URL(jwtConfig.jwksUrl));
927
+ const { payload } = await jwtVerify2(token, jwks, opts);
928
+ return payload;
929
+ }
930
+ if (jwtConfig?.publicKeyPem) {
931
+ spkiKey = spkiKey ?? await importSPKI(jwtConfig.publicKeyPem, "RS256");
932
+ const { payload } = await jwtVerify2(token, spkiKey, opts);
933
+ return payload;
934
+ }
935
+ throw new Error("JWT configuration missing (JWKS URL or PUBLIC KEY PEM).");
936
+ }
937
+ return async function requirePrivateJWT2(c, next) {
938
+ const hdr = c.req.header("authorization") ?? "";
939
+ if (!hdr.startsWith("Bearer ")) return c.json({ error: "Unauthorized" }, 401);
940
+ const token = hdr.slice("Bearer ".length);
941
+ try {
942
+ const payload = await verify(token);
943
+ c.jwtSub = String(payload.sub ?? "");
944
+ await next();
945
+ } catch {
946
+ return c.json({ error: "Unauthorized" }, 401);
947
+ }
948
+ };
949
+ }
950
+ var requirePrivateJWT = createJWTMiddleware({});
951
+
952
+ // 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
+ function createRateLimiter(config) {
960
+ const buckets = /* @__PURE__ */ new Map();
961
+ const WINDOW_MS = 6e4;
962
+ const publicLimit = config.rateLimit?.public || 60;
963
+ 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
+ }
975
+ return {
976
+ // Public: key is IP only
977
+ // Demo keys get 10x stricter rate limits
978
+ limitPublic() {
979
+ return async (c, next) => {
980
+ const apiKey = c.req.header("x-api-key");
981
+ const isDemoKey = apiKey ? DEMO_KEYS.includes(apiKey) : false;
982
+ const key = `pub:${getKeyFromIP(c)}`;
983
+ const limit = isDemoKey ? Math.floor(publicLimit / 10) : publicLimit;
984
+ if (!allow(key, limit)) {
985
+ return c.json(
986
+ {
987
+ error: isDemoKey ? "Demo rate limit exceeded. Please use your own API key for higher limits." : "Rate limit exceeded"
988
+ },
989
+ 429
990
+ );
991
+ }
992
+ await next();
993
+ };
994
+ },
995
+ // Private: key is JWT subject only
996
+ limitPrivate() {
997
+ return async (c, next) => {
998
+ const sub = c.jwtSub;
999
+ const key = `private:${sub ?? "no-sub"}`;
1000
+ if (!allow(key, privateLimit)) return c.json({ error: "Rate limit" }, 429);
1001
+ await next();
1002
+ };
1003
+ }
1004
+ };
1005
+ }
1006
+ var defaultLimiter = createRateLimiter({});
1007
+ var limitPublic = defaultLimiter.limitPublic;
1008
+ var limitPrivate = defaultLimiter.limitPrivate;
1009
+
1010
+ // src/routes/openai.ts
1011
+ function errorJson(c, status, message) {
1012
+ return c.json(
1013
+ { error: { message, type: status === 401 ? "authentication_error" : "invalid_request_error" } },
1014
+ status
1015
+ );
1016
+ }
1017
+ function openaiRoutes(deps) {
1018
+ const { client, store, config, prompts, apiKeyManager } = deps;
1019
+ const app = new Hono2();
1020
+ const { limitPublic: limitPublic2, limitPrivate: limitPrivate2 } = createRateLimiter(config);
1021
+ const requirePrivateJWT2 = createJWTMiddleware(config);
1022
+ const enablePublic = config.features?.enablePublicChat !== false;
1023
+ const enablePrivate = config.features?.enablePrivateChat !== false;
1024
+ const serverModel = config.openai.model || DEFAULT_MODEL;
1025
+ async function requireApiKey(c, next) {
1026
+ if (!apiKeyManager) {
1027
+ return errorJson(c, 401, "API key authentication is not configured on this server");
1028
+ }
1029
+ const bearer = c.req.header("authorization") ?? "";
1030
+ const key = bearer.startsWith("Bearer ") ? bearer.slice("Bearer ".length) : c.req.header("x-api-key");
1031
+ if (!key) {
1032
+ return errorJson(c, 401, "Missing API key (Authorization: Bearer or x-api-key header)");
1033
+ }
1034
+ try {
1035
+ const result = await apiKeyManager.verify(key);
1036
+ if (result.valid) return await next();
1037
+ } catch {
1038
+ }
1039
+ return errorJson(c, 401, "Invalid API key");
1040
+ }
1041
+ const handleChatCompletions = (mode) => async (c) => {
1042
+ const body = await c.req.json().catch(() => null);
1043
+ if (!body || typeof body !== "object") {
1044
+ return errorJson(c, 400, "Request body must be a JSON object");
1045
+ }
1046
+ const messages = normalizeMessages(body.messages);
1047
+ if (!messages) {
1048
+ return errorJson(c, 400, "'messages' must be a non-empty array of user/assistant messages");
1049
+ }
1050
+ const { stream: wantsStream3, temperature: rawTemperature } = body;
1051
+ const temperature = typeof rawTemperature === "number" && Number.isFinite(rawTemperature) ? Math.min(Math.max(rawTemperature, 0), 2) : void 0;
1052
+ const buckets = await resolveBuckets({
1053
+ mode,
1054
+ sender: jwtSubject(c),
1055
+ bucketsFor: config.bucketsFor
1056
+ });
1057
+ let system;
1058
+ try {
1059
+ ({ system } = await prepareChat({ store, prompts, mode, messages, buckets }));
1060
+ } catch {
1061
+ return errorJson(c, 400, "Conversation must contain at least one user message");
1062
+ }
1063
+ const id = `chatcmpl-${crypto.randomUUID()}`;
1064
+ const created = Math.floor(Date.now() / 1e3);
1065
+ if (wantsStream3 === true) {
1066
+ c.header("Content-Type", "text/event-stream");
1067
+ c.header("Cache-Control", "no-cache");
1068
+ return stream2(c, async (s) => {
1069
+ const chunk2 = (delta, finishReason) => `data: ${JSON.stringify({
1070
+ id,
1071
+ object: "chat.completion.chunk",
1072
+ created,
1073
+ model: serverModel,
1074
+ choices: [{ index: 0, delta, finish_reason: finishReason }]
1075
+ })}
1076
+
1077
+ `;
1078
+ await s.write(chunk2({ role: "assistant", content: "" }, null));
1079
+ for await (const delta of answerStream({
1080
+ answerFn: config.answerFn,
1081
+ client,
1082
+ system,
1083
+ messages,
1084
+ mode,
1085
+ temperature,
1086
+ model: serverModel
1087
+ })) {
1088
+ await s.write(chunk2({ content: delta }, null));
1089
+ }
1090
+ await s.write(chunk2({}, "stop"));
1091
+ await s.write("data: [DONE]\n\n");
1092
+ });
1093
+ }
1094
+ const out = await answerOnce({
1095
+ answerFn: config.answerFn,
1096
+ client,
1097
+ system,
1098
+ messages,
1099
+ mode,
1100
+ temperature,
1101
+ model: serverModel
1102
+ });
1103
+ return c.json({
1104
+ id,
1105
+ object: "chat.completion",
1106
+ created,
1107
+ model: serverModel,
1108
+ choices: [
1109
+ { index: 0, message: { role: "assistant", content: out.content }, finish_reason: "stop" }
1110
+ ],
1111
+ usage: out.usage
1112
+ });
1113
+ };
1114
+ if (enablePublic) {
1115
+ app.use("/v1/*", requireApiKey);
1116
+ app.use("/v1/*", limitPublic2());
1117
+ app.post("/v1/chat/completions", handleChatCompletions("public"));
1118
+ }
1119
+ if (enablePrivate) {
1120
+ app.use("/api/private/v1/*", requirePrivateJWT2);
1121
+ app.use("/api/private/v1/*", limitPrivate2());
1122
+ app.post("/api/private/v1/chat/completions", handleChatCompletions("private"));
1123
+ }
1124
+ return app;
1125
+ }
1126
+
1127
+ // src/routes/private.ts
1128
+ import { Hono as Hono3 } from "hono";
1129
+ import { stream as stream3 } from "hono/streaming";
1130
+ function wantsStream(c) {
1131
+ const url = new URL(c.req.url);
1132
+ if (url.searchParams.get("stream") === "1") return true;
1133
+ const acc = c.req.header("accept") ?? "";
1134
+ return acc.includes("text/event-stream");
1135
+ }
1136
+ function privateRoutes(deps) {
1137
+ const { client, store, config, prompts } = deps;
1138
+ 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());
1143
+ app.post("/api/private/chat", async (c) => {
1144
+ const body = await c.req.json().catch(() => ({}));
1145
+ const normalized = normalizeChatBody(body);
1146
+ if (!normalized.ok) {
1147
+ return c.json({ error: normalized.error }, 400);
1148
+ }
1149
+ const { messages } = normalized;
1150
+ const buckets = await resolveBuckets({
1151
+ mode: "private",
1152
+ sender: jwtSubject(c),
1153
+ bucketsFor: config.bucketsFor
1154
+ });
1155
+ let system;
1156
+ try {
1157
+ ({ system } = await prepareChat({ store, prompts, mode: "private", messages, buckets }));
1158
+ } catch {
1159
+ return c.json({ error: "no user message found in conversation" }, 400);
1160
+ }
1161
+ const model = config.openai.model;
1162
+ const answerFn = config.answerFn;
1163
+ if (wantsStream(c)) {
1164
+ return stream3(c, async (s) => {
1165
+ for await (const delta of answerStream({
1166
+ answerFn,
1167
+ client,
1168
+ system,
1169
+ messages,
1170
+ mode: "private",
1171
+ model
1172
+ })) {
1173
+ await s.write(`data: ${JSON.stringify({ delta })}
1174
+
1175
+ `);
1176
+ }
1177
+ await s.write("event: end\ndata: {}\n\n");
1178
+ });
1179
+ }
1180
+ const out = await answerOnce({ answerFn, client, system, messages, mode: "private", model });
1181
+ return c.json({ reply: out.content });
1182
+ });
1183
+ return app;
1184
+ }
1185
+
1186
+ // src/routes/public.ts
1187
+ import { Hono as Hono4 } from "hono";
1188
+ import { stream as stream4 } from "hono/streaming";
1189
+
1190
+ // src/middleware/auth.ts
1191
+ function createAuthMiddleware(deps) {
1192
+ const { apiKeyManager } = deps;
1193
+ return async function requirePublicKey(c, next) {
1194
+ const hdr = c.req.header("x-api-key");
1195
+ if (hdr?.startsWith("session_")) {
1196
+ console.log(`[Auth] Session key allowed: ${hdr.slice(0, 20)}...`);
1197
+ await next();
1198
+ return;
1199
+ }
1200
+ if (!apiKeyManager) {
1201
+ console.log("[Auth] No API key manager configured");
1202
+ return c.json({ error: "Unauthorized - API key authentication not configured" }, 401);
1203
+ }
1204
+ if (!hdr) {
1205
+ console.log("[Auth] No API key provided");
1206
+ return c.json({ error: "Unauthorized - Missing x-api-key header" }, 401);
1207
+ }
1208
+ try {
1209
+ const result = await apiKeyManager.verify(hdr);
1210
+ if (result.valid) {
1211
+ const payload = result.payload;
1212
+ console.log(`[Auth] JWT API key authorized: ${payload.name || payload.sub || "unknown"}`);
1213
+ await next();
1214
+ return;
1215
+ }
1216
+ console.log(`[Auth] JWT verification failed: ${result.error}`);
1217
+ } catch (error) {
1218
+ console.log(`[Auth] JWT verification error: ${error}`);
1219
+ }
1220
+ console.log(`[Auth] Invalid API key: ${hdr?.slice(0, 20) || "none"}`);
1221
+ return c.json({ error: "Unauthorized" }, 401);
1222
+ };
1223
+ }
1224
+
1225
+ // src/middleware/referrer.ts
1226
+ var DEMO_KEYS2 = ["chatter-api-key-here"];
1227
+ function requireReferrer(allowedOrigins) {
1228
+ return async (c, next) => {
1229
+ const apiKey = c.req.header("x-api-key");
1230
+ const isDemoKey = apiKey && DEMO_KEYS2.includes(apiKey);
1231
+ const isSessionKey = apiKey?.startsWith("session_");
1232
+ if (isDemoKey || isSessionKey) {
1233
+ const referer = c.req.header("referer");
1234
+ const origin = c.req.header("origin");
1235
+ console.log(
1236
+ `[Referrer] Checking ${isSessionKey ? "session" : "demo"} key: ${apiKey?.slice(0, 20)}...`
1237
+ );
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
+ });
1248
+ if (!isAllowed) {
1249
+ console.log(`[Referrer] BLOCKED - not from allowed origins: ${allowedOrigins.join(", ")}`);
1250
+ return c.json(
1251
+ {
1252
+ error: "Demo API keys only work on official demo pages. Please visit our website or use your own API key."
1253
+ },
1254
+ 403
1255
+ );
1256
+ }
1257
+ console.log("[Referrer] Allowed - origin matches");
1258
+ }
1259
+ await next();
1260
+ };
1261
+ }
1262
+
1263
+ // src/middleware/session.ts
1264
+ function validateSessionKey() {
1265
+ return async (c, next) => {
1266
+ const apiKey = c.req.header("x-api-key");
1267
+ if (apiKey?.startsWith("session_")) {
1268
+ console.log(`[Session] Validating key: ${apiKey.slice(0, 20)}...`);
1269
+ const info = getSessionInfo(apiKey);
1270
+ if (info) {
1271
+ console.log(
1272
+ `[Session] Key found - requests: ${info.requests}/${info.maxRequests}, expires: ${new Date(info.expiresAt).toISOString()}`
1273
+ );
1274
+ } else {
1275
+ console.log("[Session] Key not found in session store");
1276
+ }
1277
+ const isValid = validateSession(apiKey);
1278
+ if (!isValid) {
1279
+ console.log("[Session] REJECTED - expired or quota exceeded");
1280
+ return c.json(
1281
+ {
1282
+ error: "Session expired or quota exceeded. Please refresh the page to get a new session."
1283
+ },
1284
+ 401
1285
+ );
1286
+ }
1287
+ console.log("[Session] Valid - request count incremented");
1288
+ }
1289
+ await next();
1290
+ };
1291
+ }
1292
+
1293
+ // src/routes/public.ts
1294
+ function wantsStream2(c) {
1295
+ const url = new URL(c.req.url);
1296
+ if (url.searchParams.get("stream") === "1") return true;
1297
+ const acc = c.req.header("accept") ?? "";
1298
+ return acc.includes("text/event-stream");
1299
+ }
1300
+ function publicRoutes(deps) {
1301
+ const { client, store, config, prompts } = deps;
1302
+ const app = new Hono4();
1303
+ const requirePublicKey = createAuthMiddleware(deps);
1304
+ const { limitPublic: limitPublic2 } = createRateLimiter(config);
1305
+ 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());
1307
+ app.use("/api/public/*", requirePublicKey);
1308
+ app.use("/api/public/*", requireReferrer(referrerOrigins));
1309
+ app.use("/api/public/*", limitPublic2());
1310
+ app.post("/api/public/chat", async (c) => {
1311
+ const body = await c.req.json().catch(() => ({}));
1312
+ const normalized = normalizeChatBody(body);
1313
+ if (!normalized.ok) {
1314
+ return c.json({ error: normalized.error }, 400);
1315
+ }
1316
+ const { messages } = normalized;
1317
+ const buckets = await resolveBuckets({ mode: "public", bucketsFor: config.bucketsFor });
1318
+ let system;
1319
+ try {
1320
+ ({ system } = await prepareChat({ store, prompts, mode: "public", messages, buckets }));
1321
+ } catch {
1322
+ return c.json({ error: "no user message found in conversation" }, 400);
1323
+ }
1324
+ const model = config.openai.model;
1325
+ const answerFn = config.answerFn;
1326
+ if (wantsStream2(c)) {
1327
+ return stream4(c, async (s) => {
1328
+ for await (const delta of answerStream({
1329
+ answerFn,
1330
+ client,
1331
+ system,
1332
+ messages,
1333
+ mode: "public",
1334
+ model
1335
+ })) {
1336
+ await s.write(`data: ${JSON.stringify({ delta })}
1337
+
1338
+ `);
1339
+ }
1340
+ await s.write("event: end\ndata: {}\n\n");
1341
+ });
1342
+ }
1343
+ const out = await answerOnce({ answerFn, client, system, messages, mode: "public", model });
1344
+ return c.json({ reply: out.content });
1345
+ });
1346
+ return app;
1347
+ }
1348
+
1349
+ // src/server.ts
1350
+ async function createServer(config) {
1351
+ console.log(`\u{1F680} Starting ${config.bot.name}...`);
1352
+ const knowledgeDir = config.knowledgeDir || "./config/knowledge";
1353
+ const promptsDir = config.promptsDir || "./config/prompts";
1354
+ const publicDir = config.publicDir || "./public";
1355
+ const enablePublic = config.features?.enablePublicChat !== false;
1356
+ const enablePrivate = config.features?.enablePrivateChat !== false;
1357
+ const enableDemo = config.features?.enableDemoRoutes || false;
1358
+ const enableOpenAICompat = config.features?.enableOpenAICompat !== false;
1359
+ const headless = config.features?.headless === true;
1360
+ const { staticDir } = resolveStatic(config.staticDir);
1361
+ const client = new OpenAI({
1362
+ apiKey: config.openai.apiKey
1363
+ });
1364
+ const db = createClient2({
1365
+ url: config.database.url,
1366
+ authToken: config.database.authToken
1367
+ });
1368
+ const store = new VectorStore(client, {
1369
+ databaseClient: db,
1370
+ knowledgeDir
1371
+ });
1372
+ await store.build();
1373
+ console.log("\u2705 Knowledge base ready");
1374
+ const prompts = new PromptLoader(promptsDir, config.bot);
1375
+ let apiKeyManager;
1376
+ const secret = process.env.CHATTER_SECRET || config.auth?.secret;
1377
+ if (secret) {
1378
+ apiKeyManager = new ApiKeyManager(secret);
1379
+ console.log("\u2705 API key manager initialized");
1380
+ } else if (config.auth?.apiKeyManager) {
1381
+ apiKeyManager = config.auth.apiKeyManager;
1382
+ console.log("\u2705 Custom API key manager configured");
1383
+ }
1384
+ const app = new Hono5();
1385
+ if (config.server?.cors !== false) {
1386
+ app.use("*", cors(config.server?.allowedOrigins));
1387
+ }
1388
+ app.get("/healthz", (c) => c.text("ok"));
1389
+ app.get(
1390
+ "/config",
1391
+ (c) => c.json({
1392
+ botName: config.bot.name,
1393
+ publicUrl: config.bot.publicUrl,
1394
+ clerkPublishableKey: config.auth?.clerk?.publishableKey || null,
1395
+ clerkFrontendUrl: config.auth?.clerk?.frontendUrl || null,
1396
+ branding: config.branding || {},
1397
+ chat: config.chat || {}
1398
+ })
1399
+ );
1400
+ if (!headless && staticDir) {
1401
+ const relativePath = __require("node:path").relative(process.cwd(), staticDir);
1402
+ app.get("/chatter.js", serveStatic({ path: `${relativePath}/chatter.js` }));
1403
+ app.get("/chatter.css", serveStatic({ path: `${relativePath}/chatter.css` }));
1404
+ }
1405
+ if (!headless && publicDir) {
1406
+ app.get("/", serveStatic({ path: `${publicDir}/index.html` }));
1407
+ app.get("/chat", serveStatic({ path: `${publicDir}/chat.html` }));
1408
+ app.get("/private", serveStatic({ path: `${publicDir}/private.html` }));
1409
+ app.get("/vanilla/chat", serveStatic({ path: `${publicDir}/vanilla/chat.html` }));
1410
+ app.get(
1411
+ "/demo/widget-button-public",
1412
+ serveStatic({ path: `${publicDir}/demo/widget-button-public.html` })
1413
+ );
1414
+ app.get(
1415
+ "/demo/widget-button-private",
1416
+ serveStatic({ path: `${publicDir}/demo/widget-button-private.html` })
1417
+ );
1418
+ app.get(
1419
+ "/demo/widget-inline-public",
1420
+ serveStatic({ path: `${publicDir}/demo/widget-inline-public.html` })
1421
+ );
1422
+ app.get(
1423
+ "/demo/widget-inline-private",
1424
+ serveStatic({ path: `${publicDir}/demo/widget-inline-private.html` })
1425
+ );
1426
+ app.get(
1427
+ "/demo/react-button-public",
1428
+ serveStatic({ path: `${publicDir}/demo/react-button-public.html` })
1429
+ );
1430
+ app.get(
1431
+ "/demo/react-button-private",
1432
+ serveStatic({ path: `${publicDir}/demo/react-button-private.html` })
1433
+ );
1434
+ app.get(
1435
+ "/demo/react-inline-public",
1436
+ serveStatic({ path: `${publicDir}/demo/react-inline-public.html` })
1437
+ );
1438
+ app.get(
1439
+ "/demo/react-inline-private",
1440
+ serveStatic({ path: `${publicDir}/demo/react-inline-private.html` })
1441
+ );
1442
+ app.get("/demo/public", serveStatic({ path: `${publicDir}/demo-public.html` }));
1443
+ app.get("/demo/private", serveStatic({ path: `${publicDir}/demo-private.html` }));
1444
+ app.get("/demo/chatbot", serveStatic({ path: `${publicDir}/demo-chatbot.html` }));
1445
+ app.get("/demo/react", serveStatic({ path: `${publicDir}/react-demo.html` }));
1446
+ }
1447
+ const senders = createSenderRegistry();
1448
+ const deps = { client, store, db, config, prompts, apiKeyManager, senders };
1449
+ if (enablePublic) {
1450
+ app.route("/", publicRoutes(deps));
1451
+ }
1452
+ if (enablePrivate) {
1453
+ app.route("/", privateRoutes(deps));
1454
+ }
1455
+ if (enableDemo) {
1456
+ app.route("/", demoRoutes(deps));
1457
+ }
1458
+ if (enableOpenAICompat) {
1459
+ app.route("/", openaiRoutes(deps));
1460
+ }
1461
+ if (config.customRoutes) {
1462
+ await config.customRoutes(app, deps);
1463
+ }
1464
+ const started = [];
1465
+ if (config.channels && config.channels.length > 0) {
1466
+ for (const channel of config.channels) {
1467
+ try {
1468
+ await channel.start(deps);
1469
+ started.push(channel);
1470
+ console.log(`\u2705 Channel "${channel.name}" started`);
1471
+ } catch (error) {
1472
+ console.error(`\u274C Channel "${channel.name}" failed to start:`, error);
1473
+ }
1474
+ }
1475
+ }
1476
+ console.log(`\u2705 ${config.bot.name} server ready`);
1477
+ return Object.assign(app, {
1478
+ // Scoped to this call's started channels, not global — two servers in
1479
+ // one process each stop only their own. A channel's `stop()` throwing
1480
+ // (sync or async) is caught per-channel so one misbehaving channel can't
1481
+ // stop the rest from cleaning up.
1482
+ stopChannels: async () => {
1483
+ await Promise.allSettled(
1484
+ started.map(async (channel) => {
1485
+ try {
1486
+ await channel.stop?.();
1487
+ } catch (error) {
1488
+ console.error(`Channel "${channel.name}" failed to stop:`, error);
1489
+ }
1490
+ })
1491
+ );
1492
+ }
1493
+ });
1494
+ }
1495
+ export {
1496
+ createServer
1497
+ };