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/README.md +241 -1
- package/dist/auth.d.ts +14 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +641 -54
- 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/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();
|
|
@@ -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
|
|
3299
|
+
import jwt3 from "jsonwebtoken";
|
|
3006
3300
|
import { z as z2 } from "zod";
|
|
3007
3301
|
|
|
3008
3302
|
// user/oauth2.ts
|
|
3009
|
-
import
|
|
3010
|
-
import
|
|
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 =
|
|
3029
|
-
const clientSecret =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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:
|
|
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 =
|
|
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
|
|
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 || !
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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({},
|
|
10063
|
+
session2 = createSessionObject({}, crypto8.randomUUID(), store2, ttl, Date.now());
|
|
9735
10064
|
}
|
|
9736
10065
|
} else {
|
|
9737
|
-
|
|
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
|
-
|
|
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
|
-
|
|
9767
|
-
|
|
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
|
|
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 =
|
|
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
|
|
10358
|
+
import crypto10 from "node:crypto";
|
|
10019
10359
|
function timingSafeEqual2(a, b) {
|
|
10020
10360
|
try {
|
|
10021
|
-
return
|
|
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 =
|
|
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=${
|
|
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=${
|
|
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,
|