weifuwu 0.21.0 → 0.22.1

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";
@@ -3324,17 +3428,30 @@ function user(options) {
3324
3428
  }
3325
3429
  function middleware() {
3326
3430
  return async (req, ctx, next) => {
3327
- const header = req.headers.get("Authorization");
3328
- if (!header?.startsWith("Bearer ")) {
3329
- return new Response("Unauthorized", { status: 401, headers: { "WWW-Authenticate": "Bearer" } });
3431
+ const sessionUserId = ctx.session?.userId;
3432
+ if (sessionUserId) {
3433
+ const row = await findById(sessionUserId);
3434
+ if (row) {
3435
+ ctx.user = stripPassword(row);
3436
+ return next(req, ctx);
3437
+ }
3438
+ if (typeof ctx.session?.destroy === "function") {
3439
+ ;
3440
+ ctx.session.destroy();
3441
+ } else {
3442
+ delete ctx.session?.userId;
3443
+ }
3330
3444
  }
3331
- const token = header.slice(7);
3332
- const userData = await verify(token);
3333
- if (!userData) {
3334
- return new Response("Unauthorized", { status: 401, headers: { "WWW-Authenticate": "Bearer" } });
3445
+ const header = req.headers.get("Authorization");
3446
+ const token = header?.startsWith("Bearer ") ? header.slice(7) : null;
3447
+ if (token) {
3448
+ const userData = await verify(token);
3449
+ if (userData) {
3450
+ ctx.user = userData;
3451
+ return next(req, ctx);
3452
+ }
3335
3453
  }
3336
- ctx.user = userData;
3337
- return next(req, ctx);
3454
+ return new Response("Unauthorized", { status: 401, headers: { "WWW-Authenticate": "Bearer" } });
3338
3455
  };
3339
3456
  }
3340
3457
  function router() {
@@ -3352,12 +3469,19 @@ function user(options) {
3352
3469
  return Response.json({ error: err.message }, { status });
3353
3470
  }
3354
3471
  });
3355
- r2.post("/login", async (req) => {
3472
+ r2.post("/login", async (req, ctx) => {
3356
3473
  try {
3357
3474
  const body = await req.json();
3358
3475
  const result = await login(body);
3476
+ if (ctx.session) {
3477
+ ;
3478
+ ctx.session.userId = result.user.id;
3479
+ ctx.session.role = result.user.role;
3480
+ }
3359
3481
  const res = Response.json(result);
3360
- res.headers.set("Set-Cookie", `session=${result.token}; HttpOnly; SameSite=Lax; Path=/`);
3482
+ if (!ctx.session) {
3483
+ res.headers.set("Set-Cookie", `session=${result.token}; HttpOnly; SameSite=Lax; Path=/`);
3484
+ }
3361
3485
  return res;
3362
3486
  } catch (err) {
3363
3487
  if (err instanceof z2.ZodError) {
@@ -3476,6 +3600,8 @@ function queue(opts) {
3476
3600
  let _processed = 0;
3477
3601
  let _failed = 0;
3478
3602
  const jobKey = `${prefix}:jobs`;
3603
+ const failedKey = `${prefix}:failed`;
3604
+ const MAX_FAILED = 1e3;
3479
3605
  const mw = ((req, ctx, next) => {
3480
3606
  ctx.queue = q;
3481
3607
  return next(req, ctx);
@@ -3490,7 +3616,15 @@ function queue(opts) {
3490
3616
  _processed++;
3491
3617
  } catch (e) {
3492
3618
  _failed++;
3493
- console.error("[queue] handler error:", e.message);
3619
+ const errMsg = e.message;
3620
+ console.error("[queue] handler error:", errMsg);
3621
+ const failedEntry = JSON.stringify({
3622
+ ...job,
3623
+ error: errMsg,
3624
+ failedAt: Date.now()
3625
+ });
3626
+ await redis2.lpush(failedKey, failedEntry);
3627
+ await redis2.ltrim(failedKey, 0, MAX_FAILED - 1);
3494
3628
  } finally {
3495
3629
  inflight--;
3496
3630
  }
@@ -3571,6 +3705,101 @@ function queue(opts) {
3571
3705
  while (inflight > 0) await new Promise((r) => setTimeout(r, 50));
3572
3706
  redis2.disconnect();
3573
3707
  };
3708
+ mw.jobs = async function jobs(limit) {
3709
+ const raw = await redis2.zrevrange(jobKey, 0, (limit ?? 50) - 1);
3710
+ return raw.map((r) => {
3711
+ try {
3712
+ return JSON.parse(r);
3713
+ } catch {
3714
+ return null;
3715
+ }
3716
+ }).filter(Boolean);
3717
+ };
3718
+ mw.failedJobs = async function failedJobs(limit) {
3719
+ const raw = await redis2.lrange(failedKey, 0, (limit ?? 50) - 1);
3720
+ return raw.map((r) => {
3721
+ try {
3722
+ return JSON.parse(r);
3723
+ } catch {
3724
+ return null;
3725
+ }
3726
+ }).filter(Boolean);
3727
+ };
3728
+ mw.retryFailed = async function retryFailed(jobId) {
3729
+ const raw = await redis2.lrange(failedKey, 0, -1);
3730
+ for (const entry of raw) {
3731
+ try {
3732
+ const job = JSON.parse(entry);
3733
+ if (job.id === jobId) {
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
+ return true;
3741
+ }
3742
+ } catch {
3743
+ }
3744
+ }
3745
+ return false;
3746
+ };
3747
+ mw.retryAllFailed = async function retryAllFailed(type) {
3748
+ const raw = await redis2.lrange(failedKey, 0, -1);
3749
+ let count = 0;
3750
+ for (const entry of raw) {
3751
+ try {
3752
+ const job = JSON.parse(entry);
3753
+ if (type && job.type !== type) continue;
3754
+ await redis2.lrem(failedKey, 1, entry);
3755
+ const reJob = { ...job, runAt: Date.now() };
3756
+ delete reJob.error;
3757
+ delete reJob.failedAt;
3758
+ await redis2.zadd(jobKey, reJob.runAt, JSON.stringify(reJob));
3759
+ _failed--;
3760
+ count++;
3761
+ } catch {
3762
+ }
3763
+ }
3764
+ return count;
3765
+ };
3766
+ mw.dashboard = function dashboard() {
3767
+ const r = new Router();
3768
+ r.get("/", async (req, ctx) => {
3769
+ const s = q.stats();
3770
+ const pending = await q.jobs(100);
3771
+ const byType = {};
3772
+ for (const job of pending) {
3773
+ if (!byType[job.type]) byType[job.type] = { pending: 0, failed: 0 };
3774
+ byType[job.type].pending++;
3775
+ }
3776
+ const failed = await q.failedJobs(1e3);
3777
+ for (const job of failed) {
3778
+ if (!byType[job.type]) byType[job.type] = { pending: 0, failed: 0 };
3779
+ byType[job.type].failed++;
3780
+ }
3781
+ return Response.json({
3782
+ stats: s,
3783
+ types: byType,
3784
+ failedCount: failed.length
3785
+ });
3786
+ });
3787
+ r.get("/:type/failed", async (req, ctx) => {
3788
+ const failed = await q.failedJobs(100);
3789
+ const filtered = failed.filter((j) => j.type === ctx.params.type);
3790
+ return Response.json({ jobs: filtered, count: filtered.length });
3791
+ });
3792
+ r.post("/:type/retry", async (req, ctx) => {
3793
+ const count = await q.retryAllFailed(ctx.params.type);
3794
+ return Response.json({ retried: count });
3795
+ });
3796
+ r.post("/retry/:id", async (req, ctx) => {
3797
+ const ok = await q.retryFailed(ctx.params.id);
3798
+ if (!ok) return new Response("Job not found", { status: 404 });
3799
+ return Response.json({ retried: true });
3800
+ });
3801
+ return r;
3802
+ };
3574
3803
  mw.stats = () => ({
3575
3804
  running,
3576
3805
  inflight,
@@ -7254,12 +7483,12 @@ async function buildRouter4(deps) {
7254
7483
  const router = new Router();
7255
7484
  router.post("/sessions", async (req, ctx) => {
7256
7485
  const body = await req.json().catch(() => ({}));
7257
- const session = await createSession(sql2, {
7486
+ const session2 = await createSession(sql2, {
7258
7487
  title: body.title,
7259
7488
  model: body.model,
7260
7489
  systemPrompt: body.systemPrompt || systemPrompt
7261
7490
  }, workspace, ctx.mountPath || "");
7262
- return Response.json(session, { status: 201 });
7491
+ return Response.json(session2, { status: 201 });
7263
7492
  });
7264
7493
  router.get("/sessions", async () => {
7265
7494
  const sessions2 = await listSessions(sql2);
@@ -7267,10 +7496,10 @@ async function buildRouter4(deps) {
7267
7496
  });
7268
7497
  router.get("/sessions/:id", async (_req, ctx) => {
7269
7498
  const id2 = ctx.params.id;
7270
- const session = await getSession(sql2, id2);
7271
- if (!session) return new Response("Not Found", { status: 404 });
7499
+ const session2 = await getSession(sql2, id2);
7500
+ if (!session2) return new Response("Not Found", { status: 404 });
7272
7501
  const messages2 = await getHistory(sql2, id2);
7273
- return Response.json({ session, messages: messages2 });
7502
+ return Response.json({ session: session2, messages: messages2 });
7274
7503
  });
7275
7504
  router.delete("/sessions/:id", async (_req, ctx) => {
7276
7505
  const id2 = ctx.params.id;
@@ -7279,12 +7508,12 @@ async function buildRouter4(deps) {
7279
7508
  });
7280
7509
  router.post("/sessions/:id/message", async (req, ctx) => {
7281
7510
  const sessionId = ctx.params.id;
7282
- const session = await getSession(sql2, sessionId);
7283
- if (!session) return new Response("Session Not Found", { status: 404 });
7511
+ const session2 = await getSession(sql2, sessionId);
7512
+ if (!session2) return new Response("Session Not Found", { status: 404 });
7284
7513
  const { content } = await req.json();
7285
7514
  if (!content) return new Response("Missing content", { status: 400 });
7286
7515
  const toolCtx = {
7287
- workspace: session.workspace || workspace,
7516
+ workspace: session2.workspace || workspace,
7288
7517
  permissions,
7289
7518
  pendingQuestions,
7290
7519
  skillsRegistry: deps.skillsRegistry
@@ -7292,10 +7521,10 @@ async function buildRouter4(deps) {
7292
7521
  const tools = createTools(toolCtx);
7293
7522
  const allSkills = [...skills];
7294
7523
  const sysPrompt = buildSystemPrompt({
7295
- workspace: session.workspace || workspace,
7296
- model: session.model,
7524
+ workspace: session2.workspace || workspace,
7525
+ model: session2.model,
7297
7526
  skills: allSkills,
7298
- systemPrompt: session.system_prompt || systemPrompt
7527
+ systemPrompt: session2.system_prompt || systemPrompt
7299
7528
  });
7300
7529
  const history = await getHistory(sql2, sessionId);
7301
7530
  await addTextMessage(sql2, sessionId, "user", content);
@@ -7312,8 +7541,8 @@ async function buildRouter4(deps) {
7312
7541
  });
7313
7542
  router.get("/sessions/:id/usage", async (_req, ctx) => {
7314
7543
  const sessionId = ctx.params.id;
7315
- const session = await getSession(sql2, sessionId);
7316
- if (!session) return new Response("Not Found", { status: 404 });
7544
+ const session2 = await getSession(sql2, sessionId);
7545
+ if (!session2) return new Response("Not Found", { status: 404 });
7317
7546
  const rows = await sql2`
7318
7547
  SELECT
7319
7548
  COUNT(*)::int AS message_count,
@@ -7363,13 +7592,13 @@ function createWSHandler2(deps) {
7363
7592
  switch (msg.type) {
7364
7593
  case "create": {
7365
7594
  try {
7366
- const session = await createSession(sql2, {
7595
+ const session2 = await createSession(sql2, {
7367
7596
  userId: client.userId,
7368
7597
  title: msg.title,
7369
7598
  model: msg.model,
7370
7599
  systemPrompt: msg.systemPrompt || systemPrompt
7371
7600
  }, workspace, client.mountPath);
7372
- ws.send(JSON.stringify({ type: "session_created", session }));
7601
+ ws.send(JSON.stringify({ type: "session_created", session: session2 }));
7373
7602
  } catch (e) {
7374
7603
  ws.send(JSON.stringify({ type: "error", error: e.message }));
7375
7604
  }
@@ -7386,13 +7615,13 @@ function createWSHandler2(deps) {
7386
7615
  client.abortController = controller;
7387
7616
  client.currentSessionId = session_id;
7388
7617
  try {
7389
- const session = await getSession(sql2, session_id);
7390
- if (!session) {
7618
+ const session2 = await getSession(sql2, session_id);
7619
+ if (!session2) {
7391
7620
  ws.send(JSON.stringify({ type: "error", error: "Session not found" }));
7392
7621
  return;
7393
7622
  }
7394
7623
  const toolCtx = {
7395
- workspace: session.workspace || workspace,
7624
+ workspace: session2.workspace || workspace,
7396
7625
  permissions,
7397
7626
  pendingQuestions,
7398
7627
  skillsRegistry
@@ -7400,10 +7629,10 @@ function createWSHandler2(deps) {
7400
7629
  const tools = createTools(toolCtx);
7401
7630
  const allSkills = [...skills];
7402
7631
  const sysPrompt = buildSystemPrompt({
7403
- workspace: session.workspace || workspace,
7404
- model: session.model,
7632
+ workspace: session2.workspace || workspace,
7633
+ model: session2.model,
7405
7634
  skills: allSkills,
7406
- systemPrompt: session.system_prompt || systemPrompt
7635
+ systemPrompt: session2.system_prompt || systemPrompt
7407
7636
  });
7408
7637
  const history = await getHistory(sql2, session_id);
7409
7638
  await addTextMessage(sql2, session_id, "user", content);
@@ -7911,14 +8140,14 @@ function preferences(options) {
7911
8140
  const dir = options.dir ? resolve13(options.dir) : void 0;
7912
8141
  const localeOpts = { ...defaults.locale, ...options.locale };
7913
8142
  const themeOpts = { ...defaults.theme, ...options.theme };
7914
- const cache2 = /* @__PURE__ */ new Map();
8143
+ const cache3 = /* @__PURE__ */ new Map();
7915
8144
  function validLocale(locale) {
7916
8145
  return /^[\w-]+$/.test(locale) && !locale.includes("..");
7917
8146
  }
7918
8147
  async function load(locale) {
7919
8148
  if (!dir) return {};
7920
8149
  if (!validLocale(locale)) return {};
7921
- const cached = cache2.get(locale);
8150
+ const cached = cache3.get(locale);
7922
8151
  if (cached) return cached;
7923
8152
  const filePath = join7(dir, `${locale}.json`);
7924
8153
  let data = null;
@@ -7926,16 +8155,16 @@ function preferences(options) {
7926
8155
  await stat2(filePath);
7927
8156
  const content = await readFile2(filePath, "utf-8");
7928
8157
  data = JSON.parse(content);
7929
- cache2.set(locale, data);
8158
+ cache3.set(locale, data);
7930
8159
  return data;
7931
8160
  } catch {
7932
8161
  }
7933
8162
  if (!data) {
7934
8163
  const short = locale.split("-")[0];
7935
8164
  if (short !== locale) {
7936
- const fallback = cache2.get(short) || await load(short);
8165
+ const fallback = cache3.get(short) || await load(short);
7937
8166
  if (fallback && Object.keys(fallback).length > 0) {
7938
- cache2.set(locale, fallback);
8167
+ cache3.set(locale, fallback);
7939
8168
  return fallback;
7940
8169
  }
7941
8170
  }
@@ -9352,9 +9581,753 @@ function registerWorker(url) {
9352
9581
  }
9353
9582
  };
9354
9583
  }
9584
+
9585
+ // session.ts
9586
+ import crypto7 from "node:crypto";
9587
+ var kSaved = /* @__PURE__ */ Symbol("session.saved");
9588
+ var kDestroyed = /* @__PURE__ */ Symbol("session.destroyed");
9589
+ var kId = /* @__PURE__ */ Symbol("session.id");
9590
+ var kStore = /* @__PURE__ */ Symbol("session.store");
9591
+ var kTtl = /* @__PURE__ */ Symbol("session.ttl");
9592
+ var MAX_SESSIONS = 1e5;
9593
+ var MemoryStore = class {
9594
+ store = /* @__PURE__ */ new Map();
9595
+ interval;
9596
+ constructor(cleanupMs = 6e4) {
9597
+ this.interval = setInterval(() => this.cleanup(), cleanupMs);
9598
+ if (this.interval.unref) this.interval.unref();
9599
+ }
9600
+ async get(sid) {
9601
+ const entry = this.store.get(sid);
9602
+ if (!entry) return null;
9603
+ if (Date.now() > entry.expires) {
9604
+ this.store.delete(sid);
9605
+ return null;
9606
+ }
9607
+ return entry.data;
9608
+ }
9609
+ async set(sid, data, ttl) {
9610
+ if (this.store.size >= MAX_SESSIONS) {
9611
+ const oldest = this.store.keys().next();
9612
+ if (!oldest.done) this.store.delete(oldest.value);
9613
+ }
9614
+ this.store.set(sid, { data, expires: Date.now() + ttl });
9615
+ }
9616
+ async destroy(sid) {
9617
+ this.store.delete(sid);
9618
+ }
9619
+ cleanup() {
9620
+ const now = Date.now();
9621
+ for (const [key, entry] of this.store) {
9622
+ if (entry.expires < now) this.store.delete(key);
9623
+ }
9624
+ }
9625
+ close() {
9626
+ clearInterval(this.interval);
9627
+ this.store.clear();
9628
+ }
9629
+ /** Testing only: return approximate count. */
9630
+ get size() {
9631
+ return this.store.size;
9632
+ }
9633
+ };
9634
+ var RedisStore = class {
9635
+ redis;
9636
+ prefix;
9637
+ constructor(redis2, prefix = "session:") {
9638
+ this.redis = redis2;
9639
+ this.prefix = prefix;
9640
+ }
9641
+ key(sid) {
9642
+ return `${this.prefix}${sid}`;
9643
+ }
9644
+ async get(sid) {
9645
+ const raw = await this.redis.get(this.key(sid));
9646
+ if (!raw) return null;
9647
+ try {
9648
+ return JSON.parse(raw);
9649
+ } catch {
9650
+ await this.redis.del(this.key(sid));
9651
+ return null;
9652
+ }
9653
+ }
9654
+ async set(sid, data, ttl) {
9655
+ const ttlSec = Math.ceil(ttl / 1e3);
9656
+ await this.redis.setex(this.key(sid), ttlSec, JSON.stringify(data));
9657
+ }
9658
+ async destroy(sid) {
9659
+ await this.redis.del(this.key(sid));
9660
+ }
9661
+ };
9662
+ function createSessionObject(data, sid, store2, ttl) {
9663
+ const obj = data ?? {};
9664
+ obj[kSaved] = false;
9665
+ obj[kDestroyed] = false;
9666
+ obj[kId] = sid;
9667
+ obj[kStore] = store2;
9668
+ obj[kTtl] = ttl;
9669
+ obj.save = () => {
9670
+ obj[kSaved] = true;
9671
+ };
9672
+ obj.destroy = () => {
9673
+ obj[kDestroyed] = true;
9674
+ obj[kSaved] = false;
9675
+ for (const key of Object.keys(obj)) {
9676
+ if (typeof key === "symbol") continue;
9677
+ delete obj[key];
9678
+ }
9679
+ };
9680
+ Object.defineProperty(obj, "id", {
9681
+ get: () => obj[kId],
9682
+ enumerable: false,
9683
+ configurable: false
9684
+ });
9685
+ Object.defineProperty(obj, "save", { enumerable: false, configurable: true, writable: true, value: obj.save });
9686
+ Object.defineProperty(obj, "destroy", { enumerable: false, configurable: true, writable: true, value: obj.destroy });
9687
+ return obj;
9688
+ }
9689
+ function isSessionActive(session2) {
9690
+ for (const key of Object.keys(session2)) {
9691
+ if (key !== "save" && key !== "destroy") return true;
9692
+ }
9693
+ return false;
9694
+ }
9695
+ function session(options) {
9696
+ const ttl = options?.ttl ?? 24 * 60 * 60 * 1e3;
9697
+ const cookieName = options?.cookieName ?? "__session";
9698
+ const cookieOpts = {
9699
+ path: options?.cookie?.path ?? "/",
9700
+ domain: options?.cookie?.domain,
9701
+ httpOnly: options?.cookie?.httpOnly ?? true,
9702
+ secure: options?.cookie?.secure ?? process.env.NODE_ENV === "production",
9703
+ sameSite: options?.cookie?.sameSite ?? "lax"
9704
+ };
9705
+ let store2;
9706
+ let closeStore = null;
9707
+ if (options?.store && typeof options.store.get === "function") {
9708
+ store2 = options.store;
9709
+ } else if (options?.store === "redis") {
9710
+ if (!options.redis) throw new Error('session: redis client required when store: "redis"');
9711
+ store2 = new RedisStore(options.redis);
9712
+ } else {
9713
+ const mem = new MemoryStore();
9714
+ store2 = mem;
9715
+ closeStore = () => mem.close();
9716
+ }
9717
+ const mw = (async (req, ctx, next) => {
9718
+ const cookies = getCookies(req);
9719
+ const sid = cookies[cookieName];
9720
+ let session2;
9721
+ let loadedSid = sid ?? null;
9722
+ if (sid) {
9723
+ const data = await store2.get(sid);
9724
+ if (data) {
9725
+ session2 = createSessionObject(data, sid, store2, ttl);
9726
+ } else {
9727
+ loadedSid = null;
9728
+ session2 = createSessionObject({}, crypto7.randomUUID(), store2, ttl);
9729
+ }
9730
+ } else {
9731
+ session2 = createSessionObject({}, crypto7.randomUUID(), store2, ttl);
9732
+ }
9733
+ const snapshot = isSessionActive(session2) ? JSON.stringify(session2) : null;
9734
+ ctx.session = session2;
9735
+ ctx.sessionId = session2.id;
9736
+ const res = await next(req, ctx);
9737
+ const currentSession = ctx.session;
9738
+ if (!currentSession || currentSession[kDestroyed]) {
9739
+ if (loadedSid) {
9740
+ await store2.destroy(loadedSid);
9741
+ }
9742
+ return deleteCookie(res, cookieName, cookieOpts);
9743
+ }
9744
+ const currentData = isSessionActive(currentSession) ? JSON.stringify(currentSession) : null;
9745
+ const wasSaved = currentSession[kSaved];
9746
+ const changed = wasSaved || currentData !== snapshot;
9747
+ if (!changed) {
9748
+ if (loadedSid) {
9749
+ if (store2 instanceof RedisStore) {
9750
+ await store2.set(loadedSid, JSON.parse(currentData ?? "{}"), ttl);
9751
+ }
9752
+ }
9753
+ return res;
9754
+ }
9755
+ if (currentData && currentData !== "{}") {
9756
+ const targetSid = loadedSid ?? currentSession.id;
9757
+ const data = JSON.parse(currentData);
9758
+ await store2.set(targetSid, data, ttl);
9759
+ if (!loadedSid) {
9760
+ const cookieRes = setCookie(res, cookieName, targetSid, cookieOpts);
9761
+ return cookieRes;
9762
+ }
9763
+ } else if (loadedSid) {
9764
+ await store2.destroy(loadedSid);
9765
+ return deleteCookie(res, cookieName, cookieOpts);
9766
+ }
9767
+ return res;
9768
+ });
9769
+ mw.close = () => {
9770
+ closeStore?.();
9771
+ };
9772
+ mw.store = store2;
9773
+ return mw;
9774
+ }
9775
+
9776
+ // cache.ts
9777
+ import crypto8 from "node:crypto";
9778
+ var BINARY_PREFIXES = [
9779
+ "image/",
9780
+ "audio/",
9781
+ "video/",
9782
+ "application/octet-stream",
9783
+ "application/pdf",
9784
+ "application/zip",
9785
+ "application/gzip",
9786
+ "application/x-tar",
9787
+ "application/vnd.ms-",
9788
+ "application/vnd.openxmlformats-"
9789
+ ];
9790
+ function isCacheableContentType(ct) {
9791
+ return !BINARY_PREFIXES.some((p) => ct.startsWith(p));
9792
+ }
9793
+ function isCacheableStatus(status, allowed) {
9794
+ return allowed.includes(status);
9795
+ }
9796
+ function defaultCacheKey(req) {
9797
+ const hash = crypto8.createHash("sha256");
9798
+ hash.update(req.method);
9799
+ hash.update(req.url);
9800
+ return hash.digest("hex");
9801
+ }
9802
+ var MAX_ENTRIES = 1e5;
9803
+ var MemoryCache = class {
9804
+ store = /* @__PURE__ */ new Map();
9805
+ tagIndex = /* @__PURE__ */ new Map();
9806
+ interval;
9807
+ constructor(cleanupMs = 6e4) {
9808
+ this.interval = setInterval(() => this.cleanup(), cleanupMs);
9809
+ if (this.interval.unref) this.interval.unref();
9810
+ }
9811
+ async get(key) {
9812
+ const entry = this.store.get(key);
9813
+ if (!entry) return null;
9814
+ if (Date.now() > entry.expires) {
9815
+ this.store.delete(key);
9816
+ return null;
9817
+ }
9818
+ return entry.data;
9819
+ }
9820
+ async set(key, data, ttl) {
9821
+ if (this.store.size >= MAX_ENTRIES) {
9822
+ const oldest = this.store.keys().next();
9823
+ if (!oldest.done) this.store.delete(oldest.value);
9824
+ }
9825
+ this.store.set(key, { data, expires: Date.now() + ttl });
9826
+ for (const tag of data.tags) {
9827
+ let set = this.tagIndex.get(tag);
9828
+ if (!set) {
9829
+ set = /* @__PURE__ */ new Set();
9830
+ this.tagIndex.set(tag, set);
9831
+ }
9832
+ set.add(key);
9833
+ }
9834
+ }
9835
+ async delete(key) {
9836
+ this.store.delete(key);
9837
+ for (const [, set] of this.tagIndex) {
9838
+ set.delete(key);
9839
+ }
9840
+ }
9841
+ async invalidate(tag) {
9842
+ const keys = this.tagIndex.get(tag);
9843
+ if (!keys) return;
9844
+ for (const key of keys) {
9845
+ this.store.delete(key);
9846
+ }
9847
+ this.tagIndex.delete(tag);
9848
+ }
9849
+ async flush() {
9850
+ this.store.clear();
9851
+ this.tagIndex.clear();
9852
+ }
9853
+ cleanup() {
9854
+ const now = Date.now();
9855
+ for (const [key, entry] of this.store) {
9856
+ if (entry.expires < now) {
9857
+ this.store.delete(key);
9858
+ for (const [, set] of this.tagIndex) {
9859
+ set.delete(key);
9860
+ }
9861
+ }
9862
+ }
9863
+ }
9864
+ close() {
9865
+ clearInterval(this.interval);
9866
+ this.store.clear();
9867
+ this.tagIndex.clear();
9868
+ }
9869
+ /** Testing only. */
9870
+ get size() {
9871
+ return this.store.size;
9872
+ }
9873
+ };
9874
+ var RedisCache = class {
9875
+ redis;
9876
+ prefix;
9877
+ tagPrefix;
9878
+ constructor(redis2, prefix = "cache:") {
9879
+ this.redis = redis2;
9880
+ this.prefix = prefix;
9881
+ this.tagPrefix = `${prefix}tag:`;
9882
+ }
9883
+ key(sid) {
9884
+ return `${this.prefix}${sid}`;
9885
+ }
9886
+ tagKey(tag) {
9887
+ return `${this.tagPrefix}${tag}`;
9888
+ }
9889
+ async get(key) {
9890
+ const raw = await this.redis.get(this.key(key));
9891
+ if (!raw) return null;
9892
+ try {
9893
+ return JSON.parse(raw);
9894
+ } catch {
9895
+ await this.redis.del(this.key(key));
9896
+ return null;
9897
+ }
9898
+ }
9899
+ async set(key, entry, ttl) {
9900
+ const multi = this.redis.multi();
9901
+ multi.psetex(this.key(key), ttl, JSON.stringify(entry));
9902
+ const ttlSec = Math.ceil(ttl / 1e3);
9903
+ for (const tag of entry.tags) {
9904
+ multi.sadd(this.tagKey(tag), key);
9905
+ multi.expire(this.tagKey(tag), ttlSec);
9906
+ }
9907
+ await multi.exec();
9908
+ }
9909
+ async delete(key) {
9910
+ await this.redis.del(this.key(key));
9911
+ }
9912
+ async invalidate(tag) {
9913
+ const key = this.tagKey(tag);
9914
+ const members = await this.redis.smembers(key);
9915
+ if (members.length > 0) {
9916
+ await this.redis.del(key, ...members.map((m) => this.key(m)));
9917
+ }
9918
+ }
9919
+ async flush() {
9920
+ const keys = await this.redis.keys(`${this.prefix}*`);
9921
+ if (keys.length > 0) await this.redis.del(...keys);
9922
+ }
9923
+ };
9924
+ var DEFAULT_TTL = 3e5;
9925
+ var DEFAULT_MAX_BODY2 = 1024 * 1024;
9926
+ function shouldSkipCache(req) {
9927
+ if (req.method !== "GET" && req.method !== "HEAD") return true;
9928
+ if (req.headers.get("authorization")) return true;
9929
+ if (req.headers.get("cookie")) return true;
9930
+ return false;
9931
+ }
9932
+ function cache2(options) {
9933
+ const ttl = options?.ttl ?? DEFAULT_TTL;
9934
+ const cacheStatus = options?.cacheStatus ?? [200];
9935
+ const maxBodySize = options?.maxBodySize ?? DEFAULT_MAX_BODY2;
9936
+ const getKey = options?.key ?? defaultCacheKey;
9937
+ const getTag = options?.tag;
9938
+ const cacheCookies = options?.cacheCookies ?? false;
9939
+ let store2;
9940
+ let closeStore = null;
9941
+ if (options?.store && typeof options.store.get === "function") {
9942
+ store2 = options.store;
9943
+ } else if (options?.store === "redis") {
9944
+ if (!options.redis) throw new Error('cache: redis client required when store: "redis"');
9945
+ store2 = new RedisCache(options.redis);
9946
+ } else {
9947
+ const mem = new MemoryCache();
9948
+ store2 = mem;
9949
+ closeStore = () => mem.close();
9950
+ }
9951
+ const mw = (async (req, ctx, next) => {
9952
+ if (shouldSkipCache(req)) {
9953
+ return next(req, ctx);
9954
+ }
9955
+ const cacheKey = getKey(req);
9956
+ const cached = await store2.get(cacheKey);
9957
+ if (cached) {
9958
+ const age = Math.floor((Date.now() - cached.createdAt) / 1e3);
9959
+ const headers2 = new Headers(cached.headers);
9960
+ headers2.set("Age", String(age));
9961
+ headers2.set("X-Cache", "HIT");
9962
+ return new Response(cached.body, {
9963
+ status: cached.status,
9964
+ statusText: cached.statusText,
9965
+ headers: headers2
9966
+ });
9967
+ }
9968
+ const res = await next(req, ctx);
9969
+ if (!isCacheableStatus(res.status, cacheStatus)) return res;
9970
+ if (res.headers.get("set-cookie") && !cacheCookies) return res;
9971
+ if (res.headers.get("cache-control")?.includes("no-store")) return res;
9972
+ if (res.body && res.headers.get("content-type")?.includes("text/event-stream")) return res;
9973
+ const ct = res.headers.get("content-type") ?? "";
9974
+ if (!isCacheableContentType(ct)) return res;
9975
+ const clone = res.clone();
9976
+ const bodyText = await clone.text();
9977
+ if (bodyText.length > maxBodySize) return res;
9978
+ const tags = [];
9979
+ if (getTag) {
9980
+ const result = getTag(req, ctx);
9981
+ if (result) {
9982
+ if (Array.isArray(result)) tags.push(...result);
9983
+ else tags.push(result);
9984
+ }
9985
+ }
9986
+ const headers = {};
9987
+ res.headers.forEach((value, key) => {
9988
+ if (key.toLowerCase() === "set-cookie" && !cacheCookies) return;
9989
+ headers[key] = value;
9990
+ });
9991
+ const entry = {
9992
+ status: res.status,
9993
+ statusText: res.statusText,
9994
+ headers,
9995
+ body: bodyText,
9996
+ createdAt: Date.now(),
9997
+ tags
9998
+ };
9999
+ await store2.set(cacheKey, entry, ttl);
10000
+ return res;
10001
+ });
10002
+ mw.store = store2;
10003
+ mw.invalidate = async (tag) => store2.invalidate(tag);
10004
+ mw.flush = async () => store2.flush();
10005
+ mw.close = () => {
10006
+ closeStore?.();
10007
+ };
10008
+ return mw;
10009
+ }
10010
+
10011
+ // webhook.ts
10012
+ import crypto9 from "node:crypto";
10013
+ function timingSafeEqual2(a, b) {
10014
+ try {
10015
+ return crypto9.timingSafeEqual(Buffer.from(a), Buffer.from(b));
10016
+ } catch {
10017
+ return false;
10018
+ }
10019
+ }
10020
+ function createStripeVerifier(config) {
10021
+ return (body, headers) => {
10022
+ const sigHeader = headers["stripe-signature"];
10023
+ if (!sigHeader) return { valid: false, provider: "stripe", event: "", id: void 0 };
10024
+ const parts = sigHeader.split(",").reduce((acc, p) => {
10025
+ const [k, ...v] = p.split("=");
10026
+ if (k) acc[k.trim()] = v.join("=").trim();
10027
+ return acc;
10028
+ }, {});
10029
+ const timestamp = parts["t"];
10030
+ const signature = parts["v1"];
10031
+ if (!timestamp || !signature) return { valid: false, provider: "stripe", event: "", id: void 0 };
10032
+ const signed = `${timestamp}.${body}`;
10033
+ const expected = crypto9.createHmac("sha256", config.secret).update(signed).digest("hex");
10034
+ const valid = timingSafeEqual2(signature, expected);
10035
+ let event = "";
10036
+ let id2;
10037
+ try {
10038
+ const parsed = JSON.parse(body);
10039
+ event = parsed.type ?? "";
10040
+ id2 = parsed.id;
10041
+ } catch {
10042
+ }
10043
+ return { valid, provider: "stripe", event, id: id2 };
10044
+ };
10045
+ }
10046
+ function createGitHubVerifier(config) {
10047
+ return (body, headers) => {
10048
+ const sig = headers["x-hub-signature-256"];
10049
+ if (!sig) return { valid: false, provider: "github", event: "", id: void 0 };
10050
+ const expected = `sha256=${crypto9.createHmac("sha256", config.secret).update(body).digest("hex")}`;
10051
+ const valid = timingSafeEqual2(sig, expected);
10052
+ let event = headers["x-github-event"] ?? "";
10053
+ let id2;
10054
+ try {
10055
+ const parsed = JSON.parse(body);
10056
+ id2 = headers["x-github-delivery"] || parsed.id;
10057
+ } catch {
10058
+ }
10059
+ return { valid, provider: "github", event, id: id2 };
10060
+ };
10061
+ }
10062
+ function createSlackVerifier(config) {
10063
+ return (body, headers) => {
10064
+ const signature = headers["x-slack-signature"];
10065
+ const timestamp = headers["x-slack-request-timestamp"];
10066
+ if (!signature || !timestamp) return { valid: false, provider: "slack", event: "", id: void 0 };
10067
+ const now = Math.floor(Date.now() / 1e3);
10068
+ const ts = parseInt(timestamp, 10);
10069
+ if (isNaN(ts) || Math.abs(now - ts) > 300) {
10070
+ return { valid: false, provider: "slack", event: "", id: void 0 };
10071
+ }
10072
+ const sigBase = `v0:${timestamp}:${body}`;
10073
+ const expected = `v0=${crypto9.createHmac("sha256", config.secret).update(sigBase).digest("hex")}`;
10074
+ const valid = timingSafeEqual2(signature, expected);
10075
+ let event = "";
10076
+ let id2;
10077
+ try {
10078
+ const parsed = JSON.parse(body);
10079
+ event = parsed.event?.type ?? parsed.type ?? (parsed.challenge ? "url_verification" : "");
10080
+ id2 = parsed.event_id || parsed.ssl?.event_id;
10081
+ } catch {
10082
+ }
10083
+ return { valid, provider: "slack", event, id: id2 };
10084
+ };
10085
+ }
10086
+ var IdempotencyStore = class {
10087
+ store = /* @__PURE__ */ new Map();
10088
+ ttl;
10089
+ constructor(ttl) {
10090
+ this.ttl = ttl;
10091
+ }
10092
+ /** Returns true if this event ID has already been processed. */
10093
+ isDuplicate(id2) {
10094
+ if (this.store.has(id2)) return true;
10095
+ this.store.set(id2, Date.now());
10096
+ return false;
10097
+ }
10098
+ /** Periodic cleanup */
10099
+ cleanup() {
10100
+ const now = Date.now();
10101
+ for (const [key, ts] of this.store) {
10102
+ if (now - ts > this.ttl) this.store.delete(key);
10103
+ }
10104
+ }
10105
+ };
10106
+ var EventBus = class {
10107
+ handlers = /* @__PURE__ */ new Map();
10108
+ on(event, handler) {
10109
+ let set = this.handlers.get(event);
10110
+ if (!set) {
10111
+ set = /* @__PURE__ */ new Set();
10112
+ this.handlers.set(event, set);
10113
+ }
10114
+ set.add(handler);
10115
+ }
10116
+ off(event, handler) {
10117
+ const set = this.handlers.get(event);
10118
+ if (!set) return;
10119
+ set.delete(handler);
10120
+ if (set.size === 0) this.handlers.delete(event);
10121
+ }
10122
+ async emit(event, payload, provider, id2, ctx) {
10123
+ const we = { event, payload, provider, id: id2 };
10124
+ const specific = this.handlers.get(event);
10125
+ if (specific) {
10126
+ for (const handler of specific) {
10127
+ await handler(we, ctx);
10128
+ }
10129
+ }
10130
+ const wildcard = this.handlers.get("*");
10131
+ if (wildcard) {
10132
+ for (const handler of wildcard) {
10133
+ await handler(we, ctx);
10134
+ }
10135
+ }
10136
+ }
10137
+ };
10138
+ function webhook(options) {
10139
+ const replayProtection = options?.replayProtection ?? true;
10140
+ const idempotencyTTL = options?.idempotencyTTL ?? 36e5;
10141
+ const mountPath = options?.path ?? "/";
10142
+ const verifiers = [];
10143
+ if (options?.stripe) verifiers.push(createStripeVerifier(options.stripe));
10144
+ if (options?.github) verifiers.push(createGitHubVerifier(options.github));
10145
+ if (options?.slack) verifiers.push(createSlackVerifier(options.slack));
10146
+ if (options?.custom) {
10147
+ for (const c of options.custom) {
10148
+ verifiers.push(async (body, headers) => {
10149
+ const valid = await c.verify(body, headers);
10150
+ let event = "";
10151
+ try {
10152
+ event = c.event(JSON.parse(body), headers);
10153
+ } catch {
10154
+ }
10155
+ return { valid, provider: c.name, event, id: void 0 };
10156
+ });
10157
+ }
10158
+ }
10159
+ const bus = new EventBus();
10160
+ const idempotency = new IdempotencyStore(idempotencyTTL);
10161
+ const cleanupInterval = setInterval(() => idempotency.cleanup(), 6e4);
10162
+ if (cleanupInterval.unref) cleanupInterval.unref();
10163
+ const router = new Router();
10164
+ const handler = async (req, ctx) => {
10165
+ const body = await req.text();
10166
+ if (!body) {
10167
+ return new Response("Empty body", { status: 400 });
10168
+ }
10169
+ const headers = {};
10170
+ req.headers.forEach((v, k) => {
10171
+ headers[k] = v;
10172
+ });
10173
+ for (const verify of verifiers) {
10174
+ const result = await verify(body, headers);
10175
+ if (!result.valid) continue;
10176
+ if (replayProtection && result.id) {
10177
+ if (idempotency.isDuplicate(result.id)) {
10178
+ return new Response("OK", { status: 200 });
10179
+ }
10180
+ }
10181
+ let payload;
10182
+ try {
10183
+ payload = JSON.parse(body);
10184
+ } catch {
10185
+ return new Response("Invalid JSON body", { status: 400 });
10186
+ }
10187
+ if (result.provider === "slack" && payload?.challenge) {
10188
+ return new Response(JSON.stringify({ challenge: payload.challenge }), {
10189
+ headers: { "content-type": "application/json" }
10190
+ });
10191
+ }
10192
+ try {
10193
+ await bus.emit(result.event, payload, result.provider, result.id, ctx);
10194
+ } catch (err) {
10195
+ const msg = err instanceof Error ? err.message : String(err);
10196
+ console.error(`[webhook] handler error for ${result.provider}.${result.event}: ${msg}`);
10197
+ return new Response("Handler error", { status: 500 });
10198
+ }
10199
+ return new Response("OK", { status: 200 });
10200
+ }
10201
+ return new Response("Unauthorized", { status: 401 });
10202
+ };
10203
+ router.post(mountPath, handler);
10204
+ const mod = router;
10205
+ mod.on = (event, handler2) => {
10206
+ bus.on(event, handler2);
10207
+ return mod;
10208
+ };
10209
+ mod.off = (event, handler2) => {
10210
+ bus.off(event, handler2);
10211
+ return mod;
10212
+ };
10213
+ mod._cleanup = () => {
10214
+ clearInterval(cleanupInterval);
10215
+ };
10216
+ return mod;
10217
+ }
10218
+
10219
+ // fts.ts
10220
+ var fts_exports = {};
10221
+ __export(fts_exports, {
10222
+ createIndex: () => createIndex,
10223
+ dropIndex: () => dropIndex,
10224
+ search: () => search,
10225
+ suggest: () => suggest
10226
+ });
10227
+ function resolveTableName(table) {
10228
+ const name = table.inner?.tableName ?? table.tableName;
10229
+ if (!name || typeof name !== "string") {
10230
+ throw new Error("fts: could not determine table name. Ensure you pass a pg.table() result.");
10231
+ }
10232
+ return name;
10233
+ }
10234
+ function escapeIdent(s) {
10235
+ return `"${s.replace(/"/g, '""')}"`;
10236
+ }
10237
+ function sqlLit(s) {
10238
+ return `'${s.replace(/'/g, "''")}'`;
10239
+ }
10240
+ async function createIndex(sql2, table, fields, options) {
10241
+ const language = options?.language ?? "english";
10242
+ const tableName = resolveTableName(table);
10243
+ const indexName = options?.indexName ?? `${tableName}_fts_idx`;
10244
+ const vectorExpr = fields.map((f) => `coalesce(${escapeIdent(f)}, '')`).join(` || ' ' || `);
10245
+ const indexType = options?.indexType ?? "gin";
10246
+ await sql2.unsafe(`
10247
+ CREATE INDEX IF NOT EXISTS ${escapeIdent(indexName)}
10248
+ ON ${escapeIdent(tableName)}
10249
+ USING ${indexType}
10250
+ (to_tsvector(${sqlLit(language)}, ${vectorExpr}))
10251
+ `);
10252
+ }
10253
+ async function dropIndex(sql2, table, options) {
10254
+ const tableName = resolveTableName(table);
10255
+ const indexName = options?.indexName ?? `${tableName}_fts_idx`;
10256
+ await sql2.unsafe(`DROP INDEX IF EXISTS ${escapeIdent(indexName)}`);
10257
+ }
10258
+ async function search(sql2, table, query, options) {
10259
+ const tableName = resolveTableName(table);
10260
+ const language = options?.language ?? "english";
10261
+ const searchFields = options?.fields;
10262
+ const limit = options?.limit ?? 20;
10263
+ const offset = options?.offset ?? 0;
10264
+ const rankCol = options?.rankColumn ?? "_rank";
10265
+ if (!searchFields?.length) {
10266
+ throw new Error("fts.search: `fields` option is required. Specify which columns to search.");
10267
+ }
10268
+ const sanitized = query.trim();
10269
+ if (!sanitized) return [];
10270
+ const vectorExpr = searchFields.map((f) => `coalesce(${escapeIdent(f)}, '')`).join(` || ' ' || `);
10271
+ const langLit = sqlLit(language);
10272
+ const queryLit = sqlLit(sanitized);
10273
+ const rankColId = escapeIdent(rankCol);
10274
+ const tableId = escapeIdent(tableName);
10275
+ const headlineExpr = options?.headline ? searchFields.map(
10276
+ (f) => `ts_headline(${langLit}, ${escapeIdent(f)}, websearch_to_tsquery(${langLit}, ${queryLit}), 'MaxWords=30,MinWords=15') as ${escapeIdent(f + "_headline")}`
10277
+ ).join(",\n ") : "";
10278
+ const sql_query = `
10279
+ SELECT
10280
+ *,
10281
+ ts_rank(
10282
+ to_tsvector(${langLit}, ${vectorExpr}),
10283
+ websearch_to_tsquery(${langLit}, ${queryLit})
10284
+ ) as ${rankColId}
10285
+ ${headlineExpr ? "," + headlineExpr : ""}
10286
+ FROM ${tableId}
10287
+ WHERE to_tsvector(${langLit}, ${vectorExpr}) @@ websearch_to_tsquery(${langLit}, ${queryLit})
10288
+ ORDER BY ${rankColId} DESC
10289
+ LIMIT ${limit} OFFSET ${offset}
10290
+ `;
10291
+ const rows = await sql2.unsafe(sql_query);
10292
+ return rows.map((row) => {
10293
+ const result = {
10294
+ id: row.id,
10295
+ rank: Number(row[rankCol]) || 0,
10296
+ row
10297
+ };
10298
+ if (options?.headline && searchFields) {
10299
+ const snippets = searchFields.map((f) => row[`${f}_headline`]).filter(Boolean);
10300
+ result.headline = snippets.join(" ... ");
10301
+ }
10302
+ return result;
10303
+ });
10304
+ }
10305
+ async function suggest(sql2, table, prefix, options) {
10306
+ const tableName = resolveTableName(table);
10307
+ const field = options?.field;
10308
+ const language = options?.language ?? "english";
10309
+ const limit = options?.limit ?? 10;
10310
+ if (!field) throw new Error("fts.suggest: `field` option is required");
10311
+ const sanitized = prefix.replace(/[^\w\s-]/g, " ").trim();
10312
+ if (!sanitized) return [];
10313
+ const rows = await sql2.unsafe(`
10314
+ SELECT DISTINCT ts_lexize(${sqlLit(language)}, word) as tokens
10315
+ FROM (
10316
+ SELECT regexp_split_to_table(lower(${escapeIdent(field)}), E'\\W+') as word
10317
+ FROM ${escapeIdent(tableName)}
10318
+ ) words
10319
+ WHERE word LIKE ${sqlLit(sanitized + "%")}
10320
+ LIMIT ${limit}
10321
+ `);
10322
+ return rows.map((r) => r.tokens?.[0] ?? "").filter(Boolean);
10323
+ }
9355
10324
  export {
9356
10325
  DEFAULT_MAX_BODY,
9357
10326
  MIGRATIONS_TABLE,
10327
+ MemoryCache,
10328
+ MemoryStore,
10329
+ RedisCache,
10330
+ RedisStore,
9358
10331
  Router,
9359
10332
  TestApp,
9360
10333
  TestRequest,
@@ -9363,11 +10336,13 @@ export {
9363
10336
  aiStream,
9364
10337
  analytics,
9365
10338
  auth,
10339
+ cache2 as cache,
9366
10340
  compress,
9367
10341
  cors,
9368
10342
  createHub,
9369
10343
  createOpenAI,
9370
10344
  createSSEStream,
10345
+ createTestDb,
9371
10346
  createTestServer,
9372
10347
  createWorker,
9373
10348
  csrf,
@@ -9380,6 +10355,7 @@ export {
9380
10355
  embedMany,
9381
10356
  formatSSE,
9382
10357
  formatSSEData,
10358
+ fts_exports as fts,
9383
10359
  generateObject,
9384
10360
  generateText2 as generateText,
9385
10361
  getCookies,
@@ -9410,6 +10386,7 @@ export {
9410
10386
  seoTags,
9411
10387
  serve,
9412
10388
  serveStatic,
10389
+ session,
9413
10390
  setCookie,
9414
10391
  smoothStream,
9415
10392
  ssr,
@@ -9421,5 +10398,7 @@ export {
9421
10398
  traceElapsed,
9422
10399
  upload,
9423
10400
  user,
9424
- validate
10401
+ validate,
10402
+ webhook,
10403
+ withTestDb
9425
10404
  };