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/README.md +241 -1
- package/dist/auth.d.ts +14 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +662 -69
- package/dist/kb.d.ts +70 -0
- package/dist/oauth-client.d.ts +41 -0
- package/dist/s3.d.ts +68 -0
- package/dist/serve.d.ts +1 -1
- package/dist/session.d.ts +12 -0
- package/dist/test-utils.d.ts +10 -10
- package/package.json +3 -1
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
|
-
|
|
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
|
-
|
|
248
|
-
|
|
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
|
|
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
|
|
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 ?? (() =>
|
|
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,
|
|
1788
|
-
this.router.get(path2,
|
|
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,
|
|
1793
|
-
this.router.post(path2,
|
|
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,
|
|
1798
|
-
this.router.put(path2,
|
|
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,
|
|
1803
|
-
this.router.patch(path2,
|
|
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,
|
|
1808
|
-
this.router.delete(path2,
|
|
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
|
|
3299
|
+
import jwt3 from "jsonwebtoken";
|
|
3000
3300
|
import { z as z2 } from "zod";
|
|
3001
3301
|
|
|
3002
3302
|
// user/oauth2.ts
|
|
3003
|
-
import
|
|
3004
|
-
import
|
|
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 =
|
|
3023
|
-
const clientSecret =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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:
|
|
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 =
|
|
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
|
|
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 || !
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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({},
|
|
10063
|
+
session2 = createSessionObject({}, crypto8.randomUUID(), store2, ttl, Date.now());
|
|
9729
10064
|
}
|
|
9730
10065
|
} else {
|
|
9731
|
-
|
|
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
|
-
|
|
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
|
-
|
|
9761
|
-
|
|
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
|
|
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 =
|
|
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
|
|
10358
|
+
import crypto10 from "node:crypto";
|
|
10013
10359
|
function timingSafeEqual2(a, b) {
|
|
10014
10360
|
try {
|
|
10015
|
-
return
|
|
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 =
|
|
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=${
|
|
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=${
|
|
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,
|