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