weifuwu 0.22.1 → 0.22.3

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
@@ -186,7 +186,14 @@ function serve(handler, options) {
186
186
  if (shuttingDown) return;
187
187
  shuttingDown = true;
188
188
  server.close();
189
- process.exit(0);
189
+ const timer = setTimeout(() => {
190
+ server.closeAllConnections();
191
+ process.exit(0);
192
+ }, 1e4);
193
+ server.on("close", () => {
194
+ clearTimeout(timer);
195
+ process.exit(0);
196
+ });
190
197
  };
191
198
  shutdownHandler = shutdown;
192
199
  process.on("SIGTERM", shutdown);
@@ -232,7 +239,7 @@ function serve(handler, options) {
232
239
  console.log(`weifuwu listening on http://${displayHost}:${_cachedPort}`);
233
240
  });
234
241
  return {
235
- stop: () => {
242
+ stop: (timeoutMs = 1e4) => {
236
243
  if (shutdownHandler) {
237
244
  process.off("SIGTERM", shutdownHandler);
238
245
  process.off("SIGINT", shutdownHandler);
@@ -243,9 +250,16 @@ function serve(handler, options) {
243
250
  resolve14();
244
251
  return;
245
252
  }
253
+ server.close();
246
254
  server.closeIdleConnections();
247
- server.closeAllConnections();
248
- server.close(() => resolve14());
255
+ const timer = setTimeout(() => {
256
+ server.closeAllConnections();
257
+ resolve14();
258
+ }, timeoutMs);
259
+ server.on("close", () => {
260
+ clearTimeout(timer);
261
+ resolve14();
262
+ });
249
263
  });
250
264
  },
251
265
  ready,
@@ -965,10 +979,30 @@ function cors(options) {
965
979
 
966
980
  // auth.ts
967
981
  function auth(options) {
968
- if (!options.token && !options.verify && !options.proxy) {
969
- throw new Error("auth() requires at least one of: token, verify, or proxy");
982
+ if (!options.token && !options.verify && !options.proxy && !options.session) {
983
+ throw new Error("auth() requires at least one of: token, verify, proxy, or session");
970
984
  }
971
985
  return async (req, ctx, next) => {
986
+ if (options.session) {
987
+ const sessionUserId = ctx.session?.userId;
988
+ if (sessionUserId !== void 0 && sessionUserId !== null) {
989
+ if (options.resolveUser) {
990
+ const userData = await options.resolveUser(sessionUserId);
991
+ if (userData) {
992
+ ctx.user = userData;
993
+ return next(req, ctx);
994
+ }
995
+ if (typeof ctx.session?.destroy === "function") {
996
+ ;
997
+ ctx.session.destroy();
998
+ }
999
+ console.warn(`[${currentTraceId()}] auth: session userId ${sessionUserId} resolved to null`);
1000
+ } else {
1001
+ ctx.user = { id: sessionUserId };
1002
+ return next(req, ctx);
1003
+ }
1004
+ }
1005
+ }
972
1006
  const headerName = options.header ?? "Authorization";
973
1007
  let from = "header";
974
1008
  let header = req.headers.get(headerName);
@@ -1051,6 +1085,266 @@ function auth(options) {
1051
1085
  };
1052
1086
  }
1053
1087
 
1088
+ // oauth-client.ts
1089
+ import crypto2 from "node:crypto";
1090
+ import jwt from "jsonwebtoken";
1091
+ var BUILTIN_PROVIDERS = {
1092
+ google: {
1093
+ authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
1094
+ tokenUrl: "https://oauth2.googleapis.com/token",
1095
+ userUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
1096
+ scope: "openid email profile",
1097
+ parseUser: (data) => ({
1098
+ id: data.id,
1099
+ email: data.email,
1100
+ name: data.name,
1101
+ avatarUrl: data.picture
1102
+ })
1103
+ },
1104
+ github: {
1105
+ authUrl: "https://github.com/login/oauth/authorize",
1106
+ tokenUrl: "https://github.com/login/oauth/access_token",
1107
+ userUrl: "https://api.github.com/user",
1108
+ scope: "read:user user:email",
1109
+ parseUser: (data) => ({
1110
+ id: String(data.id),
1111
+ email: data.email ?? "",
1112
+ name: data.name ?? data.login,
1113
+ avatarUrl: data.avatar_url
1114
+ })
1115
+ }
1116
+ };
1117
+ function oauthClient(options) {
1118
+ const {
1119
+ pg,
1120
+ jwtSecret,
1121
+ providers,
1122
+ redirectUrl = "/",
1123
+ expiresIn = "24h"
1124
+ } = options;
1125
+ const providerTable = options.table ?? "_auth_providers";
1126
+ const router = new Router();
1127
+ async function saveOAuthState(ctx, state, provider) {
1128
+ if (ctx.session) {
1129
+ ctx.session.oauthState = { state, provider };
1130
+ }
1131
+ }
1132
+ function verifyOAuthState(ctx, state, provider) {
1133
+ const saved = ctx.session?.oauthState;
1134
+ if (!saved) return false;
1135
+ if (saved.state !== state || saved.provider !== provider) return false;
1136
+ delete ctx.session.oauthState;
1137
+ return true;
1138
+ }
1139
+ async function ensureTable() {
1140
+ await pg.sql`
1141
+ CREATE TABLE IF NOT EXISTS ${pg.sql(providerTable)} (
1142
+ id SERIAL PRIMARY KEY,
1143
+ user_id INTEGER NOT NULL REFERENCES "_users"(id) ON DELETE CASCADE,
1144
+ provider TEXT NOT NULL,
1145
+ provider_id TEXT NOT NULL,
1146
+ email TEXT NOT NULL DEFAULT '',
1147
+ name TEXT NOT NULL DEFAULT '',
1148
+ avatar_url TEXT NOT NULL DEFAULT '',
1149
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
1150
+ UNIQUE(provider, provider_id)
1151
+ )
1152
+ `;
1153
+ await pg.sql`
1154
+ CREATE INDEX IF NOT EXISTS ${pg.sql(providerTable + "_user_idx")}
1155
+ ON ${pg.sql(providerTable)}(user_id)
1156
+ `;
1157
+ }
1158
+ async function findUserByProvider(provider, providerId) {
1159
+ const [row] = await pg.sql`
1160
+ SELECT * FROM ${pg.sql(providerTable)}
1161
+ WHERE provider = ${provider} AND provider_id = ${providerId}
1162
+ LIMIT 1
1163
+ `;
1164
+ return row ?? null;
1165
+ }
1166
+ async function findUserByEmail(email) {
1167
+ const [row] = await pg.sql`
1168
+ SELECT * FROM "_users" WHERE email = ${email} LIMIT 1
1169
+ `;
1170
+ return row ?? null;
1171
+ }
1172
+ async function createUser(email, name) {
1173
+ const randomPassword = crypto2.randomBytes(32).toString("hex");
1174
+ const [row] = await pg.sql`
1175
+ INSERT INTO "_users" (email, password, name, role)
1176
+ VALUES (${email}, ${randomPassword}, ${name}, 'user')
1177
+ RETURNING *
1178
+ `;
1179
+ return row;
1180
+ }
1181
+ async function linkProvider(userId, provider, providerId, email, name, avatarUrl) {
1182
+ await pg.sql`
1183
+ INSERT INTO ${pg.sql(providerTable)} (user_id, provider, provider_id, email, name, avatar_url)
1184
+ VALUES (${userId}, ${provider}, ${providerId}, ${email}, ${name}, ${avatarUrl})
1185
+ ON CONFLICT (provider, provider_id) DO NOTHING
1186
+ `;
1187
+ }
1188
+ async function findOrCreateUser(provider, providerId, email, name, avatarUrl) {
1189
+ const link = await findUserByProvider(provider, providerId);
1190
+ if (link) {
1191
+ const [user2] = await pg.sql`SELECT * FROM "_users" WHERE id = ${link.user_id} LIMIT 1`;
1192
+ return user2 ?? null;
1193
+ }
1194
+ if (email) {
1195
+ const existingUser = await findUserByEmail(email);
1196
+ if (existingUser) {
1197
+ await linkProvider(existingUser.id, provider, providerId, email, name, avatarUrl);
1198
+ return existingUser;
1199
+ }
1200
+ }
1201
+ const newUser = await createUser(email || `${provider}_${providerId}@oauth.local`, name || provider);
1202
+ await linkProvider(newUser.id, provider, providerId, email, name, avatarUrl);
1203
+ return newUser;
1204
+ }
1205
+ function signToken(user2) {
1206
+ return jwt.sign(
1207
+ { sub: user2.id, email: user2.email, role: user2.role },
1208
+ jwtSecret,
1209
+ { expiresIn }
1210
+ );
1211
+ }
1212
+ let tableReady = null;
1213
+ function ensureInit() {
1214
+ if (!tableReady) tableReady = ensureTable();
1215
+ return tableReady;
1216
+ }
1217
+ function getProviderMeta(providerName) {
1218
+ const config = providers[providerName];
1219
+ if (!config) return null;
1220
+ const builtin = BUILTIN_PROVIDERS[providerName];
1221
+ const parseUser = config.parseUser ?? builtin?.parseUser;
1222
+ if (!parseUser) return null;
1223
+ const meta = {
1224
+ authUrl: config.authUrl ?? builtin?.authUrl ?? "",
1225
+ tokenUrl: config.tokenUrl ?? builtin?.tokenUrl ?? "",
1226
+ userUrl: config.userUrl ?? builtin?.userUrl ?? "",
1227
+ scope: config.scope ?? builtin?.scope ?? "openid",
1228
+ parseUser
1229
+ };
1230
+ if (!meta.authUrl || !meta.tokenUrl || !meta.userUrl) return null;
1231
+ return { config, meta };
1232
+ }
1233
+ router.get("/:provider", async (req, ctx) => {
1234
+ await ensureInit();
1235
+ const providerName = ctx.params.provider;
1236
+ const resolved = getProviderMeta(providerName);
1237
+ if (!resolved) {
1238
+ return Response.json({ error: `Unsupported provider: ${providerName}` }, { status: 400 });
1239
+ }
1240
+ const { config, meta } = resolved;
1241
+ const state = crypto2.randomUUID();
1242
+ const redirectUri = new URL(req.url);
1243
+ redirectUri.pathname = redirectUri.pathname.replace(/\/[^/]+$/, "/") + providerName + "/callback";
1244
+ await saveOAuthState(ctx, state, providerName);
1245
+ const scope = config.scope ?? meta.scope;
1246
+ const params = new URLSearchParams({
1247
+ client_id: config.clientId,
1248
+ redirect_uri: redirectUri.origin + redirectUri.pathname,
1249
+ response_type: "code",
1250
+ scope,
1251
+ state,
1252
+ access_type: "offline",
1253
+ prompt: "consent"
1254
+ });
1255
+ return Response.redirect(`${meta.authUrl}?${params.toString()}`, 302);
1256
+ });
1257
+ router.get("/:provider/callback", async (req, ctx) => {
1258
+ await ensureInit();
1259
+ const providerName = ctx.params.provider;
1260
+ const resolved = getProviderMeta(providerName);
1261
+ if (!resolved) {
1262
+ return Response.json({ error: `Unsupported provider: ${providerName}` }, { status: 400 });
1263
+ }
1264
+ const { config, meta } = resolved;
1265
+ const url = new URL(req.url);
1266
+ const code = url.searchParams.get("code");
1267
+ const state = url.searchParams.get("state");
1268
+ if (!code || !state) {
1269
+ return Response.json({ error: "Missing code or state parameter" }, { status: 400 });
1270
+ }
1271
+ if (!verifyOAuthState(ctx, state, providerName)) {
1272
+ return Response.json({ error: "Invalid state \u2014 possible CSRF attack" }, { status: 403 });
1273
+ }
1274
+ const redirectUri = url.origin + url.pathname.replace(/\/callback$/, "");
1275
+ let tokenRes;
1276
+ try {
1277
+ tokenRes = await fetch(meta.tokenUrl, {
1278
+ method: "POST",
1279
+ headers: {
1280
+ "Content-Type": "application/json",
1281
+ "Accept": "application/json"
1282
+ },
1283
+ body: JSON.stringify({
1284
+ code,
1285
+ client_id: config.clientId,
1286
+ client_secret: config.clientSecret,
1287
+ redirect_uri: redirectUri,
1288
+ grant_type: "authorization_code"
1289
+ })
1290
+ });
1291
+ } catch (err) {
1292
+ console.error(`[oauth] token exchange network error for ${providerName}:`, err);
1293
+ return Response.json({ error: "Failed to connect to OAuth provider" }, { status: 502 });
1294
+ }
1295
+ if (!tokenRes.ok) {
1296
+ const errBody = await tokenRes.text();
1297
+ console.error(`[oauth] token exchange failed for ${providerName}:`, errBody);
1298
+ return Response.json({ error: "Failed to exchange authorization code" }, { status: 502 });
1299
+ }
1300
+ const tokenData = await tokenRes.json();
1301
+ const accessToken = tokenData.access_token;
1302
+ if (!accessToken) {
1303
+ return Response.json({ error: "No access_token in response" }, { status: 502 });
1304
+ }
1305
+ let userRes;
1306
+ try {
1307
+ userRes = await fetch(meta.userUrl, {
1308
+ headers: { Authorization: `Bearer ${accessToken}` }
1309
+ });
1310
+ } catch (err) {
1311
+ console.error(`[oauth] user info network error for ${providerName}:`, err);
1312
+ return Response.json({ error: "Failed to connect to OAuth provider" }, { status: 502 });
1313
+ }
1314
+ if (!userRes.ok) {
1315
+ return Response.json({ error: "Failed to fetch user profile" }, { status: 502 });
1316
+ }
1317
+ const userData = await userRes.json();
1318
+ const providerUser = meta.parseUser(userData, accessToken);
1319
+ const user2 = await findOrCreateUser(
1320
+ providerName,
1321
+ providerUser.id,
1322
+ providerUser.email,
1323
+ providerUser.name,
1324
+ providerUser.avatarUrl ?? ""
1325
+ );
1326
+ if (!user2) {
1327
+ return Response.json({ error: "Failed to create/link user" }, { status: 500 });
1328
+ }
1329
+ const token = signToken(user2);
1330
+ if (ctx.session) {
1331
+ ctx.session.userId = user2.id;
1332
+ ctx.session.role = user2.role;
1333
+ }
1334
+ const accept = req.headers.get("accept") ?? "";
1335
+ if (accept.includes("application/json")) {
1336
+ return Response.json({
1337
+ token,
1338
+ user: { id: user2.id, email: user2.email, name: user2.name, role: user2.role }
1339
+ });
1340
+ }
1341
+ const finalUrl = new URL(redirectUrl, url.origin);
1342
+ finalUrl.searchParams.set("token", token);
1343
+ return Response.redirect(finalUrl.toString(), 302);
1344
+ });
1345
+ return router;
1346
+ }
1347
+
1054
1348
  // static.ts
1055
1349
  import { open, realpath } from "node:fs/promises";
1056
1350
  import { extname, resolve as resolve2, normalize, sep } from "node:path";
@@ -1624,10 +1918,10 @@ function helmet(options) {
1624
1918
  }
1625
1919
 
1626
1920
  // request-id.ts
1627
- import crypto2 from "node:crypto";
1921
+ import crypto3 from "node:crypto";
1628
1922
  function requestId(options) {
1629
1923
  const header = options?.header ?? "X-Request-ID";
1630
- const gen = options?.generator ?? (() => crypto2.randomUUID());
1924
+ const gen = options?.generator ?? (() => crypto3.randomUUID());
1631
1925
  return async (req, ctx, next) => {
1632
1926
  const existing = req.headers.get(header);
1633
1927
  const id2 = existing ?? gen();
@@ -1703,6 +1997,12 @@ var TestResponseImpl = class {
1703
1997
  async text() {
1704
1998
  return this.response.text();
1705
1999
  }
2000
+ async bytes() {
2001
+ return this.response.bytes();
2002
+ }
2003
+ async arrayBuffer() {
2004
+ return this.response.arrayBuffer();
2005
+ }
1706
2006
  };
1707
2007
  var TestRequest = class {
1708
2008
  headers = {};
@@ -1783,29 +2083,29 @@ var TestApp = class {
1783
2083
  this.router.use(mw);
1784
2084
  return this;
1785
2085
  }
1786
- /** Register a GET route */
1787
- get(path2, handler) {
1788
- this.router.get(path2, handler);
2086
+ /** Register a GET route — supports route-level middleware via spread args. */
2087
+ get(path2, ...args) {
2088
+ this.router.get(path2, ...args);
1789
2089
  return this;
1790
2090
  }
1791
- /** Register a POST route */
1792
- post(path2, handler) {
1793
- this.router.post(path2, handler);
2091
+ /** Register a POST route. */
2092
+ post(path2, ...args) {
2093
+ this.router.post(path2, ...args);
1794
2094
  return this;
1795
2095
  }
1796
- /** Register a PUT route */
1797
- put(path2, handler) {
1798
- this.router.put(path2, handler);
2096
+ /** Register a PUT route. */
2097
+ put(path2, ...args) {
2098
+ this.router.put(path2, ...args);
1799
2099
  return this;
1800
2100
  }
1801
- /** Register a PATCH route */
1802
- patch(path2, handler) {
1803
- this.router.patch(path2, handler);
2101
+ /** Register a PATCH route. */
2102
+ patch(path2, ...args) {
2103
+ this.router.patch(path2, ...args);
1804
2104
  return this;
1805
2105
  }
1806
- /** Register a DELETE route */
1807
- delete(path2, handler) {
1808
- this.router.delete(path2, handler);
2106
+ /** Register a DELETE route. */
2107
+ delete(path2, ...args) {
2108
+ this.router.delete(path2, ...args);
1809
2109
  return this;
1810
2110
  }
1811
2111
  /** Start building a GET request */
@@ -2996,12 +3296,12 @@ var PgModule = class {
2996
3296
 
2997
3297
  // user/client.ts
2998
3298
  import { randomBytes, scryptSync, timingSafeEqual } from "node:crypto";
2999
- import jwt2 from "jsonwebtoken";
3299
+ import jwt3 from "jsonwebtoken";
3000
3300
  import { z as z2 } from "zod";
3001
3301
 
3002
3302
  // user/oauth2.ts
3003
- import crypto3 from "node:crypto";
3004
- import jwt from "jsonwebtoken";
3303
+ import crypto4 from "node:crypto";
3304
+ import jwt2 from "jsonwebtoken";
3005
3305
  function createOAuth2Server(deps) {
3006
3306
  const { pg, users, jwtSecret, expiresIn } = deps;
3007
3307
  async function getClient(clientId) {
@@ -3019,8 +3319,8 @@ function createOAuth2Server(deps) {
3019
3319
  };
3020
3320
  }
3021
3321
  async function registerClient(data) {
3022
- const clientId = crypto3.randomUUID();
3023
- const clientSecret = crypto3.randomBytes(32).toString("hex");
3322
+ const clientId = crypto4.randomUUID();
3323
+ const clientSecret = crypto4.randomBytes(32).toString("hex");
3024
3324
  const [row] = await pg.sql`
3025
3325
  INSERT INTO "_oauth2_clients" ("name", "client_id", "client_secret", "redirect_uris")
3026
3326
  VALUES (${data.name}, ${clientId}, ${clientSecret}, ${pg.sql.array(data.redirectUris)})
@@ -3044,7 +3344,7 @@ function createOAuth2Server(deps) {
3044
3344
  const header = req.headers.get("Authorization");
3045
3345
  if (header?.startsWith("Bearer ")) {
3046
3346
  try {
3047
- const payload = jwt.verify(header.slice(7), jwtSecret);
3347
+ const payload = jwt2.verify(header.slice(7), jwtSecret);
3048
3348
  return { id: payload.sub, email: payload.email, role: payload.role };
3049
3349
  } catch {
3050
3350
  return null;
@@ -3054,7 +3354,7 @@ function createOAuth2Server(deps) {
3054
3354
  const qsToken = url.searchParams.get("access_token");
3055
3355
  if (qsToken) {
3056
3356
  try {
3057
- const payload = jwt.verify(qsToken, jwtSecret);
3357
+ const payload = jwt2.verify(qsToken, jwtSecret);
3058
3358
  return { id: payload.sub, email: payload.email, role: payload.role };
3059
3359
  } catch {
3060
3360
  return null;
@@ -3065,7 +3365,7 @@ function createOAuth2Server(deps) {
3065
3365
  const match = cookie.split(";").map((c) => c.trim()).find((c) => c.startsWith("session="));
3066
3366
  if (match) {
3067
3367
  try {
3068
- const payload = jwt.verify(match.slice(8), jwtSecret);
3368
+ const payload = jwt2.verify(match.slice(8), jwtSecret);
3069
3369
  return { id: payload.sub, email: payload.email, role: payload.role };
3070
3370
  } catch {
3071
3371
  return null;
@@ -3168,7 +3468,7 @@ h2{color:#dc2626}.desc{color:#555}</style>
3168
3468
  const loc2 = `${redirectUri}?error=access_denied${state ? `&state=${state}` : ""}`;
3169
3469
  return Response.redirect(loc2, 302);
3170
3470
  }
3171
- const code = crypto3.randomUUID();
3471
+ const code = crypto4.randomUUID();
3172
3472
  const expiresAt = new Date(Date.now() + 10 * 60 * 1e3);
3173
3473
  await pg.sql`
3174
3474
  INSERT INTO "_oauth2_codes" ("code", "client_id", "user_id", "redirect_uri", "code_challenge", "code_challenge_method", "scope", "expires_at")
@@ -3233,7 +3533,7 @@ h2{color:#dc2626}.desc{color:#555}</style>
3233
3533
  if (stored.code_challenge_method === "plain") {
3234
3534
  expected = codeVerifier;
3235
3535
  } else {
3236
- expected = crypto3.createHash("sha256").update(codeVerifier).digest().toString("base64url");
3536
+ expected = crypto4.createHash("sha256").update(codeVerifier).digest().toString("base64url");
3237
3537
  }
3238
3538
  if (expected !== stored.code_challenge) {
3239
3539
  return Response.json({ error: "invalid_grant", error_description: "code_verifier mismatch" }, { status: 400 });
@@ -3245,12 +3545,12 @@ h2{color:#dc2626}.desc{color:#555}</style>
3245
3545
  return Response.json({ error: "invalid_grant" }, { status: 400 });
3246
3546
  }
3247
3547
  const scope = stored.scope || "";
3248
- const accessToken = jwt.sign(
3548
+ const accessToken = jwt2.sign(
3249
3549
  { sub: user2.id, email: user2.email, role: user2.role, client_id: clientId, scope },
3250
3550
  jwtSecret,
3251
3551
  { expiresIn }
3252
3552
  );
3253
- const refreshToken = crypto3.randomUUID();
3553
+ const refreshToken = crypto4.randomUUID();
3254
3554
  const refreshExpires = new Date(Date.now() + 30 * 24 * 60 * 60 * 1e3);
3255
3555
  await pg.sql`
3256
3556
  INSERT INTO "_oauth2_tokens" ("token", "client_id", "user_id", "scope", "expires_at")
@@ -3272,7 +3572,7 @@ h2{color:#dc2626}.desc{color:#555}</style>
3272
3572
  if (!client || client.clientSecret !== clientSecret) {
3273
3573
  return Response.json({ error: "invalid_client" }, { status: 401 });
3274
3574
  }
3275
- const accessToken = jwt.sign(
3575
+ const accessToken = jwt2.sign(
3276
3576
  { sub: clientId, client_id: clientId, scope, token_type: "client_credentials" },
3277
3577
  jwtSecret,
3278
3578
  { expiresIn }
@@ -3366,7 +3666,7 @@ function user(options) {
3366
3666
  await tokens.create();
3367
3667
  }
3368
3668
  function signToken(user2) {
3369
- return jwt2.sign(
3669
+ return jwt3.sign(
3370
3670
  { sub: user2.id, email: user2.email, role: user2.role },
3371
3671
  secret,
3372
3672
  { expiresIn }
@@ -3417,7 +3717,7 @@ function user(options) {
3417
3717
  }
3418
3718
  async function verify(token) {
3419
3719
  try {
3420
- const payload = jwt2.verify(token, secret);
3720
+ const payload = jwt3.verify(token, secret);
3421
3721
  if (payload.token_type === "client_credentials") return null;
3422
3722
  const row = await findById(payload.sub);
3423
3723
  if (!row) return null;
@@ -3536,7 +3836,7 @@ function redis(opts) {
3536
3836
 
3537
3837
  // queue/index.ts
3538
3838
  import { Redis as IORedis2 } from "ioredis";
3539
- import crypto4 from "node:crypto";
3839
+ import crypto5 from "node:crypto";
3540
3840
  function cronNext(expr, from = /* @__PURE__ */ new Date()) {
3541
3841
  const parts = expr.trim().split(/\s+/);
3542
3842
  if (parts.length !== 5) throw new Error(`Invalid cron expression "${expr}": expected 5 fields`);
@@ -3631,7 +3931,7 @@ function queue(opts) {
3631
3931
  if (job.schedule) {
3632
3932
  try {
3633
3933
  const nextRun = cronNext(job.schedule);
3634
- const nextJob = { ...job, id: crypto4.randomUUID(), runAt: nextRun, createdAt: Date.now() };
3934
+ const nextJob = { ...job, id: crypto5.randomUUID(), runAt: nextRun, createdAt: Date.now() };
3635
3935
  await redis2.zadd(jobKey, nextRun, JSON.stringify(nextJob));
3636
3936
  } catch (e) {
3637
3937
  console.error("[queue] cron re-queue failed:", e.message);
@@ -3671,7 +3971,7 @@ function queue(opts) {
3671
3971
  }
3672
3972
  }
3673
3973
  mw.add = function add(type, payload, opts2) {
3674
- const id2 = crypto4.randomUUID();
3974
+ const id2 = crypto5.randomUUID();
3675
3975
  let runAt;
3676
3976
  if (opts2?.schedule) {
3677
3977
  runAt = cronNext(opts2.schedule);
@@ -5629,7 +5929,7 @@ function createGateway(config, getPort) {
5629
5929
  }
5630
5930
 
5631
5931
  // deploy/manager.ts
5632
- import crypto5 from "node:crypto";
5932
+ import crypto6 from "node:crypto";
5633
5933
 
5634
5934
  // deploy/process.ts
5635
5935
  import { fork } from "node:child_process";
@@ -5688,7 +5988,7 @@ function createManager(config, apps, manager) {
5688
5988
  const token = header.replace("Bearer ", "");
5689
5989
  const tokenBuf = Buffer.from(token);
5690
5990
  const secretBuf = Buffer.from(config.deployToken);
5691
- if (tokenBuf.length !== secretBuf.length || !crypto5.timingSafeEqual(tokenBuf, secretBuf)) {
5991
+ if (tokenBuf.length !== secretBuf.length || !crypto6.timingSafeEqual(tokenBuf, secretBuf)) {
5692
5992
  return Response.json({ error: "Unauthorized" }, { status: 401 });
5693
5993
  }
5694
5994
  return next(req, ctx);
@@ -8614,7 +8914,7 @@ function logdb(options) {
8614
8914
  }
8615
8915
 
8616
8916
  // iii/client.ts
8617
- import crypto6 from "node:crypto";
8917
+ import crypto7 from "node:crypto";
8618
8918
 
8619
8919
  // iii/stream.ts
8620
8920
  function notify(channels, stream, group, item, event, data) {
@@ -9143,7 +9443,7 @@ function iii(opts = {}) {
9143
9443
  registerBuiltin("stream::send", (p) => stream.send(p.stream_name, p.group_id, p.type, p.data, p.id));
9144
9444
  registerBuiltin("stream::update", (p) => stream.update(p.stream_name, p.group_id, p.item_id, p.ops));
9145
9445
  function addLocalWorker(worker) {
9146
- const workerId = crypto6.randomUUID();
9446
+ const workerId = crypto7.randomUUID();
9147
9447
  const reg = {
9148
9448
  id: workerId,
9149
9449
  name: worker.name,
@@ -9158,7 +9458,7 @@ function iii(opts = {}) {
9158
9458
  const triggerIds = [];
9159
9459
  for (const t of worker.getTriggers()) {
9160
9460
  if (t.input.function_id === fn.id) {
9161
- const tid = crypto6.randomUUID();
9461
+ const tid = crypto7.randomUUID();
9162
9462
  triggers.set(tid, {
9163
9463
  id: tid,
9164
9464
  type: t.input.type,
@@ -9187,7 +9487,7 @@ function iii(opts = {}) {
9187
9487
  if (!worker) return;
9188
9488
  const handler = async (payload) => {
9189
9489
  if (!worker.ws) throw new Error(`Worker "${worker.name}" disconnected`);
9190
- const invocationId = crypto6.randomUUID();
9490
+ const invocationId = crypto7.randomUUID();
9191
9491
  return new Promise((resolve14, reject) => {
9192
9492
  const timer = setTimeout(() => {
9193
9493
  pending.delete(invocationId);
@@ -9222,7 +9522,7 @@ function iii(opts = {}) {
9222
9522
  let engineRef = null;
9223
9523
  const wsHandler = createWsHandler({
9224
9524
  registerRemoteWorker(ws, name) {
9225
- const id2 = crypto6.randomUUID();
9525
+ const id2 = crypto7.randomUUID();
9226
9526
  workers.set(id2, { id: id2, name, ws, functions: [], triggers: [] });
9227
9527
  return id2;
9228
9528
  },
@@ -9233,7 +9533,7 @@ function iii(opts = {}) {
9233
9533
  addRemoteFunction(workerId, id2);
9234
9534
  },
9235
9535
  registerRemoteTrigger(workerId, input) {
9236
- const tid = crypto6.randomUUID();
9536
+ const tid = crypto7.randomUUID();
9237
9537
  const reg = { id: tid, ...input, workerId };
9238
9538
  triggers.set(tid, reg);
9239
9539
  const worker = workers.get(workerId);
@@ -9583,7 +9883,7 @@ function registerWorker(url) {
9583
9883
  }
9584
9884
 
9585
9885
  // session.ts
9586
- import crypto7 from "node:crypto";
9886
+ import crypto8 from "node:crypto";
9587
9887
  var kSaved = /* @__PURE__ */ Symbol("session.saved");
9588
9888
  var kDestroyed = /* @__PURE__ */ Symbol("session.destroyed");
9589
9889
  var kId = /* @__PURE__ */ Symbol("session.id");
@@ -9659,13 +9959,33 @@ var RedisStore = class {
9659
9959
  await this.redis.del(this.key(sid));
9660
9960
  }
9661
9961
  };
9662
- function createSessionObject(data, sid, store2, ttl) {
9962
+ var COOKIE_SEPARATOR = ".";
9963
+ function signSessionId(sid, secret) {
9964
+ const hmac = crypto8.createHmac("sha256", secret).update(sid).digest("base64url").slice(0, 16);
9965
+ return sid + COOKIE_SEPARATOR + hmac;
9966
+ }
9967
+ function unsignSessionId(value, secret) {
9968
+ const dot = value.lastIndexOf(COOKIE_SEPARATOR);
9969
+ if (dot === -1) return null;
9970
+ const sid = value.slice(0, dot);
9971
+ const sig = value.slice(dot + 1);
9972
+ const expected = crypto8.createHmac("sha256", secret).update(sid).digest("base64url").slice(0, 16);
9973
+ if (sig.length !== expected.length) return null;
9974
+ try {
9975
+ return crypto8.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)) ? sid : null;
9976
+ } catch {
9977
+ return null;
9978
+ }
9979
+ }
9980
+ var kCreatedAt = "__createdAt";
9981
+ function createSessionObject(data, sid, store2, ttl, createdAt) {
9663
9982
  const obj = data ?? {};
9664
9983
  obj[kSaved] = false;
9665
9984
  obj[kDestroyed] = false;
9666
9985
  obj[kId] = sid;
9667
9986
  obj[kStore] = store2;
9668
9987
  obj[kTtl] = ttl;
9988
+ if (createdAt) obj[kCreatedAt] = createdAt;
9669
9989
  obj.save = () => {
9670
9990
  obj[kSaved] = true;
9671
9991
  };
@@ -9695,6 +10015,8 @@ function isSessionActive(session2) {
9695
10015
  function session(options) {
9696
10016
  const ttl = options?.ttl ?? 24 * 60 * 60 * 1e3;
9697
10017
  const cookieName = options?.cookieName ?? "__session";
10018
+ const secret = options?.secret;
10019
+ const rotateInterval = options?.rotateInterval ?? 9e5;
9698
10020
  const cookieOpts = {
9699
10021
  path: options?.cookie?.path ?? "/",
9700
10022
  domain: options?.cookie?.domain,
@@ -9714,21 +10036,35 @@ function session(options) {
9714
10036
  store2 = mem;
9715
10037
  closeStore = () => mem.close();
9716
10038
  }
10039
+ function writeCookie(res, sid) {
10040
+ const value = secret ? signSessionId(sid, secret) : sid;
10041
+ return setCookie(res, cookieName, value, cookieOpts);
10042
+ }
9717
10043
  const mw = (async (req, ctx, next) => {
9718
10044
  const cookies = getCookies(req);
9719
- const sid = cookies[cookieName];
10045
+ const rawSid = cookies[cookieName];
10046
+ let sid;
10047
+ if (rawSid) {
10048
+ sid = secret ? unsignSessionId(rawSid, secret) : rawSid;
10049
+ }
9720
10050
  let session2;
9721
10051
  let loadedSid = sid ?? null;
10052
+ let needsRotation = false;
9722
10053
  if (sid) {
9723
10054
  const data = await store2.get(sid);
9724
10055
  if (data) {
9725
- session2 = createSessionObject(data, sid, store2, ttl);
10056
+ const createdAt = data[kCreatedAt] ?? Date.now();
10057
+ session2 = createSessionObject(data, sid, store2, ttl, createdAt);
10058
+ if (rotateInterval > 0 && Date.now() - createdAt > rotateInterval) {
10059
+ needsRotation = true;
10060
+ }
9726
10061
  } else {
9727
10062
  loadedSid = null;
9728
- session2 = createSessionObject({}, crypto7.randomUUID(), store2, ttl);
10063
+ session2 = createSessionObject({}, crypto8.randomUUID(), store2, ttl, Date.now());
9729
10064
  }
9730
10065
  } else {
9731
- session2 = createSessionObject({}, crypto7.randomUUID(), store2, ttl);
10066
+ loadedSid = null;
10067
+ session2 = createSessionObject({}, crypto8.randomUUID(), store2, ttl, Date.now());
9732
10068
  }
9733
10069
  const snapshot = isSessionActive(session2) ? JSON.stringify(session2) : null;
9734
10070
  ctx.session = session2;
@@ -9741,14 +10077,22 @@ function session(options) {
9741
10077
  }
9742
10078
  return deleteCookie(res, cookieName, cookieOpts);
9743
10079
  }
10080
+ if (needsRotation && loadedSid) {
10081
+ const newId = crypto8.randomUUID();
10082
+ const data = JSON.parse(JSON.stringify(currentSession));
10083
+ data[kCreatedAt] = Date.now();
10084
+ await store2.set(newId, data, ttl);
10085
+ await store2.destroy(loadedSid);
10086
+ loadedSid = newId;
10087
+ currentSession[kId] = newId;
10088
+ currentSession[kCreatedAt] = data[kCreatedAt];
10089
+ }
9744
10090
  const currentData = isSessionActive(currentSession) ? JSON.stringify(currentSession) : null;
9745
10091
  const wasSaved = currentSession[kSaved];
9746
- const changed = wasSaved || currentData !== snapshot;
10092
+ const changed = wasSaved || needsRotation || currentData !== snapshot;
9747
10093
  if (!changed) {
9748
- if (loadedSid) {
9749
- if (store2 instanceof RedisStore) {
9750
- await store2.set(loadedSid, JSON.parse(currentData ?? "{}"), ttl);
9751
- }
10094
+ if (loadedSid && store2 instanceof RedisStore) {
10095
+ await store2.set(loadedSid, JSON.parse(currentData ?? "{}"), ttl);
9752
10096
  }
9753
10097
  return res;
9754
10098
  }
@@ -9757,8 +10101,10 @@ function session(options) {
9757
10101
  const data = JSON.parse(currentData);
9758
10102
  await store2.set(targetSid, data, ttl);
9759
10103
  if (!loadedSid) {
9760
- const cookieRes = setCookie(res, cookieName, targetSid, cookieOpts);
9761
- return cookieRes;
10104
+ return writeCookie(res, targetSid);
10105
+ }
10106
+ if (needsRotation) {
10107
+ return writeCookie(res, targetSid);
9762
10108
  }
9763
10109
  } else if (loadedSid) {
9764
10110
  await store2.destroy(loadedSid);
@@ -9774,7 +10120,7 @@ function session(options) {
9774
10120
  }
9775
10121
 
9776
10122
  // cache.ts
9777
- import crypto8 from "node:crypto";
10123
+ import crypto9 from "node:crypto";
9778
10124
  var BINARY_PREFIXES = [
9779
10125
  "image/",
9780
10126
  "audio/",
@@ -9794,7 +10140,7 @@ function isCacheableStatus(status, allowed) {
9794
10140
  return allowed.includes(status);
9795
10141
  }
9796
10142
  function defaultCacheKey(req) {
9797
- const hash = crypto8.createHash("sha256");
10143
+ const hash = crypto9.createHash("sha256");
9798
10144
  hash.update(req.method);
9799
10145
  hash.update(req.url);
9800
10146
  return hash.digest("hex");
@@ -10009,10 +10355,10 @@ function cache2(options) {
10009
10355
  }
10010
10356
 
10011
10357
  // webhook.ts
10012
- import crypto9 from "node:crypto";
10358
+ import crypto10 from "node:crypto";
10013
10359
  function timingSafeEqual2(a, b) {
10014
10360
  try {
10015
- return crypto9.timingSafeEqual(Buffer.from(a), Buffer.from(b));
10361
+ return crypto10.timingSafeEqual(Buffer.from(a), Buffer.from(b));
10016
10362
  } catch {
10017
10363
  return false;
10018
10364
  }
@@ -10030,7 +10376,7 @@ function createStripeVerifier(config) {
10030
10376
  const signature = parts["v1"];
10031
10377
  if (!timestamp || !signature) return { valid: false, provider: "stripe", event: "", id: void 0 };
10032
10378
  const signed = `${timestamp}.${body}`;
10033
- const expected = crypto9.createHmac("sha256", config.secret).update(signed).digest("hex");
10379
+ const expected = crypto10.createHmac("sha256", config.secret).update(signed).digest("hex");
10034
10380
  const valid = timingSafeEqual2(signature, expected);
10035
10381
  let event = "";
10036
10382
  let id2;
@@ -10047,7 +10393,7 @@ function createGitHubVerifier(config) {
10047
10393
  return (body, headers) => {
10048
10394
  const sig = headers["x-hub-signature-256"];
10049
10395
  if (!sig) return { valid: false, provider: "github", event: "", id: void 0 };
10050
- const expected = `sha256=${crypto9.createHmac("sha256", config.secret).update(body).digest("hex")}`;
10396
+ const expected = `sha256=${crypto10.createHmac("sha256", config.secret).update(body).digest("hex")}`;
10051
10397
  const valid = timingSafeEqual2(sig, expected);
10052
10398
  let event = headers["x-github-event"] ?? "";
10053
10399
  let id2;
@@ -10070,7 +10416,7 @@ function createSlackVerifier(config) {
10070
10416
  return { valid: false, provider: "slack", event: "", id: void 0 };
10071
10417
  }
10072
10418
  const sigBase = `v0:${timestamp}:${body}`;
10073
- const expected = `v0=${crypto9.createHmac("sha256", config.secret).update(sigBase).digest("hex")}`;
10419
+ const expected = `v0=${crypto10.createHmac("sha256", config.secret).update(sigBase).digest("hex")}`;
10074
10420
  const valid = timingSafeEqual2(signature, expected);
10075
10421
  let event = "";
10076
10422
  let id2;
@@ -10321,6 +10667,250 @@ async function suggest(sql2, table, prefix, options) {
10321
10667
  `);
10322
10668
  return rows.map((r) => r.tokens?.[0] ?? "").filter(Boolean);
10323
10669
  }
10670
+
10671
+ // s3.ts
10672
+ import {
10673
+ S3Client,
10674
+ PutObjectCommand,
10675
+ GetObjectCommand,
10676
+ DeleteObjectCommand,
10677
+ HeadObjectCommand,
10678
+ ListObjectsV2Command
10679
+ } from "@aws-sdk/client-s3";
10680
+ import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
10681
+ function s3(options) {
10682
+ const { bucket, publicUrl } = options;
10683
+ const client = new S3Client({
10684
+ region: options.region ?? "us-east-1",
10685
+ endpoint: options.endpoint,
10686
+ forcePathStyle: options.forcePathStyle,
10687
+ credentials: options.credentials
10688
+ });
10689
+ async function put(key, body, putOpts) {
10690
+ const command = new PutObjectCommand({
10691
+ Bucket: bucket,
10692
+ Key: key,
10693
+ Body: body,
10694
+ ContentType: putOpts?.contentType,
10695
+ CacheControl: putOpts?.cacheControl ?? "public, max-age=31536000",
10696
+ Metadata: putOpts?.metadata
10697
+ });
10698
+ await client.send(command);
10699
+ return key;
10700
+ }
10701
+ async function get(key) {
10702
+ try {
10703
+ const command = new GetObjectCommand({ Bucket: bucket, Key: key });
10704
+ const response = await client.send(command);
10705
+ const body = response.Body;
10706
+ if (!body) return null;
10707
+ return Buffer.from(await body.transformToByteArray());
10708
+ } catch (err) {
10709
+ if (err.name === "NoSuchKey") return null;
10710
+ throw err;
10711
+ }
10712
+ }
10713
+ async function del(key) {
10714
+ const command = new DeleteObjectCommand({ Bucket: bucket, Key: key });
10715
+ await client.send(command);
10716
+ }
10717
+ async function exists(key) {
10718
+ try {
10719
+ const command = new HeadObjectCommand({ Bucket: bucket, Key: key });
10720
+ await client.send(command);
10721
+ return true;
10722
+ } catch (err) {
10723
+ if (err.name === "NotFound" || err.name === "NoSuchKey") return false;
10724
+ throw err;
10725
+ }
10726
+ }
10727
+ async function url(key, urlOpts) {
10728
+ const expiresIn = urlOpts?.expiresIn ?? 3600;
10729
+ if (expiresIn === 0) {
10730
+ if (!publicUrl) {
10731
+ throw new Error(
10732
+ "s3.url() with expiresIn=0 requires publicUrl in S3Options. Set publicUrl to enable unsigned public URLs."
10733
+ );
10734
+ }
10735
+ const base = publicUrl.replace(/\/+$/, "");
10736
+ const objectKey = key.startsWith("/") ? key.slice(1) : key;
10737
+ return `${base}/${objectKey}`;
10738
+ }
10739
+ const command = new GetObjectCommand({ Bucket: bucket, Key: key });
10740
+ return getSignedUrl(client, command, { expiresIn });
10741
+ }
10742
+ async function list(prefix) {
10743
+ const keys = [];
10744
+ let continuationToken;
10745
+ do {
10746
+ const command = new ListObjectsV2Command({
10747
+ Bucket: bucket,
10748
+ Prefix: prefix,
10749
+ ContinuationToken: continuationToken
10750
+ });
10751
+ const response = await client.send(command);
10752
+ if (response.Contents) {
10753
+ for (const obj of response.Contents) {
10754
+ if (obj.Key) keys.push(obj.Key);
10755
+ }
10756
+ }
10757
+ continuationToken = response.NextContinuationToken;
10758
+ } while (continuationToken);
10759
+ return keys;
10760
+ }
10761
+ const mod = {
10762
+ put,
10763
+ get,
10764
+ delete: del,
10765
+ exists,
10766
+ url,
10767
+ list,
10768
+ client
10769
+ };
10770
+ const mw = ((req, ctx, next) => {
10771
+ ;
10772
+ ctx.s3 = mod;
10773
+ return next(req, ctx);
10774
+ });
10775
+ mw.put = put;
10776
+ mw.get = get;
10777
+ mw.delete = del;
10778
+ mw.exists = exists;
10779
+ mw.url = url;
10780
+ mw.list = list;
10781
+ mw.client = client;
10782
+ return mw;
10783
+ }
10784
+
10785
+ // kb.ts
10786
+ function chunkContent2(content, chunkSize, overlap) {
10787
+ const paragraphs = content.split(/\n\n+/);
10788
+ const chunks = [];
10789
+ let current = "";
10790
+ for (const p of paragraphs) {
10791
+ if (current.length + p.length > chunkSize && current.length > 0) {
10792
+ chunks.push(current);
10793
+ current = current.slice(-overlap);
10794
+ }
10795
+ current += (current ? "\n\n" : "") + p;
10796
+ }
10797
+ if (current) chunks.push(current);
10798
+ return chunks;
10799
+ }
10800
+ function escapeIdent2(s) {
10801
+ return `"${s.replace(/"/g, '""')}"`;
10802
+ }
10803
+ function knowledgeBase(options) {
10804
+ const {
10805
+ sql: sql2,
10806
+ embedding: embedFn,
10807
+ dimensions = 1536,
10808
+ table = "_kb_docs",
10809
+ chunkSize = 512,
10810
+ chunkOverlap = 64,
10811
+ searchLimit = 5,
10812
+ searchThreshold = 0
10813
+ } = options;
10814
+ async function migrate() {
10815
+ await sql2.unsafe(`CREATE EXTENSION IF NOT EXISTS "vector"`);
10816
+ await sql2.unsafe(`
10817
+ CREATE TABLE IF NOT EXISTS ${escapeIdent2(table)} (
10818
+ id SERIAL PRIMARY KEY,
10819
+ doc_key TEXT NOT NULL,
10820
+ title TEXT NOT NULL DEFAULT '',
10821
+ content TEXT NOT NULL,
10822
+ chunk_index INTEGER NOT NULL DEFAULT 0,
10823
+ metadata JSONB NOT NULL DEFAULT '{}',
10824
+ embedding vector(${dimensions}),
10825
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
10826
+ )
10827
+ `);
10828
+ await sql2.unsafe(`
10829
+ CREATE INDEX IF NOT EXISTS ${escapeIdent2(table + "_key_idx")}
10830
+ ON ${escapeIdent2(table)}(doc_key)
10831
+ `);
10832
+ await sql2.unsafe(`
10833
+ CREATE INDEX IF NOT EXISTS ${escapeIdent2(table + "_embedding_idx")}
10834
+ ON ${escapeIdent2(table)}
10835
+ USING hnsw (embedding vector_cosine_ops)
10836
+ `);
10837
+ }
10838
+ async function ingest(key, content, ingestOpts) {
10839
+ await sql2.unsafe(`DELETE FROM ${escapeIdent2(table)} WHERE doc_key = $1`, [key]);
10840
+ const title = ingestOpts?.title ?? key;
10841
+ const meta = ingestOpts?.metadata ?? {};
10842
+ const cs = ingestOpts?.chunkSize ?? chunkSize;
10843
+ const co = ingestOpts?.chunkOverlap ?? chunkOverlap;
10844
+ const chunks = chunkContent2(content, cs, co);
10845
+ const metaJson = JSON.stringify(meta);
10846
+ for (let i = 0; i < chunks.length; i++) {
10847
+ const chunk = chunks[i];
10848
+ const embedding = await embedFn(chunk);
10849
+ const vec = `[${embedding.join(",")}]`;
10850
+ await sql2.unsafe(
10851
+ `INSERT INTO ${escapeIdent2(table)} (doc_key, title, content, chunk_index, metadata, embedding)
10852
+ VALUES ($1, $2, $3, $4, $5::jsonb, $6::vector)`,
10853
+ [key, title, chunk, i, metaJson, vec]
10854
+ );
10855
+ }
10856
+ return chunks.length;
10857
+ }
10858
+ async function search2(query, searchOpts) {
10859
+ const limit = searchOpts?.limit ?? searchLimit;
10860
+ const threshold = searchOpts?.threshold ?? searchThreshold;
10861
+ const embedding = await embedFn(query);
10862
+ const vec = `[${embedding.join(",")}]`;
10863
+ const whereClause = threshold > 0 ? `WHERE (1 - (embedding <=> $1::vector) / 2) >= ${threshold}` : "";
10864
+ const rows = await sql2.unsafe(
10865
+ `SELECT id, doc_key, title, content, chunk_index, metadata,
10866
+ 1 - (embedding <=> $1::vector) / 2 AS _score
10867
+ FROM ${escapeIdent2(table)}
10868
+ ${whereClause}
10869
+ ORDER BY embedding <=> $1::vector
10870
+ LIMIT ${limit}`,
10871
+ [vec]
10872
+ );
10873
+ return rows.map((r) => ({
10874
+ id: r.id,
10875
+ key: r.doc_key,
10876
+ title: r.title,
10877
+ content: r.content,
10878
+ score: r._score,
10879
+ metadata: typeof r.metadata === "string" ? JSON.parse(r.metadata) : r.metadata ?? {}
10880
+ }));
10881
+ }
10882
+ async function del(key) {
10883
+ await sql2.unsafe(`DELETE FROM ${escapeIdent2(table)} WHERE doc_key = $1`, [key]);
10884
+ }
10885
+ async function list() {
10886
+ const rows = await sql2.unsafe(`
10887
+ SELECT doc_key, title, COUNT(*) AS chunks
10888
+ FROM ${escapeIdent2(table)}
10889
+ GROUP BY doc_key, title
10890
+ ORDER BY doc_key
10891
+ `);
10892
+ return rows.map((r) => ({
10893
+ key: r.doc_key,
10894
+ title: r.title,
10895
+ chunks: Number(r.chunks)
10896
+ }));
10897
+ }
10898
+ function mw() {
10899
+ return (req, ctx, next) => {
10900
+ ;
10901
+ ctx.kb = { search: search2 };
10902
+ return next(req, ctx);
10903
+ };
10904
+ }
10905
+ return {
10906
+ ingest,
10907
+ search: search2,
10908
+ delete: del,
10909
+ list,
10910
+ migrate,
10911
+ middleware: mw
10912
+ };
10913
+ }
10324
10914
  export {
10325
10915
  DEFAULT_MAX_BODY,
10326
10916
  MIGRATIONS_TABLE,
@@ -10365,11 +10955,13 @@ export {
10365
10955
  iii,
10366
10956
  isDev,
10367
10957
  isProd,
10958
+ knowledgeBase,
10368
10959
  loadEnv,
10369
10960
  logdb,
10370
10961
  logger,
10371
10962
  mailer,
10372
10963
  messager,
10964
+ oauthClient,
10373
10965
  openai,
10374
10966
  opencode,
10375
10967
  postgres,
@@ -10381,6 +10973,7 @@ export {
10381
10973
  requestId,
10382
10974
  runWithTrace,
10383
10975
  runWorkflow,
10976
+ s3,
10384
10977
  seo,
10385
10978
  seoMiddleware,
10386
10979
  seoTags,