mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2026-11-07 21:32:25 +00:00
Add Nostr sync reset/fresh namespace workflow and docs
This commit is contained in:
1 parent
24aa3f12a2
commit
b9d6c0d49d
10 files changed
+328
-3
No files matched your search
+115
-2
@@ -424,8 +424,16 @@ def handle_post_to_nostr(
|
||||
print(f" delta: {did}")
|
||||
logging.info("Encrypted index posted to Nostr successfully.")
|
||||
else:
|
||||
print(colored("\N{CROSS MARK} Sync failed…", "red"))
|
||||
logging.error("Failed to post encrypted index to Nostr.")
|
||||
client = getattr(password_manager, "nostr_client", None)
|
||||
detail = (
|
||||
getattr(client, "last_error", None) if client is not None else None
|
||||
)
|
||||
if detail:
|
||||
print(colored(f"\N{CROSS MARK} Sync failed… {detail}", "red"))
|
||||
logging.error("Failed to post encrypted index to Nostr: %s", detail)
|
||||
else:
|
||||
print(colored("\N{CROSS MARK} Sync failed…", "red"))
|
||||
logging.error("Failed to post encrypted index to Nostr.")
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to post to Nostr: {e}", exc_info=True)
|
||||
print(colored(f"Error: Failed to post to Nostr: {e}", "red"))
|
||||
@@ -599,6 +607,105 @@ def handle_reset_relays(password_manager: PasswordManager) -> None:
|
||||
pause()
|
||||
|
||||
|
||||
def _get_profile_state_manager(password_manager: PasswordManager):
|
||||
"""Return the active profile StateManager or create one from fingerprint dir."""
|
||||
state_mgr = getattr(password_manager, "state_manager", None)
|
||||
if state_mgr is not None:
|
||||
return state_mgr
|
||||
fingerprint_dir = getattr(password_manager, "fingerprint_dir", None)
|
||||
if fingerprint_dir is None:
|
||||
raise ValueError("State manager unavailable for current profile.")
|
||||
from seedpass.core.state_manager import StateManager
|
||||
|
||||
return StateManager(Path(fingerprint_dir))
|
||||
|
||||
|
||||
def _clear_nostr_runtime_state(password_manager: PasswordManager) -> None:
|
||||
"""Clear in-memory Nostr sync metadata for the active profile."""
|
||||
password_manager.manifest_id = None
|
||||
password_manager.delta_since = 0
|
||||
password_manager.last_sync_ts = 0
|
||||
client = getattr(password_manager, "nostr_client", None)
|
||||
if client is not None:
|
||||
if hasattr(client, "last_error"):
|
||||
client.last_error = None
|
||||
if hasattr(client, "current_manifest_id"):
|
||||
client.current_manifest_id = None
|
||||
if hasattr(client, "current_manifest"):
|
||||
client.current_manifest = None
|
||||
if hasattr(client, "_delta_events"):
|
||||
client._delta_events = []
|
||||
|
||||
|
||||
def handle_reset_nostr_sync_state(password_manager: PasswordManager) -> None:
|
||||
"""Reset Nostr sync state for the active profile without changing key index."""
|
||||
if not confirm_action(
|
||||
"Reset Nostr sync state for this profile (manifest/delta metadata)? (Y/N): "
|
||||
):
|
||||
print(colored("Nostr sync-state reset cancelled.", "yellow"))
|
||||
pause()
|
||||
return
|
||||
try:
|
||||
state_mgr = _get_profile_state_manager(password_manager)
|
||||
state = state_mgr.state
|
||||
state_mgr.update_state(manifest_id=None, delta_since=0, last_sync_ts=0)
|
||||
_clear_nostr_runtime_state(password_manager)
|
||||
password_manager.nostr_account_idx = int(state.get("nostr_account_idx", 0))
|
||||
print(colored("Nostr sync state reset for current profile.", "green"))
|
||||
logging.info(
|
||||
"Nostr sync state reset for profile %s",
|
||||
getattr(password_manager, "current_fingerprint", "unknown"),
|
||||
)
|
||||
except Exception as exc:
|
||||
logging.error("Failed to reset Nostr sync state: %s", exc, exc_info=True)
|
||||
print(colored(f"Error resetting Nostr sync state: {exc}", "red"))
|
||||
finally:
|
||||
pause()
|
||||
|
||||
|
||||
def handle_start_fresh_nostr_namespace(password_manager: PasswordManager) -> None:
|
||||
"""Increment Nostr account index and reset sync metadata for fresh publishing."""
|
||||
if not confirm_action(
|
||||
"Start a fresh Nostr namespace (new deterministic key index) for this profile? (Y/N): "
|
||||
):
|
||||
print(colored("Fresh Nostr namespace action cancelled.", "yellow"))
|
||||
pause()
|
||||
return
|
||||
try:
|
||||
state_mgr = _get_profile_state_manager(password_manager)
|
||||
state = state_mgr.state
|
||||
next_idx = int(state.get("nostr_account_idx", 0)) + 1
|
||||
state_mgr.update_state(
|
||||
manifest_id=None,
|
||||
delta_since=0,
|
||||
last_sync_ts=0,
|
||||
nostr_account_idx=next_idx,
|
||||
)
|
||||
_clear_nostr_runtime_state(password_manager)
|
||||
password_manager.nostr_account_idx = next_idx
|
||||
|
||||
reinit = getattr(password_manager, "_initialize_nostr_client", None)
|
||||
if callable(reinit):
|
||||
_safe_close_client_pool(password_manager)
|
||||
reinit()
|
||||
print(
|
||||
colored(
|
||||
f"Started fresh Nostr namespace at account index {next_idx}.",
|
||||
"green",
|
||||
)
|
||||
)
|
||||
logging.info(
|
||||
"Started fresh Nostr namespace for profile %s at account index %s",
|
||||
getattr(password_manager, "current_fingerprint", "unknown"),
|
||||
next_idx,
|
||||
)
|
||||
except Exception as exc:
|
||||
logging.error("Failed to start fresh Nostr namespace: %s", exc, exc_info=True)
|
||||
print(colored(f"Error starting fresh Nostr namespace: {exc}", "red"))
|
||||
finally:
|
||||
pause()
|
||||
|
||||
|
||||
def handle_set_inactivity_timeout(password_manager: PasswordManager) -> None:
|
||||
"""Change the inactivity timeout for the current seed profile."""
|
||||
cfg_mgr = password_manager.config_manager
|
||||
@@ -962,6 +1069,8 @@ def handle_nostr_menu(password_manager: PasswordManager) -> None:
|
||||
print(color_text("5. Remove a relay by number", "menu"))
|
||||
print(color_text("6. Reset to default relays", "menu"))
|
||||
print(color_text("7. Display Nostr Public Key", "menu"))
|
||||
print(color_text("8. Reset Nostr sync state", "menu"))
|
||||
print(color_text("9. Start fresh Nostr namespace (new key index)", "menu"))
|
||||
choice = input("Select an option or press Enter to go back: ").strip()
|
||||
password_manager.update_activity()
|
||||
try:
|
||||
@@ -979,6 +1088,10 @@ def handle_nostr_menu(password_manager: PasswordManager) -> None:
|
||||
handle_reset_relays(password_manager)
|
||||
elif choice == "7":
|
||||
handle_display_npub(password_manager)
|
||||
elif choice == "8":
|
||||
handle_reset_nostr_sync_state(password_manager)
|
||||
elif choice == "9":
|
||||
handle_start_fresh_nostr_namespace(password_manager)
|
||||
elif not choice:
|
||||
break
|
||||
else:
|
||||
|
||||
@@ -3967,6 +3967,12 @@ class PasswordManager:
|
||||
"delta_ids": list(delta_ids),
|
||||
}
|
||||
except Exception as e:
|
||||
client = getattr(self, "nostr_client", None)
|
||||
if client is not None and hasattr(client, "last_error"):
|
||||
try:
|
||||
client.last_error = str(e)
|
||||
except Exception:
|
||||
pass
|
||||
logging.error(f"Failed to sync vault: {e}", exc_info=True)
|
||||
return None
|
||||
|
||||
|
||||
@@ -24,3 +24,23 @@ def test_start_background_sync_offline(monkeypatch):
|
||||
pm.sync_index_from_nostr = lambda: called.__setitem__("sync", True)
|
||||
PasswordManager.start_background_sync(pm)
|
||||
assert called["sync"] is False
|
||||
|
||||
|
||||
def test_sync_vault_sets_last_error_on_exception():
|
||||
pm = PasswordManager.__new__(PasswordManager)
|
||||
pm.offline_mode = False
|
||||
pm.get_encrypted_data = lambda: b"data"
|
||||
pm.state_manager = None
|
||||
|
||||
async def boom(_data):
|
||||
raise RuntimeError("relay write failed")
|
||||
|
||||
pm.nostr_client = SimpleNamespace(
|
||||
publish_snapshot=boom,
|
||||
get_delta_events=lambda: [],
|
||||
last_error=None,
|
||||
)
|
||||
|
||||
result = PasswordManager.sync_vault(pm)
|
||||
assert result is None
|
||||
assert pm.nostr_client.last_error == "relay write failed"
|
||||
@@ -31,6 +31,16 @@ def test_handle_post_failure(capsys):
|
||||
assert "❌ Sync failed…" in out
|
||||
|
||||
|
||||
def test_handle_post_failure_shows_nostr_error(capsys):
|
||||
pm = SimpleNamespace(
|
||||
sync_vault=lambda alt_summary=None: None,
|
||||
nostr_client=SimpleNamespace(last_error="Manifest out of date"),
|
||||
)
|
||||
main.handle_post_to_nostr(pm)
|
||||
out = capsys.readouterr().out
|
||||
assert "❌ Sync failed… Manifest out of date" in out
|
||||
|
||||
|
||||
def test_handle_post_prints_all_ids(capsys):
|
||||
pm = SimpleNamespace(
|
||||
sync_vault=lambda alt_summary=None: {
|
||||
|
||||
@@ -17,6 +17,7 @@ import main
|
||||
from nostr.client import DEFAULT_RELAYS
|
||||
from seedpass.core.config_manager import ConfigManager
|
||||
from seedpass.core.manager import Notification, PasswordManager
|
||||
from seedpass.core.state_manager import StateManager
|
||||
from seedpass.core.vault import Vault
|
||||
from seedpass.core.errors import SeedPassError
|
||||
from utils.fingerprint_manager import FingerprintManager
|
||||
@@ -263,3 +264,97 @@ def test_settings_import_missing_path_message(monkeypatch, capsys):
|
||||
|
||||
out = capsys.readouterr().out
|
||||
assert "Import failed: file 'missing.json.enc' not found." in out
|
||||
|
||||
|
||||
def test_reset_nostr_sync_state(monkeypatch):
|
||||
with TemporaryDirectory() as tmpdir:
|
||||
tmp_path = Path(tmpdir)
|
||||
pm, _, _ = setup_pm(tmp_path, monkeypatch)
|
||||
fp_dir = tmp_path / ".seedpass" / "fp"
|
||||
state_mgr = StateManager(fp_dir)
|
||||
state_mgr.update_state(
|
||||
manifest_id="manifest-old",
|
||||
delta_since=123,
|
||||
last_sync_ts=456,
|
||||
nostr_account_idx=2,
|
||||
)
|
||||
pm.state_manager = state_mgr
|
||||
pm.fingerprint_dir = fp_dir
|
||||
pm.current_fingerprint = "fp"
|
||||
pm.manifest_id = "manifest-old"
|
||||
pm.delta_since = 123
|
||||
pm.last_sync_ts = 456
|
||||
|
||||
monkeypatch.setattr(main, "confirm_action", lambda *_: True)
|
||||
monkeypatch.setattr(main, "pause", lambda: None)
|
||||
main.handle_reset_nostr_sync_state(pm)
|
||||
|
||||
state = state_mgr.state
|
||||
assert state["manifest_id"] is None
|
||||
assert state["delta_since"] == 0
|
||||
assert state["last_sync_ts"] == 0
|
||||
assert state["nostr_account_idx"] == 2
|
||||
assert pm.manifest_id is None
|
||||
assert pm.delta_since == 0
|
||||
assert pm.last_sync_ts == 0
|
||||
assert pm.nostr_account_idx == 2
|
||||
|
||||
|
||||
def test_start_fresh_nostr_namespace(monkeypatch):
|
||||
with TemporaryDirectory() as tmpdir:
|
||||
tmp_path = Path(tmpdir)
|
||||
pm, _, _ = setup_pm(tmp_path, monkeypatch)
|
||||
fp_dir = tmp_path / ".seedpass" / "fp"
|
||||
state_mgr = StateManager(fp_dir)
|
||||
state_mgr.update_state(
|
||||
manifest_id="manifest-old",
|
||||
delta_since=123,
|
||||
last_sync_ts=456,
|
||||
nostr_account_idx=2,
|
||||
)
|
||||
pm.state_manager = state_mgr
|
||||
pm.fingerprint_dir = fp_dir
|
||||
pm.current_fingerprint = "fp"
|
||||
pm.manifest_id = "manifest-old"
|
||||
pm.delta_since = 123
|
||||
pm.last_sync_ts = 456
|
||||
called = {"reinit": 0}
|
||||
pm._initialize_nostr_client = lambda: called.__setitem__(
|
||||
"reinit", called["reinit"] + 1
|
||||
)
|
||||
|
||||
monkeypatch.setattr(main, "confirm_action", lambda *_: True)
|
||||
monkeypatch.setattr(main, "pause", lambda: None)
|
||||
main.handle_start_fresh_nostr_namespace(pm)
|
||||
|
||||
state = state_mgr.state
|
||||
assert state["manifest_id"] is None
|
||||
assert state["delta_since"] == 0
|
||||
assert state["last_sync_ts"] == 0
|
||||
assert state["nostr_account_idx"] == 3
|
||||
assert pm.nostr_account_idx == 3
|
||||
assert called["reinit"] == 1
|
||||
|
||||
|
||||
def test_nostr_menu_dispatches_reset_sync_state(monkeypatch):
|
||||
with TemporaryDirectory() as tmpdir:
|
||||
tmp_path = Path(tmpdir)
|
||||
pm, _, _ = setup_pm(tmp_path, monkeypatch)
|
||||
pm.update_activity = lambda: None
|
||||
inputs = iter(["8", ""])
|
||||
with patch("main.handle_reset_nostr_sync_state") as handler:
|
||||
with patch("builtins.input", side_effect=lambda *_: next(inputs)):
|
||||
main.handle_nostr_menu(pm)
|
||||
handler.assert_called_once_with(pm)
|
||||
|
||||
|
||||
def test_nostr_menu_dispatches_fresh_namespace(monkeypatch):
|
||||
with TemporaryDirectory() as tmpdir:
|
||||
tmp_path = Path(tmpdir)
|
||||
pm, _, _ = setup_pm(tmp_path, monkeypatch)
|
||||
pm.update_activity = lambda: None
|
||||
inputs = iter(["9", ""])
|
||||
with patch("main.handle_start_fresh_nostr_namespace") as handler:
|
||||
with patch("builtins.input", side_effect=lambda *_: next(inputs)):
|
||||
main.handle_nostr_menu(pm)
|
||||
handler.assert_called_once_with(pm)
|
||||
Reference in new issue
Block a user