asaihost-nodejs 0.0.9 → 0.0.11

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.
Files changed (44) hide show
  1. package/dist/InterventionLog-WNVDQFAH.cjs.map +1 -1
  2. package/dist/LogStore-AP4T2CFD.cjs.map +1 -1
  3. package/dist/asaihost-nodejs.cjs +708 -513
  4. package/dist/asaihost-nodejs.cjs.map +1 -1
  5. package/dist/asaihost-nodejs.es.js +319 -124
  6. package/dist/asaihost-nodejs.es.js.map +1 -1
  7. package/dist/chunk-2BCIIKRG.cjs.map +1 -1
  8. package/dist/{chunk-OTL2B4WS.es.js → chunk-3E4Y5ATG.es.js} +2 -2
  9. package/dist/{chunk-ZK4O4Z2O.cjs → chunk-5EWEKXQI.cjs} +20 -20
  10. package/dist/chunk-5EWEKXQI.cjs.map +1 -0
  11. package/dist/chunk-ARHPHWT5.cjs.map +1 -1
  12. package/dist/{chunk-GXOFLFFW.cjs → chunk-FRPN33BY.cjs} +3 -2
  13. package/dist/chunk-FRPN33BY.cjs.map +1 -0
  14. package/dist/{chunk-WXW5DYNU.cjs → chunk-H2QP5J7X.cjs} +9 -9
  15. package/dist/chunk-H2QP5J7X.cjs.map +1 -0
  16. package/dist/chunk-HR5JKN7Y.cjs.map +1 -1
  17. package/dist/{chunk-BOIRAQXE.es.js → chunk-M3NURHGU.es.js} +2 -1
  18. package/dist/chunk-O6LVLSNB.cjs.map +1 -1
  19. package/dist/chunk-OCEBEEP5.cjs.map +1 -1
  20. package/dist/chunk-SMVZ3ZDJ.cjs.map +1 -1
  21. package/dist/chunk-U2YJH2RW.cjs.map +1 -1
  22. package/dist/{chunk-5WJQD4RZ.es.js → chunk-UKZ3DP37.es.js} +3 -3
  23. package/dist/intervention-hooks-4NAAAZK3.cjs.map +1 -1
  24. package/dist/log-alert-LAJ6VSAG.cjs.map +1 -1
  25. package/dist/log-formats-TUXGMCCU.cjs.map +1 -1
  26. package/dist/{password-4P7IZUDQ.es.js → password-2ZBHQQBF.es.js} +3 -3
  27. package/dist/password-T762KJOI.cjs +20 -0
  28. package/dist/password-T762KJOI.cjs.map +1 -0
  29. package/dist/user-store-6QNAL4CK.cjs +12 -0
  30. package/dist/user-store-6QNAL4CK.cjs.map +1 -0
  31. package/dist/{user-store-FXD2N55G.es.js → user-store-ZI67TXSI.es.js} +3 -3
  32. package/package.json +1 -1
  33. package/dist/chunk-GXOFLFFW.cjs.map +0 -1
  34. package/dist/chunk-WXW5DYNU.cjs.map +0 -1
  35. package/dist/chunk-ZK4O4Z2O.cjs.map +0 -1
  36. package/dist/password-MDYRL6ZD.cjs +0 -20
  37. package/dist/password-MDYRL6ZD.cjs.map +0 -1
  38. package/dist/user-store-55HOLHEM.cjs +0 -12
  39. package/dist/user-store-55HOLHEM.cjs.map +0 -1
  40. /package/dist/{chunk-OTL2B4WS.es.js.map → chunk-3E4Y5ATG.es.js.map} +0 -0
  41. /package/dist/{chunk-BOIRAQXE.es.js.map → chunk-M3NURHGU.es.js.map} +0 -0
  42. /package/dist/{chunk-5WJQD4RZ.es.js.map → chunk-UKZ3DP37.es.js.map} +0 -0
  43. /package/dist/{password-4P7IZUDQ.es.js.map → password-2ZBHQQBF.es.js.map} +0 -0
  44. /package/dist/{user-store-FXD2N55G.es.js.map → user-store-ZI67TXSI.es.js.map} +0 -0
@@ -50,7 +50,7 @@ import {
50
50
  secureCompare,
51
51
  validatePasswordChange,
52
52
  verifyPassword
53
- } from "./chunk-OTL2B4WS.es.js";
53
+ } from "./chunk-3E4Y5ATG.es.js";
54
54
  import {
55
55
  USER_LV,
56
56
  applySecretsToHostConfig,
@@ -62,20 +62,21 @@ import {
62
62
  resolveHttpsCertFromPaths,
63
63
  stripForbiddenSecretsFromHostConfig,
64
64
  validateAsaiSecrets
65
- } from "./chunk-5WJQD4RZ.es.js";
65
+ } from "./chunk-UKZ3DP37.es.js";
66
66
  import {
67
67
  getDefaultResetPassword,
68
68
  getPasswordPolicy,
69
69
  getPermissionThreshold,
70
70
  getPublicSecurityConfig,
71
71
  getUserLevelConfigSync,
72
+ getUserStoreConfig,
72
73
  isPasswordExpired,
73
74
  isValidUserLv,
74
75
  loadUserLevelConfig,
75
76
  resolveRoleForLevel,
76
77
  saveUserLevelConfig,
77
78
  validatePasswordWithPolicy
78
- } from "./chunk-BOIRAQXE.es.js";
79
+ } from "./chunk-M3NURHGU.es.js";
79
80
  import {
80
81
  getDataServerForAsDb
81
82
  } from "./chunk-ZQBHJRAO.es.js";
@@ -993,19 +994,24 @@ function terminateWs(wsclient) {
993
994
  }
994
995
  }
995
996
  __name(terminateWs, "terminateWs");
