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
+7 -11
View File
@@ -29,8 +29,9 @@
* This is recorded by writing a sentinel file `data/.bootstrapped` with the
* admin email so we never bootstrap twice (e.g. after a restore from backup).
*
* Atomic writes: every persistence op writes to a .tmp file then renames.
* process restart loses nothing in flight because rename is atomic on POSIX.
* Atomic writes: every persistence op goes through the canonical shared
* atomic-write util (DC-099) — fsync'd same-dir tmp+rename, so a crash or
* process restart loses nothing in flight and never leaves a torn file.
*
* Concurrency: a single in-process mutex serializes mutating ops. We don't
* need cross-process locks because this API is single-instance by design.
@@ -42,6 +43,7 @@ const path = require('path');
const fs = require('fs');
const crypto = require('crypto');
const platformPaths = require('../../platform-paths');
const { atomicWriteJSON } = require('../utils/atomic-write');
const ROLES = Object.freeze({
ADMIN: 'admin',
@@ -76,12 +78,6 @@ function _resolveDataDir(opts) {
return require('os').tmpdir();
}
function _atomicWriteJSON(filePath, data) {
const tmp = filePath + '.tmp.' + process.pid + '.' + Date.now();
fs.writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n', { mode: 0o600 });
fs.renameSync(tmp, filePath);
}
function _readJSON(filePath, fallback) {
try {
const raw = fs.readFileSync(filePath, 'utf8');
@@ -133,8 +129,8 @@ function createUserStore(opts = {}) {
return data;
}
function _saveUsers(data) { _atomicWriteJSON(usersFile, data); }
function _saveAllowlist(data) { _atomicWriteJSON(allowlistFile, data); }
function _saveUsers(data) { atomicWriteJSON(usersFile, data); }
function _saveAllowlist(data) { atomicWriteJSON(allowlistFile, data); }
function _bootstrapDone() {
try { return fs.existsSync(bootstrapSentinel); }
@@ -142,7 +138,7 @@ function createUserStore(opts = {}) {
}
function _writeBootstrapSentinel(adminEmail) {
_atomicWriteJSON(bootstrapSentinel, {
atomicWriteJSON(bootstrapSentinel, {
bootstrappedAt: _nowIso(),
adminEmail: adminEmail.toLowerCase(),
});