refactor(persistence): migrate user-store to canonical atomic-write util (DC-101) [glm-grade=A]
CI / Test & Lint (push) Canceled after 0s
CI / Security audit (push) Canceled after 0s

Drops user-store's private _atomicWriteJSON copy (pid+Date.now() tmp
names, no fsync, no failure cleanup) in favor of src/utils/atomic-write.js
(DC-099 canonical: fsync'd same-dir exclusive-create 0600 tmp -> rename ->
parent-dir fsync, cleanup-on-failure). All 3 persisted files routed:
users.json, authorized-users.json, .bootstrapped sentinel. Sole consumers
are JSON.parse readers — dropped trailing newline unobservable (judge
verified repo-wide). +2 store-level regression tests pin 0600 / complete
JSON / no temp leftovers across all three files, incl. the bootstrap path
writing three files back-to-back in one login.
Judge: GLM-5.3 cold read, round-1 A, deleg_f632f05c.
Verdict: urn:ump:5fivvveqhcbkl6os4dhidchkvjjzbvi7rgj6znaovp6bfnvmcmsq
Full suite: 121 suites / 2772 tests green.
This commit is contained in:
Hermes
2026-08-23 00:21:41 -07:00
parent 3742e2658d
commit 09d56fde2c
2 changed files with 69 additions and 11 deletions
@@ -235,3 +235,65 @@ describe('DC-100 invite-store on canonical atomic-write (real fs)', () => {
expect(leftovers).toEqual([]);
});
});
// DC-101: user-store migrated off its private _atomicWriteJSON copy onto
// the canonical writer. Store-level pins across ALL THREE persisted files
// (users.json, authorized-users.json, .bootstrapped sentinel): 0600 mode,
// complete JSON, no temp leftovers — including the bootstrap path that
// writes two JSON files plus the sentinel back-to-back in one login.
describe('DC-101 user-store on canonical atomic-write (real fs)', () => {
let dir, store;
beforeEach(() => {
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'dc101-user-'));
store = require('../src/security/user-store').createUserStore({ dataDir: dir });
});
afterEach(() => { try { fs.rmSync(dir, { recursive: true, force: true }); } catch (_) {} });
test('bootstrap login persists users.json + allowlist + sentinel at 0600, complete JSON, no leftovers', async () => {
const r = await store.login({ email: 'dc101@x.com', ip: '10.0.0.1' });
expect(r.ok).toBe(true);
expect(r.isBootstrap).toBe(true);
const usersSt = fs.statSync(path.join(dir, 'users.json'));
const allowSt = fs.statSync(path.join(dir, 'authorized-users.json'));
const sentSt = fs.statSync(path.join(dir, '.bootstrapped'));
expect(usersSt.mode & 0o777).toBe(0o600);
expect(allowSt.mode & 0o777).toBe(0o600);
expect(sentSt.mode & 0o777).toBe(0o600);
const users = JSON.parse(fs.readFileSync(path.join(dir, 'users.json'), 'utf8'));
expect(Object.keys(users.users)).toHaveLength(1);
expect(users.users[users.order[0]].role).toBe('admin');
const allowlist = JSON.parse(fs.readFileSync(path.join(dir, 'authorized-users.json'), 'utf8'));
expect(allowlist.emails).toEqual(['dc101@x.com']);
const sentinel = JSON.parse(fs.readFileSync(path.join(dir, '.bootstrapped'), 'utf8'));
expect(sentinel.adminEmail).toBe('dc101@x.com');
const leftovers = fs.readdirSync(dir).filter(
(f) => f !== 'users.json' && f !== 'authorized-users.json' && f !== '.bootstrapped'
);
expect(leftovers).toEqual([]);
});
test('back-to-back mutations (login, allowlist add/remove, role set) never collide on tmp names', async () => {
const a = await store.login({ email: 'admin@x.com' });
expect(a.isBootstrap).toBe(true);
await store.addToAllowlist('b@x.com');
const b = await store.login({ email: 'b@x.com' });
expect(b.ok).toBe(true);
expect(b.role).toBe('operator');
await store.setRole(b.user.id, 'viewer');
await store.removeFromAllowlist('b@x.com');
const users = JSON.parse(fs.readFileSync(path.join(dir, 'users.json'), 'utf8'));
expect(users.users[b.user.id].role).toBe('viewer');
const allowlist = JSON.parse(fs.readFileSync(path.join(dir, 'authorized-users.json'), 'utf8'));
expect(allowlist.emails).toEqual(['admin@x.com']);
const leftovers = fs.readdirSync(dir).filter(
(f) => f !== 'users.json' && f !== 'authorized-users.json' && f !== '.bootstrapped'
);
expect(leftovers).toEqual([]);
});
});