weifuwu 0.21.0 → 0.22.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/dist/index.js CHANGED
@@ -1,3 +1,9 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
+
1
7
  // trace.ts
2
8
  import { AsyncLocalStorage } from "node:async_hooks";
3
9
  import { randomUUID } from "node:crypto";
@@ -1159,11 +1165,33 @@ var MIME_TYPES = {
1159
1165
  };
1160
1166
 
1161
1167
  // validate.ts
1168
+ function parseFormBody(text2) {
1169
+ const params = new URLSearchParams(text2);
1170
+ const result = {};
1171
+ for (const [key, value] of params) {
1172
+ result[key] = value;
1173
+ }
1174
+ return result;
1175
+ }
1176
+ function parseBody(text2, ct) {
1177
+ if (ct.includes("application/x-www-form-urlencoded")) {
1178
+ return parseFormBody(text2);
1179
+ }
1180
+ const isExplicitJson = ct.includes("application/json") || ct.includes("+json") || ct.includes("text/") || ct.includes("*/json");
1181
+ const isNotSpecialMultipart = !ct.includes("multipart/form-data") && !ct.includes("application/x-www-form-urlencoded");
1182
+ if (isExplicitJson || isNotSpecialMultipart) {
1183
+ try {
1184
+ return JSON.parse(text2);
1185
+ } catch {
1186
+ }
1187
+ }
1188
+ return text2;
1189
+ }
1162
1190
  function validate(schemas) {
1163
1191
  return async (req, ctx, next) => {
1164
1192
  const parsed = {};
1165
1193
  const issues = [];
1166
- if (schemas.params) {
1194
+ if (schemas?.params) {
1167
1195
  const result = schemas.params.safeParse(ctx.params);
1168
1196
  if (result.success) {
1169
1197
  parsed.params = result.data;
@@ -1174,7 +1202,7 @@ function validate(schemas) {
1174
1202
  })));
1175
1203
  }
1176
1204
  }
1177
- if (schemas.query) {
1205
+ if (schemas?.query) {
1178
1206
  const result = schemas.query.safeParse(ctx.query);
1179
1207
  if (result.success) {
1180
1208
  parsed.query = result.data;
@@ -1185,7 +1213,7 @@ function validate(schemas) {
1185
1213
  })));
1186
1214
  }
1187
1215
  }
1188
- if (schemas.headers) {
1216
+ if (schemas?.headers) {
1189
1217
  const rawHeaders = {};
1190
1218
  req.headers.forEach((v, k) => {
1191
1219
  rawHeaders[k] = v;
@@ -1200,32 +1228,35 @@ function validate(schemas) {
1200
1228
  })));
1201
1229
  }
1202
1230
  }
