diff --git a/cmd/gui/main.js b/cmd/gui/main.js index 0350c6e..ab33943 100644 --- a/cmd/gui/main.js +++ b/cmd/gui/main.js @@ -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 role= 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 = () =>