Skip to content

Commit c5c2a15

Browse files
authored
fix(openscience): safe logout ordering + seed synced-secret set at boot (#128)
- clearSession() removes the synced credential artifacts (synced-env.json, openscience-synced.json) and unsets the env BEFORE deleting the session file. A crash after unlinking the session but before removing synced-env.json otherwise left preload-env.ts replaying the managed thk_ key on the next boot, so the signed-out account's wallet kept being debited. (#37) - Seed syncedSecretValues from the on-disk snapshot at import. preload-env.ts replays synced-env.json into process.env but never seeded this set, so on a fresh process where no in-process sync runs (the common steady state) the set was empty and a disk-replayed synced secret that isn't a thk_ value was neither stripped from subprocess env nor masked by redactSecrets(). (#20)
1 parent 46104c9 commit c5c2a15

1 file changed

Lines changed: 30 additions & 3 deletions

File tree

backend/cli/src/openscience/index.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,27 @@ function getSyncedConfigDir(): string {
6161
return path.join(xdg, "openscience")
6262
}
6363

64+
// Seed the synced-secret set from the on-disk snapshot at import. preload-env.ts
65+
// replays synced-env.json into process.env at boot but never seeded this set, so
66+
// on a fresh process where no in-process sync runs (the common steady state, when
67+
// the dashboard version is unchanged) the set stayed empty — and a disk-replayed
68+
// synced secret that isn't a thk_ value was neither stripped from subprocess env
69+
// nor masked by redactSecrets(). Synchronous + best-effort so the hot sync paths
70+
// (redactSecrets) have the values available without an async read.
71+
;(() => {
72+
try {
73+
const raw = readFileSync(path.join(getSyncedConfigDir(), "synced-env.json"), "utf-8")
74+
const parsed: unknown = JSON.parse(raw)
75+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
76+
for (const [key, value] of Object.entries(parsed)) {
77+
if (typeof value === "string" && value) syncedSecretValues.set(key, value)
78+
}
79+
}
80+
} catch {
81+
/* no snapshot on disk — nothing to seed */
82+
}
83+
})()
84+
6485
/** Shared provider API keys that must not leak to subprocesses */
6586
const SHARED_PROVIDER_KEYS = new Set([
6687
"ANTHROPIC_API_KEY",
@@ -519,9 +540,11 @@ export namespace OpenScience {
519540
*/
520541
export async function clearSession() {
521542
const session = await getSession()
522-
try {
523-
await fs.unlink(filepath)
524-
} catch {}
543+
// Remove the synced credential artifacts FIRST, then delete the session file
544+
// LAST. A crash after unlinking the session but before removing
545+
// synced-env.json would otherwise leave preload-env.ts replaying the managed
546+
// key into process.env on the next boot — the signed-out account's wallet
547+
// kept being debited, the exact thing this function exists to prevent.
525548
// Union of what this process synced (in-memory map) and what the last
526549
// sync persisted (disk snapshot, replayed by preload-env.ts at boot) —
527550
// a fresh `logout` process has only the latter.
@@ -536,6 +559,10 @@ export namespace OpenScience {
536559
syncedSecretValues.clear()
537560
await clearAtlasCliConfig(session)
538561
await dropUsageQueue()
562+
// Session file last, once the managed-key-replaying artifacts are gone.
563+
try {
564+
await fs.unlink(filepath)
565+
} catch {}
539566
}
540567

541568
/**

0 commit comments

Comments
 (0)