weifuwu 0.23.4 → 0.24.0
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 +234 -63
- package/cli.ts +3 -0
- package/dist/ai/provider.d.ts +10 -1
- package/dist/cli.js +3 -0
- package/dist/csrf.d.ts +11 -5
- package/dist/env.d.ts +33 -0
- package/dist/flash.d.ts +5 -0
- package/dist/i18n.d.ts +27 -2
- package/dist/iii/types.d.ts +2 -0
- package/dist/index.d.ts +8 -10
- package/dist/index.js +321 -268
- package/dist/rate-limit.d.ts +2 -1
- package/dist/request-id.d.ts +16 -7
- package/dist/router.d.ts +3 -0
- package/dist/theme.d.ts +25 -2
- package/dist/trace.d.ts +44 -0
- package/dist/types.d.ts +0 -17
- package/dist/upload.d.ts +5 -0
- package/dist/user/client.d.ts +11 -3
- package/dist/user/types.d.ts +19 -4
- package/dist/validate.d.ts +5 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5,8 +5,8 @@ var __export = (target, all) => {
|
|
|
5
5
|
};
|
|
6
6
|
|
|
7
7
|
// trace.ts
|
|
8
|
+
import crypto2 from "node:crypto";
|
|
8
9
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
9
|
-
import { randomUUID } from "node:crypto";
|
|
10
10
|
var als = new AsyncLocalStorage();
|
|
11
11
|
function currentTraceId() {
|
|
12
12
|
return als.getStore()?.traceId;
|
|
@@ -15,7 +15,7 @@ function currentTrace() {
|
|
|
15
15
|
return als.getStore();
|
|
16
16
|
}
|
|
17
17
|
function runWithTrace(incomingTraceId, fn) {
|
|
18
|
-
const traceId = incomingTraceId || randomUUID();
|
|
18
|
+
const traceId = incomingTraceId || crypto2.randomUUID();
|
|
19
19
|
const startTime = Date.now();
|
|
20
20
|
return als.run({ traceId, startTime }, fn);
|
|
21
21
|
}
|
|
@@ -24,10 +24,43 @@ function traceElapsed() {
|
|
|
24
24
|
if (!ctx) return 0;
|
|
25
25
|
return Date.now() - ctx.startTime;
|
|
26
26
|
}
|
|
27
|
+
function trace(options) {
|
|
28
|
+
const header = options?.header ?? "X-Request-ID";
|
|
29
|
+
const gen = options?.generator ?? (() => crypto2.randomUUID());
|
|
30
|
+
return async (req, ctx, next) => {
|
|
31
|
+
const existing = req.headers.get(header);
|
|
32
|
+
const requestId2 = existing ?? gen();
|
|
33
|
+
const tc = als.getStore();
|
|
34
|
+
ctx.trace = {
|
|
35
|
+
requestId: requestId2,
|
|
36
|
+
traceId: tc?.traceId ?? requestId2,
|
|
37
|
+
startTime: tc?.startTime ?? Date.now(),
|
|
38
|
+
elapsed: () => {
|
|
39
|
+
const t = als.getStore();
|
|
40
|
+
return t ? Date.now() - t.startTime : 0;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
const res = await next(req, ctx);
|
|
44
|
+
if (res.headers.has(header)) return res;
|
|
45
|
+
const h = new Headers(res.headers);
|
|
46
|
+
h.set(header, requestId2);
|
|
47
|
+
return new Response(res.body, { status: res.status, statusText: res.statusText, headers: h });
|
|
48
|
+
};
|
|
49
|
+
}
|
|
27
50
|
|
|
28
51
|
// env.ts
|
|
29
52
|
import { readFileSync } from "node:fs";
|
|
30
53
|
import { resolve } from "node:path";
|
|
54
|
+
var PUBLIC_PREFIX = "WEIFUWU_PUBLIC_";
|
|
55
|
+
function getPublicEnv() {
|
|
56
|
+
const result = {};
|
|
57
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
58
|
+
if (key.startsWith(PUBLIC_PREFIX) && value !== void 0) {
|
|
59
|
+
result[key.slice(PUBLIC_PREFIX.length)] = value;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
31
64
|
function isBundled() {
|
|
32
65
|
return true ? true : false;
|
|
33
66
|
}
|
|
@@ -65,6 +98,14 @@ function loadEnv(path2) {
|
|
|
65
98
|
process.env[key] = value;
|
|
66
99
|
}
|
|
67
100
|
}
|
|
101
|
+
function env() {
|
|
102
|
+
const entries = getPublicEnv();
|
|
103
|
+
return async (req, ctx, next) => {
|
|
104
|
+
;
|
|
105
|
+
ctx.env = entries;
|
|
106
|
+
return next(req, ctx);
|
|
107
|
+
};
|
|
108
|
+
}
|
|
68
109
|
|
|
69
110
|
// serve.ts
|
|
70
111
|
import http from "node:http";
|
|
@@ -447,6 +488,10 @@ var Router = class _Router {
|
|
|
447
488
|
}
|
|
448
489
|
} else if (typeof arg1 === "function") {
|
|
449
490
|
this.globalMws.push(arg1);
|
|
491
|
+
} else if (typeof arg1 === "object" && arg1 !== null && "middleware" in arg1 && typeof arg1.middleware === "function" && arg1 instanceof _Router) {
|
|
492
|
+
const mod = arg1;
|
|
493
|
+
this.globalMws.push(mod.middleware());
|
|
494
|
+
this._mountRouter("/", mod);
|
|
450
495
|
}
|
|
451
496
|
return this;
|
|
452
497
|
}
|
|
@@ -991,114 +1036,6 @@ function cors(options) {
|
|
|
991
1036
|
};
|
|
992
1037
|
}
|
|
993
1038
|
|
|
994
|
-
// auth.ts
|
|
995
|
-
function auth(options) {
|
|
996
|
-
if (!options.token && !options.verify && !options.proxy && !options.session) {
|
|
997
|
-
throw new Error("auth() requires at least one of: token, verify, proxy, or session");
|
|
998
|
-
}
|
|
999
|
-
return async (req, ctx, next) => {
|
|
1000
|
-
if (options.session) {
|
|
1001
|
-
const sessionUserId = ctx.session?.userId;
|
|
1002
|
-
if (sessionUserId !== void 0 && sessionUserId !== null) {
|
|
1003
|
-
if (options.resolveUser) {
|
|
1004
|
-
const userData = await options.resolveUser(sessionUserId);
|
|
1005
|
-
if (userData) {
|
|
1006
|
-
ctx.user = userData;
|
|
1007
|
-
return next(req, ctx);
|
|
1008
|
-
}
|
|
1009
|
-
if (typeof ctx.session?.destroy === "function") {
|
|
1010
|
-
;
|
|
1011
|
-
ctx.session.destroy();
|
|
1012
|
-
}
|
|
1013
|
-
console.warn(`[${currentTraceId()}] auth: session userId ${sessionUserId} resolved to null`);
|
|
1014
|
-
} else {
|
|
1015
|
-
ctx.user = { id: sessionUserId };
|
|
1016
|
-
return next(req, ctx);
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
const headerName = options.header ?? "Authorization";
|
|
1021
|
-
let from = "header";
|
|
1022
|
-
let header = req.headers.get(headerName);
|
|
1023
|
-
let token = "";
|
|
1024
|
-
if (header) {
|
|
1025
|
-
token = header.trim();
|
|
1026
|
-
if (headerName.toLowerCase() === "authorization") {
|
|
1027
|
-
const parts = header.split(" ");
|
|
1028
|
-
if (parts[0]?.toLowerCase() === "bearer") {
|
|
1029
|
-
token = parts.slice(1).join(" ").trim();
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
|
-
} else if (!options.header) {
|
|
1033
|
-
const url = new URL(req.url);
|
|
1034
|
-
const qsToken = url.searchParams.get("access_token");
|
|
1035
|
-
if (qsToken) {
|
|
1036
|
-
token = qsToken;
|
|
1037
|
-
from = "query";
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
if (!token) {
|
|
1041
|
-
return new Response("Unauthorized", {
|
|
1042
|
-
status: 401,
|
|
1043
|
-
headers: headerName.toLowerCase() === "authorization" ? { "WWW-Authenticate": "Bearer" } : void 0
|
|
1044
|
-
});
|
|
1045
|
-
}
|
|
1046
|
-
if (options.proxy) {
|
|
1047
|
-
let proxyUrl;
|
|
1048
|
-
try {
|
|
1049
|
-
proxyUrl = typeof options.proxy === "string" ? new URL(options.proxy) : options.proxy;
|
|
1050
|
-
} catch {
|
|
1051
|
-
return new Response("Invalid proxy URL", { status: 500 });
|
|
1052
|
-
}
|
|
1053
|
-
const proxyHeaders = {};
|
|
1054
|
-
if (from === "header" && header) {
|
|
1055
|
-
proxyHeaders[headerName] = header;
|
|
1056
|
-
} else {
|
|
1057
|
-
proxyUrl.searchParams.set("access_token", token);
|
|
1058
|
-
}
|
|
1059
|
-
for (const name of ["x-forwarded-for", "x-real-ip", "user-agent", "content-type"]) {
|
|
1060
|
-
const v = req.headers.get(name);
|
|
1061
|
-
if (v) proxyHeaders[name] = v;
|
|
1062
|
-
}
|
|
1063
|
-
const proxyRes = await fetch(proxyUrl.href, { headers: proxyHeaders });
|
|
1064
|
-
if (proxyRes.status >= 400) {
|
|
1065
|
-
return new Response(await proxyRes.text() || "Forbidden", { status: proxyRes.status });
|
|
1066
|
-
}
|
|
1067
|
-
let userData = void 0;
|
|
1068
|
-
if (proxyRes.status === 200) {
|
|
1069
|
-
const ct = proxyRes.headers.get("content-type");
|
|
1070
|
-
if (ct?.includes("application/json")) {
|
|
1071
|
-
try {
|
|
1072
|
-
userData = await proxyRes.json();
|
|
1073
|
-
} catch {
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
ctx.user = userData;
|
|
1078
|
-
return next(req, ctx);
|
|
1079
|
-
}
|
|
1080
|
-
if (options.token) {
|
|
1081
|
-
if (token !== options.token) {
|
|
1082
|
-
console.warn(`[${currentTraceId()}] auth: invalid static token`);
|
|
1083
|
-
return new Response("Forbidden", { status: 403 });
|
|
1084
|
-
}
|
|
1085
|
-
return next(req, ctx);
|
|
1086
|
-
}
|
|
1087
|
-
if (options.verify) {
|
|
1088
|
-
const result = await options.verify(token, req);
|
|
1089
|
-
if (!result) {
|
|
1090
|
-
console.warn(`[${currentTraceId()}] auth: verify failed for token`);
|
|
1091
|
-
return new Response("Forbidden", { status: 403 });
|
|
1092
|
-
}
|
|
1093
|
-
if (typeof result === "object" && result !== null) {
|
|
1094
|
-
ctx.user = result;
|
|
1095
|
-
}
|
|
1096
|
-
return next(req, ctx);
|
|
1097
|
-
}
|
|
1098
|
-
return next(req, ctx);
|
|
1099
|
-
};
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
1039
|
// static.ts
|
|
1103
1040
|
import { open, realpath } from "node:fs/promises";
|
|
1104
1041
|
import { extname, resolve as resolve2, normalize, sep } from "node:path";
|
|
@@ -1382,7 +1319,7 @@ function deleteCookie(res, name, options) {
|
|
|
1382
1319
|
|
|
1383
1320
|
// upload.ts
|
|
1384
1321
|
import { writeFile, mkdir } from "node:fs/promises";
|
|
1385
|
-
import { randomUUID
|
|
1322
|
+
import { randomUUID } from "node:crypto";
|
|
1386
1323
|
import { join, extname as extname2 } from "node:path";
|
|
1387
1324
|
var extensionMimeMap = {
|
|
1388
1325
|
".jpg": "image/jpeg",
|
|
@@ -1454,7 +1391,7 @@ function upload(options) {
|
|
|
1454
1391
|
};
|
|
1455
1392
|
if (saveDir) {
|
|
1456
1393
|
const safeName = value.name.replace(/[/\\\0]/g, "_").replace(/\.\./g, "_");
|
|
1457
|
-
const filePath = join(saveDir, `${
|
|
1394
|
+
const filePath = join(saveDir, `${randomUUID()}-${safeName}`);
|
|
1458
1395
|
await writeFile(filePath, buf);
|
|
1459
1396
|
uf.path = filePath;
|
|
1460
1397
|
}
|
|
@@ -1551,10 +1488,11 @@ function rateLimit(options) {
|
|
|
1551
1488
|
const res = await next(req, ctx);
|
|
1552
1489
|
return addRateLimitHeaders(res, max, remaining, reset);
|
|
1553
1490
|
};
|
|
1554
|
-
mw.
|
|
1491
|
+
mw.close = () => {
|
|
1555
1492
|
if (interval) clearInterval(interval);
|
|
1556
1493
|
hits.clear();
|
|
1557
1494
|
};
|
|
1495
|
+
mw.stop = mw.close;
|
|
1558
1496
|
mw.stats = () => ({
|
|
1559
1497
|
store: storeType,
|
|
1560
1498
|
entries: storeType === "memory" ? hits.size : void 0,
|
|
@@ -1672,10 +1610,10 @@ var DEFAULTS = {
|
|
|
1672
1610
|
};
|
|
1673
1611
|
|
|
1674
1612
|
// request-id.ts
|
|
1675
|
-
import
|
|
1613
|
+
import crypto3 from "node:crypto";
|
|
1676
1614
|
function requestId(options) {
|
|
1677
1615
|
const header = options?.header ?? "X-Request-ID";
|
|
1678
|
-
const gen = options?.generator ?? (() =>
|
|
1616
|
+
const gen = options?.generator ?? (() => crypto3.randomUUID());
|
|
1679
1617
|
return async (req, ctx, next) => {
|
|
1680
1618
|
const existing = req.headers.get(header);
|
|
1681
1619
|
const id2 = existing ?? gen();
|
|
@@ -2425,7 +2363,7 @@ function aiProvider(options) {
|
|
|
2425
2363
|
const client = createOpenAI({ baseURL, apiKey });
|
|
2426
2364
|
let _model;
|
|
2427
2365
|
let _embedModel;
|
|
2428
|
-
|
|
2366
|
+
const provider = {
|
|
2429
2367
|
get dimension() {
|
|
2430
2368
|
return dimension;
|
|
2431
2369
|
},
|
|
@@ -2454,6 +2392,12 @@ function aiProvider(options) {
|
|
|
2454
2392
|
return aiStreamText({ ...params, model: this.model() });
|
|
2455
2393
|
}
|
|
2456
2394
|
};
|
|
2395
|
+
const mw = async (req, ctx, next) => {
|
|
2396
|
+
;
|
|
2397
|
+
ctx.ai = provider;
|
|
2398
|
+
return next(req, ctx);
|
|
2399
|
+
};
|
|
2400
|
+
return Object.assign(mw, provider);
|
|
2457
2401
|
}
|
|
2458
2402
|
|
|
2459
2403
|
// ai-sdk.ts
|
|
@@ -3126,7 +3070,7 @@ import jwt2 from "jsonwebtoken";
|
|
|
3126
3070
|
import { z as z2 } from "zod";
|
|
3127
3071
|
|
|
3128
3072
|
// user/oauth2.ts
|
|
3129
|
-
import
|
|
3073
|
+
import crypto4 from "node:crypto";
|
|
3130
3074
|
import jwt from "jsonwebtoken";
|
|
3131
3075
|
function createOAuth2Server(deps) {
|
|
3132
3076
|
const { pg, users, jwtSecret, expiresIn } = deps;
|
|
@@ -3145,8 +3089,8 @@ function createOAuth2Server(deps) {
|
|
|
3145
3089
|
};
|
|
3146
3090
|
}
|
|
3147
3091
|
async function registerClient(data) {
|
|
3148
|
-
const clientId =
|
|
3149
|
-
const clientSecret =
|
|
3092
|
+
const clientId = crypto4.randomUUID();
|
|
3093
|
+
const clientSecret = crypto4.randomBytes(32).toString("hex");
|
|
3150
3094
|
const [row] = await pg.sql`
|
|
3151
3095
|
INSERT INTO "_oauth2_clients" ("name", "client_id", "client_secret", "redirect_uris")
|
|
3152
3096
|
VALUES (${data.name}, ${clientId}, ${clientSecret}, ${pg.sql.array(data.redirectUris)})
|
|
@@ -3294,7 +3238,7 @@ h2{color:#dc2626}.desc{color:#555}</style>
|
|
|
3294
3238
|
const loc2 = `${redirectUri}?error=access_denied${state ? `&state=${state}` : ""}`;
|
|
3295
3239
|
return Response.redirect(loc2, 302);
|
|
3296
3240
|
}
|
|
3297
|
-
const code =
|
|
3241
|
+
const code = crypto4.randomUUID();
|
|
3298
3242
|
const expiresAt = new Date(Date.now() + 10 * 60 * 1e3);
|
|
3299
3243
|
await pg.sql`
|
|
3300
3244
|
INSERT INTO "_oauth2_codes" ("code", "client_id", "user_id", "redirect_uri", "code_challenge", "code_challenge_method", "scope", "expires_at")
|
|
@@ -3359,7 +3303,7 @@ h2{color:#dc2626}.desc{color:#555}</style>
|
|
|
3359
3303
|
if (stored.code_challenge_method === "plain") {
|
|
3360
3304
|
expected = codeVerifier;
|
|
3361
3305
|
} else {
|
|
3362
|
-
expected =
|
|
3306
|
+
expected = crypto4.createHash("sha256").update(codeVerifier).digest().toString("base64url");
|
|
3363
3307
|
}
|
|
3364
3308
|
if (expected !== stored.code_challenge) {
|
|
3365
3309
|
return Response.json({ error: "invalid_grant", error_description: "code_verifier mismatch" }, { status: 400 });
|
|
@@ -3376,7 +3320,7 @@ h2{color:#dc2626}.desc{color:#555}</style>
|
|
|
3376
3320
|
jwtSecret,
|
|
3377
3321
|
{ expiresIn }
|
|
3378
3322
|
);
|
|
3379
|
-
const refreshToken =
|
|
3323
|
+
const refreshToken = crypto4.randomUUID();
|
|
3380
3324
|
const refreshExpires = new Date(Date.now() + 30 * 24 * 60 * 60 * 1e3);
|
|
3381
3325
|
await pg.sql`
|
|
3382
3326
|
INSERT INTO "_oauth2_tokens" ("token", "client_id", "user_id", "scope", "expires_at")
|
|
@@ -3414,7 +3358,7 @@ h2{color:#dc2626}.desc{color:#555}</style>
|
|
|
3414
3358
|
}
|
|
3415
3359
|
|
|
3416
3360
|
// user/oauth-login.ts
|
|
3417
|
-
import
|
|
3361
|
+
import crypto5 from "node:crypto";
|
|
3418
3362
|
var BUILTIN_PROVIDERS = {
|
|
3419
3363
|
google: {
|
|
3420
3364
|
authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
@@ -3526,7 +3470,7 @@ function registerOAuthLoginRoutes(router, deps, providers) {
|
|
|
3526
3470
|
return Response.json({ error: `Unsupported provider: ${providerName}` }, { status: 400 });
|
|
3527
3471
|
}
|
|
3528
3472
|
const { config, meta } = resolved;
|
|
3529
|
-
const state =
|
|
3473
|
+
const state = crypto5.randomUUID();
|
|
3530
3474
|
const redirectUri = new URL(req.url);
|
|
3531
3475
|
redirectUri.pathname = redirectUri.pathname.replace(/\/[^/]+$/, "/") + providerName + "/callback";
|
|
3532
3476
|
if (ctx.session) {
|
|
@@ -3657,14 +3601,40 @@ function verifyPassword(password, stored) {
|
|
|
3657
3601
|
if (hash.length !== verify.length) return false;
|
|
3658
3602
|
return timingSafeEqual(Buffer.from(hash), Buffer.from(verify));
|
|
3659
3603
|
}
|
|
3604
|
+
function extractToken(req, headerName, cookieName) {
|
|
3605
|
+
const header = req.headers.get(headerName);
|
|
3606
|
+
if (header) {
|
|
3607
|
+
if (headerName.toLowerCase() === "authorization") {
|
|
3608
|
+
const parts = header.split(" ");
|
|
3609
|
+
if (parts[0]?.toLowerCase() === "bearer") {
|
|
3610
|
+
return parts.slice(1).join(" ").trim();
|
|
3611
|
+
}
|
|
3612
|
+
}
|
|
3613
|
+
return header.trim();
|
|
3614
|
+
}
|
|
3615
|
+
if (headerName.toLowerCase() === "authorization") {
|
|
3616
|
+
const url = new URL(req.url);
|
|
3617
|
+
const qsToken = url.searchParams.get("access_token");
|
|
3618
|
+
if (qsToken) return qsToken;
|
|
3619
|
+
}
|
|
3620
|
+
if (cookieName) {
|
|
3621
|
+
const cookies = req.headers.get("cookie")?.split(";").map((c) => c.trim()).filter(Boolean) || [];
|
|
3622
|
+
for (const c of cookies) {
|
|
3623
|
+
const eq2 = c.indexOf("=");
|
|
3624
|
+
if (eq2 > 0 && c.slice(0, eq2) === cookieName) return c.slice(eq2 + 1);
|
|
3625
|
+
}
|
|
3626
|
+
}
|
|
3627
|
+
return null;
|
|
3628
|
+
}
|
|
3660
3629
|
function user(options) {
|
|
3630
|
+
const hasDb = !!options.pg;
|
|
3661
3631
|
const table = options.table ?? "_users";
|
|
3662
3632
|
const pg = options.pg;
|
|
3663
3633
|
const secret = options.jwtSecret;
|
|
3664
3634
|
const expiresIn = options.expiresIn ?? "24h";
|
|
3665
3635
|
const oauth2Enabled = options.oauth2?.server ?? false;
|
|
3666
|
-
const base = new PgModule(pg);
|
|
3667
|
-
const users = pg.table(table, {
|
|
3636
|
+
const base = hasDb ? new PgModule(pg) : null;
|
|
3637
|
+
const users = hasDb ? pg.table(table, {
|
|
3668
3638
|
id: serial("id").primaryKey(),
|
|
3669
3639
|
email: text("email").unique().notNull(),
|
|
3670
3640
|
password: text("password").notNull(),
|
|
@@ -3672,15 +3642,17 @@ function user(options) {
|
|
|
3672
3642
|
role: text("role").default("user"),
|
|
3673
3643
|
created_at: timestamptz("created_at").default(sql`NOW()`),
|
|
3674
3644
|
updated_at: timestamptz("updated_at").default(sql`NOW()`)
|
|
3675
|
-
});
|
|
3645
|
+
}) : null;
|
|
3646
|
+
const _pg = pg;
|
|
3647
|
+
const _users = users;
|
|
3676
3648
|
let oauth2 = null;
|
|
3677
3649
|
if (oauth2Enabled) {
|
|
3678
|
-
oauth2 = createOAuth2Server({ pg, users, jwtSecret: secret, expiresIn });
|
|
3650
|
+
oauth2 = createOAuth2Server({ pg: _pg, users: _users, jwtSecret: secret, expiresIn });
|
|
3679
3651
|
}
|
|
3680
3652
|
async function migrate() {
|
|
3681
|
-
await
|
|
3653
|
+
await _users.create();
|
|
3682
3654
|
if (options.oauthLogin) {
|
|
3683
|
-
await
|
|
3655
|
+
await _pg.sql.unsafe(`
|
|
3684
3656
|
CREATE TABLE IF NOT EXISTS "_auth_providers" (
|
|
3685
3657
|
id SERIAL PRIMARY KEY,
|
|
3686
3658
|
user_id INTEGER NOT NULL REFERENCES ${escapeIdent2(table)}(id) ON DELETE CASCADE,
|
|
@@ -3693,13 +3665,13 @@ function user(options) {
|
|
|
3693
3665
|
UNIQUE(provider, provider_id)
|
|
3694
3666
|
)
|
|
3695
3667
|
`);
|
|
3696
|
-
await
|
|
3668
|
+
await _pg.sql.unsafe(`
|
|
3697
3669
|
CREATE INDEX IF NOT EXISTS "_auth_providers_user_idx"
|
|
3698
3670
|
ON "_auth_providers"(user_id)
|
|
3699
3671
|
`);
|
|
3700
3672
|
}
|
|
3701
3673
|
if (!oauth2Enabled) return;
|
|
3702
|
-
const clients3 =
|
|
3674
|
+
const clients3 = _pg.table("_oauth2_clients", {
|
|
3703
3675
|
id: serial("id").primaryKey(),
|
|
3704
3676
|
name: text("name").notNull(),
|
|
3705
3677
|
client_id: text("client_id").unique().notNull(),
|
|
@@ -3709,7 +3681,7 @@ function user(options) {
|
|
|
3709
3681
|
created_at: timestamptz("created_at").default(sql`NOW()`)
|
|
3710
3682
|
});
|
|
3711
3683
|
await clients3.create();
|
|
3712
|
-
const codes =
|
|
3684
|
+
const codes = _pg.table("_oauth2_codes", {
|
|
3713
3685
|
id: serial("id").primaryKey(),
|
|
3714
3686
|
code: text("code").unique().notNull(),
|
|
3715
3687
|
client_id: text("client_id").notNull(),
|
|
@@ -3722,7 +3694,7 @@ function user(options) {
|
|
|
3722
3694
|
used: boolean_("used").default(false)
|
|
3723
3695
|
});
|
|
3724
3696
|
await codes.create();
|
|
3725
|
-
const tokens =
|
|
3697
|
+
const tokens = _pg.table("_oauth2_tokens", {
|
|
3726
3698
|
id: serial("id").primaryKey(),
|
|
3727
3699
|
token: text("token").unique().notNull(),
|
|
3728
3700
|
client_id: text("client_id").notNull(),
|
|
@@ -3745,15 +3717,15 @@ function user(options) {
|
|
|
3745
3717
|
return user2;
|
|
3746
3718
|
}
|
|
3747
3719
|
async function findByEmail(email) {
|
|
3748
|
-
const { data: rows } = await
|
|
3720
|
+
const { data: rows } = await _users.readMany({ email });
|
|
3749
3721
|
return rows[0];
|
|
3750
3722
|
}
|
|
3751
3723
|
async function findById(id2) {
|
|
3752
|
-
return await
|
|
3724
|
+
return await _users.read(id2);
|
|
3753
3725
|
}
|
|
3754
3726
|
async function createPlaceholderUser(email, name) {
|
|
3755
3727
|
const randomPassword = randomBytes(32).toString("hex");
|
|
3756
|
-
const row = await
|
|
3728
|
+
const row = await _users.insert({ email, password: randomPassword, name });
|
|
3757
3729
|
return row;
|
|
3758
3730
|
}
|
|
3759
3731
|
async function register(data) {
|
|
@@ -3765,14 +3737,14 @@ function user(options) {
|
|
|
3765
3737
|
throw err;
|
|
3766
3738
|
}
|
|
3767
3739
|
const hashed = hashPassword(password);
|
|
3768
|
-
const row = await
|
|
3740
|
+
const row = await _users.insert({ email, password: hashed, name });
|
|
3769
3741
|
const userData = row;
|
|
3770
3742
|
const token = signToken(userData);
|
|
3771
3743
|
return { user: stripPassword(userData), token };
|
|
3772
3744
|
}
|
|
3773
3745
|
async function login(data) {
|
|
3774
3746
|
const { email, password } = LoginSchema.parse(data);
|
|
3775
|
-
const { data: rows } = await
|
|
3747
|
+
const { data: rows } = await _users.readMany({ email });
|
|
3776
3748
|
const row = rows[0];
|
|
3777
3749
|
if (!row) {
|
|
3778
3750
|
const err = new Error("Invalid email or password");
|
|
@@ -3792,6 +3764,7 @@ function user(options) {
|
|
|
3792
3764
|
try {
|
|
3793
3765
|
const payload = jwt2.verify(token, secret);
|
|
3794
3766
|
if (payload.token_type === "client_credentials") return null;
|
|
3767
|
+
if (!hasDb || !findById) return null;
|
|
3795
3768
|
const row = await findById(payload.sub);
|
|
3796
3769
|
if (!row) return null;
|
|
3797
3770
|
return stripPassword(row);
|
|
@@ -3799,26 +3772,14 @@ function user(options) {
|
|
|
3799
3772
|
return null;
|
|
3800
3773
|
}
|
|
3801
3774
|
}
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
if (
|
|
3806
|
-
|
|
3807
|
-
for (const c of cookies) {
|
|
3808
|
-
const eq2 = c.indexOf("=");
|
|
3809
|
-
if (eq2 > 0 && c.slice(0, eq2) === cookieName) return c.slice(eq2 + 1);
|
|
3810
|
-
}
|
|
3811
|
-
}
|
|
3812
|
-
return null;
|
|
3813
|
-
}
|
|
3814
|
-
function middleware() {
|
|
3815
|
-
return async (req, ctx, next) => {
|
|
3816
|
-
const sessionUserId = ctx.session?.userId;
|
|
3817
|
-
if (sessionUserId) {
|
|
3775
|
+
const headerName = options.header ?? "Authorization";
|
|
3776
|
+
async function resolveUser(req, ctx) {
|
|
3777
|
+
const sessionUserId = ctx.session?.userId;
|
|
3778
|
+
if (sessionUserId !== void 0 && sessionUserId !== null) {
|
|
3779
|
+
if (hasDb) {
|
|
3818
3780
|
const row = await findById(sessionUserId);
|
|
3819
3781
|
if (row) {
|
|
3820
|
-
|
|
3821
|
-
return next(req, ctx);
|
|
3782
|
+
return stripPassword(row);
|
|
3822
3783
|
}
|
|
3823
3784
|
if (typeof ctx.session?.destroy === "function") {
|
|
3824
3785
|
;
|
|
@@ -3826,27 +3787,99 @@ function user(options) {
|
|
|
3826
3787
|
} else {
|
|
3827
3788
|
delete ctx.session?.userId;
|
|
3828
3789
|
}
|
|
3829
|
-
}
|
|
3830
|
-
|
|
3831
|
-
if (token) {
|
|
3832
|
-
const userData = await verify(token);
|
|
3790
|
+
} else if (options.resolveUser) {
|
|
3791
|
+
const userData = await options.resolveUser(sessionUserId);
|
|
3833
3792
|
if (userData) {
|
|
3834
|
-
|
|
3835
|
-
return next(req, ctx);
|
|
3793
|
+
return userData;
|
|
3836
3794
|
}
|
|
3795
|
+
if (typeof ctx.session?.destroy === "function") {
|
|
3796
|
+
;
|
|
3797
|
+
ctx.session.destroy();
|
|
3798
|
+
}
|
|
3799
|
+
console.warn(`[${currentTraceId()}] user: session userId ${sessionUserId} resolved to null`);
|
|
3800
|
+
} else {
|
|
3801
|
+
return { id: sessionUserId };
|
|
3802
|
+
}
|
|
3803
|
+
}
|
|
3804
|
+
const token = extractToken(req, headerName);
|
|
3805
|
+
if (!token) return null;
|
|
3806
|
+
if (options.tokens?.length) {
|
|
3807
|
+
if (options.tokens.includes(token)) {
|
|
3808
|
+
return { id: token };
|
|
3809
|
+
}
|
|
3810
|
+
console.warn(`[${currentTraceId()}] user: invalid static token`);
|
|
3811
|
+
return null;
|
|
3812
|
+
}
|
|
3813
|
+
if (options.verify) {
|
|
3814
|
+
const result = await options.verify(token, req);
|
|
3815
|
+
if (!result) {
|
|
3816
|
+
console.warn(`[${currentTraceId()}] user: verify failed for token`);
|
|
3817
|
+
return null;
|
|
3818
|
+
}
|
|
3819
|
+
return result;
|
|
3820
|
+
}
|
|
3821
|
+
if (options.proxy) {
|
|
3822
|
+
let proxyUrl;
|
|
3823
|
+
try {
|
|
3824
|
+
proxyUrl = typeof options.proxy === "string" ? new URL(options.proxy) : options.proxy;
|
|
3825
|
+
} catch {
|
|
3826
|
+
return null;
|
|
3827
|
+
}
|
|
3828
|
+
const proxyHeaders = {};
|
|
3829
|
+
proxyHeaders[headerName] = req.headers.get(headerName) ?? `Bearer ${token}`;
|
|
3830
|
+
for (const name of ["x-forwarded-for", "x-real-ip", "user-agent", "content-type"]) {
|
|
3831
|
+
const v = req.headers.get(name);
|
|
3832
|
+
if (v) proxyHeaders[name] = v;
|
|
3833
|
+
}
|
|
3834
|
+
try {
|
|
3835
|
+
const proxyRes = await fetch(proxyUrl.href, { headers: proxyHeaders });
|
|
3836
|
+
if (proxyRes.status >= 400) {
|
|
3837
|
+
console.warn(`[${currentTraceId()}] user: proxy auth rejected (${proxyRes.status})`);
|
|
3838
|
+
return null;
|
|
3839
|
+
}
|
|
3840
|
+
const ct = proxyRes.headers.get("content-type");
|
|
3841
|
+
if (ct?.includes("application/json")) {
|
|
3842
|
+
try {
|
|
3843
|
+
return await proxyRes.json();
|
|
3844
|
+
} catch {
|
|
3845
|
+
}
|
|
3846
|
+
}
|
|
3847
|
+
return { id: token };
|
|
3848
|
+
} catch (err) {
|
|
3849
|
+
console.warn(`[${currentTraceId()}] user: proxy auth error: ${err}`);
|
|
3850
|
+
return null;
|
|
3837
3851
|
}
|
|
3838
|
-
|
|
3852
|
+
}
|
|
3853
|
+
if (secret && hasDb) {
|
|
3854
|
+
try {
|
|
3855
|
+
const payload = jwt2.verify(token, secret);
|
|
3856
|
+
if (payload.token_type === "client_credentials") return null;
|
|
3857
|
+
const row = await findById(payload.sub);
|
|
3858
|
+
if (row) return stripPassword(row);
|
|
3859
|
+
} catch {
|
|
3860
|
+
}
|
|
3861
|
+
return null;
|
|
3862
|
+
}
|
|
3863
|
+
return null;
|
|
3864
|
+
}
|
|
3865
|
+
function middleware() {
|
|
3866
|
+
return async (req, ctx, next) => {
|
|
3867
|
+
const userData = await resolveUser(req, ctx);
|
|
3868
|
+
if (userData) {
|
|
3869
|
+
ctx.user = userData;
|
|
3870
|
+
return next(req, ctx);
|
|
3871
|
+
}
|
|
3872
|
+
return new Response("Unauthorized", {
|
|
3873
|
+
status: 401,
|
|
3874
|
+
headers: headerName.toLowerCase() === "authorization" ? { "WWW-Authenticate": "Bearer" } : void 0
|
|
3875
|
+
});
|
|
3839
3876
|
};
|
|
3840
3877
|
}
|
|
3841
|
-
function middlewareOptional(
|
|
3842
|
-
const cookieName = opts?.cookie;
|
|
3878
|
+
function middlewareOptional(_opts) {
|
|
3843
3879
|
return async (req, ctx, next) => {
|
|
3844
|
-
const
|
|
3845
|
-
if (
|
|
3846
|
-
|
|
3847
|
-
if (userData) {
|
|
3848
|
-
ctx.user = userData;
|
|
3849
|
-
}
|
|
3880
|
+
const userData = await resolveUser(req, ctx);
|
|
3881
|
+
if (userData) {
|
|
3882
|
+
ctx.user = userData;
|
|
3850
3883
|
}
|
|
3851
3884
|
return next(req, ctx);
|
|
3852
3885
|
};
|
|
@@ -3863,9 +3896,9 @@ function user(options) {
|
|
|
3863
3896
|
}
|
|
3864
3897
|
return obj;
|
|
3865
3898
|
}
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3899
|
+
const r = new Router();
|
|
3900
|
+
if (hasDb) {
|
|
3901
|
+
r.post("/register", async (req) => {
|
|
3869
3902
|
try {
|
|
3870
3903
|
const body = await parseBody2(req);
|
|
3871
3904
|
const result = await register(body);
|
|
@@ -3878,7 +3911,7 @@ function user(options) {
|
|
|
3878
3911
|
return Response.json({ error: err.message }, { status });
|
|
3879
3912
|
}
|
|
3880
3913
|
});
|
|
3881
|
-
|
|
3914
|
+
r.post("/login", async (req, ctx) => {
|
|
3882
3915
|
try {
|
|
3883
3916
|
const body = await parseBody2(req);
|
|
3884
3917
|
const result = await login(body);
|
|
@@ -3900,17 +3933,15 @@ function user(options) {
|
|
|
3900
3933
|
return Response.json({ error: err.message }, { status });
|
|
3901
3934
|
}
|
|
3902
3935
|
});
|
|
3903
|
-
if (oauth2) {
|
|
3904
|
-
r2.get("/oauth/authorize", (req, ctx) => oauth2.authorizeHandler(req, ctx));
|
|
3905
|
-
r2.post("/oauth/consent", (req) => oauth2.consentHandler(req));
|
|
3906
|
-
r2.post("/oauth/token", (req) => oauth2.tokenHandler(req));
|
|
3907
|
-
}
|
|
3908
|
-
return r2;
|
|
3909
3936
|
}
|
|
3910
|
-
|
|
3911
|
-
|
|
3937
|
+
if (oauth2) {
|
|
3938
|
+
r.get("/oauth/authorize", (req, ctx) => oauth2.authorizeHandler(req, ctx));
|
|
3939
|
+
r.post("/oauth/consent", (req) => oauth2.consentHandler(req));
|
|
3940
|
+
r.post("/oauth/token", (req) => oauth2.tokenHandler(req));
|
|
3941
|
+
}
|
|
3942
|
+
if (hasDb && options.oauthLogin) {
|
|
3912
3943
|
registerOAuthLoginRoutes(r, {
|
|
3913
|
-
sql:
|
|
3944
|
+
sql: _pg.sql,
|
|
3914
3945
|
jwtSecret: secret,
|
|
3915
3946
|
expiresIn,
|
|
3916
3947
|
usersTable: table,
|
|
@@ -3925,10 +3956,15 @@ function user(options) {
|
|
|
3925
3956
|
const mod = r;
|
|
3926
3957
|
mod.middleware = middleware;
|
|
3927
3958
|
mod.middlewareOptional = middlewareOptional;
|
|
3928
|
-
mod.migrate = migrate
|
|
3929
|
-
|
|
3930
|
-
mod.
|
|
3931
|
-
|
|
3959
|
+
mod.migrate = hasDb ? migrate : async () => {
|
|
3960
|
+
};
|
|
3961
|
+
mod.register = hasDb ? register : async () => {
|
|
3962
|
+
throw new Error("user(): pg required for register");
|
|
3963
|
+
};
|
|
3964
|
+
mod.login = hasDb ? login : async () => {
|
|
3965
|
+
throw new Error("user(): pg required for login");
|
|
3966
|
+
};
|
|
3967
|
+
mod.verify = hasDb ? verify : async () => null;
|
|
3932
3968
|
mod.registerClient = oauth2 ? (data) => oauth2.registerClient(data) : async () => {
|
|
3933
3969
|
throw new Error("OAuth2 server is not enabled");
|
|
3934
3970
|
};
|
|
@@ -3938,7 +3974,8 @@ function user(options) {
|
|
|
3938
3974
|
mod.revokeClient = oauth2 ? (clientId) => oauth2.revokeClient(clientId) : async () => {
|
|
3939
3975
|
throw new Error("OAuth2 server is not enabled");
|
|
3940
3976
|
};
|
|
3941
|
-
mod.close = () => base.close()
|
|
3977
|
+
mod.close = hasDb ? () => base.close() : async () => {
|
|
3978
|
+
};
|
|
3942
3979
|
return mod;
|
|
3943
3980
|
}
|
|
3944
3981
|
|
|
@@ -3960,7 +3997,7 @@ function redis(opts) {
|
|
|
3960
3997
|
|
|
3961
3998
|
// queue/index.ts
|
|
3962
3999
|
import { Redis as IORedis2 } from "ioredis";
|
|
3963
|
-
import
|
|
4000
|
+
import crypto6 from "node:crypto";
|
|
3964
4001
|
|
|
3965
4002
|
// cron-utils.ts
|
|
3966
4003
|
function parseField(field, min, max) {
|
|
@@ -4046,7 +4083,7 @@ function escapeIdent3(s) {
|
|
|
4046
4083
|
function attachCron(q, handlers) {
|
|
4047
4084
|
;
|
|
4048
4085
|
q.cron = function(pattern, handler) {
|
|
4049
|
-
const id2 = "__cron_" + pattern.replace(/[^a-zA-Z0-9]/g, "_") + "_" +
|
|
4086
|
+
const id2 = "__cron_" + pattern.replace(/[^a-zA-Z0-9]/g, "_") + "_" + crypto6.randomUUID().slice(0, 8);
|
|
4050
4087
|
q.process(id2, async () => {
|
|
4051
4088
|
await handler();
|
|
4052
4089
|
});
|
|
@@ -4085,7 +4122,7 @@ function createMemoryQueue(opts) {
|
|
|
4085
4122
|
}
|
|
4086
4123
|
if (job.schedule) {
|
|
4087
4124
|
try {
|
|
4088
|
-
insertJob({ ...job, id:
|
|
4125
|
+
insertJob({ ...job, id: crypto6.randomUUID(), runAt: cronNext(job.schedule), createdAt: Date.now() });
|
|
4089
4126
|
} catch (e) {
|
|
4090
4127
|
console.error("[queue] cron re-queue failed:", e.message);
|
|
4091
4128
|
}
|
|
@@ -4107,7 +4144,7 @@ function createMemoryQueue(opts) {
|
|
|
4107
4144
|
});
|
|
4108
4145
|
const q = mw;
|
|
4109
4146
|
mw.add = function add(type, payload, opts2) {
|
|
4110
|
-
const id2 =
|
|
4147
|
+
const id2 = crypto6.randomUUID();
|
|
4111
4148
|
let runAt;
|
|
4112
4149
|
if (opts2?.schedule) {
|
|
4113
4150
|
try {
|
|
@@ -4209,7 +4246,7 @@ function createPgQueue(opts) {
|
|
|
4209
4246
|
if (job.schedule) {
|
|
4210
4247
|
try {
|
|
4211
4248
|
const nextRun = cronNext(job.schedule);
|
|
4212
|
-
await sql2.unsafe(`INSERT INTO ${escapeIdent3(table)} (id, type, payload, run_at, schedule) VALUES ($1, $2, $3::jsonb, $4, $5)`, [
|
|
4249
|
+
await sql2.unsafe(`INSERT INTO ${escapeIdent3(table)} (id, type, payload, run_at, schedule) VALUES ($1, $2, $3::jsonb, $4, $5)`, [crypto6.randomUUID(), job.type, JSON.stringify(job.payload), new Date(nextRun).toISOString(), job.schedule]);
|
|
4213
4250
|
} catch (e) {
|
|
4214
4251
|
console.error("[queue] cron re-queue failed:", e.message);
|
|
4215
4252
|
}
|
|
@@ -4243,7 +4280,7 @@ function createPgQueue(opts) {
|
|
|
4243
4280
|
const q = mw;
|
|
4244
4281
|
mw.add = function add(type, payload, opts2) {
|
|
4245
4282
|
return (async () => {
|
|
4246
|
-
const id2 =
|
|
4283
|
+
const id2 = crypto6.randomUUID();
|
|
4247
4284
|
let runAt;
|
|
4248
4285
|
if (opts2?.schedule) {
|
|
4249
4286
|
try {
|
|
@@ -4330,7 +4367,7 @@ function createRedisQueue(opts) {
|
|
|
4330
4367
|
if (job.schedule) {
|
|
4331
4368
|
try {
|
|
4332
4369
|
const nextRun = cronNext(job.schedule);
|
|
4333
|
-
await redis2.zadd(jobKey, nextRun, JSON.stringify({ ...job, id:
|
|
4370
|
+
await redis2.zadd(jobKey, nextRun, JSON.stringify({ ...job, id: crypto6.randomUUID(), runAt: nextRun, createdAt: Date.now() }));
|
|
4334
4371
|
} catch (e) {
|
|
4335
4372
|
console.error("[queue] cron re-queue failed:", e.message);
|
|
4336
4373
|
}
|
|
@@ -4369,7 +4406,7 @@ function createRedisQueue(opts) {
|
|
|
4369
4406
|
});
|
|
4370
4407
|
const q = mw;
|
|
4371
4408
|
mw.add = function add(type, payload, opts2) {
|
|
4372
|
-
const id2 =
|
|
4409
|
+
const id2 = crypto6.randomUUID();
|
|
4373
4410
|
let runAt;
|
|
4374
4411
|
if (opts2?.schedule) {
|
|
4375
4412
|
runAt = cronNext(opts2.schedule);
|
|
@@ -6294,7 +6331,7 @@ function createGateway(config, getPort) {
|
|
|
6294
6331
|
}
|
|
6295
6332
|
|
|
6296
6333
|
// deploy/manager.ts
|
|
6297
|
-
import
|
|
6334
|
+
import crypto7 from "node:crypto";
|
|
6298
6335
|
|
|
6299
6336
|
// deploy/process.ts
|
|
6300
6337
|
import { fork } from "node:child_process";
|
|
@@ -6347,27 +6384,27 @@ async function healthCheck(port, path2 = "/") {
|
|
|
6347
6384
|
// deploy/manager.ts
|
|
6348
6385
|
function createManager(config, apps, manager) {
|
|
6349
6386
|
const router = new Router();
|
|
6350
|
-
const
|
|
6387
|
+
const auth = (req, ctx, next) => {
|
|
6351
6388
|
if (!config.deployToken) return next(req, ctx);
|
|
6352
6389
|
const header = req.headers.get("authorization") ?? "";
|
|
6353
6390
|
const token = header.replace("Bearer ", "");
|
|
6354
6391
|
const tokenBuf = Buffer.from(token);
|
|
6355
6392
|
const secretBuf = Buffer.from(config.deployToken);
|
|
6356
|
-
if (tokenBuf.length !== secretBuf.length || !
|
|
6393
|
+
if (tokenBuf.length !== secretBuf.length || !crypto7.timingSafeEqual(tokenBuf, secretBuf)) {
|
|
6357
6394
|
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
6358
6395
|
}
|
|
6359
6396
|
return next(req, ctx);
|
|
6360
6397
|
};
|
|
6361
|
-
router.get("/apps",
|
|
6398
|
+
router.get("/apps", auth, () => {
|
|
6362
6399
|
const list = Array.from(apps.values()).map((a) => a.status);
|
|
6363
6400
|
return Response.json(list);
|
|
6364
6401
|
});
|
|
6365
|
-
router.get("/apps/:name",
|
|
6402
|
+
router.get("/apps/:name", auth, (req, ctx) => {
|
|
6366
6403
|
const app = apps.get(ctx.params.name);
|
|
6367
6404
|
if (!app) return new Response("Not Found", { status: 404 });
|
|
6368
6405
|
return Response.json(app.status);
|
|
6369
6406
|
});
|
|
6370
|
-
router.post("/apps/:name/deploy",
|
|
6407
|
+
router.post("/apps/:name/deploy", auth, async (req, ctx) => {
|
|
6371
6408
|
const app = apps.get(ctx.params.name);
|
|
6372
6409
|
if (!app) return new Response("Not Found", { status: 404 });
|
|
6373
6410
|
try {
|
|
@@ -6378,7 +6415,7 @@ function createManager(config, apps, manager) {
|
|
|
6378
6415
|
return Response.json({ error: msg }, { status: 500 });
|
|
6379
6416
|
}
|
|
6380
6417
|
});
|
|
6381
|
-
router.post("/apps/:name/restart",
|
|
6418
|
+
router.post("/apps/:name/restart", auth, async (req, ctx) => {
|
|
6382
6419
|
const app = apps.get(ctx.params.name);
|
|
6383
6420
|
if (!app) return new Response("Not Found", { status: 404 });
|
|
6384
6421
|
try {
|
|
@@ -6389,7 +6426,7 @@ function createManager(config, apps, manager) {
|
|
|
6389
6426
|
return Response.json({ error: msg }, { status: 500 });
|
|
6390
6427
|
}
|
|
6391
6428
|
});
|
|
6392
|
-
router.post("/apps/:name/stop",
|
|
6429
|
+
router.post("/apps/:name/stop", auth, async (req, ctx) => {
|
|
6393
6430
|
const app = apps.get(ctx.params.name);
|
|
6394
6431
|
if (!app) return new Response("Not Found", { status: 404 });
|
|
6395
6432
|
if (app.process) {
|
|
@@ -6399,7 +6436,7 @@ function createManager(config, apps, manager) {
|
|
|
6399
6436
|
app.status = { ...app.status, status: "stopped", pid: void 0 };
|
|
6400
6437
|
return Response.json({ success: true });
|
|
6401
6438
|
});
|
|
6402
|
-
router.post("/apps/:name/start",
|
|
6439
|
+
router.post("/apps/:name/start", auth, async (req, ctx) => {
|
|
6403
6440
|
const app = apps.get(ctx.params.name);
|
|
6404
6441
|
if (!app) return new Response("Not Found", { status: 404 });
|
|
6405
6442
|
try {
|
|
@@ -6410,7 +6447,7 @@ function createManager(config, apps, manager) {
|
|
|
6410
6447
|
return Response.json({ error: msg }, { status: 500 });
|
|
6411
6448
|
}
|
|
6412
6449
|
});
|
|
6413
|
-
router.get("/apps/:name/logs",
|
|
6450
|
+
router.get("/apps/:name/logs", auth, (req, ctx) => {
|
|
6414
6451
|
const app = apps.get(ctx.params.name);
|
|
6415
6452
|
if (!app) return new Response("Not Found", { status: 404 });
|
|
6416
6453
|
let index = app.logs.length;
|
|
@@ -6462,9 +6499,9 @@ function defineConfig(config) {
|
|
|
6462
6499
|
async function deploy(config) {
|
|
6463
6500
|
const apps = /* @__PURE__ */ new Map();
|
|
6464
6501
|
let httpServer;
|
|
6465
|
-
async function forkAndCheck(name, cwd, entry, port,
|
|
6502
|
+
async function forkAndCheck(name, cwd, entry, port, env2, onLog, healthEndpoint) {
|
|
6466
6503
|
try {
|
|
6467
|
-
const mp = forkApp({ cwd, entry, port, env, onLog });
|
|
6504
|
+
const mp = forkApp({ cwd, entry, port, env: env2, onLog });
|
|
6468
6505
|
onLog(`[deploy] forked ${name} (pid ${mp.child.pid}) on port ${mp.port}`);
|
|
6469
6506
|
const healthy = await healthCheck(port, healthEndpoint ?? "/");
|
|
6470
6507
|
if (healthy) onLog(`[deploy] health check passed`);
|
|
@@ -6903,7 +6940,7 @@ async function compileHotComponent(path2) {
|
|
|
6903
6940
|
// stream.ts
|
|
6904
6941
|
import { TextDecoder as TextDecoder2, TextEncoder as TextEncoder2 } from "node:util";
|
|
6905
6942
|
var _publicEnv = null;
|
|
6906
|
-
function
|
|
6943
|
+
function getPublicEnv2() {
|
|
6907
6944
|
if (_publicEnv) return _publicEnv;
|
|
6908
6945
|
_publicEnv = {};
|
|
6909
6946
|
for (const key of Object.keys(process.env)) {
|
|
@@ -6954,7 +6991,7 @@ function buildHeadPayload(opts) {
|
|
|
6954
6991
|
}
|
|
6955
6992
|
ctxData.user = safeUser;
|
|
6956
6993
|
}
|
|
6957
|
-
const publicEnv =
|
|
6994
|
+
const publicEnv = getPublicEnv2();
|
|
6958
6995
|
if (Object.keys(publicEnv).length > 0) {
|
|
6959
6996
|
ctxData.env = publicEnv;
|
|
6960
6997
|
}
|
|
@@ -7662,7 +7699,7 @@ function ssr(opts) {
|
|
|
7662
7699
|
}
|
|
7663
7700
|
|
|
7664
7701
|
// opencode/session.ts
|
|
7665
|
-
import { randomUUID as
|
|
7702
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
7666
7703
|
import { join as join6 } from "node:path";
|
|
7667
7704
|
import { mkdir as mkdir2 } from "node:fs/promises";
|
|
7668
7705
|
var sessions = pgTable("_opencode_sessions", {
|
|
@@ -7688,7 +7725,7 @@ var messages = pgTable("_opencode_messages", {
|
|
|
7688
7725
|
created_at: timestamptz("created_at")
|
|
7689
7726
|
});
|
|
7690
7727
|
async function createSession(sql2, opts, cwd, mountPath) {
|
|
7691
|
-
const id2 =
|
|
7728
|
+
const id2 = randomUUID2();
|
|
7692
7729
|
const ws = computeSessionWorkspace(cwd, mountPath, id2);
|
|
7693
7730
|
await mkdir2(ws, { recursive: true });
|
|
7694
7731
|
const [row] = await sql2`
|
|
@@ -8860,30 +8897,37 @@ function makeSetTheme(cookie, location) {
|
|
|
8860
8897
|
}
|
|
8861
8898
|
function theme(options) {
|
|
8862
8899
|
const opts = { default: "system", cookie: "theme", ...options };
|
|
8863
|
-
|
|
8864
|
-
const url = new URL(req.url);
|
|
8865
|
-
const match = url.pathname.match(/^\/__theme\/([\w-]+)$/);
|
|
8866
|
-
if (match && req.method === "GET") {
|
|
8867
|
-
const value = match[1];
|
|
8868
|
-
const cookie = `${opts.cookie}=${encodeURIComponent(value)}; Path=/; SameSite=Lax`;
|
|
8869
|
-
const accept = req.headers.get("accept") ?? "";
|
|
8870
|
-
if (accept.includes("application/json")) {
|
|
8871
|
-
return Response.json({ ok: true, theme: value }, { headers: { "Set-Cookie": cookie } });
|
|
8872
|
-
}
|
|
8873
|
-
const referer = req.headers.get("referer") || "/";
|
|
8874
|
-
return new Response(null, { status: 302, headers: { Location: referer, "Set-Cookie": cookie } });
|
|
8875
|
-
}
|
|
8900
|
+
const mw = async (req, ctx, next) => {
|
|
8876
8901
|
let themeValue = opts.default;
|
|
8877
8902
|
if (opts.cookie) {
|
|
8878
8903
|
const fromCookie = getCookies(req)[opts.cookie];
|
|
8879
8904
|
if (fromCookie) themeValue = fromCookie;
|
|
8880
8905
|
}
|
|
8906
|
+
;
|
|
8881
8907
|
ctx.theme = {
|
|
8882
8908
|
value: themeValue,
|
|
8883
8909
|
set: makeSetTheme(opts.cookie, req.headers.get("referer") || "/")
|
|
8884
8910
|
};
|
|
8885
8911
|
return next(req, ctx);
|
|
8886
8912
|
};
|
|
8913
|
+
class ThemeRouter extends Router {
|
|
8914
|
+
middleware() {
|
|
8915
|
+
return mw;
|
|
8916
|
+
}
|
|
8917
|
+
}
|
|
8918
|
+
const router = new ThemeRouter();
|
|
8919
|
+
router.get("/__theme/:value", (req) => {
|
|
8920
|
+
const url = new URL(req.url);
|
|
8921
|
+
const value = url.pathname.split("/__theme/")[1] ?? "";
|
|
8922
|
+
const cookie = `${opts.cookie}=${encodeURIComponent(value)}; Path=/; SameSite=Lax`;
|
|
8923
|
+
const accept = req.headers.get("accept") ?? "";
|
|
8924
|
+
if (accept.includes("application/json")) {
|
|
8925
|
+
return Response.json({ ok: true, theme: value }, { headers: { "Set-Cookie": cookie } });
|
|
8926
|
+
}
|
|
8927
|
+
const referer = req.headers.get("referer") || "/";
|
|
8928
|
+
return new Response(null, { status: 302, headers: { Location: referer, "Set-Cookie": cookie } });
|
|
8929
|
+
});
|
|
8930
|
+
return router;
|
|
8887
8931
|
}
|
|
8888
8932
|
|
|
8889
8933
|
// i18n.ts
|
|
@@ -8949,23 +8993,7 @@ function i18n(options) {
|
|
|
8949
8993
|
}
|
|
8950
8994
|
return opts.default;
|
|
8951
8995
|
}
|
|
8952
|
-
|
|
8953
|
-
const url = new URL(req.url);
|
|
8954
|
-
const match = url.pathname.match(/^\/__lang\/([\w-]+)$/);
|
|
8955
|
-
if (match && req.method === "GET") {
|
|
8956
|
-
const value = match[1];
|
|
8957
|
-
const cookie = `${opts.cookie}=${encodeURIComponent(value)}; Path=/; SameSite=Lax`;
|
|
8958
|
-
const messages2 = await loadMessages(value);
|
|
8959
|
-
const accept = req.headers.get("accept") ?? "";
|
|
8960
|
-
if (accept.includes("application/json")) {
|
|
8961
|
-
return Response.json(
|
|
8962
|
-
{ ok: true, locale: value, messages: Object.keys(messages2).length > 0 ? messages2 : void 0 },
|
|
8963
|
-
{ headers: { "Set-Cookie": cookie } }
|
|
8964
|
-
);
|
|
8965
|
-
}
|
|
8966
|
-
const referer = req.headers.get("referer") || "/";
|
|
8967
|
-
return new Response(null, { status: 302, headers: { Location: referer, "Set-Cookie": cookie } });
|
|
8968
|
-
}
|
|
8996
|
+
const mw = async (req, ctx, next) => {
|
|
8969
8997
|
const locale = detectLocale(req);
|
|
8970
8998
|
const msgs = await loadMessages(locale);
|
|
8971
8999
|
ctx.i18n = {
|
|
@@ -8980,6 +9008,28 @@ function i18n(options) {
|
|
|
8980
9008
|
};
|
|
8981
9009
|
return next(req, ctx);
|
|
8982
9010
|
};
|
|
9011
|
+
class I18nRouter extends Router {
|
|
9012
|
+
middleware() {
|
|
9013
|
+
return mw;
|
|
9014
|
+
}
|
|
9015
|
+
}
|
|
9016
|
+
const router = new I18nRouter();
|
|
9017
|
+
router.get("/__lang/:locale", async (req) => {
|
|
9018
|
+
const url = new URL(req.url);
|
|
9019
|
+
const value = url.pathname.split("/__lang/")[1] ?? "";
|
|
9020
|
+
const cookie = `${opts.cookie}=${encodeURIComponent(value)}; Path=/; SameSite=Lax`;
|
|
9021
|
+
const messages2 = await loadMessages(value);
|
|
9022
|
+
const accept = req.headers.get("accept") ?? "";
|
|
9023
|
+
if (accept.includes("application/json")) {
|
|
9024
|
+
return Response.json(
|
|
9025
|
+
{ ok: true, locale: value, messages: Object.keys(messages2).length > 0 ? messages2 : void 0 },
|
|
9026
|
+
{ headers: { "Set-Cookie": cookie } }
|
|
9027
|
+
);
|
|
9028
|
+
}
|
|
9029
|
+
const referer = req.headers.get("referer") || "/";
|
|
9030
|
+
return new Response(null, { status: 302, headers: { Location: referer, "Set-Cookie": cookie } });
|
|
9031
|
+
});
|
|
9032
|
+
return router;
|
|
8983
9033
|
}
|
|
8984
9034
|
|
|
8985
9035
|
// flash.ts
|
|
@@ -9209,13 +9259,13 @@ function csrf(options) {
|
|
|
9209
9259
|
let token = getCookies(req)[cookieName];
|
|
9210
9260
|
if (!token) {
|
|
9211
9261
|
token = crypto.randomUUID();
|
|
9212
|
-
ctx.
|
|
9262
|
+
ctx.csrf = { token };
|
|
9213
9263
|
} else {
|
|
9214
9264
|
;
|
|
9215
|
-
ctx.
|
|
9265
|
+
ctx.csrf = { token };
|
|
9216
9266
|
}
|
|
9217
9267
|
const res = await next(req, ctx);
|
|
9218
|
-
const tokenToSet = ctx.
|
|
9268
|
+
const tokenToSet = ctx.csrf?.token;
|
|
9219
9269
|
if (tokenToSet && !getCookies(req)[cookieName]) {
|
|
9220
9270
|
return setCookie(res, cookieName, tokenToSet, {
|
|
9221
9271
|
httpOnly: true,
|
|
@@ -9400,7 +9450,7 @@ function logdb(options) {
|
|
|
9400
9450
|
}
|
|
9401
9451
|
|
|
9402
9452
|
// iii/client.ts
|
|
9403
|
-
import
|
|
9453
|
+
import crypto8 from "node:crypto";
|
|
9404
9454
|
|
|
9405
9455
|
// iii/stream.ts
|
|
9406
9456
|
function notify(channels, stream, group, item, event, data) {
|
|
@@ -9929,7 +9979,7 @@ function iii(opts = {}) {
|
|
|
9929
9979
|
registerBuiltin("stream::send", (p) => stream.send(p.stream_name, p.group_id, p.type, p.data, p.id));
|
|
9930
9980
|
registerBuiltin("stream::update", (p) => stream.update(p.stream_name, p.group_id, p.item_id, p.ops));
|
|
9931
9981
|
function addLocalWorker(worker) {
|
|
9932
|
-
const workerId =
|
|
9982
|
+
const workerId = crypto8.randomUUID();
|
|
9933
9983
|
const reg = {
|
|
9934
9984
|
id: workerId,
|
|
9935
9985
|
name: worker.name,
|
|
@@ -9944,7 +9994,7 @@ function iii(opts = {}) {
|
|
|
9944
9994
|
const triggerIds = [];
|
|
9945
9995
|
for (const t of worker.getTriggers()) {
|
|
9946
9996
|
if (t.input.function_id === fn.id) {
|
|
9947
|
-
const tid =
|
|
9997
|
+
const tid = crypto8.randomUUID();
|
|
9948
9998
|
triggers.set(tid, {
|
|
9949
9999
|
id: tid,
|
|
9950
10000
|
type: t.input.type,
|
|
@@ -9973,7 +10023,7 @@ function iii(opts = {}) {
|
|
|
9973
10023
|
if (!worker) return;
|
|
9974
10024
|
const handler = async (payload) => {
|
|
9975
10025
|
if (!worker.ws) throw new Error(`Worker "${worker.name}" disconnected`);
|
|
9976
|
-
const invocationId =
|
|
10026
|
+
const invocationId = crypto8.randomUUID();
|
|
9977
10027
|
return new Promise((resolve14, reject) => {
|
|
9978
10028
|
const timer = setTimeout(() => {
|
|
9979
10029
|
pending.delete(invocationId);
|
|
@@ -10008,7 +10058,7 @@ function iii(opts = {}) {
|
|
|
10008
10058
|
let engineRef = null;
|
|
10009
10059
|
const wsHandler = createWsHandler({
|
|
10010
10060
|
registerRemoteWorker(ws, name) {
|
|
10011
|
-
const id2 =
|
|
10061
|
+
const id2 = crypto8.randomUUID();
|
|
10012
10062
|
workers.set(id2, { id: id2, name, ws, functions: [], triggers: [] });
|
|
10013
10063
|
return id2;
|
|
10014
10064
|
},
|
|
@@ -10019,7 +10069,7 @@ function iii(opts = {}) {
|
|
|
10019
10069
|
addRemoteFunction(workerId, id2);
|
|
10020
10070
|
},
|
|
10021
10071
|
registerRemoteTrigger(workerId, input) {
|
|
10022
|
-
const tid =
|
|
10072
|
+
const tid = crypto8.randomUUID();
|
|
10023
10073
|
const reg = { id: tid, ...input, workerId };
|
|
10024
10074
|
triggers.set(tid, reg);
|
|
10025
10075
|
const worker = workers.get(workerId);
|
|
@@ -10155,6 +10205,7 @@ function iii(opts = {}) {
|
|
|
10155
10205
|
triggers.clear();
|
|
10156
10206
|
await stream.close();
|
|
10157
10207
|
};
|
|
10208
|
+
mod.close = mod.shutdown;
|
|
10158
10209
|
return mod;
|
|
10159
10210
|
}
|
|
10160
10211
|
|
|
@@ -10369,7 +10420,7 @@ function registerWorker(url) {
|
|
|
10369
10420
|
}
|
|
10370
10421
|
|
|
10371
10422
|
// session.ts
|
|
10372
|
-
import
|
|
10423
|
+
import crypto9 from "node:crypto";
|
|
10373
10424
|
var kSaved = /* @__PURE__ */ Symbol("session.saved");
|
|
10374
10425
|
var kDestroyed = /* @__PURE__ */ Symbol("session.destroyed");
|
|
10375
10426
|
var kId = /* @__PURE__ */ Symbol("session.id");
|
|
@@ -10447,7 +10498,7 @@ var RedisStore = class {
|
|
|
10447
10498
|
};
|
|
10448
10499
|
var COOKIE_SEPARATOR = ".";
|
|
10449
10500
|
function signSessionId(sid, secret) {
|
|
10450
|
-
const hmac =
|
|
10501
|
+
const hmac = crypto9.createHmac("sha256", secret).update(sid).digest("base64url").slice(0, 16);
|
|
10451
10502
|
return sid + COOKIE_SEPARATOR + hmac;
|
|
10452
10503
|
}
|
|
10453
10504
|
function unsignSessionId(value, secret) {
|
|
@@ -10455,10 +10506,10 @@ function unsignSessionId(value, secret) {
|
|
|
10455
10506
|
if (dot === -1) return null;
|
|
10456
10507
|
const sid = value.slice(0, dot);
|
|
10457
10508
|
const sig = value.slice(dot + 1);
|
|
10458
|
-
const expected =
|
|
10509
|
+
const expected = crypto9.createHmac("sha256", secret).update(sid).digest("base64url").slice(0, 16);
|
|
10459
10510
|
if (sig.length !== expected.length) return null;
|
|
10460
10511
|
try {
|
|
10461
|
-
return
|
|
10512
|
+
return crypto9.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)) ? sid : null;
|
|
10462
10513
|
} catch {
|
|
10463
10514
|
return null;
|
|
10464
10515
|
}
|
|
@@ -10546,11 +10597,11 @@ function session(options) {
|
|
|
10546
10597
|
}
|
|
10547
10598
|
} else {
|
|
10548
10599
|
loadedSid = null;
|
|
10549
|
-
session2 = createSessionObject({},
|
|
10600
|
+
session2 = createSessionObject({}, crypto9.randomUUID(), store2, ttl, Date.now());
|
|
10550
10601
|
}
|
|
10551
10602
|
} else {
|
|
10552
10603
|
loadedSid = null;
|
|
10553
|
-
session2 = createSessionObject({},
|
|
10604
|
+
session2 = createSessionObject({}, crypto9.randomUUID(), store2, ttl, Date.now());
|
|
10554
10605
|
}
|
|
10555
10606
|
const snapshot = isSessionActive(session2) ? JSON.stringify(session2) : null;
|
|
10556
10607
|
ctx.session = session2;
|
|
@@ -10563,7 +10614,7 @@ function session(options) {
|
|
|
10563
10614
|
return deleteCookie(res, cookieName, cookieOpts);
|
|
10564
10615
|
}
|
|
10565
10616
|
if (needsRotation && loadedSid) {
|
|
10566
|
-
const newId =
|
|
10617
|
+
const newId = crypto9.randomUUID();
|
|
10567
10618
|
const data = JSON.parse(JSON.stringify(currentSession));
|
|
10568
10619
|
data[kCreatedAt] = Date.now();
|
|
10569
10620
|
await store2.set(newId, data, ttl);
|
|
@@ -10604,7 +10655,7 @@ function session(options) {
|
|
|
10604
10655
|
}
|
|
10605
10656
|
|
|
10606
10657
|
// cache.ts
|
|
10607
|
-
import
|
|
10658
|
+
import crypto10 from "node:crypto";
|
|
10608
10659
|
var BINARY_PREFIXES = [
|
|
10609
10660
|
"image/",
|
|
10610
10661
|
"audio/",
|
|
@@ -10624,7 +10675,7 @@ function isCacheableStatus(status, allowed) {
|
|
|
10624
10675
|
return allowed.includes(status);
|
|
10625
10676
|
}
|
|
10626
10677
|
function defaultCacheKey(req) {
|
|
10627
|
-
const hash =
|
|
10678
|
+
const hash = crypto10.createHash("sha256");
|
|
10628
10679
|
hash.update(req.method);
|
|
10629
10680
|
hash.update(req.url);
|
|
10630
10681
|
return hash.digest("hex");
|
|
@@ -10839,10 +10890,10 @@ function cache2(options) {
|
|
|
10839
10890
|
}
|
|
10840
10891
|
|
|
10841
10892
|
// webhook.ts
|
|
10842
|
-
import
|
|
10893
|
+
import crypto11 from "node:crypto";
|
|
10843
10894
|
function timingSafeEqual2(a, b) {
|
|
10844
10895
|
try {
|
|
10845
|
-
return
|
|
10896
|
+
return crypto11.timingSafeEqual(Buffer.from(a), Buffer.from(b));
|
|
10846
10897
|
} catch {
|
|
10847
10898
|
return false;
|
|
10848
10899
|
}
|
|
@@ -10860,7 +10911,7 @@ function createStripeVerifier(config) {
|
|
|
10860
10911
|
const signature = parts["v1"];
|
|
10861
10912
|
if (!timestamp || !signature) return { valid: false, provider: "stripe", event: "", id: void 0 };
|
|
10862
10913
|
const signed = `${timestamp}.${body}`;
|
|
10863
|
-
const expected =
|
|
10914
|
+
const expected = crypto11.createHmac("sha256", config.secret).update(signed).digest("hex");
|
|
10864
10915
|
const valid = timingSafeEqual2(signature, expected);
|
|
10865
10916
|
let event = "";
|
|
10866
10917
|
let id2;
|
|
@@ -10877,7 +10928,7 @@ function createGitHubVerifier(config) {
|
|
|
10877
10928
|
return (body, headers) => {
|
|
10878
10929
|
const sig = headers["x-hub-signature-256"];
|
|
10879
10930
|
if (!sig) return { valid: false, provider: "github", event: "", id: void 0 };
|
|
10880
|
-
const expected = `sha256=${
|
|
10931
|
+
const expected = `sha256=${crypto11.createHmac("sha256", config.secret).update(body).digest("hex")}`;
|
|
10881
10932
|
const valid = timingSafeEqual2(sig, expected);
|
|
10882
10933
|
let event = headers["x-github-event"] ?? "";
|
|
10883
10934
|
let id2;
|
|
@@ -10900,7 +10951,7 @@ function createSlackVerifier(config) {
|
|
|
10900
10951
|
return { valid: false, provider: "slack", event: "", id: void 0 };
|
|
10901
10952
|
}
|
|
10902
10953
|
const sigBase = `v0:${timestamp}:${body}`;
|
|
10903
|
-
const expected = `v0=${
|
|
10954
|
+
const expected = `v0=${crypto11.createHmac("sha256", config.secret).update(sigBase).digest("hex")}`;
|
|
10904
10955
|
const valid = timingSafeEqual2(signature, expected);
|
|
10905
10956
|
let event = "";
|
|
10906
10957
|
let id2;
|
|
@@ -11534,7 +11585,6 @@ export {
|
|
|
11534
11585
|
aiProvider,
|
|
11535
11586
|
aiStream,
|
|
11536
11587
|
analytics,
|
|
11537
|
-
auth,
|
|
11538
11588
|
cache2 as cache,
|
|
11539
11589
|
compress,
|
|
11540
11590
|
cors,
|
|
@@ -11552,6 +11602,7 @@ export {
|
|
|
11552
11602
|
deploy,
|
|
11553
11603
|
embed,
|
|
11554
11604
|
embedMany,
|
|
11605
|
+
env,
|
|
11555
11606
|
flash,
|
|
11556
11607
|
formatSSE,
|
|
11557
11608
|
formatSSEData,
|
|
@@ -11559,6 +11610,7 @@ export {
|
|
|
11559
11610
|
generateObject,
|
|
11560
11611
|
generateText2 as generateText,
|
|
11561
11612
|
getCookies,
|
|
11613
|
+
getPublicEnv,
|
|
11562
11614
|
graphql,
|
|
11563
11615
|
health,
|
|
11564
11616
|
helmet,
|
|
@@ -11600,6 +11652,7 @@ export {
|
|
|
11600
11652
|
testApp,
|
|
11601
11653
|
theme,
|
|
11602
11654
|
tool2 as tool,
|
|
11655
|
+
trace,
|
|
11603
11656
|
traceElapsed,
|
|
11604
11657
|
upload,
|
|
11605
11658
|
user,
|