Skip to content

fix(storage): preferences and automation rules are lost across an identity change #355

Description

@kNoAPP

Part of #353, but a live bug today — worth fixing regardless of whether the rest of that epic ships.

Problem or Motivation

Restoring a backup with the identity onto a radio whose current public key differs silently discards the restored preferences and automation rules. That is precisely the flagship recovery scenario from #299: the radio died, and the backup is being restored onto a replacement.

The cause is ordering. session.ts:127 awaits flushSessionAsync before importPrivateKey at session.ts:134:

const persisted = await flushSessionAsync(client);   // writes under the OLD pubkey
...
await client.importPrivateKey(key);                  // identity changes here

flushSessionAsync reads client.selfInfo.pubkey and the module-level storageKey (persistence.ts:117-129), both bound at connect from the pre-import identity. So all four blobs are written into the old identity's namespace, encrypted under the old radio-derived key.

The identity then swaps and the radio reboots. The dropped link routes to beginReconnect, not teardownSession (useMeshCore.ts:460), so store.reset() never runs and the in-memory data survives. On reconnect the app derives a fresh key and namespace from the new public key and hydrates an empty record. From there the four blobs diverge:

  • msgHistory survives, by accident. restoreHistory merges rather than replaces (meshStore.ts:1468) and always returns a fresh object, which trips the save subscription's state.msgHistory !== prev.msgHistory check and rewrites it under the new key.
  • advertCache survives — merged, then written explicitly by flushAdvertCache(c).
  • preferences are lost. A null blob coerces to {} and every field normalizes to its default (meshStore.ts:1338).
  • automationRules are lost. restoreAutomationRules(rules ?? []) is a plain set({ automationRules }) (meshStore.ts:1718); the ?? [] wipes them.

Two further consequences:

  • The write leaves an orphaned encrypted record under the old public key that nothing ever collects.
  • applyBackup returns persisted: true, and the UI reports success, for a write that landed in a namespace the user will never read again.

Proposed Solution

Make a deliberate public-key change carry the browser data with it, rather than flushing into the outgoing namespace.

  • Do not flush before the identity write when opts.restoreIdentity is set and the backup's identity differs from the live one. The in-memory store already survives the reconnect; the flush is what misfires.
  • After the reconnect binds the new key, write all four blobs under the new namespace — not only the two that currently survive by side effect. The hydrate must not normalize preferences and automation rules to defaults when the store already holds restored values for them; the existing explicit flag on restorePreferences is the precedent for distinguishing the two cases.
  • Delete the old namespace's records once the new ones are written, so a deliberate identity change does not leave a readable orphan behind.
  • ApplyResult.persisted must describe the namespace the user will actually read from.

This is a prerequisite for the seed-identity wizard, which performs the same import-then-reboot sequence on every run.

Verification

Restore a backup containing an identity, preferences and automation rules onto a radio with a different current identity. After the reboot and reconnect, all four blobs must be present, and the old record must be gone.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions