refactor(persistence): migrate credential-manager to canonical atomic-write util (DC-106) [glm-grade=B]
CI / Test & Lint (push) Canceled after 0s
CI / Security audit (push) Canceled after 0s

All three writeFileSync sites in the encrypted-credentials manager now
delegate to src/utils/atomic-write.js atomicWriteJSON — rotateEncryptionKey
save, _ensureFileExists bootstrap, and the _lockedUpdate commit under the
proper-lockfile lock. A crash can no longer tear credentials.json
mid-write: power loss through the old path could leave an empty/short
file and silently drop every stored credential (DNS provider tokens etc).
Lock-safety pre-study: proper-lockfile only stats its own sibling .lock
dir, never the target file, so the rename swap cannot trip ECOMPROMISED.

Test mock extended to the fd-level fs API (openSync/writeSync/
fsyncSync/closeSync/renameSync + fdMap/closedTmp state) so the canonical
path is exercised end-to-end under the mock; write assertions moved from
writeFileSync.mock.calls to destination-state reads. New DC-106 pins:
wx+0600+fsync+rename discipline, plaintext-secret canary never on disk,
fd-lifecycle order fsync->close->rename via invocationCallOrder, and
atomic _ensureFileExists create at 0600. Eighth store migrated
(DC-099..DC-105 preceded).

Judge: GLM-5.3 cold read, round-1 B/ship (deleg_b3c038c2).
URN urn:ump:vscequdet7wt5un7jhtl2nlg6cfkabsbazy5m2tjnyyguu5wstxa
(readback verified: grade B, topic codex-judge-verdict).
Sole finding is pre-existing and non-blocking: rotateEncryptionKey
persists the new key before writing rotated creds (crash window) —
queued as DC-107 follow-up.
Full suite: 121 suites / 2783 tests green.
This commit is contained in:
Hermes
2026-08-23 03:06:58 -07:00
parent b1464d9b85
commit eb2bab7a96
2 changed files with 185 additions and 21 deletions
@@ -8,6 +8,7 @@ const keychainManager = require('../security/keychain-manager');
const cryptoUtils = require('../security/crypto-utils');
const lockfile = require('proper-lockfile');
const fs = require('fs');
const { atomicWriteJSON } = require('../utils/atomic-write');
const { log } = require('../utils/logging');
const path = require('path');
const platformPaths = require('../../platform-paths');
@@ -214,8 +215,9 @@ class CredentialManager {
};
}
// Save with new encryption
fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(rotated, null, 2), { mode: 0o600 });
// Save with new encryption (DC-106: canonical atomic-write; proper-lockfile
// only tracks its own .lock dir, so the rename swap is lock-safe)
atomicWriteJSON(CREDENTIALS_FILE, rotated, { mode: 0o600 });
// Clear cache to force reload
this.cache.clear();
@@ -278,7 +280,9 @@ class CredentialManager {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(CREDENTIALS_FILE, '{}', { mode: 0o600 });
// DC-106: canonical atomic-write — same wx/fsync/rename discipline as every
// other store. '{}' initial payload; 0600 mode is atomicWriteFile's default.
atomicWriteJSON(CREDENTIALS_FILE, {});
}
}
@@ -296,7 +300,10 @@ class CredentialManager {
const data = fs.readFileSync(CREDENTIALS_FILE, 'utf8');
const credentials = JSON.parse(data);
const updated = await updateFn(credentials);
fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(updated, null, 2), { mode: 0o600 });
// DC-106: canonical atomic-write under the proper-lockfile lock — a crash
// can no longer tear credentials.json mid-write (the lockfile dir is a
// sibling of the target path, so the rename swap stays lock-safe).
atomicWriteJSON(CREDENTIALS_FILE, updated, { mode: 0o600 });
return updated;
} catch (error) {
if (error.code === 'ELOCKED') {