initialize() only filled npub when it was empty, so an existing node
with a stale NSEC still lingering in its env/blob kept that value's
derived npub even after the vault took ownership of a different nsec.
The node then held the vault's private key but announced the old env
key's public key — a split identity that anything reading settings.npub
would broadcast.
npub is a pure derivation of nsec and is never configured on its own, so
derive it from the live (vault) nsec and override rather than only fill.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two ways a stale legacy NSEC could override or resurrect an nsec the
vault already owns (issue #553):
- initialize() re-applied env/blob values onto live settings after
bootstrap had decrypted the authoritative nsec, so a stale NSEC left
in .env would clobber it on restart (e.g. after rotating the key in
the admin UI). _apply_to_live_settings now never re-applies secret
fields; bootstrap_secrets is their only writer.
- An empty encrypted_nsec could not distinguish "never migrated" from
"intentionally cleared", so clearing the identity via the admin API
and restarting re-imported the old NSEC from env/blob. Record vault
ownership in a new secrets.nsec_managed column (set on legacy import
and on every set_nsec write); bootstrap skips the legacy import once
the vault owns the nsec, so a cleared identity stays cleared.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When the nsec lives only in the encrypted Secret store (env carries no NSEC) and
the settings blob holds no npub, bootstrap_secrets decrypted the nsec and derived
the npub into memory, but SettingsService.initialize then re-derived settings
from the npub-less blob and overwrote the live npub back to empty — leaving a
private key with no matching public key, so the node silently stopped announcing
a usable Nostr identity.
Derive npub from the live nsec during initialize when the merged settings carry
none, so the public key stays consistent with the identity and is persisted to
the blob.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Encryption of the Nostr identity at rest is mandatory. When a legacy nsec is
present (env or blob) but no ROUTSTR_SECRET_KEY is set, bootstrap previously
fell into vault.encrypt and surfaced its generic "key not set" error. Raise an
explicit, nsec-contextual error first so the boot failure is intentional and
actionable — it names the missing key and prints the generation command —
rather than relying on vault throwing incidentally. No secret is dropped: the
node refuses to start until the operator sets the key.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Persist and load admin password and nsec from the encrypted Secret store on
boot: generate a temporary admin password on first run (logged once), encrypt
a provided nsec, and fail fast if a stored nsec cannot be decrypted with the
current key. Stop clobbering live secret settings with empty env values.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>