fix(gui): 缓存解封结果,否则每个请求都要 spawn 一个进程

The auth rule calls readAdminKey() on every outbound request so injection
never depends on ordering. Once the sealed-config path shells out to the
core, that turns each request into a process spawn: 200 simulated requests
took 1012ms and launched 200 cores.

Cache the unsealed key against config.yaml's mtime. Editing the config still
invalidates it, which is what the auth rule actually needs -- the port
rewrite, the first write, and a user edit all change mtime. Measured: 200
requests now cost 10ms and one spawn.

Only a successful unseal is cached. Caching a failure would pin an empty key
until the config next changes, turning a momentary spawn error into a locked
out user.
This commit is contained in:
JianFeeeee
2026-10-01 19:23:20 +08:00
parent 429afce67e
commit 980f4a0e40

View File

@ -142,7 +142,17 @@ function readConfigRaw() {
// the core generate a *second* master key in the CWD and then fail to decrypt
// ("master key changed?"). Electron's CWD is not the profile dir, so this is
// not optional: without it the desktop build cannot read its own key back.
//
// readAdminKey runs on every outbound request, and this spawns a process, so
// the result is cached against the config's mtime. Editing the config (or the
// port rewrite, or the first write) changes the mtime and invalidates it, which
// keeps the "read live so ordering never matters" property the auth rule
// depends on.
function unsealViaCore() {
const st = safeStat(CONFIG_FILE);
const stamp = st ? st.mtimeMs : 0;
if (unsealCache && unsealCache.stamp === stamp) return unsealCache.key;
let key = "";
try {
const out = execFileSync(
CORE_EXE,
@ -157,14 +167,29 @@ function unsealViaCore() {
// -show-secrets prints one line per credential:
// key <name> role=<role> <value>
const admin = /^key\s+\S+\s+role=admin\s+(\S+)\s*$/m.exec(out);
if (admin && admin[1]) return admin[1];
if (admin && admin[1]) key = admin[1];
// Tolerate a field-order or spacing change rather than locking the user out.
const loose = /role=admin\s+(\S+)/.exec(out);
if (loose && loose[1]) return loose[1];
if (!key) {
const loose = /role=admin\s+(\S+)/.exec(out);
if (loose && loose[1]) key = loose[1];
}
} catch (e) {
console.error("unseal via core failed:", e.message);
}
return "";
// Only cache a success. A transient failure must not pin an empty key until
// the config next changes, or a momentary spawn error locks the user out.
if (key) unsealCache = { stamp, key };
return key;
}
let unsealCache = null;
function safeStat(p) {
try {
return fs.statSync(p);
} catch (e) {
return null;
}
}
const embeddedBaseUrl = () =>