996
- async function clearStaleWsIfDead($asai, hostdir, us, existingWs) {
997
+ async function clearStaleWsIfDead($asai, hostdir, us, existingWs, timeoutMs) {
997
998
  if (!us || us === "asai") return true;
998
999
  const map = $asai.connectionsws?.[hostdir];
999
1000
  if (!existingWs) {
1000
1001
  if (map?.[us]) delete map[us];
1001
1002
  return true;
1002
1003
  }
1004
+ if (existingWs._asaiKicked) {
1005
+ terminateWs(existingWs);
1006
+ if (map?.[us] === existingWs) delete map[us];
1007
+ return true;
1008
+ }
1003
1009
  if (!isWsStillActive(existingWs)) {
1004
1010
  terminateWs(existingWs);
1005
1011
  if (map?.[us] === existingWs) delete map[us];
1006
1012
  return true;
1007
1013
  }
1008
- const alive = await probeWsAlive($asai, existingWs);
1014
+ const alive = await probeWsAlive($asai, existingWs, timeoutMs);
1009
1015
  if (alive) return false;
1010
1016
  terminateWs(existingWs);
1011
1017
  if (map?.[us] === existingWs) delete map[us];
@@ -1025,22 +1031,38 @@ function getWsUserinfo(req, $asai) {
1025
1031
  }
1026
1032
  __name(getWsUserinfo, "getWsUserinfo");
1027
1033
  function listOnlineWsUsers($asai, hostdir) {
1034
+ return listOnlineWsEntries($asai, hostdir).map((e) => e.us);
1035
+ }
1036
+ __name(listOnlineWsUsers, "listOnlineWsUsers");
1037
+ function listOnlineWsEntries($asai, hostdir) {
1028
1038
  const users = [];
1039
+ const seen = /* @__PURE__ */ new Set();
1029
1040
  const map = $asai.connectionsws || {};
1030
1041
  const dirs = hostdir ? [hostdir] : Object.keys(map);
1031
1042
  for (const dir of dirs) {
1032
1043
  for (const [us, ws2] of Object.entries(map[dir] || {})) {
1033
1044
  if (!us || us === "asai") continue;
1034
1045
  if (isWsStillActive(ws2)) {
1035
- users.push(us);
1046
+ if (seen.has(us)) continue;
1047
+ seen.add(us);
1048
+ const rawLv = ws2?.Userinfo?.[1];
1049
+ if (rawLv === "" || rawLv == null) {
1050
+ users.push({ us, lv: null });
1051
+ } else {
1052
+ const lvNum = Number(rawLv);
1053
+ users.push({
1054
+ us,
1055
+ lv: Number.isFinite(lvNum) ? lvNum : null
1056
+ });
1057
+ }
1036
1058
  } else if (map[dir]?.[us] === ws2) {
1037
1059
  delete map[dir][us];
1038
1060
  }
1039
1061
  }
1040
1062
  }
1041
- return [...new Set(users)];
1063
+ return users;
1042
1064
  }
1043
- __name(listOnlineWsUsers, "listOnlineWsUsers");
1065
+ __name(listOnlineWsEntries, "listOnlineWsEntries");
1044
1066
  function isUserOnlineOnWs($asai, us, hostdir) {
1045
1067
  if (!us || us === "asai") return false;
1046
1068
  const map = $asai.connectionsws || {};
@@ -1054,19 +1076,73 @@ function isUserOnlineOnWs($asai, us, hostdir) {
1054
1076
  return false;
1055
1077
  }
1056
1078
  __name(isUserOnlineOnWs, "isUserOnlineOnWs");
1057
- function kickUserWs($asai, us, hostdir) {
1079
+ async function kickUserWs($asai, us, hostdir) {
1058
1080
  if (!us || us === "asai") return;
1059
1081
  const map = $asai.connectionsws || {};
1060
1082
  const dirs = hostdir ? [hostdir] : Object.keys(map);
1083
+ const waits = [];
1061
1084
  for (const dir of dirs) {
1062
1085
  const ws2 = map[dir]?.[us];
1063
1086
  if (!ws2) continue;
1087
+ if (map[dir]?.[us] === ws2) delete map[dir][us];
1064
1088
  try {
1065
- ws2.close();
1089
+ ws2._asaiKicked = Date.now();
1066
1090
  } catch {
1067
1091
  }
1068
- if (map[dir]?.[us] === ws2) delete map[dir][us];
1092
+ waits.push(
1093
+ new Promise((resolve6) => {
1094
+ let done = false;
1095
+ const finish = /* @__PURE__ */ __name(() => {
1096
+ if (done) return;
1097
+ done = true;
1098
+ try {
1099
+ ws2.off?.("close", finish);
1100
+ } catch {
1101
+ }
1102
+ resolve6();
1103
+ }, "finish");
1104
+ try {
1105
+ ws2.once?.("close", finish);
1106
+ } catch {
1107
+ }
1108
+ const payload = JSON.stringify({
1109
+ ty: "publish/web",
1110
+ db: {
1111
+ exit: 1,
1112
+ type: "err",
1113
+ lang: "forcedout",
1114
+ langpm: `${us}`,
1115
+ con: `${us} forced out by another login`
1116
+ }
1117
+ });
1118
+ const hardClose = /* @__PURE__ */ __name(() => {
1119
+ try {
1120
+ if (typeof ws2.terminate === "function") ws2.terminate();
1121
+ else ws2.close();
1122
+ } catch {
1123
+ try {
1124
+ ws2.close();
1125
+ } catch {
1126
+ }
1127
+ }
1128
+ }, "hardClose");
1129
+ try {
1130
+ ws2.send(payload, () => {
1131
+ hardClose();
1132
+ finish();
1133
+ });
1134
+ } catch {
1135
+ hardClose();
1136
+ finish();
1137
+ }
1138
+ setTimeout(() => {
1139
+ hardClose();
1140
+ finish();
1141
+ }, 150);
1142
+ })
1143
+ );
1069
1144
  }
1145
+ if (waits.length) await Promise.all(waits);
1070
1146
  }
1071
1147
  __name(kickUserWs, "kickUserWs");
1072
1148
  async function removeSessionOnWsClose($asai, us, token) {
@@ -1105,27 +1181,31 @@ async function isOkWs($asai, hostdir, ws2, _req, hostcfg) {
1105
1181
  if (us !== "asai" && $asai.connectionsws[hostdir]?.[us]) {
1106
1182
  const existingWs = $asai.connectionsws[hostdir][us];
1107
1183
  if (existingWs?.Userinfo?.[2] === tk) {
1108
- if (isWsStillActive(existingWs)) {
1109
- rejectIncomingWs(ws2, {
1110
- lang: "pageworking",
1111
- langpm: `${us}`,
1112
- con: `${us} page already running`
1113
- });
1114
- return false;
1115
- }
1116
- try {
1117
- existingWs.close();
1118
- } catch {
1184
+ if (!isWsStillActive(existingWs)) {
1185
+ try {
1186
+ existingWs.close();
1187
+ } catch {
1188
+ }
1189
+ delete $asai.connectionsws[hostdir][us];
1190
+ return true;
1119
1191
  }
1120
- delete $asai.connectionsws[hostdir][us];
1121
- return true;
1192
+ const cleared2 = await clearStaleWsIfDead($asai, hostdir, us, existingWs, 800);
1193
+ if (cleared2) return true;
1194
+ rejectIncomingWs(ws2, {
1195
+ lang: "pageworking",
1196
+ langpm: `${us}`,
1197
+ con: `${us} page already running`,
1198
+ online: listOnlineWsEntries($asai, hostdir)
1199
+ });
1200
+ return false;
1122
1201
  }
1123
1202
  const cleared = await clearStaleWsIfDead($asai, hostdir, us, existingWs);
1124
1203
  if (!cleared) {
1125
1204
  rejectIncomingWs(ws2, {
1126
1205
  lang: "logged",
1127
1206
  langpm: `${us}`,
1128
- con: `${us} logged in!`
1207
+ con: `${us} logged in!`,
1208
+ online: listOnlineWsEntries($asai, hostdir)
1129
1209
  });
1130
1210
  return false;
1131
1211
  }
@@ -1137,19 +1217,22 @@ async function isOkWs($asai, hostdir, ws2, _req, hostcfg) {
1137
1217
  (item) => item.Userinfo?.[2] === tk && item.Userinfo?.[0] !== us
1138
1218
  );
1139
1219
  const maxOnline = hostcfg.maxonline || 100;
1220
+ const online = listOnlineWsEntries($asai, hostdir);
1140
1221
  if (sameTkWs) {
1141
1222
  rejectIncomingWs(ws2, {
1142
1223
  lang: "loggedsametk",
1143
1224
  langpm: `${us}(${sameTkWs.Userinfo[0]})`,
1144
- con: `${us}(${sameTkWs.Userinfo[0]}) logged in!`
1225
+ con: `${us}(${sameTkWs.Userinfo[0]}) logged in!`,
1226
+ online
1145
1227
  });
1146
1228
  return false;
1147
1229
  }
1148
- if (Object.keys(connections).length >= maxOnline) {
1230
+ if (online.length >= maxOnline) {
1149
1231
  rejectIncomingWs(ws2, {
1150
1232
  lang: "maxlogged",
1151
1233
  langpm: `${maxOnline}`,
1152
- con: `max ${maxOnline} users online!`
1234
+ con: `max ${maxOnline} users online!`,
1235
+ online
1153
1236
  });
1154
1237
  return false;
1155
1238
  }
@@ -1533,11 +1616,11 @@ function createSkipChecker($asai) {
1533
1616
  }
1534
1617
  if (exact.size === 0 && prefixes.length === 0) return () => false;
1535
1618
  return (url) => {
1536
- const path8 = normalizeRequestPath(url);
1537
- if (!path8) return false;
1538
- if (exact.has(path8)) return true;
1619
+ const path9 = normalizeRequestPath(url);
1620
+ if (!path9) return false;
1621
+ if (exact.has(path9)) return true;
1539
1622
  for (let i = 0; i < prefixes.length; i++) {
1540
- if (path8.startsWith(prefixes[i])) return true;
1623
+ if (path9.startsWith(prefixes[i])) return true;
1541
1624
  }
1542
1625
  return false;
1543
1626
  };
@@ -2424,7 +2507,7 @@ async function attachServerUserLevel(req, $asai) {
2424
2507
  if (!Array.isArray(userinfo) || !userinfo[0]) return;
2425
2508
  const us = String(userinfo[0]);
2426
2509
  try {
2427
- const { createUserStore: createUserStore2 } = await import("./user-store-FXD2N55G.es.js");
2510
+ const { createUserStore: createUserStore2 } = await import("./user-store-ZI67TXSI.es.js");
2428
2511
  const user = await createUserStore2($asai).findByUsername(us);
2429
2512
  if (user && user.status === "active") {
2430
2513
  req._serverUser = { us: user.us, lv: user.lv, status: user.status };
@@ -2455,11 +2538,11 @@ function getSecurityConfig($asai) {
2455
2538
  return $asai?.hostconfig?.security || {};
2456
2539
  }
2457
2540
  __name(getSecurityConfig, "getSecurityConfig");
2458
- function pathMatchesPattern(path8, pattern) {
2541
+ function pathMatchesPattern(path9, pattern) {
2459
2542
  if (pattern.endsWith("*")) {
2460
- return path8.startsWith(pattern.slice(0, -1));
2543
+ return path9.startsWith(pattern.slice(0, -1));
2461
2544
  }
2462
- return path8 === pattern;
2545
+ return path9 === pattern;
2463
2546
  }
2464
2547
  __name(pathMatchesPattern, "pathMatchesPattern");
2465
2548
  function isAuthSkipped($asai, pathname) {
@@ -4066,6 +4149,37 @@ async function purgeOfflineSessions($asai, hostdir, store) {
4066
4149
  __name(purgeOfflineSessions, "purgeOfflineSessions");
4067
4150
 
4068
4151
  // src/sysserver/api/login/index.ts
4152
+ import fs9 from "fs";
4153
+ import path4 from "path";
4154
+ async function resolveUserLv($asai, store, us, wsLv) {
4155
+ if (!us) return null;
4156
+ try {
4157
+ const user = await store.findByUsername(us);
4158
+ if (user && user.lv != null && Number.isFinite(Number(user.lv))) {
4159
+ return Number(user.lv);
4160
+ }
4161
+ } catch {
4162
+ }
4163
+ try {
4164
+ const cfg = getUserStoreConfig($asai);
4165
+ const dir = String(cfg?.opt?.dir || "");
4166
+ if (dir) {
4167
+ const base = path4.isAbsolute(dir) ? dir : path4.join(process.cwd(), dir);
4168
+ const safeUs = us.replace(/[\\/:*?"<>|]/g, "");
4169
+ const file = path4.join(base, "json", `${safeUs}.json`);
4170
+ if (safeUs && fs9.existsSync(file)) {
4171
+ const raw = JSON.parse(fs9.readFileSync(file, "utf8"));
4172
+ const lv = Number(raw?.lv);
4173
+ if (Number.isFinite(lv)) return lv;
4174
+ }
4175
+ }
4176
+ } catch {
4177
+ }
4178
+ if (wsLv === null || wsLv === void 0 || wsLv === "") return null;
4179
+ const n = Number(wsLv);
4180
+ return Number.isFinite(n) ? n : null;
4181
+ }
4182
+ __name(resolveUserLv, "resolveUserLv");
4069
4183
  function getSecurity($asai) {
4070
4184
  return $asai?.hostconfig?.security || {};
4071
4185
  }
@@ -4229,10 +4343,7 @@ var login_default = /* @__PURE__ */ __name(($asai) => {
4229
4343
  if (hostcfg.ckws) {
4230
4344
  const existingWs = $asai.connectionsws?.[hostdir]?.[us];
4231
4345
  if (existingWs) {
4232
- const cleared = await clearStaleWsIfDead($asai, hostdir, us, existingWs);
4233
- if (!cleared) {
4234
- kickUserWs($asai, us, hostdir);
4235
- }
4346
+ await clearStaleWsIfDead($asai, hostdir, us, existingWs);
4236
4347
  }
4237
4348
  }
4238
4349
  const quotaCheck = await canLogin($asai, us, hostdir);
@@ -4279,6 +4390,56 @@ var login_default = /* @__PURE__ */ __name(($asai) => {
4279
4390
  return sendFail(res, err?.message || err, opt);
4280
4391
  }
4281
4392
  }
4393
+ if (url.startsWith("/api/user/login/force/")) {
4394
+ try {
4395
+ const us = sanitizeString(String(req?.Userinfo?.[0] || ""), 64);
4396
+ if (!us) return sendErr(res, "AUTH_MISSING", void 0, opt);
4397
+ const myLv = Number(req?.Userinfo?.[1] ?? 0);
4398
+ const targetUs = sanitizeString(String(body?.targetUs || body?.us || ""), 64);
4399
+ const kicked = [];
4400
+ if (isUserOnlineOnWs($asai, us, hostdir)) {
4401
+ await kickUserWs($asai, us, hostdir);
4402
+ kicked.push(us);
4403
+ }
4404
+ const onlineRaw = listOnlineWsEntries($asai, hostdir).filter((e) => e.us !== us);
4405
+ const online = await Promise.all(
4406
+ onlineRaw.map(async (e) => ({
4407
+ us: e.us,
4408
+ lv: await resolveUserLv($asai, store, e.us, e.lv)
4409
+ }))
4410
+ );
4411
+ const candidates = targetUs ? online.filter((e) => e.us === targetUs) : online;
4412
+ for (const entry of candidates) {
4413
+ const entryLv = entry.lv == null ? NaN : Number(entry.lv);
4414
+ if (!Number.isFinite(entryLv) || myLv < entryLv) {
4415
+ if (targetUs) {
4416
+ return sendErr(res, "AUTH_FORBIDDEN", void 0, opt);
4417
+ }
4418
+ continue;
4419
+ }
4420
+ await kickUserWs($asai, entry.us, hostdir);
4421
+ kicked.push(entry.us);
4422
+ }
4423
+ if (!kicked.length && online.length) {
4424
+ return sendErr(res, "AUTH_FORBIDDEN", void 0, opt);
4425
+ }
4426
+ logSecurityEvent($asai, {
4427
+ type: "AUTH_SUCCESS",
4428
+ path: url,
4429
+ method: "POST",
4430
+ user: us,
4431
+ ip,
4432
+ detail: `force_login_kick:${kicked.join(",") || "none"}`
4433
+ });
4434
+ return sendSuccess(
4435
+ res,
4436
+ { ok: true, us, kicked, online: listOnlineWsEntries($asai, hostdir) },
4437
+ opt
4438
+ );
4439
+ } catch (err) {
4440
+ return sendFail(res, err?.message || err, opt);
4441
+ }
4442
+ }
4282
4443
  if (url.startsWith("/api/user/login/admin/kick/")) {
4283
4444
  if (!await checkAdmin(req, url, getPermissionThreshold($asai, "adminWrite"))) {
4284
4445
  return sendErr(res, "AUTH_FORBIDDEN", void 0, opt);
@@ -4292,7 +4453,7 @@ var login_default = /* @__PURE__ */ __name(($asai) => {
4292
4453
  if (!isSessionUserOnline($asai, target.us, hostdir)) {
4293
4454
  return sendErr(res, "SESSION_NOT_ONLINE", void 0, opt);
4294
4455
  }
4295
- kickUserWs($asai, target.us, hostdir);
4456
+ await kickUserWs($asai, target.us, hostdir);
4296
4457
  await store.removeSession(sessionId);
4297
4458
  const operator = String(req?.Userinfo?.[0] || "");
4298
4459
  await logSessionTerminate($asai, {
@@ -4491,6 +4652,39 @@ var login_default = /* @__PURE__ */ __name(($asai) => {
4491
4652
  await loadUserLevelConfig($asai);
4492
4653
  return sendSuccess(res, getPublicSecurityConfig($asai, lang), opt);
4493
4654
  }
4655
+ if (url.startsWith("/api/user/login/online/")) {
4656
+ try {
4657
+ const us = sanitizeString(String(req?.Userinfo?.[0] || ""), 64);
4658
+ if (!us) return sendErr(res, "AUTH_MISSING", void 0, opt);
4659
+ const rawOnline = listOnlineWsEntries($asai, hostdir);
4660
+ const online = await Promise.all(
4661
+ rawOnline.map(async (e) => ({
4662
+ us: e.us,
4663
+ lv: await resolveUserLv($asai, store, e.us, e.lv)
4664
+ }))
4665
+ );
4666
+ const rawMy = req?.Userinfo?.[1];
4667
+ const wsMyLv = rawMy === null || rawMy === void 0 || rawMy === "" ? null : Number(rawMy);
4668
+ const myLvOut = await resolveUserLv(
4669
+ $asai,
4670
+ store,
4671
+ us,
4672
+ Number.isFinite(wsMyLv) ? wsMyLv : null
4673
+ );
4674
+ return sendSuccess(
4675
+ res,
4676
+ {
4677
+ us,
4678
+ lv: myLvOut,
4679
+ online,
4680
+ maxonline: $asai.getHost?.(hostdir)?.maxonline ?? 100
4681
+ },
4682
+ opt
4683
+ );
4684
+ } catch (err) {
4685
+ return sendFail(res, err?.message || err, opt);
4686
+ }
4687
+ }
4494
4688
  if (url.startsWith("/api/user/login/admin/levels/")) {
4495
4689
  if (!await checkAdmin(req, url, getPermissionThreshold($asai, "adminRead"))) {
4496
4690
  return sendErr(res, "AUTH_FORBIDDEN", void 0, opt);
@@ -4613,10 +4807,10 @@ var As_default = {
4613
4807
  return relativePath;
4614
4808
  },
4615
4809
  //创建多层文件夹 同步
4616
- ensureDir(fs11, relativePath) {
4810
+ ensureDir(fs12, relativePath) {
4617
4811
  const absPathDir = this.toDirPath(relativePath);
4618
4812
  const absPath = this.toAbsolutePath(relativePath);
4619
- if (!fs11.existsSync(absPath)) {
4813
+ if (!fs12.existsSync(absPath)) {
4620
4814
  let pathTmp = "";
4621
4815
  absPathDir.split("/").forEach((el) => {
4622
4816
  if (el) {
@@ -4624,8 +4818,8 @@ var As_default = {
4624
4818
  pathTmp += "/";
4625
4819
  }
4626
4820
  pathTmp += el;
4627
- if (!fs11.existsSync(pathTmp)) {
4628
- if (!fs11.mkdirSync(pathTmp, { recursive: true })) {
4821
+ if (!fs12.existsSync(pathTmp)) {
4822
+ if (!fs12.mkdirSync(pathTmp, { recursive: true })) {
4629
4823
  return absPath;
4630
4824
  }
4631
4825
  }
@@ -4634,10 +4828,10 @@ var As_default = {
4634
4828
  }
4635
4829
  return absPath;
4636
4830
  },
4637
- makeDir(fs11, filePaths) {
4831
+ makeDir(fs12, filePaths) {
4638
4832
  return new Promise((resolve6, reject) => {
4639
4833
  try {
4640
- const newPath = this.ensureDir(fs11, filePaths);
4834
+ const newPath = this.ensureDir(fs12, filePaths);
4641
4835
  resolve6(newPath);
4642
4836
  } catch (err) {
4643
4837
  reject(err);
@@ -5234,7 +5428,7 @@ var AsDb_default = /* @__PURE__ */ __name(($asai, cfg) => {
5234
5428
  }, "default");
5235
5429
 
5236
5430
  // src/sysserver/lib/common/reqWork.ts
5237
- import * as path4 from "path";
5431
+ import * as path5 from "path";
5238
5432
  var reqWork_default = /* @__PURE__ */ __name(($asai) => {
5239
5433
  function hostDefault2(mod) {
5240
5434
  if (mod && typeof mod === "object" && "default" in mod) {
@@ -5250,28 +5444,29 @@ var reqWork_default = /* @__PURE__ */ __name(($asai) => {
5250
5444
  throw new Error("Missing path");
5251
5445
  }
5252
5446
  const decoded = decodeURIComponent(String(rawPath));
5253
- if (path4.isAbsolute(decoded)) {
5254
- return path4.normalize(decoded);
5447
+ if (decoded.includes("\0")) {
5448
+ throw new Error("Invalid path");
5255
5449
  }
5256
- const rel = decoded.replace(/^\.(\/)?/, "");
5257
- const serverRoot = $asai?.serverRoot;
5258
- if (serverRoot && rel) {
5259
- const absRoot = path4.resolve(serverRoot);
5260
- const abs = path4.resolve(absRoot, rel);
5261
- const relCheck = path4.relative(absRoot, abs);
5262
- if (relCheck === "" || !relCheck.startsWith("..") && !path4.isAbsolute(relCheck)) {
5263
- return abs;
5264
- }
5450
+ const serverRoot = path5.resolve($asai?.serverRoot || process.cwd() || ".");
5451
+ const cleaned = String(decoded).replace(/\\/g, "/").trim();
5452
+ if (path5.isAbsolute(cleaned) || /^[a-zA-Z]:/.test(cleaned) || cleaned.startsWith("//")) {
5453
+ return path5.normalize(cleaned);
5265
5454
  }
5455
+ const absTarget = path5.resolve(serverRoot, cleaned.replace(/^\.\//, ""));
5266
5456
  const roots = $asai?.asaihost?.getHostAllowedRoots?.($asai?.hostconfig) || [];
5267
- if (!roots.length) {
5268
- const fallback = $asai?.serverRoot || process.cwd();
5269
- if (fallback) roots.push(fallback);
5457
+ const allow = /* @__PURE__ */ new Set();
5458
+ for (const r of roots) {
5459
+ if (r) allow.add(path5.resolve(r));
5270
5460
  }
5271
- if (!$asai?.asaihost?.resolvePathUnderRoots) {
5272
- throw new Error("Path resolver unavailable");
5461
+ allow.add(serverRoot);
5462
+ allow.add(path5.resolve(serverRoot, ".."));
5463
+ for (const root of allow) {
5464
+ const rel = path5.relative(root, absTarget);
5465
+ if (rel === "" || !rel.startsWith("..") && !path5.isAbsolute(rel)) {
5466
+ return absTarget;
5467
+ }
5273
5468
  }
5274
- return $asai.asaihost.resolvePathUnderRoots(decoded, roots);
5469
+ throw new Error("Path not allowed");
5275
5470
  }
5276
5471
  __name(getPathFromOpt, "getPathFromOpt");
5277
5472
  function sendSuccess(res, data, opt) {
@@ -5319,18 +5514,18 @@ __export(AsFs_exports, {
5319
5514
  write: () => write,
5320
5515
  writeJson: () => writeJson
5321
5516
  });
5322
- import fs9 from "fs/promises";
5323
- import path5 from "path";
5517
+ import fs10 from "fs/promises";
5518
+ import path6 from "path";
5324
5519
  async function ensureFile(filePath) {
5325
- const normalized = path5.normalize(filePath);
5326
- await fs9.mkdir(path5.dirname(normalized), { recursive: true });
5327
- const fh = await fs9.open(normalized, "a");
5520
+ const normalized = path6.normalize(filePath);
5521
+ await fs10.mkdir(path6.dirname(normalized), { recursive: true });
5522
+ const fh = await fs10.open(normalized, "a");
5328
5523
  await fh.close();
5329
5524
  }
5330
5525
  __name(ensureFile, "ensureFile");
5331
5526
  async function pathExists(filePath) {
5332
5527
  try {
5333
- await fs9.access(path5.normalize(filePath));
5528
+ await fs10.access(path6.normalize(filePath));
5334
5529
  return true;
5335
5530
  } catch {
5336
5531
  return false;
@@ -5338,7 +5533,7 @@ async function pathExists(filePath) {
5338
5533
  }
5339
5534
  __name(pathExists, "pathExists");
5340
5535
  async function readJson(filePath) {
5341
- const raw = await fs9.readFile(path5.normalize(filePath), "utf8");
5536
+ const raw = await fs10.readFile(path6.normalize(filePath), "utf8");
5342
5537
  return JSON.parse(raw);
5343
5538
  }
5344
5539
  __name(readJson, "readJson");
@@ -5349,34 +5544,34 @@ function resolvePathUnderBase(baseDir, relPath) {
5349
5544
  if (relPath.includes("\0")) {
5350
5545
  throw new Error("Invalid path");
5351
5546
  }
5352
- const base = path5.resolve(baseDir);
5353
- const target = path5.resolve(base, relPath.replace(/\\/g, "/"));
5354
- const rel = path5.relative(base, target);
5355
- if (rel.startsWith("..") || path5.isAbsolute(rel)) {
5547
+ const base = path6.resolve(baseDir);
5548
+ const target = path6.resolve(base, relPath.replace(/\\/g, "/"));
5549
+ const rel = path6.relative(base, target);
5550
+ if (rel.startsWith("..") || path6.isAbsolute(rel)) {
5356
5551
  throw new Error("Path not allowed");
5357
5552
  }
5358
5553
  return target;
5359
5554
  }
5360
5555
  __name(resolvePathUnderBase, "resolvePathUnderBase");
5361
5556
  async function readLogDir(dir) {
5362
- const resolved = path5.resolve(dir);
5363
- const stats = await fs9.stat(resolved);
5557
+ const resolved = path6.resolve(dir);
5558
+ const stats = await fs10.stat(resolved);
5364
5559
  if (!stats.isDirectory()) {
5365
5560
  throw new Error("Not a directory");
5366
5561
  }
5367
- const dirName = path5.basename(resolved);
5368
- const entries = await fs9.readdir(resolved, { withFileTypes: true });
5562
+ const dirName = path6.basename(resolved);
5563
+ const entries = await fs10.readdir(resolved, { withFileTypes: true });
5369
5564
  const children = await Promise.all(
5370
5565
  entries.map(async (entry) => {
5371
- const fullPath = path5.join(resolved, entry.name);
5372
- const st = await fs9.stat(fullPath);
5566
+ const fullPath = path6.join(resolved, entry.name);
5567
+ const st = await fs10.stat(fullPath);
5373
5568
  const dot = entry.name.lastIndexOf(".");
5374
5569
  const type = entry.isDirectory() ? "dir" : dot > -1 ? entry.name.substring(dot) : "file";
5375
5570
  return {
5376
5571
  name: entry.name,
5377
5572
  size: st.size,
5378
5573
  time: st.mtime.toISOString(),
5379
- path: path5.normalize(fullPath),
5574
+ path: path6.normalize(fullPath),
5380
5575
  type,
5381
5576
  created: st.birthtime.toISOString(),
5382
5577
  permissions: st.mode.toString(8).slice(-3)
@@ -5387,7 +5582,7 @@ async function readLogDir(dir) {
5387
5582
  name: dirName,
5388
5583
  size: stats.size,
5389
5584
  time: stats.mtime.toISOString(),
5390
- path: path5.normalize(resolved),
5585
+ path: path6.normalize(resolved),
5391
5586
  type: "dir",
5392
5587
  created: stats.birthtime.toISOString(),
5393
5588
  permissions: stats.mode.toString(8).slice(-3),
@@ -5397,7 +5592,7 @@ async function readLogDir(dir) {
5397
5592
  __name(readLogDir, "readLogDir");
5398
5593
  async function removeFile(filePath) {
5399
5594
  try {
5400
- await fs9.unlink(path5.normalize(filePath));
5595
+ await fs10.unlink(path6.normalize(filePath));
5401
5596
  } catch (err) {
5402
5597
  if (err?.code !== "ENOENT") throw err;
5403
5598
  }
@@ -5405,13 +5600,13 @@ async function removeFile(filePath) {
5405
5600
  __name(removeFile, "removeFile");
5406
5601
  var remove = removeFile;
5407
5602
  async function write(filePath, content) {
5408
- const normalized = path5.normalize(filePath);
5409
- await fs9.mkdir(path5.dirname(normalized), { recursive: true });
5410
- await fs9.writeFile(normalized, content, "utf8");
5603
+ const normalized = path6.normalize(filePath);
5604
+ await fs10.mkdir(path6.dirname(normalized), { recursive: true });
5605
+ await fs10.writeFile(normalized, content, "utf8");
5411
5606
  }
5412
5607
  __name(write, "write");
5413
5608
  async function read(filePath) {
5414
- return fs9.readFile(path5.normalize(filePath), "utf8");
5609
+ return fs10.readFile(path6.normalize(filePath), "utf8");
5415
5610
  }
5416
5611
  __name(read, "read");
5417
5612
  async function writeJson(filePath, data) {
@@ -5447,7 +5642,7 @@ __export(common_exports, {
5447
5642
  toSetObject: () => toSetObject,
5448
5643
  toWherePairs: () => toWherePairs
5449
5644
  });
5450
- import * as path6 from "path";
5645
+ import * as path7 from "path";
5451
5646
  function promisifySqlDb(query, sql) {
5452
5647
  return new Promise((resolve6, reject) => {
5453
5648
  query(sql, (err, result) => {
@@ -5512,11 +5707,11 @@ function toInsertRows(sql) {
5512
5707
  }
5513
5708
  __name(toInsertRows, "toInsertRows");
5514
5709
  function isPathInside(baseDir, targetPath) {
5515
- const base = path6.resolve(baseDir);
5516
- const target = path6.resolve(targetPath);
5517
- const relative5 = path6.relative(base, target);
5710
+ const base = path7.resolve(baseDir);
5711
+ const target = path7.resolve(targetPath);
5712
+ const relative5 = path7.relative(base, target);
5518
5713
  if (!relative5) return true;
5519
- return !relative5.startsWith("..") && !path6.isAbsolute(relative5);
5714
+ return !relative5.startsWith("..") && !path7.isAbsolute(relative5);
5520
5715
  }
5521
5716
  __name(isPathInside, "isPathInside");
5522
5717
  function joinTablePath(table, ...segments) {
@@ -5697,8 +5892,8 @@ var Sql_default = {
5697
5892
  };
5698
5893
 
5699
5894
  // src/sysserver/db/src/file/Index.ts
5700
- import * as fs10 from "fs";
5701
- import * as path7 from "path";
5895
+ import * as fs11 from "fs";
5896
+ import * as path8 from "path";
5702
5897
  function dbDriver(opt = {}) {
5703
5898
  const { Sql, DbCommon } = opt;
5704
5899
  const { isPathInside: isPathInside2, joinTablePath: joinTablePath2 } = DbCommon;
@@ -5712,20 +5907,20 @@ function dbDriver(opt = {}) {
5712
5907
  this.fileDel = (config.del || 0) !== 0;
5713
5908
  this.createDir = config.create || false;
5714
5909
  const root = config.$asai?.serverRoot || getPrimaryServerRoot();
5715
- this.baseDirAbs = path7.resolve(root, config.dir);
5910
+ this.baseDirAbs = path8.resolve(root, config.dir);
5716
5911
  }
5717
5912
  dbLog(...args) {
5718
5913
  (this.config?.$asai?.$log || console.log)("FILE", ...args);
5719
5914
  }
5720
5915
  checkFile(file) {
5721
- return fs10.existsSync(this.getAbsPath(file, false));
5916
+ return fs11.existsSync(this.getAbsPath(file, false));
5722
5917
  }
5723
5918
  /** 解析相对路径并限制在数据根目录内,防止路径穿越(跨平台) */
5724
5919
  resolvePath(relPath) {
5725
5920
  if (!relPath) {
5726
5921
  return this.baseDirAbs;
5727
5922
  }
5728
- const abs = path7.isAbsolute(relPath) ? path7.normalize(relPath) : path7.resolve(this.baseDirAbs, relPath);
5923
+ const abs = path8.isAbsolute(relPath) ? path8.normalize(relPath) : path8.resolve(this.baseDirAbs, relPath);
5729
5924
  if (!isPathInside2(this.baseDirAbs, abs)) {
5730
5925
  throw new Error("Path not allowed");
5731
5926
  }
@@ -5739,15 +5934,15 @@ function dbDriver(opt = {}) {
5739
5934
  if (!relPath) {
5740
5935
  return "";
5741
5936
  }
5742
- return path7.isAbsolute(relPath) ? path7.relative(this.baseDirAbs, relPath) : relPath;
5937
+ return path8.isAbsolute(relPath) ? path8.relative(this.baseDirAbs, relPath) : relPath;
5743
5938
  }
5744
5939
  ensureDir(relPath) {
5745
5940
  const relative5 = this.normalizeRelPath(relPath);
5746
- const dirPath = path7.dirname(relative5);
5941
+ const dirPath = path8.dirname(relative5);
5747
5942
  const absDir = this.resolvePath(dirPath === "." ? "" : dirPath);
5748
- if (this.createDir && !fs10.existsSync(absDir)) {
5943
+ if (this.createDir && !fs11.existsSync(absDir)) {
5749
5944
  this.dbLog("mkdir", "[MKDIR]", absDir);
5750
- fs10.mkdirSync(absDir, { recursive: true });
5945
+ fs11.mkdirSync(absDir, { recursive: true });
5751
5946
  }
5752
5947
  return absDir;
5753
5948
  }
@@ -5763,34 +5958,34 @@ function dbDriver(opt = {}) {
5763
5958
  }
5764
5959
  deldir(relativePath) {
5765
5960
  const absPath = this.resolvePath(relativePath);
5766
- if (!fs10.existsSync(absPath)) {
5961
+ if (!fs11.existsSync(absPath)) {
5767
5962
  return;
5768
5963
  }
5769
5964
  this.dbLog("delete", "[RMDIR]", absPath);
5770
- const entries = fs10.readdirSync(absPath, { withFileTypes: true });
5965
+ const entries = fs11.readdirSync(absPath, { withFileTypes: true });
5771
5966
  for (const entry of entries) {
5772
- const current = path7.join(absPath, entry.name);
5967
+ const current = path8.join(absPath, entry.name);
5773
5968
  if (entry.isDirectory()) {
5774
- this.deldir(path7.join(relativePath, entry.name));
5969
+ this.deldir(path8.join(relativePath, entry.name));
5775
5970
  } else {
5776
5971
  this.dbLog("delete", "[UNLINK]", current);
5777
- fs10.unlinkSync(current);
5972
+ fs11.unlinkSync(current);
5778
5973
  }
5779
5974
  }
5780
- fs10.rmdirSync(absPath);
5975
+ fs11.rmdirSync(absPath);
5781
5976
  }
5782
5977
  getReadPath(file, sql) {
5783
5978
  const candidates = [];
5784
5979
  if (sql?.filename) {
5785
- candidates.push({ dir: path7.join(file, sql.filename) });
5786
- candidates.push({ dir: path7.join(file + ".delete" + this.fileExt, sql.filename) });
5980
+ candidates.push({ dir: path8.join(file, sql.filename) });
5981
+ candidates.push({ dir: path8.join(file + ".delete" + this.fileExt, sql.filename) });
5787
5982
  } else {
5788
5983
  candidates.push({ dir: file });
5789
5984
  candidates.push({ dir: file + this.fileExt + ".delete" });
5790
5985
  }
5791
5986
  for (const candidate of candidates) {
5792
5987
  const abs = this.getAbsPath(candidate.dir, false);
5793
- if (fs10.existsSync(abs)) {
5988
+ if (fs11.existsSync(abs)) {
5794
5989
  return { path: abs, dir: candidate.dir };
5795
5990
  }
5796
5991
  }
@@ -5802,13 +5997,13 @@ function dbDriver(opt = {}) {
5802
5997
  return { path: "", dir: read2.dir };
5803
5998
  }
5804
5999
  if (sql?.filename) {
5805
- return { path: path7.dirname(read2.path), dir: path7.dirname(read2.dir) };
6000
+ return { path: path8.dirname(read2.path), dir: path8.dirname(read2.dir) };
5806
6001
  }
5807
6002
  return { path: read2.path, dir: read2.dir };
5808
6003
  }
5809
6004
  getNewPath(file, sql) {
5810
6005
  if (sql?.filename) {
5811
- return this.getAbsPath(path7.join(file, sql.filename), true);
6006
+ return this.getAbsPath(path8.join(file, sql.filename), true);
5812
6007
  }
5813
6008
  return this.getAbsPath(file, true);
5814
6009
  }
@@ -5816,7 +6011,7 @@ function dbDriver(opt = {}) {
5816
6011
  try {
5817
6012
  const filePath = sql?.type === "insert" ? this.getNewPath(file, sql) : this.getReadPath(file, sql).path || this.getNewPath(file, sql);
5818
6013
  this.dbLog("write", "[WRITE]", filePath);
5819
- fs10.writeFileSync(filePath, data, "utf8");
6014
+ fs11.writeFileSync(filePath, data, "utf8");
5820
6015
  return "ok";
5821
6016
  } catch (e) {
5822
6017
  this.dbLog("write failed", "[WRITE ERROR]", e);
@@ -5830,7 +6025,7 @@ function dbDriver(opt = {}) {
5830
6025
  return { error: "empty" };
5831
6026
  }
5832
6027
  this.dbLog("read", "[READ]", read2.path);
5833
- const data = fs10.readFileSync(read2.path, { encoding }) || "";
6028
+ const data = fs11.readFileSync(read2.path, { encoding }) || "";
5834
6029
  const fileName = this.fileExt && read2.dir.endsWith(this.fileExt) ? read2.dir : read2.dir + this.fileExt;
5835
6030
  return [{ data, file: fileName }];
5836
6031
  } catch (e) {
@@ -5847,12 +6042,12 @@ function dbDriver(opt = {}) {
5847
6042
  if (this.fileDel && sql?.deltrue) {
5848
6043
  const target = del.path + ".delete" + this.fileExt;
5849
6044
  this.dbLog("soft delete", "[SOFT DELETE]", `${del.path} -> ${target}`);
5850
- fs10.renameSync(del.path, target);
6045
+ fs11.renameSync(del.path, target);
5851
6046
  } else if (sql?.filename) {
5852
6047
  this.deldir(del.dir);
5853
6048
  } else {
5854
6049
  this.dbLog("delete", "[DELETE]", del.path);
5855
- fs10.unlinkSync(del.path);
6050
+ fs11.unlinkSync(del.path);
5856
6051
  }
5857
6052
  return "ok";
5858
6053
  } catch (e) {
@@ -5871,11 +6066,11 @@ function dbDriver(opt = {}) {
5871
6066
  const original = del.path.replace(new RegExp(`.delete${extPattern}$`), "");
5872
6067
  if (original !== del.path) {
5873
6068
  this.dbLog("restore", "[RESTORE]", `${del.path} -> ${original}`);
5874
- fs10.renameSync(del.path, original);
6069
+ fs11.renameSync(del.path, original);
5875
6070
  }
5876
6071
  } else {
5877
6072
  this.dbLog("force delete", "[FORCE DELETE]", del.path);
5878
- fs10.unlinkSync(del.path);
6073
+ fs11.unlinkSync(del.path);
5879
6074
  }
5880
6075
  return "ok";
5881
6076
  } catch (e) {
@@ -5889,7 +6084,7 @@ function dbDriver(opt = {}) {
5889
6084
  this.dbLog("list", "[LIST]", absDir);
5890
6085
  let entries;
5891
6086
  try {
5892
- entries = fs10.readdirSync(absDir, { withFileTypes: true });
6087
+ entries = fs11.readdirSync(absDir, { withFileTypes: true });
5893
6088
  } catch {
5894
6089
  return sql?.list ? { open: [], delete: [] } : [];
5895
6090
  }
@@ -5901,9 +6096,9 @@ function dbDriver(opt = {}) {
5901
6096
  const isDelete = fileName.endsWith(deleteSuffix);
5902
6097
  const baseName = isDelete ? fileName.slice(0, -deleteSuffix.length) : fileName;
5903
6098
  if (this.fileExt && !baseName.endsWith(this.fileExt)) continue;
5904
- const fullPath = path7.join(absDir, fileName);
6099
+ const fullPath = path8.join(absDir, fileName);
5905
6100
  try {
5906
- const data = fs10.readFileSync(fullPath, "utf8");
6101
+ const data = fs11.readFileSync(fullPath, "utf8");
5907
6102
  const entryData = { data, file: baseName };
5908
6103
  if (isDelete) {
5909
6104
  if (sql?.list) result.delete.push(entryData);
@@ -6669,7 +6864,7 @@ var initAsaiHostNodejs2 = {
6669
6864
  createTransportDataHandler,
6670
6865
  wireTransportConnectionLogs
6671
6866
  };
6672
- var PLUGIN_VERSION = true ? "0.0.9" : "0.0.1";
6867
+ var PLUGIN_VERSION = true ? "0.0.11" : "0.0.1";
6673
6868
  var index_default = initAsaiHostNodejs2;
6674
6869
  export {
6675
6870
  Index_default as AsaiCommon,