refactor(persistence): migrate credential-manager to canonical atomic-write util (DC-106) [glm-grade=B]
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:
@@ -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') {
|
||||
|
||||
Reference in New Issue
Block a user