Files
Discord-Authenticator/crates
DSH Agent b21ade05eb v0.1.3: make the vault survive a power cut, and recover if it doesn't
The vault was being written with `fs::write(tmp)` → `rename(tmp, path)`. The
rename is atomic, so readers never saw a half-written file — but atomicity is
not durability. Nothing fsynced the new bytes before the rename, so on ext4
(`data=ordered`, the default) a crash or hard power-off could commit the
rename while the data it published never landed. The result is precisely the
failure that stopped the container booting: `vault.json` present, zero bytes,
"EOF while parsing a value at line 1 column 0".

Two things made it worse. The write happened on *every* push tick, so a
ten-account vault was rewriting all of its secrets thousands of times a day,
and there was no backup to fall back on — a damaged file meant a crash loop
with the web UI locked behind the service that would not start.

Storage

  * `store_io::write_atomic` fsyncs the new bytes, renames, then fsyncs the
    directory. That ordering is the whole fix; the module docs explain why.
  * Every vault write refreshes `vault.json.bak`, taken *after* the new file is
    safely in place so the two always agree. Backing up first would leave the
    backup a full change behind and quietly lose the most recent edit.
  * `Vault::load` falls back to the backup when `vault.json` is unparseable,
    moves the damaged file to `vault.json.corrupt-<ts>` rather than deleting
    it, and writes the recovered accounts back so the next boot is ordinary.
    With no usable backup it still refuses to start, as before.

Write volume

  * Discord bookkeeping — message ids, last codes, last errors — moves to its
    own `push-state.json`, batched and flushed at most every five seconds, with
    a final flush on shutdown. Codes roll constantly; accounts change only when
    the operator edits them, and keeping the two together was what put the
    secrets in the path of every rotation.
  * `push-state.json` is a cache by construction: losing or corrupting it costs
    a reposted tile, never a secret, so it is never a reason to refuse to boot.

Tests: recovery from a zero-length vault, backup retention, write-back on
recovery, refusal when both files are bad, reload of tile ids, batching, and
that a rotation leaves `vault.json` byte-identical. 143 passing.
2026-09-20 02:30:22 -04:00
..