refactor(persistence): migrate user-store to canonical atomic-write util (DC-101) [glm-grade=A]
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:
@@ -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(),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user