weifuwu 0.22.2 → 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();
@@ -3002,12 +3296,12 @@ var PgModule = class {
3002
3296
 
3003
3297
  // user/client.ts
3004
3298
  import { randomBytes, scryptSync, timingSafeEqual } from "node:crypto";
3005
- import jwt2 from "jsonwebtoken";
3299
+ import jwt3 from "jsonwebtoken";
3006
3300
  import { z as z2 } from "zod";
3007
3301
 
3008
3302
  // user/oauth2.ts
3009
- import crypto3 from "node:crypto";
3010
- import jwt from "jsonwebtoken";
3303
+ import crypto4 from "node:crypto";
3304
+ import jwt2 from "jsonwebtoken";
3011
3305
  function createOAuth2Server(deps) {
3012
3306
  const { pg, users, jwtSecret, expiresIn } = deps;
3013
3307
  async function getClient(clientId) {
@@ -3025,8 +3319,8 @@ function createOAuth2Server(deps) {
3025
3319
  };
3026
3320
  }
3027
3321
  async function registerClient(data) {
3028
- const clientId = crypto3.randomUUID();
3029
- const clientSecret = crypto3.randomBytes(32).toString("hex");
3322
+ const clientId = crypto4.randomUUID();
3323
+ const clientSecret = crypto4.randomBytes(32).toString("hex");
3030
3324
  const [row] = await pg.sql`
3031
3325
  INSERT INTO "_oauth2_clients" ("name", "client_id", "client_secret", "redirect_uris")
3032
3326
  VALUES (${data.name}, ${clientId}, ${clientSecret}, ${pg.sql.array(data.redirectUris)})
@@ -3050,7 +3344,7 @@ function createOAuth2Server(deps) {
3050
3344
  const header = req.headers.get("Authorization");
3051
3345
  if (header?.startsWith("Bearer ")) {
3052
3346
  try {
3053
- const payload = jwt.verify(header.slice(7), jwtSecret);
3347
+ const payload = jwt2.verify(header.slice(7), jwtSecret);
3054
3348
  return { id: payload.sub, email: payload.email, role: payload.role };
3055
3349
  } catch {
3056
3350
  return null;
@@ -3060,7 +3354,7 @@ function createOAuth2Server(deps) {
3060
3354
  const qsToken = url.searchParams.get("access_token");
3061
3355
  if (qsToken) {
3062
3356
  try {
3063
- const payload = jwt.verify(qsToken, jwtSecret);
3357
+ const payload = jwt2.verify(qsToken, jwtSecret);
3064
3358
  return { id: payload.sub, email: payload.email, role: payload.role };
3065
3359
  } catch {
3066
3360
  return null;
@@ -3071,7 +3365,7 @@ function createOAuth2Server(deps) {
3071
3365
  const match = cookie.split(";").map((c) => c.trim()).find((c) => c.startsWith("session="));
3072
3366
  if (match) {
3073
3367
  try {
3074
- const payload = jwt.verify(match.slice(8), jwtSecret);
3368
+ const payload = jwt2.verify(match.slice(8), jwtSecret);
3075
3369
  return { id: payload.sub, email: payload.email, role: payload.role };
3076
3370
  } catch {
3077
3371
  return null;
@@ -3174,7 +3468,7 @@ h2{color:#dc2626}.desc{color:#555}</style>
3174
3468
  const loc2 = `${redirectUri}?error=access_denied${state ? `&state=${state}` : ""}`;
3175
3469
  return Response.redirect(loc2, 302);
3176
3470
  }
3177
- const code = crypto3.randomUUID();
3471
+ const code = crypto4.randomUUID();
3178
3472
  const expiresAt = new Date(Date.now() + 10 * 60 * 1e3);
3179
3473
  await pg.sql`
3180
3474
  INSERT INTO "_oauth2_codes" ("code", "client_id", "user_id", "redirect_uri", "code_challenge", "code_challenge_method", "scope", "expires_at")
@@ -3239,7 +3533,7 @@ h2{color:#dc2626}.desc{color:#555}</style>
3239
3533
  if (stored.code_challenge_method === "plain") {
3240
3534
  expected = codeVerifier;
3241
3535
  } else {
3242
- expected = crypto3.createHash("sha256").update(codeVerifier).digest().toString("base64url");
3536
+ expected = crypto4.createHash("sha256").update(codeVerifier).digest().toString("base64url");
3243
3537
  }
3244
3538
  if (expected !== stored.code_challenge) {
3245
3539
  return Response.json({ error: "invalid_grant", error_description: "code_verifier mismatch" }, { status: 400 });
@@ -3251,12 +3545,12 @@ h2{color:#dc2626}.desc{color:#555}</style>
3251
3545
  return Response.json({ error: "invalid_grant" }, { status: 400 });
3252
3546
  }
3253
3547
  const scope = stored.scope || "";
3254
- const accessToken = jwt.sign(
3548
+ const accessToken = jwt2.sign(
3255
3549
  { sub: user2.id, email: user2.email, role: user2.role, client_id: clientId, scope },
3256
3550
  jwtSecret,
3257
3551
  { expiresIn }
3258
3552
  );
3259
- const refreshToken = crypto3.randomUUID();
3553
+ const refreshToken = crypto4.randomUUID();
3260
3554
  const refreshExpires = new Date(Date.now() + 30 * 24 * 60 * 60 * 1e3);
3261
3555
  await pg.sql`
3262
3556
  INSERT INTO "_oauth2_tokens" ("token", "client_id", "user_id", "scope", "expires_at")
@@ -3278,7 +3572,7 @@ h2{color:#dc2626}.desc{color:#555}</style>
3278
3572
  if (!client || client.clientSecret !== clientSecret) {
3279
3573
  return Response.json({ error: "invalid_client" }, { status: 401 });
3280
3574
  }
3281
- const accessToken = jwt.sign(
3575
+ const accessToken = jwt2.sign(
3282
3576
  { sub: clientId, client_id: clientId, scope, token_type: "client_credentials" },
3283
3577
  jwtSecret,
3284
3578
  { expiresIn }
@@ -3372,7 +3666,7 @@ function user(options) {
3372
3666
  await tokens.create();
3373
3667
  }
3374
3668
  function signToken(user2) {
3375
- return jwt2.sign(
3669
+ return jwt3.sign(
3376
3670
  { sub: user2.id, email: user2.email, role: user2.role },
3377
3671
  secret,
3378
3672
  { expiresIn }
@@ -3423,7 +3717,7 @@ function user(options) {
3423
3717
  }
3424
3718
  async function verify(token) {
3425
3719
  try {
3426
- const payload = jwt2.verify(token, secret);
3720
+ const payload = jwt3.verify(token, secret);
3427
3721
  if (payload.token_type === "client_credentials") return null;
3428
3722
  const row = await findById(payload.sub);
3429
3723
  if (!row) return null;
@@ -3542,7 +3836,7 @@ function redis(opts) {
3542
3836
 
3543
3837
  // queue/index.ts
3544
3838
  import { Redis as IORedis2 } from "ioredis";
3545
- import crypto4 from "node:crypto";
3839
+ import crypto5 from "node:crypto";
3546
3840
  function cronNext(expr, from = /* @__PURE__ */ new Date()) {
3547
3841
  const parts = expr.trim().split(/\s+/);
3548
3842
  if (parts.length !== 5) throw new Error(`Invalid cron expression "${expr}": expected 5 fields`);
@@ -3637,7 +3931,7 @@ function queue(opts) {
3637
3931
  if (job.schedule) {
3638
3932
  try {
3639
3933
  const nextRun = cronNext(job.schedule);
3640
- const nextJob = { ...job, id: crypto4.randomUUID(), runAt: nextRun, createdAt: Date.now() };
3934
+ const nextJob = { ...job, id: crypto5.randomUUID(), runAt: nextRun, createdAt: Date.now() };
3641
3935
  await redis2.zadd(jobKey, nextRun, JSON.stringify(nextJob));
3642
3936
  } catch (e) {
3643
3937
  console.error("[queue] cron re-queue failed:", e.message);
@@ -3677,7 +3971,7 @@ function queue(opts) {
3677
3971
  }
3678
3972
  }
3679
3973
  mw.add = function add(type, payload, opts2) {
3680
- const id2 = crypto4.randomUUID();
3974
+ const id2 = crypto5.randomUUID();
3681
3975
  let runAt;
3682
3976
  if (opts2?.schedule) {
3683
3977
  runAt = cronNext(opts2.schedule);
@@ -5635,7 +5929,7 @@ function createGateway(config, getPort) {
5635
5929
  }
5636
5930
 
5637
5931
  // deploy/manager.ts
5638
- import crypto5 from "node:crypto";
5932
+ import crypto6 from "node:crypto";
5639
5933
 
5640
5934
  // deploy/process.ts
5641
5935
  import { fork } from "node:child_process";
@@ -5694,7 +5988,7 @@ function createManager(config, apps, manager) {
5694
5988
  const token = header.replace("Bearer ", "");
5695
5989
  const tokenBuf = Buffer.from(token);
5696
5990
  const secretBuf = Buffer.from(config.deployToken);
5697
- if (tokenBuf.length !== secretBuf.length || !crypto5.timingSafeEqual(tokenBuf, secretBuf)) {
5991
+ if (tokenBuf.length !== secretBuf.length || !crypto6.timingSafeEqual(tokenBuf, secretBuf)) {
5698
5992
  return Response.json({ error: "Unauthorized" }, { status: 401 });
5699
5993
  }
5700
5994
  return next(req, ctx);
@@ -8620,7 +8914,7 @@ function logdb(options) {
8620
8914
  }
8621
8915
 
8622
8916
  // iii/client.ts
8623
- import crypto6 from "node:crypto";
8917
+ import crypto7 from "node:crypto";
8624
8918
 
8625
8919
  // iii/stream.ts
8626
8920
  function notify(channels, stream, group, item, event, data) {
@@ -9149,7 +9443,7 @@ function iii(opts = {}) {
9149
9443
  registerBuiltin("stream::send", (p) => stream.send(p.stream_name, p.group_id, p.type, p.data, p.id));
9150
9444
  registerBuiltin("stream::update", (p) => stream.update(p.stream_name, p.group_id, p.item_id, p.ops));
9151
9445
  function addLocalWorker(worker) {
9152
- const workerId = crypto6.randomUUID();
9446
+ const workerId = crypto7.randomUUID();
9153
9447
  const reg = {
9154
9448
  id: workerId,
9155
9449
  name: worker.name,
@@ -9164,7 +9458,7 @@ function iii(opts = {}) {
9164
9458
  const triggerIds = [];
9165
9459
  for (const t of worker.getTriggers()) {
9166
9460
  if (t.input.function_id === fn.id) {
9167
- const tid = crypto6.randomUUID();
9461
+ const tid = crypto7.randomUUID();
9168
9462
  triggers.set(tid, {
9169
9463
  id: tid,
9170
9464
  type: t.input.type,
@@ -9193,7 +9487,7 @@ function iii(opts = {}) {
9193
9487
  if (!worker) return;
9194
9488
  const handler = async (payload) => {
9195
9489
  if (!worker.ws) throw new Error(`Worker "${worker.name}" disconnected`);
9196
- const invocationId = crypto6.randomUUID();
9490
+ const invocationId = crypto7.randomUUID();
9197
9491
  return new Promise((resolve14, reject) => {
9198
9492
  const timer = setTimeout(() => {
9199
9493
  pending.delete(invocationId);
@@ -9228,7 +9522,7 @@ function iii(opts = {}) {
9228
9522
  let engineRef = null;
9229
9523
  const wsHandler = createWsHandler({
9230
9524
  registerRemoteWorker(ws, name) {
9231
- const id2 = crypto6.randomUUID();
9525
+ const id2 = crypto7.randomUUID();
9232
9526
  workers.set(id2, { id: id2, name, ws, functions: [], triggers: [] });
9233
9527
  return id2;
9234
9528
  },
@@ -9239,7 +9533,7 @@ function iii(opts = {}) {
9239
9533
  addRemoteFunction(workerId, id2);
9240
9534
  },
9241
9535
  registerRemoteTrigger(workerId, input) {
9242
- const tid = crypto6.randomUUID();
9536
+ const tid = crypto7.randomUUID();
9243
9537
  const reg = { id: tid, ...input, workerId };
9244
9538
  triggers.set(tid, reg);
9245
9539
  const worker = workers.get(workerId);
@@ -9589,7 +9883,7 @@ function registerWorker(url) {
9589
9883
  }
9590
9884
 
9591
9885
  // session.ts
9592
- import crypto7 from "node:crypto";
9886
+ import crypto8 from "node:crypto";
9593
9887
  var kSaved = /* @__PURE__ */ Symbol("session.saved");
9594
9888
  var kDestroyed = /* @__PURE__ */ Symbol("session.destroyed");
9595
9889
  var kId = /* @__PURE__ */ Symbol("session.id");
@@ -9665,13 +9959,33 @@ var RedisStore = class {
9665
9959
  await this.redis.del(this.key(sid));
9666
9960
  }
9667
9961
  };
9668
- 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) {
9669
9982
  const obj = data ?? {};
9670
9983
  obj[kSaved] = false;
9671
9984
  obj[kDestroyed] = false;
9672
9985
  obj[kId] = sid;
9673
9986
  obj[kStore] = store2;
9674
9987
  obj[kTtl] = ttl;
9988
+ if (createdAt) obj[kCreatedAt] = createdAt;
9675
9989
  obj.save = () => {
9676
9990
  obj[kSaved] = true;
9677
9991
  };
@@ -9701,6 +10015,8 @@ function isSessionActive(session2) {
9701
10015
  function session(options) {
9702
10016
  const ttl = options?.ttl ?? 24 * 60 * 60 * 1e3;
9703
10017
  const cookieName = options?.cookieName ?? "__session";
10018
+ const secret = options?.secret;
10019
+ const rotateInterval = options?.rotateInterval ?? 9e5;
9704
10020
  const cookieOpts = {
9705
10021
  path: options?.cookie?.path ?? "/",
9706
10022
  domain: options?.cookie?.domain,
@@ -9720,21 +10036,35 @@ function session(options) {
9720
10036
  store2 = mem;
9721
10037
  closeStore = () => mem.close();
9722
10038
  }
10039
+ function writeCookie(res, sid) {
10040
+ const value = secret ? signSessionId(sid, secret) : sid;
10041
+ return setCookie(res, cookieName, value, cookieOpts);
10042
+ }
9723
10043
  const mw = (async (req, ctx, next) => {
9724
10044
  const cookies = getCookies(req);
9725
- const sid = cookies[cookieName];
10045
+ const rawSid = cookies[cookieName];
10046
+ let sid;
10047
+ if (rawSid) {
10048
+ sid = secret ? unsignSessionId(rawSid, secret) : rawSid;
10049
+ }
9726
10050
  let session2;
9727
10051
  let loadedSid = sid ?? null;
10052
+ let needsRotation = false;
9728
10053
  if (sid) {
9729
10054
  const data = await store2.get(sid);
9730
10055
  if (data) {
9731
- 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
+ }
9732
10061
  } else {
9733
10062
  loadedSid = null;
9734
- session2 = createSessionObject({}, crypto7.randomUUID(), store2, ttl);
10063
+ session2 = createSessionObject({}, crypto8.randomUUID(), store2, ttl, Date.now());
9735
10064
  }
9736
10065
  } else {
9737
- session2 = createSessionObject({}, crypto7.randomUUID(), store2, ttl);
10066
+ loadedSid = null;
10067
+ session2 = createSessionObject({}, crypto8.randomUUID(), store2, ttl, Date.now());
9738
10068
  }
9739
10069
  const snapshot = isSessionActive(session2) ? JSON.stringify(session2) : null;
9740
10070
  ctx.session = session2;
@@ -9747,14 +10077,22 @@ function session(options) {
9747
10077
  }
9748
10078
  return deleteCookie(res, cookieName, cookieOpts);
9749
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
+ }
9750
10090
  const currentData = isSessionActive(currentSession) ? JSON.stringify(currentSession) : null;
9751
10091
  const wasSaved = currentSession[kSaved];
9752
- const changed = wasSaved || currentData !== snapshot;
10092
+ const changed = wasSaved || needsRotation || currentData !== snapshot;
9753
10093
  if (!changed) {
9754
- if (loadedSid) {
9755
- if (store2 instanceof RedisStore) {
9756
- await store2.set(loadedSid, JSON.parse(currentData ?? "{}"), ttl);
9757
- }
10094
+ if (loadedSid && store2 instanceof RedisStore) {
10095
+ await store2.set(loadedSid, JSON.parse(currentData ?? "{}"), ttl);
9758
10096
  }
9759
10097
  return res;
9760
10098
  }
@@ -9763,8 +10101,10 @@ function session(options) {
9763
10101
  const data = JSON.parse(currentData);
9764
10102
  await store2.set(targetSid, data, ttl);
9765
10103
  if (!loadedSid) {
9766
- const cookieRes = setCookie(res, cookieName, targetSid, cookieOpts);
9767
- return cookieRes;
10104
+ return writeCookie(res, targetSid);
10105
+ }
10106
+ if (needsRotation) {
10107
+ return writeCookie(res, targetSid);
9768
10108
  }
9769
10109
  } else if (loadedSid) {
9770
10110
  await store2.destroy(loadedSid);
@@ -9780,7 +10120,7 @@ function session(options) {
9780
10120
  }
9781
10121
 
9782
10122
  // cache.ts
9783
- import crypto8 from "node:crypto";
10123
+ import crypto9 from "node:crypto";
9784
10124
  var BINARY_PREFIXES = [
9785
10125
  "image/",
9786
10126
  "audio/",
@@ -9800,7 +10140,7 @@ function isCacheableStatus(status, allowed) {
9800
10140
  return allowed.includes(status);
9801
10141
  }
9802
10142
  function defaultCacheKey(req) {
9803
- const hash = crypto8.createHash("sha256");
10143
+ const hash = crypto9.createHash("sha256");
9804
10144
  hash.update(req.method);
9805
10145
  hash.update(req.url);
9806
10146
  return hash.digest("hex");
@@ -10015,10 +10355,10 @@ function cache2(options) {
10015
10355
  }
10016
10356
 
10017
10357
  // webhook.ts
10018
- import crypto9 from "node:crypto";
10358
+ import crypto10 from "node:crypto";
10019
10359
  function timingSafeEqual2(a, b) {
10020
10360
  try {
10021
- return crypto9.timingSafeEqual(Buffer.from(a), Buffer.from(b));
10361
+ return crypto10.timingSafeEqual(Buffer.from(a), Buffer.from(b));
10022
10362
  } catch {
10023
10363
  return false;
10024
10364
  }
@@ -10036,7 +10376,7 @@ function createStripeVerifier(config) {
10036
10376
  const signature = parts["v1"];
10037
10377
  if (!timestamp || !signature) return { valid: false, provider: "stripe", event: "", id: void 0 };
10038
10378
  const signed = `${timestamp}.${body}`;
10039
- const expected = crypto9.createHmac("sha256", config.secret).update(signed).digest("hex");
10379
+ const expected = crypto10.createHmac("sha256", config.secret).update(signed).digest("hex");
10040
10380
  const valid = timingSafeEqual2(signature, expected);
10041
10381
  let event = "";
10042
10382
  let id2;
@@ -10053,7 +10393,7 @@ function createGitHubVerifier(config) {
10053
10393
  return (body, headers) => {
10054
10394
  const sig = headers["x-hub-signature-256"];
10055
10395
  if (!sig) return { valid: false, provider: "github", event: "", id: void 0 };
10056
- 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")}`;
10057
10397
  const valid = timingSafeEqual2(sig, expected);
10058
10398
  let event = headers["x-github-event"] ?? "";
10059
10399
  let id2;
@@ -10076,7 +10416,7 @@ function createSlackVerifier(config) {
10076
10416
  return { valid: false, provider: "slack", event: "", id: void 0 };
10077
10417
  }
10078
10418
  const sigBase = `v0:${timestamp}:${body}`;
10079
- 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")}`;
10080
10420
  const valid = timingSafeEqual2(signature, expected);
10081
10421
  let event = "";
10082
10422
  let id2;
@@ -10327,6 +10667,250 @@ async function suggest(sql2, table, prefix, options) {
10327
10667
  `);
10328
10668
  return rows.map((r) => r.tokens?.[0] ?? "").filter(Boolean);
10329
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
+ }
10330
10914
  export {
10331
10915
  DEFAULT_MAX_BODY,
10332
10916
  MIGRATIONS_TABLE,
@@ -10371,11 +10955,13 @@ export {
10371
10955
  iii,
10372
10956
  isDev,
10373
10957
  isProd,
10958
+ knowledgeBase,
10374
10959
  loadEnv,
10375
10960
  logdb,
10376
10961
  logger,
10377
10962
  mailer,
10378
10963
  messager,
10964
+ oauthClient,
10379
10965
  openai,
10380
10966
  opencode,
10381
10967
  postgres,
@@ -10387,6 +10973,7 @@ export {
10387
10973
  requestId,
10388
10974
  runWithTrace,
10389
10975
  runWorkflow,
10976
+ s3,
10390
10977
  seo,
10391
10978
  seoMiddleware,
10392
10979
  seoTags,