1203
- if (schemas.body) {
1204
- if (req.method === "GET" || req.method === "HEAD") {
1205
- } else if (req.body === null) {
1206
- issues.push({ path: ["body"], message: "Request body is required" });
1207
- } else {
1208
- const bodyText = await req.text();
1209
- if (!bodyText) {
1210
- issues.push({ path: ["body"], message: "Request body is required" });
1231
+ if (req.method !== "GET" && req.method !== "HEAD") {
1232
+ const ct = req.headers.get("content-type") ?? "";
1233
+ const isForm = ct.includes("application/x-www-form-urlencoded");
1234
+ if (schemas?.body || isForm) {
1235
+ if (req.body === null) {
1236
+ if (schemas?.body) {
1237
+ issues.push({ path: ["body"], message: "Request body is required" });
1238
+ }
1211
1239
  } else {
1212
- const ct = req.headers.get("content-type") ?? "";
1213
- const shouldParseJson = ct.includes("application/json") || ct.includes("text/") || ct.includes("*/json") || !ct.includes("multipart/form-data") && !ct.includes("application/x-www-form-urlencoded");
1214
- let bodyValue = bodyText;
1215
- if (shouldParseJson) {
1216
- try {
1217
- bodyValue = JSON.parse(bodyText);
1218
- } catch {
1240
+ const bodyText = await req.text();
1241
+ if (!bodyText) {
1242
+ if (schemas?.body) {
1243
+ issues.push({ path: ["body"], message: "Request body is required" });
1219
1244
  }
1220
- }
1221
- const result = schemas.body.safeParse(bodyValue);
1222
- if (result.success) {
1223
- parsed.body = result.data;
1224
1245
  } else {
1225
- issues.push(...result.error.issues.map((i) => ({
1226
- path: ["body", ...i.path.map(String)],
1227
- message: i.message
1228
- })));
1246
+ const bodyValue = parseBody(bodyText, ct);
1247
+ if (schemas?.body) {
1248
+ const result = schemas.body.safeParse(bodyValue);
1249
+ if (result.success) {
1250
+ parsed.body = result.data;
1251
+ } else {
1252
+ issues.push(...result.error.issues.map((i) => ({
1253
+ path: ["body", ...i.path.map(String)],
1254
+ message: i.message
1255
+ })));
1256
+ }
1257
+ } else {
1258
+ parsed.body = bodyValue;
1259
+ }
1229
1260
  }
1230
1261
  }
1231
1262
  }
@@ -1395,28 +1426,35 @@ function upload(options) {
1395
1426
  }
1396
1427
 
1397
1428
  // rate-limit.ts
1429
+ function defaultKey(_req, _ctx) {
1430
+ const forwarded = _req.headers.get("x-forwarded-for");
1431
+ if (forwarded) return forwarded.split(",")[0].trim();
1432
+ const realIp = _req.headers.get("x-real-ip");
1433
+ if (realIp) return realIp;
1434
+ const cfIp = _req.headers.get("cf-connecting-ip");
1435
+ if (cfIp) return cfIp;
1436
+ return "global";
1437
+ }
1398
1438
  function rateLimit(options) {
1399
1439
  const max = options?.max ?? 100;
1400
1440
  const window2 = options?.window ?? 6e4;
1401
- const getKey = options?.key ?? ((_req, _ctx) => {
1402
- const forwarded = _req.headers.get("x-forwarded-for");
1403
- if (forwarded) return forwarded.split(",")[0].trim();
1404
- const realIp = _req.headers.get("x-real-ip");
1405
- if (realIp) return realIp;
1406
- const cfIp = _req.headers.get("cf-connecting-ip");
1407
- if (cfIp) return cfIp;
1408
- return "global";
1409
- });
1441
+ const getKey = options?.key ?? defaultKey;
1410
1442
  const message = options?.message ?? "Too Many Requests";
1411
- const MAX_ENTRIES = 1e4;
1443
+ const storeType = options?.store ?? "memory";
1444
+ if (storeType === "redis" && !options?.redis) {
1445
+ throw new Error('rateLimit: redis client required when store: "redis"');
1446
+ }
1447
+ const redis2 = options?.redis ?? null;
1448
+ const keyPrefix = options?.prefix ?? "ratelimit:";
1449
+ const MAX_ENTRIES2 = 1e4;
1412
1450
  const hits = /* @__PURE__ */ new Map();
1413
- const interval = setInterval(() => {
1451
+ const interval = storeType === "memory" ? setInterval(() => {
1414
1452
  const now = Date.now();
1415
1453
  for (const [key, entry] of hits) {
1416
1454
  if (entry.reset < now) hits.delete(key);
1417
1455
  }
1418
- if (hits.size > MAX_ENTRIES) {
1419
- const toDelete = hits.size - MAX_ENTRIES;
1456
+ if (hits.size > MAX_ENTRIES2) {
1457
+ const toDelete = hits.size - MAX_ENTRIES2;
1420
1458
  let deleted = 0;
1421
1459
  for (const key of hits.keys()) {
1422
1460
  if (deleted >= toDelete) break;
@@ -1424,39 +1462,56 @@ function rateLimit(options) {
1424
1462
  deleted++;
1425
1463
  }
1426
1464
  }
1427
- }, Math.min(window2, 3e4));
1428
- if (interval.unref) interval.unref();
1429
- const mw = async (req, ctx, next) => {
1430
- const key = getKey(req, ctx);
1465
+ }, Math.min(window2, 3e4)) : null;
1466
+ if (interval?.unref) interval.unref();
1467
+ async function checkAndIncrement(key) {
1431
1468
  const now = Date.now();
1469
+ if (storeType === "redis" && redis2) {
1470
+ const redisKey = `${keyPrefix}${key}`;
1471
+ const count = await redis2.incr(redisKey);
1472
+ if (count === 1) {
1473
+ await redis2.pexpire(redisKey, window2);
1474
+ }
1475
+ const pttl = await redis2.pttl(redisKey);
1476
+ const reset = pttl > 0 ? now + pttl : now + window2;
1477
+ return { count, reset };
1478
+ }
1432
1479
  let entry = hits.get(key);
1433
1480
  if (!entry || entry.reset < now) {
1434
1481
  hits.set(key, { count: 1, reset: now + window2 });
1435
- entry = { count: 1, reset: now + window2 };
1436
- const res2 = await next(req, ctx);
1437
- return addRateLimitHeaders(res2, max, max - 1, now + window2);
1482
+ return { count: 1, reset: now + window2 };
1438
1483
  }
1439
1484
  entry.count++;
1440
- const remaining = Math.max(0, max - entry.count);
1441
- if (entry.count > max) {
1485
+ return { count: entry.count, reset: entry.reset };
1486
+ }
1487
+ const mw = async (req, ctx, next) => {
1488
+ const key = getKey(req, ctx);
1489
+ const now = Date.now();
1490
+ const { count, reset } = await checkAndIncrement(key);
1491
+ if (count > max) {
1442
1492
  return new Response(message, {
1443
1493
  status: 429,
1444
1494
  headers: {
1445
- "Retry-After": String(Math.ceil((entry.reset - now) / 1e3)),
1495
+ "Retry-After": String(Math.ceil((reset - now) / 1e3)),
1446
1496
  "X-RateLimit-Limit": String(max),
1447
1497
  "X-RateLimit-Remaining": "0",
1448
- "X-RateLimit-Reset": String(Math.ceil(entry.reset / 1e3))
1498
+ "X-RateLimit-Reset": String(Math.ceil(reset / 1e3))
1449
1499
  }
1450
1500
  });
1451
1501
  }
1502
+ const remaining = max - count;
1452
1503
  const res = await next(req, ctx);
1453
- return addRateLimitHeaders(res, max, remaining, entry.reset);
1504
+ return addRateLimitHeaders(res, max, remaining, reset);
1454
1505
  };
1455
1506
  mw.stop = () => {
1456
- clearInterval(interval);
1507
+ if (interval) clearInterval(interval);
1457
1508
  hits.clear();
1458
1509
  };
1459
- mw.stats = () => ({ entries: hits.size, maxEntries: MAX_ENTRIES });
1510
+ mw.stats = () => ({
1511
+ store: storeType,
1512
+ entries: storeType === "memory" ? hits.size : void 0,
1513
+ maxEntries: MAX_ENTRIES2
1514
+ });
1460
1515
  return mw;
1461
1516
  }
1462
1517
  function addRateLimitHeaders(res, limit, remaining, reset) {
@@ -1781,6 +1836,55 @@ var TestApp = class {
1781
1836
  function testApp() {
1782
1837
  return new TestApp();
1783
1838
  }
1839
+ async function createTestDb(options) {
1840
+ const dbUrl = options?.url || process.env.TEST_DATABASE_URL || process.env.DATABASE_URL;
1841
+ if (!dbUrl) throw new Error("createTestDb: DATABASE_URL or TEST_DATABASE_URL required");
1842
+ const schema = options?.schema || `test_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
1843
+ const { default: postgres2 } = await import("postgres");
1844
+ const adminSql = postgres2(dbUrl);
1845
+ await adminSql.unsafe('CREATE SCHEMA IF NOT EXISTS "' + schema.replace(/"/g, '""') + '"');
1846
+ const schemaUrl = new URL(dbUrl);
1847
+ schemaUrl.searchParams.set("search_path", schema);
1848
+ const sql2 = postgres2(schemaUrl.toString());
1849
+ await adminSql.end();
1850
+ return {
1851
+ sql: sql2,
1852
+ url: schemaUrl.toString(),
1853
+ schema,
1854
+ destroy: async () => {
1855
+ const destroySql = postgres2(dbUrl);
1856
+ await destroySql.unsafe('DROP SCHEMA IF EXISTS "' + schema.replace(/"/g, '""') + '" CASCADE');
1857
+ await destroySql.end();
1858
+ await sql2.end();
1859
+ }
1860
+ };
1861
+ }
1862
+ async function withTestDb(optionsOrFn, fn) {
1863
+ let dbUrl;
1864
+ let callback;
1865
+ if (typeof optionsOrFn === "function") {
1866
+ callback = optionsOrFn;
1867
+ } else if (typeof optionsOrFn === "string") {
1868
+ dbUrl = optionsOrFn;
1869
+ callback = fn;
1870
+ } else {
1871
+ dbUrl = optionsOrFn?.url;
1872
+ callback = fn;
1873
+ }
1874
+ const resolvedUrl = dbUrl || process.env.TEST_DATABASE_URL || process.env.DATABASE_URL;
1875
+ if (!resolvedUrl) throw new Error("withTestDb: DATABASE_URL or TEST_DATABASE_URL required");
1876
+ const { default: postgres2 } = await import("postgres");
1877
+ const sql2 = postgres2(resolvedUrl);
1878
+ try {
1879
+ await sql2.begin(async (txSql) => {
1880
+ await callback(txSql);
1881
+ throw void 0;
1882
+ });
1883
+ } catch {
1884
+ } finally {
1885
+ await sql2.end();
1886
+ }
1887
+ }
1784
1888
 
1785
1889
  // graphql.ts
1786
1890
  import { buildSchema, graphql as executeGraphQL, validate as validateQuery, parse } from "graphql";
@@ -3476,6 +3580,8 @@ function queue(opts) {
3476
3580
  let _processed = 0;
3477
3581
  let _failed = 0;
3478
3582
  const jobKey = `${prefix}:jobs`;
3583
+ const failedKey = `${prefix}:failed`;
3584
+ const MAX_FAILED = 1e3;
3479
3585
  const mw = ((req, ctx, next) => {
3480
3586
  ctx.queue = q;
3481
3587
  return next(req, ctx);
@@ -3490,7 +3596,15 @@ function queue(opts) {
3490
3596
  _processed++;
3491
3597
  } catch (e) {
3492
3598
  _failed++;
3493
- console.error("[queue] handler error:", e.message);
3599
+ const errMsg = e.message;
3600
+ console.error("[queue] handler error:", errMsg);
3601
+ const failedEntry = JSON.stringify({
3602
+ ...job,
3603
+ error: errMsg,
3604
+ failedAt: Date.now()
3605
+ });
3606
+ await redis2.lpush(failedKey, failedEntry);
3607
+ await redis2.ltrim(failedKey, 0, MAX_FAILED - 1);
3494
3608
  } finally {
3495
3609
  inflight--;
3496
3610
  }
@@ -3571,6 +3685,101 @@ function queue(opts) {
3571
3685
  while (inflight > 0) await new Promise((r) => setTimeout(r, 50));
3572
3686
  redis2.disconnect();
3573
3687
  };
3688
+ mw.jobs = async function jobs(limit) {
3689
+ const raw = await redis2.zrevrange(jobKey, 0, (limit ?? 50) - 1);
3690
+ return raw.map((r) => {
3691
+ try {
3692
+ return JSON.parse(r);
3693
+ } catch {
3694
+ return null;
3695
+ }
3696
+ }).filter(Boolean);
3697
+ };
3698
+ mw.failedJobs = async function failedJobs(limit) {
3699
+ const raw = await redis2.lrange(failedKey, 0, (limit ?? 50) - 1);
3700
+ return raw.map((r) => {
3701
+ try {
3702
+ return JSON.parse(r);
3703
+ } catch {
3704
+ return null;
3705
+ }
3706
+ }).filter(Boolean);
3707
+ };
3708
+ mw.retryFailed = async function retryFailed(jobId) {
3709
+ const raw = await redis2.lrange(failedKey, 0, -1);
3710
+ for (const entry of raw) {
3711
+ try {
3712
+ const job = JSON.parse(entry);
3713
+ if (job.id === jobId) {
3714
+ await redis2.lrem(failedKey, 1, entry);
3715
+ const reJob = { ...job, runAt: Date.now() };
3716
+ delete reJob.error;
3717
+ delete reJob.failedAt;
3718
+ await redis2.zadd(jobKey, reJob.runAt, JSON.stringify(reJob));
3719
+ _failed--;
3720
+ return true;
3721
+ }
3722
+ } catch {
3723
+ }
3724
+ }
3725
+ return false;
3726
+ };
3727
+ mw.retryAllFailed = async function retryAllFailed(type) {
3728
+ const raw = await redis2.lrange(failedKey, 0, -1);
3729
+ let count = 0;
3730
+ for (const entry of raw) {
3731
+ try {
3732
+ const job = JSON.parse(entry);
3733
+ if (type && job.type !== type) continue;
3734
+ await redis2.lrem(failedKey, 1, entry);
3735
+ const reJob = { ...job, runAt: Date.now() };
3736
+ delete reJob.error;
3737
+ delete reJob.failedAt;
3738
+ await redis2.zadd(jobKey, reJob.runAt, JSON.stringify(reJob));
3739
+ _failed--;
3740
+ count++;
3741
+ } catch {
3742
+ }
3743
+ }
3744
+ return count;
3745
+ };
3746
+ mw.dashboard = function dashboard() {
3747
+ const r = new Router();
3748
+ r.get("/", async (req, ctx) => {
3749
+ const s = q.stats();
3750
+ const pending = await q.jobs(100);
3751
+ const byType = {};
3752
+ for (const job of pending) {
3753
+ if (!byType[job.type]) byType[job.type] = { pending: 0, failed: 0 };
3754
+ byType[job.type].pending++;
3755
+ }
3756
+ const failed = await q.failedJobs(1e3);
3757
+ for (const job of failed) {
3758
+ if (!byType[job.type]) byType[job.type] = { pending: 0, failed: 0 };
3759
+ byType[job.type].failed++;
3760
+ }
3761
+ return Response.json({
3762
+ stats: s,
3763
+ types: byType,
3764
+ failedCount: failed.length
3765
+ });
3766
+ });
3767
+ r.get("/:type/failed", async (req, ctx) => {
3768
+ const failed = await q.failedJobs(100);
3769
+ const filtered = failed.filter((j) => j.type === ctx.params.type);
3770
+ return Response.json({ jobs: filtered, count: filtered.length });
3771
+ });
3772
+ r.post("/:type/retry", async (req, ctx) => {
3773
+ const count = await q.retryAllFailed(ctx.params.type);
3774
+ return Response.json({ retried: count });
3775
+ });
3776
+ r.post("/retry/:id", async (req, ctx) => {
3777
+ const ok = await q.retryFailed(ctx.params.id);
3778
+ if (!ok) return new Response("Job not found", { status: 404 });
3779
+ return Response.json({ retried: true });
3780
+ });
3781
+ return r;
3782
+ };
3574
3783
  mw.stats = () => ({
3575
3784
  running,
3576
3785
  inflight,
@@ -7254,12 +7463,12 @@ async function buildRouter4(deps) {
7254
7463
  const router = new Router();
7255
7464
  router.post("/sessions", async (req, ctx) => {
7256
7465
  const body = await req.json().catch(() => ({}));
7257
- const session = await createSession(sql2, {
7466
+ const session2 = await createSession(sql2, {
7258
7467
  title: body.title,
7259
7468
  model: body.model,
7260
7469
  systemPrompt: body.systemPrompt || systemPrompt
7261
7470
  }, workspace, ctx.mountPath || "");
7262
- return Response.json(session, { status: 201 });
7471
+ return Response.json(session2, { status: 201 });
7263
7472
  });
7264
7473
  router.get("/sessions", async () => {
7265
7474
  const sessions2 = await listSessions(sql2);
@@ -7267,10 +7476,10 @@ async function buildRouter4(deps) {
7267
7476
  });
7268
7477
  router.get("/sessions/:id", async (_req, ctx) => {
7269
7478
  const id2 = ctx.params.id;
7270
- const session = await getSession(sql2, id2);
7271
- if (!session) return new Response("Not Found", { status: 404 });
7479
+ const session2 = await getSession(sql2, id2);
7480
+ if (!session2) return new Response("Not Found", { status: 404 });
7272
7481
  const messages2 = await getHistory(sql2, id2);
7273
- return Response.json({ session, messages: messages2 });
7482
+ return Response.json({ session: session2, messages: messages2 });
7274
7483
  });
7275
7484
  router.delete("/sessions/:id", async (_req, ctx) => {
7276
7485
  const id2 = ctx.params.id;
@@ -7279,12 +7488,12 @@ async function buildRouter4(deps) {
7279
7488
  });
7280
7489
  router.post("/sessions/:id/message", async (req, ctx) => {
7281
7490
  const sessionId = ctx.params.id;
7282
- const session = await getSession(sql2, sessionId);
7283
- if (!session) return new Response("Session Not Found", { status: 404 });
7491
+ const session2 = await getSession(sql2, sessionId);
7492
+ if (!session2) return new Response("Session Not Found", { status: 404 });
7284
7493
  const { content } = await req.json();
7285
7494
  if (!content) return new Response("Missing content", { status: 400 });
7286
7495
  const toolCtx = {
7287
- workspace: session.workspace || workspace,
7496
+ workspace: session2.workspace || workspace,
7288
7497
  permissions,
7289
7498
  pendingQuestions,
7290
7499
  skillsRegistry: deps.skillsRegistry
@@ -7292,10 +7501,10 @@ async function buildRouter4(deps) {
7292
7501
  const tools = createTools(toolCtx);
7293
7502
  const allSkills = [...skills];
7294
7503
  const sysPrompt = buildSystemPrompt({
7295
- workspace: session.workspace || workspace,
7296
- model: session.model,
7504
+ workspace: session2.workspace || workspace,
7505
+ model: session2.model,
7297
7506
  skills: allSkills,
7298
- systemPrompt: session.system_prompt || systemPrompt
7507
+ systemPrompt: session2.system_prompt || systemPrompt
7299
7508
  });
7300
7509
  const history = await getHistory(sql2, sessionId);
7301
7510
  await addTextMessage(sql2, sessionId, "user", content);
@@ -7312,8 +7521,8 @@ async function buildRouter4(deps) {
7312
7521
  });
7313
7522
  router.get("/sessions/:id/usage", async (_req, ctx) => {
7314
7523
  const sessionId = ctx.params.id;
7315
- const session = await getSession(sql2, sessionId);
7316
- if (!session) return new Response("Not Found", { status: 404 });
7524
+ const session2 = await getSession(sql2, sessionId);
7525
+ if (!session2) return new Response("Not Found", { status: 404 });
7317
7526
  const rows = await sql2`
7318
7527
  SELECT
7319
7528
  COUNT(*)::int AS message_count,
@@ -7363,13 +7572,13 @@ function createWSHandler2(deps) {
7363
7572
  switch (msg.type) {
7364
7573
  case "create": {
7365
7574
  try {
7366
- const session = await createSession(sql2, {
7575
+ const session2 = await createSession(sql2, {
7367
7576
  userId: client.userId,
7368
7577
  title: msg.title,
7369
7578
  model: msg.model,
7370
7579
  systemPrompt: msg.systemPrompt || systemPrompt
7371
7580
  }, workspace, client.mountPath);
7372
- ws.send(JSON.stringify({ type: "session_created", session }));
7581
+ ws.send(JSON.stringify({ type: "session_created", session: session2 }));
7373
7582
  } catch (e) {
7374
7583
  ws.send(JSON.stringify({ type: "error", error: e.message }));
7375
7584
  }
@@ -7386,13 +7595,13 @@ function createWSHandler2(deps) {
7386
7595
  client.abortController = controller;
7387
7596
  client.currentSessionId = session_id;
7388
7597
  try {
7389
- const session = await getSession(sql2, session_id);
7390
- if (!session) {
7598
+ const session2 = await getSession(sql2, session_id);
7599
+ if (!session2) {
7391
7600
  ws.send(JSON.stringify({ type: "error", error: "Session not found" }));
7392
7601
  return;
7393
7602
  }
7394
7603
  const toolCtx = {
7395
- workspace: session.workspace || workspace,
7604
+ workspace: session2.workspace || workspace,
7396
7605
  permissions,
7397
7606
  pendingQuestions,
7398
7607
  skillsRegistry
@@ -7400,10 +7609,10 @@ function createWSHandler2(deps) {
7400
7609
  const tools = createTools(toolCtx);
7401
7610
  const allSkills = [...skills];
7402
7611
  const sysPrompt = buildSystemPrompt({
7403
- workspace: session.workspace || workspace,
7404
- model: session.model,
7612
+ workspace: session2.workspace || workspace,
7613
+ model: session2.model,
7405
7614
  skills: allSkills,
7406
- systemPrompt: session.system_prompt || systemPrompt
7615
+ systemPrompt: session2.system_prompt || systemPrompt
7407
7616
  });
7408
7617
  const history = await getHistory(sql2, session_id);
7409
7618
  await addTextMessage(sql2, session_id, "user", content);
@@ -7911,14 +8120,14 @@ function preferences(options) {
7911
8120
  const dir = options.dir ? resolve13(options.dir) : void 0;
7912
8121
  const localeOpts = { ...defaults.locale, ...options.locale };
7913
8122
  const themeOpts = { ...defaults.theme, ...options.theme };
7914
- const cache2 = /* @__PURE__ */ new Map();
8123
+ const cache3 = /* @__PURE__ */ new Map();
7915
8124
  function validLocale(locale) {
7916
8125
  return /^[\w-]+$/.test(locale) && !locale.includes("..");
7917
8126
  }
7918
8127
  async function load(locale) {
7919
8128
  if (!dir) return {};
7920
8129
  if (!validLocale(locale)) return {};
7921
- const cached = cache2.get(locale);
8130
+ const cached = cache3.get(locale);
7922
8131
  if (cached) return cached;
7923
8132
  const filePath = join7(dir, `${locale}.json`);
7924
8133
  let data = null;
@@ -7926,16 +8135,16 @@ function preferences(options) {
7926
8135
  await stat2(filePath);
7927
8136
  const content = await readFile2(filePath, "utf-8");
7928
8137
  data = JSON.parse(content);
7929
- cache2.set(locale, data);
8138
+ cache3.set(locale, data);
7930
8139
  return data;
7931
8140
  } catch {
7932
8141
  }
7933
8142
  if (!data) {
7934
8143
  const short = locale.split("-")[0];
7935
8144
  if (short !== locale) {
7936
- const fallback = cache2.get(short) || await load(short);
8145
+ const fallback = cache3.get(short) || await load(short);
7937
8146
  if (fallback && Object.keys(fallback).length > 0) {
7938
- cache2.set(locale, fallback);
8147
+ cache3.set(locale, fallback);
7939
8148
  return fallback;
7940
8149
  }
7941
8150
  }
@@ -9352,9 +9561,753 @@ function registerWorker(url) {
9352
9561
  }
9353
9562
  };
9354
9563
  }
9564
+
9565
+ // session.ts
9566
+ import crypto7 from "node:crypto";
9567
+ var kSaved = /* @__PURE__ */ Symbol("session.saved");
9568
+ var kDestroyed = /* @__PURE__ */ Symbol("session.destroyed");
9569
+ var kId = /* @__PURE__ */ Symbol("session.id");
9570
+ var kStore = /* @__PURE__ */ Symbol("session.store");
9571
+ var kTtl = /* @__PURE__ */ Symbol("session.ttl");
9572
+ var MAX_SESSIONS = 1e5;
9573
+ var MemoryStore = class {
9574
+ store = /* @__PURE__ */ new Map();
9575
+ interval;
9576
+ constructor(cleanupMs = 6e4) {
9577
+ this.interval = setInterval(() => this.cleanup(), cleanupMs);
9578
+ if (this.interval.unref) this.interval.unref();
9579
+ }
9580
+ async get(sid) {
9581
+ const entry = this.store.get(sid);
9582
+ if (!entry) return null;
9583
+ if (Date.now() > entry.expires) {
9584
+ this.store.delete(sid);
9585
+ return null;
9586
+ }
9587
+ return entry.data;
9588
+ }
9589
+ async set(sid, data, ttl) {
9590
+ if (this.store.size >= MAX_SESSIONS) {
9591
+ const oldest = this.store.keys().next();
9592
+ if (!oldest.done) this.store.delete(oldest.value);
9593
+ }
9594
+ this.store.set(sid, { data, expires: Date.now() + ttl });
9595
+ }
9596
+ async destroy(sid) {
9597
+ this.store.delete(sid);
9598
+ }
9599
+ cleanup() {
9600
+ const now = Date.now();
9601
+ for (const [key, entry] of this.store) {
9602
+ if (entry.expires < now) this.store.delete(key);
9603
+ }
9604
+ }
9605
+ close() {
9606
+ clearInterval(this.interval);
9607
+ this.store.clear();
9608
+ }
9609
+ /** Testing only: return approximate count. */
9610
+ get size() {
9611
+ return this.store.size;
9612
+ }
9613
+ };
9614
+ var RedisStore = class {
9615
+ redis;
9616
+ prefix;
9617
+ constructor(redis2, prefix = "session:") {
9618
+ this.redis = redis2;
9619
+ this.prefix = prefix;
9620
+ }
9621
+ key(sid) {
9622
+ return `${this.prefix}${sid}`;
9623
+ }
9624
+ async get(sid) {
9625
+ const raw = await this.redis.get(this.key(sid));
9626
+ if (!raw) return null;
9627
+ try {
9628
+ return JSON.parse(raw);
9629
+ } catch {
9630
+ await this.redis.del(this.key(sid));
9631
+ return null;
9632
+ }
9633
+ }
9634
+ async set(sid, data, ttl) {
9635
+ const ttlSec = Math.ceil(ttl / 1e3);
9636
+ await this.redis.setex(this.key(sid), ttlSec, JSON.stringify(data));
9637
+ }
9638
+ async destroy(sid) {
9639
+ await this.redis.del(this.key(sid));
9640
+ }
9641
+ };
9642
+ function createSessionObject(data, sid, store2, ttl) {
9643
+ const obj = data ?? {};
9644
+ obj[kSaved] = false;
9645
+ obj[kDestroyed] = false;
9646
+ obj[kId] = sid;
9647
+ obj[kStore] = store2;
9648
+ obj[kTtl] = ttl;
9649
+ obj.save = () => {
9650
+ obj[kSaved] = true;
9651
+ };
9652
+ obj.destroy = () => {
9653
+ obj[kDestroyed] = true;
9654
+ obj[kSaved] = false;
9655
+ for (const key of Object.keys(obj)) {
9656
+ if (typeof key === "symbol") continue;
9657
+ delete obj[key];
9658
+ }
9659
+ };
9660
+ Object.defineProperty(obj, "id", {
9661
+ get: () => obj[kId],
9662
+ enumerable: false,
9663
+ configurable: false
9664
+ });
9665
+ Object.defineProperty(obj, "save", { enumerable: false, configurable: true, writable: true, value: obj.save });
9666
+ Object.defineProperty(obj, "destroy", { enumerable: false, configurable: true, writable: true, value: obj.destroy });
9667
+ return obj;
9668
+ }
9669
+ function isSessionActive(session2) {
9670
+ for (const key of Object.keys(session2)) {
9671
+ if (key !== "save" && key !== "destroy") return true;
9672
+ }
9673
+ return false;
9674
+ }
9675
+ function session(options) {
9676
+ const ttl = options?.ttl ?? 24 * 60 * 60 * 1e3;
9677
+ const cookieName = options?.cookieName ?? "__session";
9678
+ const cookieOpts = {
9679
+ path: options?.cookie?.path ?? "/",
9680
+ domain: options?.cookie?.domain,
9681
+ httpOnly: options?.cookie?.httpOnly ?? true,
9682
+ secure: options?.cookie?.secure ?? process.env.NODE_ENV === "production",
9683
+ sameSite: options?.cookie?.sameSite ?? "lax"
9684
+ };
9685
+ let store2;
9686
+ let closeStore = null;
9687
+ if (options?.store && typeof options.store.get === "function") {
9688
+ store2 = options.store;
9689
+ } else if (options?.store === "redis") {
9690
+ if (!options.redis) throw new Error('session: redis client required when store: "redis"');
9691
+ store2 = new RedisStore(options.redis);
9692
+ } else {
9693
+ const mem = new MemoryStore();
9694
+ store2 = mem;
9695
+ closeStore = () => mem.close();
9696
+ }
9697
+ const mw = (async (req, ctx, next) => {
9698
+ const cookies = getCookies(req);
9699
+ const sid = cookies[cookieName];
9700
+ let session2;
9701
+ let loadedSid = sid ?? null;
9702
+ if (sid) {
9703
+ const data = await store2.get(sid);
9704
+ if (data) {
9705
+ session2 = createSessionObject(data, sid, store2, ttl);
9706
+ } else {
9707
+ loadedSid = null;
9708
+ session2 = createSessionObject({}, crypto7.randomUUID(), store2, ttl);
9709
+ }
9710
+ } else {
9711
+ session2 = createSessionObject({}, crypto7.randomUUID(), store2, ttl);
9712
+ }
9713
+ const snapshot = isSessionActive(session2) ? JSON.stringify(session2) : null;
9714
+ ctx.session = session2;
9715
+ ctx.sessionId = session2.id;
9716
+ const res = await next(req, ctx);
9717
+ const currentSession = ctx.session;
9718
+ if (!currentSession || currentSession[kDestroyed]) {
9719
+ if (loadedSid) {
9720
+ await store2.destroy(loadedSid);
9721
+ }
9722
+ return deleteCookie(res, cookieName, cookieOpts);
9723
+ }
9724
+ const currentData = isSessionActive(currentSession) ? JSON.stringify(currentSession) : null;
9725
+ const wasSaved = currentSession[kSaved];
9726
+ const changed = wasSaved || currentData !== snapshot;
9727
+ if (!changed) {
9728
+ if (loadedSid) {
9729
+ if (store2 instanceof RedisStore) {
9730
+ await store2.set(loadedSid, JSON.parse(currentData ?? "{}"), ttl);
9731
+ }
9732
+ }
9733
+ return res;
9734
+ }
9735
+ if (currentData && currentData !== "{}") {
9736
+ const targetSid = loadedSid ?? currentSession.id;
9737
+ const data = JSON.parse(currentData);
9738
+ await store2.set(targetSid, data, ttl);
9739
+ if (!loadedSid) {
9740
+ const cookieRes = setCookie(res, cookieName, targetSid, cookieOpts);
9741
+ return cookieRes;
9742
+ }
9743
+ } else if (loadedSid) {
9744
+ await store2.destroy(loadedSid);
9745
+ return deleteCookie(res, cookieName, cookieOpts);
9746
+ }
9747
+ return res;
9748
+ });
9749
+ mw.close = () => {
9750
+ closeStore?.();
9751
+ };
9752
+ mw.store = store2;
9753
+ return mw;
9754
+ }
9755
+
9756
+ // cache.ts
9757
+ import crypto8 from "node:crypto";
9758
+ var BINARY_PREFIXES = [
9759
+ "image/",
9760
+ "audio/",
9761
+ "video/",
9762
+ "application/octet-stream",
9763
+ "application/pdf",
9764
+ "application/zip",
9765
+ "application/gzip",
9766
+ "application/x-tar",
9767
+ "application/vnd.ms-",
9768
+ "application/vnd.openxmlformats-"
9769
+ ];
9770
+ function isCacheableContentType(ct) {
9771
+ return !BINARY_PREFIXES.some((p) => ct.startsWith(p));
9772
+ }
9773
+ function isCacheableStatus(status, allowed) {
9774
+ return allowed.includes(status);
9775
+ }
9776
+ function defaultCacheKey(req) {
9777
+ const hash = crypto8.createHash("sha256");
9778
+ hash.update(req.method);
9779
+ hash.update(req.url);
9780
+ return hash.digest("hex");
9781
+ }
9782
+ var MAX_ENTRIES = 1e5;
9783
+ var MemoryCache = class {
9784
+ store = /* @__PURE__ */ new Map();
9785
+ tagIndex = /* @__PURE__ */ new Map();
9786
+ interval;
9787
+ constructor(cleanupMs = 6e4) {
9788
+ this.interval = setInterval(() => this.cleanup(), cleanupMs);
9789
+ if (this.interval.unref) this.interval.unref();
9790
+ }
9791
+ async get(key) {
9792
+ const entry = this.store.get(key);
9793
+ if (!entry) return null;
9794
+ if (Date.now() > entry.expires) {
9795
+ this.store.delete(key);
9796
+ return null;
9797
+ }
9798
+ return entry.data;
9799
+ }
9800
+ async set(key, data, ttl) {
9801
+ if (this.store.size >= MAX_ENTRIES) {
9802
+ const oldest = this.store.keys().next();
9803
+ if (!oldest.done) this.store.delete(oldest.value);
9804
+ }
9805
+ this.store.set(key, { data, expires: Date.now() + ttl });
9806
+ for (const tag of data.tags) {
9807
+ let set = this.tagIndex.get(tag);
9808
+ if (!set) {
9809
+ set = /* @__PURE__ */ new Set();
9810
+ this.tagIndex.set(tag, set);
9811
+ }
9812
+ set.add(key);
9813
+ }
9814
+ }
9815
+ async delete(key) {
9816
+ this.store.delete(key);
9817
+ for (const [, set] of this.tagIndex) {
9818
+ set.delete(key);
9819
+ }
9820
+ }
9821
+ async invalidate(tag) {
9822
+ const keys = this.tagIndex.get(tag);
9823
+ if (!keys) return;
9824
+ for (const key of keys) {
9825
+ this.store.delete(key);
9826
+ }
9827
+ this.tagIndex.delete(tag);
9828
+ }
9829
+ async flush() {
9830
+ this.store.clear();
9831
+ this.tagIndex.clear();
9832
+ }
9833
+ cleanup() {
9834
+ const now = Date.now();
9835
+ for (const [key, entry] of this.store) {
9836
+ if (entry.expires < now) {
9837
+ this.store.delete(key);
9838
+ for (const [, set] of this.tagIndex) {
9839
+ set.delete(key);
9840
+ }
9841
+ }
9842
+ }
9843
+ }
9844
+ close() {
9845
+ clearInterval(this.interval);
9846
+ this.store.clear();
9847
+ this.tagIndex.clear();
9848
+ }
9849
+ /** Testing only. */
9850
+ get size() {
9851
+ return this.store.size;
9852
+ }
9853
+ };
9854
+ var RedisCache = class {
9855
+ redis;
9856
+ prefix;
9857
+ tagPrefix;
9858
+ constructor(redis2, prefix = "cache:") {
9859
+ this.redis = redis2;
9860
+ this.prefix = prefix;
9861
+ this.tagPrefix = `${prefix}tag:`;
9862
+ }
9863
+ key(sid) {
9864
+ return `${this.prefix}${sid}`;
9865
+ }
9866
+ tagKey(tag) {
9867
+ return `${this.tagPrefix}${tag}`;
9868
+ }
9869
+ async get(key) {
9870
+ const raw = await this.redis.get(this.key(key));
9871
+ if (!raw) return null;
9872
+ try {
9873
+ return JSON.parse(raw);
9874
+ } catch {
9875
+ await this.redis.del(this.key(key));
9876
+ return null;
9877
+ }
9878
+ }
9879
+ async set(key, entry, ttl) {
9880
+ const multi = this.redis.multi();
9881
+ multi.psetex(this.key(key), ttl, JSON.stringify(entry));
9882
+ const ttlSec = Math.ceil(ttl / 1e3);
9883
+ for (const tag of entry.tags) {
9884
+ multi.sadd(this.tagKey(tag), key);
9885
+ multi.expire(this.tagKey(tag), ttlSec);
9886
+ }
9887
+ await multi.exec();
9888
+ }
9889
+ async delete(key) {
9890
+ await this.redis.del(this.key(key));
9891
+ }
9892
+ async invalidate(tag) {
9893
+ const key = this.tagKey(tag);
9894
+ const members = await this.redis.smembers(key);
9895
+ if (members.length > 0) {
9896
+ await this.redis.del(key, ...members.map((m) => this.key(m)));
9897
+ }
9898
+ }
9899
+ async flush() {
9900
+ const keys = await this.redis.keys(`${this.prefix}*`);
9901
+ if (keys.length > 0) await this.redis.del(...keys);
9902
+ }
9903
+ };
9904
+ var DEFAULT_TTL = 3e5;
9905
+ var DEFAULT_MAX_BODY2 = 1024 * 1024;
9906
+ function shouldSkipCache(req) {
9907
+ if (req.method !== "GET" && req.method !== "HEAD") return true;
9908
+ if (req.headers.get("authorization")) return true;
9909
+ if (req.headers.get("cookie")) return true;
9910
+ return false;
9911
+ }
9912
+ function cache2(options) {
9913
+ const ttl = options?.ttl ?? DEFAULT_TTL;
9914
+ const cacheStatus = options?.cacheStatus ?? [200];
9915
+ const maxBodySize = options?.maxBodySize ?? DEFAULT_MAX_BODY2;
9916
+ const getKey = options?.key ?? defaultCacheKey;
9917
+ const getTag = options?.tag;
9918
+ const cacheCookies = options?.cacheCookies ?? false;
9919
+ let store2;
9920
+ let closeStore = null;
9921
+ if (options?.store && typeof options.store.get === "function") {
9922
+ store2 = options.store;
9923
+ } else if (options?.store === "redis") {
9924
+ if (!options.redis) throw new Error('cache: redis client required when store: "redis"');
9925
+ store2 = new RedisCache(options.redis);
9926
+ } else {
9927
+ const mem = new MemoryCache();
9928
+ store2 = mem;
9929
+ closeStore = () => mem.close();
9930
+ }
9931
+ const mw = (async (req, ctx, next) => {
9932
+ if (shouldSkipCache(req)) {
9933
+ return next(req, ctx);
9934
+ }
9935
+ const cacheKey = getKey(req);
9936
+ const cached = await store2.get(cacheKey);
9937
+ if (cached) {
9938
+ const age = Math.floor((Date.now() - cached.createdAt) / 1e3);
9939
+ const headers2 = new Headers(cached.headers);
9940
+ headers2.set("Age", String(age));
9941
+ headers2.set("X-Cache", "HIT");
9942
+ return new Response(cached.body, {
9943
+ status: cached.status,
9944
+ statusText: cached.statusText,
9945
+ headers: headers2
9946
+ });
9947
+ }
9948
+ const res = await next(req, ctx);
9949
+ if (!isCacheableStatus(res.status, cacheStatus)) return res;
9950
+ if (res.headers.get("set-cookie") && !cacheCookies) return res;
9951
+ if (res.headers.get("cache-control")?.includes("no-store")) return res;
9952
+ if (res.body && res.headers.get("content-type")?.includes("text/event-stream")) return res;
9953
+ const ct = res.headers.get("content-type") ?? "";
9954
+ if (!isCacheableContentType(ct)) return res;
9955
+ const clone = res.clone();
9956
+ const bodyText = await clone.text();
9957
+ if (bodyText.length > maxBodySize) return res;
9958
+ const tags = [];
9959
+ if (getTag) {
9960
+ const result = getTag(req, ctx);
9961
+ if (result) {
9962
+ if (Array.isArray(result)) tags.push(...result);
9963
+ else tags.push(result);
9964
+ }
9965
+ }
9966
+ const headers = {};
9967
+ res.headers.forEach((value, key) => {
9968
+ if (key.toLowerCase() === "set-cookie" && !cacheCookies) return;
9969
+ headers[key] = value;
9970
+ });
9971
+ const entry = {
9972
+ status: res.status,
9973
+ statusText: res.statusText,
9974
+ headers,
9975
+ body: bodyText,
9976
+ createdAt: Date.now(),
9977
+ tags
9978
+ };
9979
+ await store2.set(cacheKey, entry, ttl);
9980
+ return res;
9981
+ });
9982
+ mw.store = store2;
9983
+ mw.invalidate = async (tag) => store2.invalidate(tag);
9984
+ mw.flush = async () => store2.flush();
9985
+ mw.close = () => {
9986
+ closeStore?.();
9987
+ };
9988
+ return mw;
9989
+ }
9990
+
9991
+ // webhook.ts
9992
+ import crypto9 from "node:crypto";
9993
+ function timingSafeEqual2(a, b) {
9994
+ try {
9995
+ return crypto9.timingSafeEqual(Buffer.from(a), Buffer.from(b));
9996
+ } catch {
9997
+ return false;
9998
+ }
9999
+ }
10000
+ function createStripeVerifier(config) {
10001
+ return (body, headers) => {
10002
+ const sigHeader = headers["stripe-signature"];
10003
+ if (!sigHeader) return { valid: false, provider: "stripe", event: "", id: void 0 };
10004
+ const parts = sigHeader.split(",").reduce((acc, p) => {
10005
+ const [k, ...v] = p.split("=");
10006
+ if (k) acc[k.trim()] = v.join("=").trim();
10007
+ return acc;
10008
+ }, {});
10009
+ const timestamp = parts["t"];
10010
+ const signature = parts["v1"];
10011
+ if (!timestamp || !signature) return { valid: false, provider: "stripe", event: "", id: void 0 };
10012
+ const signed = `${timestamp}.${body}`;
10013
+ const expected = crypto9.createHmac("sha256", config.secret).update(signed).digest("hex");
10014
+ const valid = timingSafeEqual2(signature, expected);
10015
+ let event = "";
10016
+ let id2;
10017
+ try {
10018
+ const parsed = JSON.parse(body);
10019
+ event = parsed.type ?? "";
10020
+ id2 = parsed.id;
10021
+ } catch {
10022
+ }
10023
+ return { valid, provider: "stripe", event, id: id2 };
10024
+ };
10025
+ }
10026
+ function createGitHubVerifier(config) {
10027
+ return (body, headers) => {
10028
+ const sig = headers["x-hub-signature-256"];
10029
+ if (!sig) return { valid: false, provider: "github", event: "", id: void 0 };
10030
+ const expected = `sha256=${crypto9.createHmac("sha256", config.secret).update(body).digest("hex")}`;
10031
+ const valid = timingSafeEqual2(sig, expected);
10032
+ let event = headers["x-github-event"] ?? "";
10033
+ let id2;
10034
+ try {
10035
+ const parsed = JSON.parse(body);
10036
+ id2 = headers["x-github-delivery"] || parsed.id;
10037
+ } catch {
10038
+ }
10039
+ return { valid, provider: "github", event, id: id2 };
10040
+ };
10041
+ }
10042
+ function createSlackVerifier(config) {
10043
+ return (body, headers) => {
10044
+ const signature = headers["x-slack-signature"];
10045
+ const timestamp = headers["x-slack-request-timestamp"];
10046
+ if (!signature || !timestamp) return { valid: false, provider: "slack", event: "", id: void 0 };
10047
+ const now = Math.floor(Date.now() / 1e3);
10048
+ const ts = parseInt(timestamp, 10);
10049
+ if (isNaN(ts) || Math.abs(now - ts) > 300) {
10050
+ return { valid: false, provider: "slack", event: "", id: void 0 };
10051
+ }
10052
+ const sigBase = `v0:${timestamp}:${body}`;
10053
+ const expected = `v0=${crypto9.createHmac("sha256", config.secret).update(sigBase).digest("hex")}`;
10054
+ const valid = timingSafeEqual2(signature, expected);
10055
+ let event = "";
10056
+ let id2;
10057
+ try {
10058
+ const parsed = JSON.parse(body);
10059
+ event = parsed.event?.type ?? parsed.type ?? (parsed.challenge ? "url_verification" : "");
10060
+ id2 = parsed.event_id || parsed.ssl?.event_id;
10061
+ } catch {
10062
+ }
10063
+ return { valid, provider: "slack", event, id: id2 };
10064
+ };
10065
+ }
10066
+ var IdempotencyStore = class {
10067
+ store = /* @__PURE__ */ new Map();
10068
+ ttl;
10069
+ constructor(ttl) {
10070
+ this.ttl = ttl;
10071
+ }
10072
+ /** Returns true if this event ID has already been processed. */
10073
+ isDuplicate(id2) {
10074
+ if (this.store.has(id2)) return true;
10075
+ this.store.set(id2, Date.now());
10076
+ return false;
10077
+ }
10078
+ /** Periodic cleanup */
10079
+ cleanup() {
10080
+ const now = Date.now();
10081
+ for (const [key, ts] of this.store) {
10082
+ if (now - ts > this.ttl) this.store.delete(key);
10083
+ }
10084
+ }
10085
+ };
10086
+ var EventBus = class {
10087
+ handlers = /* @__PURE__ */ new Map();
10088
+ on(event, handler) {
10089
+ let set = this.handlers.get(event);
10090
+ if (!set) {
10091
+ set = /* @__PURE__ */ new Set();
10092
+ this.handlers.set(event, set);
10093
+ }
10094
+ set.add(handler);
10095
+ }
10096
+ off(event, handler) {
10097
+ const set = this.handlers.get(event);
10098
+ if (!set) return;
10099
+ set.delete(handler);
10100
+ if (set.size === 0) this.handlers.delete(event);
10101
+ }
10102
+ async emit(event, payload, provider, id2, ctx) {
10103
+ const we = { event, payload, provider, id: id2 };
10104
+ const specific = this.handlers.get(event);
10105
+ if (specific) {
10106
+ for (const handler of specific) {
10107
+ await handler(we, ctx);
10108
+ }
10109
+ }
10110
+ const wildcard = this.handlers.get("*");
10111
+ if (wildcard) {
10112
+ for (const handler of wildcard) {
10113
+ await handler(we, ctx);
10114
+ }
10115
+ }
10116
+ }
10117
+ };
10118
+ function webhook(options) {
10119
+ const replayProtection = options?.replayProtection ?? true;
10120
+ const idempotencyTTL = options?.idempotencyTTL ?? 36e5;
10121
+ const mountPath = options?.path ?? "/";
10122
+ const verifiers = [];
10123
+ if (options?.stripe) verifiers.push(createStripeVerifier(options.stripe));
10124
+ if (options?.github) verifiers.push(createGitHubVerifier(options.github));
10125
+ if (options?.slack) verifiers.push(createSlackVerifier(options.slack));
10126
+ if (options?.custom) {
10127
+ for (const c of options.custom) {
10128
+ verifiers.push(async (body, headers) => {
10129
+ const valid = await c.verify(body, headers);
10130
+ let event = "";
10131
+ try {
10132
+ event = c.event(JSON.parse(body), headers);
10133
+ } catch {
10134
+ }
10135
+ return { valid, provider: c.name, event, id: void 0 };
10136
+ });
10137
+ }
10138
+ }
10139
+ const bus = new EventBus();
10140
+ const idempotency = new IdempotencyStore(idempotencyTTL);
10141
+ const cleanupInterval = setInterval(() => idempotency.cleanup(), 6e4);
10142
+ if (cleanupInterval.unref) cleanupInterval.unref();
10143
+ const router = new Router();
10144
+ const handler = async (req, ctx) => {
10145
+ const body = await req.text();
10146
+ if (!body) {
10147
+ return new Response("Empty body", { status: 400 });
10148
+ }
10149
+ const headers = {};
10150
+ req.headers.forEach((v, k) => {
10151
+ headers[k] = v;
10152
+ });
10153
+ for (const verify of verifiers) {
10154
+ const result = await verify(body, headers);
10155
+ if (!result.valid) continue;
10156
+ if (replayProtection && result.id) {
10157
+ if (idempotency.isDuplicate(result.id)) {
10158
+ return new Response("OK", { status: 200 });
10159
+ }
10160
+ }
10161
+ let payload;
10162
+ try {
10163
+ payload = JSON.parse(body);
10164
+ } catch {
10165
+ return new Response("Invalid JSON body", { status: 400 });
10166
+ }
10167
+ if (result.provider === "slack" && payload?.challenge) {
10168
+ return new Response(JSON.stringify({ challenge: payload.challenge }), {
10169
+ headers: { "content-type": "application/json" }
10170
+ });
10171
+ }
10172
+ try {
10173
+ await bus.emit(result.event, payload, result.provider, result.id, ctx);
10174
+ } catch (err) {
10175
+ const msg = err instanceof Error ? err.message : String(err);
10176
+ console.error(`[webhook] handler error for ${result.provider}.${result.event}: ${msg}`);
10177
+ return new Response("Handler error", { status: 500 });
10178
+ }
10179
+ return new Response("OK", { status: 200 });
10180
+ }
10181
+ return new Response("Unauthorized", { status: 401 });
10182
+ };
10183
+ router.post(mountPath, handler);
10184
+ const mod = router;
10185
+ mod.on = (event, handler2) => {
10186
+ bus.on(event, handler2);
10187
+ return mod;
10188
+ };
10189
+ mod.off = (event, handler2) => {
10190
+ bus.off(event, handler2);
10191
+ return mod;
10192
+ };
10193
+ mod._cleanup = () => {
10194
+ clearInterval(cleanupInterval);
10195
+ };
10196
+ return mod;
10197
+ }
10198
+
10199
+ // fts.ts
10200
+ var fts_exports = {};
10201
+ __export(fts_exports, {
10202
+ createIndex: () => createIndex,
10203
+ dropIndex: () => dropIndex,
10204
+ search: () => search,
10205
+ suggest: () => suggest
10206
+ });
10207
+ function resolveTableName(table) {
10208
+ const name = table.inner?.tableName ?? table.tableName;
10209
+ if (!name || typeof name !== "string") {
10210
+ throw new Error("fts: could not determine table name. Ensure you pass a pg.table() result.");
10211
+ }
10212
+ return name;
10213
+ }
10214
+ function escapeIdent(s) {
10215
+ return `"${s.replace(/"/g, '""')}"`;
10216
+ }
10217
+ function sqlLit(s) {
10218
+ return `'${s.replace(/'/g, "''")}'`;
10219
+ }
10220
+ async function createIndex(sql2, table, fields, options) {
10221
+ const language = options?.language ?? "english";
10222
+ const tableName = resolveTableName(table);
10223
+ const indexName = options?.indexName ?? `${tableName}_fts_idx`;
10224
+ const vectorExpr = fields.map((f) => `coalesce(${escapeIdent(f)}, '')`).join(` || ' ' || `);
10225
+ const indexType = options?.indexType ?? "gin";
10226
+ await sql2.unsafe(`
10227
+ CREATE INDEX IF NOT EXISTS ${escapeIdent(indexName)}
10228
+ ON ${escapeIdent(tableName)}
10229
+ USING ${indexType}
10230
+ (to_tsvector(${sqlLit(language)}, ${vectorExpr}))
10231
+ `);
10232
+ }
10233
+ async function dropIndex(sql2, table, options) {
10234
+ const tableName = resolveTableName(table);
10235
+ const indexName = options?.indexName ?? `${tableName}_fts_idx`;
10236
+ await sql2.unsafe(`DROP INDEX IF EXISTS ${escapeIdent(indexName)}`);
10237
+ }
10238
+ async function search(sql2, table, query, options) {
10239
+ const tableName = resolveTableName(table);
10240
+ const language = options?.language ?? "english";
10241
+ const searchFields = options?.fields;
10242
+ const limit = options?.limit ?? 20;
10243
+ const offset = options?.offset ?? 0;
10244
+ const rankCol = options?.rankColumn ?? "_rank";
10245
+ if (!searchFields?.length) {
10246
+ throw new Error("fts.search: `fields` option is required. Specify which columns to search.");
10247
+ }
10248
+ const sanitized = query.trim();
10249
+ if (!sanitized) return [];
10250
+ const vectorExpr = searchFields.map((f) => `coalesce(${escapeIdent(f)}, '')`).join(` || ' ' || `);
10251
+ const langLit = sqlLit(language);
10252
+ const queryLit = sqlLit(sanitized);
10253
+ const rankColId = escapeIdent(rankCol);
10254
+ const tableId = escapeIdent(tableName);
10255
+ const headlineExpr = options?.headline ? searchFields.map(
10256
+ (f) => `ts_headline(${langLit}, ${escapeIdent(f)}, websearch_to_tsquery(${langLit}, ${queryLit}), 'MaxWords=30,MinWords=15') as ${escapeIdent(f + "_headline")}`
10257
+ ).join(",\n ") : "";
10258
+ const sql_query = `
10259
+ SELECT
10260
+ *,
10261
+ ts_rank(
10262
+ to_tsvector(${langLit}, ${vectorExpr}),
10263
+ websearch_to_tsquery(${langLit}, ${queryLit})
10264
+ ) as ${rankColId}
10265
+ ${headlineExpr ? "," + headlineExpr : ""}
10266
+ FROM ${tableId}
10267
+ WHERE to_tsvector(${langLit}, ${vectorExpr}) @@ websearch_to_tsquery(${langLit}, ${queryLit})
10268
+ ORDER BY ${rankColId} DESC
10269
+ LIMIT ${limit} OFFSET ${offset}
10270
+ `;
10271
+ const rows = await sql2.unsafe(sql_query);
10272
+ return rows.map((row) => {
10273
+ const result = {
10274
+ id: row.id,
10275
+ rank: Number(row[rankCol]) || 0,
10276
+ row
10277
+ };
10278
+ if (options?.headline && searchFields) {
10279
+ const snippets = searchFields.map((f) => row[`${f}_headline`]).filter(Boolean);
10280
+ result.headline = snippets.join(" ... ");
10281
+ }
10282
+ return result;
10283
+ });
10284
+ }
10285
+ async function suggest(sql2, table, prefix, options) {
10286
+ const tableName = resolveTableName(table);
10287
+ const field = options?.field;
10288
+ const language = options?.language ?? "english";
10289
+ const limit = options?.limit ?? 10;
10290
+ if (!field) throw new Error("fts.suggest: `field` option is required");
10291
+ const sanitized = prefix.replace(/[^\w\s-]/g, " ").trim();
10292
+ if (!sanitized) return [];
10293
+ const rows = await sql2.unsafe(`
10294
+ SELECT DISTINCT ts_lexize(${sqlLit(language)}, word) as tokens
10295
+ FROM (
10296
+ SELECT regexp_split_to_table(lower(${escapeIdent(field)}), E'\\W+') as word
10297
+ FROM ${escapeIdent(tableName)}
10298
+ ) words
10299
+ WHERE word LIKE ${sqlLit(sanitized + "%")}
10300
+ LIMIT ${limit}
10301
+ `);
10302
+ return rows.map((r) => r.tokens?.[0] ?? "").filter(Boolean);
10303
+ }
9355
10304
  export {
9356
10305
  DEFAULT_MAX_BODY,
9357
10306
  MIGRATIONS_TABLE,
10307
+ MemoryCache,
10308
+ MemoryStore,
10309
+ RedisCache,
10310
+ RedisStore,
9358
10311
  Router,
9359
10312
  TestApp,
9360
10313
  TestRequest,
@@ -9363,11 +10316,13 @@ export {
9363
10316
  aiStream,
9364
10317
  analytics,
9365
10318
  auth,
10319
+ cache2 as cache,
9366
10320
  compress,
9367
10321
  cors,
9368
10322
  createHub,
9369
10323
  createOpenAI,
9370
10324
  createSSEStream,
10325
+ createTestDb,
9371
10326
  createTestServer,
9372
10327
  createWorker,
9373
10328
  csrf,
@@ -9380,6 +10335,7 @@ export {
9380
10335
  embedMany,
9381
10336
  formatSSE,
9382
10337
  formatSSEData,
10338
+ fts_exports as fts,
9383
10339
  generateObject,
9384
10340
  generateText2 as generateText,
9385
10341
  getCookies,
@@ -9410,6 +10366,7 @@ export {
9410
10366
  seoTags,
9411
10367
  serve,
9412
10368
  serveStatic,
10369
+ session,
9413
10370
  setCookie,
9414
10371
  smoothStream,
9415
10372
  ssr,
@@ -9421,5 +10378,7 @@ export {
9421
10378
  traceElapsed,
9422
10379
  upload,
9423
10380
  user,
9424
- validate
10381
+ validate,
10382
+ webhook,
10383
+ withTestDb
9425
10384
  };