From c88f0d860c089e49cebabd53d652859d04143457 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Fri, 16 Oct 2026 12:54:48 -0700 Subject: names: refuse a corrupt state file; flush before replacing it The loader treated any read or parse error as a fresh registrar and started with no names, then persisted that empty state over the real one. A crash can leave the state file truncated, because persist() renamed the new file into place without flushing it. Now a missing file is a fresh start, an unreadable one stops startup, and persist() fsyncs the file and its directory around the rename. Co-Authored-By: Claude Opus 5.5 --- names/server.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/names/server.js b/names/server.js index 104aa5e..dacbda6 100644 --- a/names/server.js +++ b/names/server.js @@ -15,7 +15,7 @@ // // Deliberately dumb: one JSON state file, no accounts, no email. -import { readFileSync, writeFileSync, renameSync, mkdirSync } from 'node:fs'; +import { readFileSync, writeFileSync, renameSync, mkdirSync, openSync, fsyncSync, closeSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { verifyEvent, finalizeEvent, generateSecretKey, getPublicKey } from 'nostr-tools/pure'; import * as nip44 from 'nostr-tools/nip44'; @@ -65,14 +65,21 @@ const NAME_RE = /^[a-z0-9][a-z0-9._-]{0,29}$/; const NEW_NAME_RE = /^[a-z0-9]{2,24}$/; const NEW_NAME_ERR = 'names can only have letters and numbers (2–24)'; +// A missing file is a fresh registrar; an unreadable one is not. Starting +// empty over a corrupt file would persist the empty state on top of it. let state = (() => { - try { return JSON.parse(readFileSync(STATE, 'utf8')); } catch { return { names: {} }; } + let raw; + try { raw = readFileSync(STATE, 'utf8'); } catch (e) { if (e.code === 'ENOENT') return { names: {} }; throw e; } + return JSON.parse(raw); })(); function persist() { mkdirSync(dirname(STATE), { recursive: true }); const tmp = STATE + '.tmp'; writeFileSync(tmp, JSON.stringify(state)); + // flush before the rename, or a crash can leave a renamed-but-empty file + const fd = openSync(tmp, 'r'); fsyncSync(fd); closeSync(fd); renameSync(tmp, STATE); + const dfd = openSync(dirname(STATE), 'r'); fsyncSync(dfd); closeSync(dfd); } // --------------------------------------------------------------------------- -- cgit v1.2.3