Skip to content

Relay privacy over Tor, encrypted account backup, and a recoverable restore - #5

Open
anthonyonazure wants to merge 14 commits into
lnbits:mainfrom
anthonyonazure:integrate/upstream-v0.2.0
Open

anthonyonazure wants to merge 14 commits into
lnbits:mainfrom
anthonyonazure:integrate/upstream-v0.2.0

Conversation

@anthonyonazure

@anthonyonazure anthonyonazure commented Sep 5, 2026 •

Copy link
Copy Markdown

Attribution first

Ten of the twelve commits here are @BIMbeamFLX's work, from their feat/relays-over-tor branch. They had not opened a PR, so this rebases their branch onto current main and adds two commits of my own on top. Authorship is preserved per commit.

What it does

Relay privacy (@BIMbeamFLX). File transfers already go over Tor — transfer.rs refuses any offer without a valid v3 onion. What still went out in the clear were the Nostr relay connections, which let a relay operator tie a home IP to an identity key, its searches, and its published catalogue. This routes those through the bundled Tor as an opt-in mode, defaulting to off.

It fails closed: if Tor will not start, the connection errors rather than silently falling back to clearnet. Two smaller leaks are closed alongside it — the GitHub update check becomes cache-only in privacy mode, and img-src no longer permits arbitrary remote hosts (nothing renders remote images today, so this costs nothing).

Also from @BIMbeamFLX: encrypted account backup and restore via NIP-49, browsing a seeder's full shared library from their profile, sortable result columns, a working file-type filter, seeder and delivery counts, shuffle playback, and plainer status copy.

My two commits address one defect in that backup feature. Restoring a backup overwrote the keyring identity on the same click that submitted the passphrase. The dialog warned in prose, but never named the account being replaced and offered no second chance, so a mistaken restore destroyed an identity that is unrecoverable by design.

  • inspect_identity_backup decrypts only to report whose account a file holds, storing nothing. The destructive import now runs from a separate confirmation step naming both keys.
  • Replacing an account archives the outgoing key to a dated keyring entry first, surfaced as "Previous accounts on this computer" with a switch-back button that itself archives what it displaces. Restore is no longer destructive.
  • Exporting records which account a backup file covers, so the confirmation can state the real consequence: an exported account is merely set aside, while a never-exported one will exist only in that computer's keychain. Each case requires ticking its matching acknowledgement.

Merge notes

Rebasing across the 15 commits to v0.2.0 produced 19 conflicts. Four were substantive:

  • save_settings was restructured upstream; the Tor setting is folded into the new shape.
  • The service wiring gained the mobile service; the Tor handle is passed through it.
  • The format dropdown became an audiobook switch upstream and a file-type filter in the fork. Both meanings now coexist, and the fork's filter still applies through mergeSearchResults.
  • The shared library gained a folder picker, audiobook banners, and paging. Upstream's structure is kept, with the fork's seeder-count and delivery columns re-applied.

Verification

  • 48 Rust tests pass, including 3 new ones covering the preview, the archive records, and the backed-up marker.
  • svelte-check clean across 158 files.
  • cargo clippy --all-targets: 15 lints, every one of which reproduces verbatim on main. This branch adds none.
  • gitleaks clean; osv-scanner reports one fewer advisory than main, and the branch adds no new crates (only the nip49 feature on the existing nostr-sdk).
  • Built and run on macOS arm64 against the live network, and the restore flow driven by hand end to end.

That manual pass found three defects in my own commits, now fixed in b7caefd: the dialog would not close after a successful restore (its dismissal guard also blocked completion), the archived-account list loaded before the native bridge was ready and so stayed permanently empty, and restoring blocked on reconnecting to every relay before returning. None were reachable by the type checker or the test suite — all three were lifecycle and ordering faults that only surfaced by clicking. Worth knowing if you review the earlier commits in isolation.

Still unverified: the "switch back" path from the archived-account list, and the wording of the two acknowledgement variants under a real screen reader.

🤖 Generated with Claude Code

BIMbeamFLX and others added 13 commits August 23, 2026 12:05
Relay traffic previously always used the clearnet, so relay operators
could correlate the user's real IP with their pubkey, catalogue, and
NIP-17 gift-wrap timing while only file transfers were protected.
An opt-in setting now waits for the bundled Tor SOCKS listener and
builds the nostr-sdk client with a proxy targeting all relays.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The webview CSP allowed img-src https: although no view renders remote
images, leaving an open IP-leak channel past Tor for any future markup.
The GitHub release check also raced the settings snapshot and always
fetched clearnet; it now waits for the snapshot and uses only the local
cache while relay privacy mode is on.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Discovery previously ended at full-text search and random picks; the
profile dialog showed name and npub with no way to see the rest of a
seeder's music, although the aggregated remote catalogue already
carries every source pubkey.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
First-run users saw protocol jargon (NIP-17, seeder racing, file IDs,
Connect Nostr) and dead-end empty panes that never explained that the
catalogue only arrives after connecting. Status copy now leads with
plain words and the search/details empty states tell the user what to
do next depending on connection state.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The result ranking was fixed to seeder count and the file-type select
was a disabled placeholder, so a lossless-only collector could neither
sort by size nor narrow a search to FLAC. Name, type, size, and seeder
columns now toggle ascending/descending/default, and the existing
format select filters both network and local searches.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The shared view claimed 'Active peers: 0' for every file because the
UI hardcoded the count and the backend never persisted completed
uploads. Verified deliveries (peer-confirmed TransferComplete after a
full send) are now counted per file in upload_stats, active capability
grants are reported live, and the shared table shows both — the first
signal a sharer gets that their music actually reached anyone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The player offered only stop, folder, and sequential all-tracks
continuation; the queue machinery already existed, so shuffle keeps
the current track first and randomises the rest of the active scope.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Withdrawing a catalogue entry only removes the owner's listing, but
the UI never said so — sharers could believe removal makes a file
disappear from the network. The shared view now counts distinct
foreign seeders per own file from the aggregated remote catalogue and
states the withdrawal reality next to the listing status.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
What Napstr publishes was documented in the protocol but scattered
across three one-line notes in the UI, and nothing told users that
chat, catalogue, and profile hang off one linkable identity. The
profile view now states both lists in plain words, including that kept
downloads are re-shared publicly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A reinstall or new computer silently created a fresh identity and the
published catalogue, chat history, and download negotiations lost
their author, because the keyring was the only copy of the key. The
profile view can now export the identity as a passphrase-encrypted
ncryptsec file and restore from one, following the keys.justworks
model: encryption happens locally and there is deliberately no
recovery path, which the dialog states in plain words.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Restoring a backup overwrote the keyring identity on the same click that
submitted the passphrase. The dialog warned in prose, but never named the
account being lost and gave no second chance, so a mistaken restore silently
destroyed an identity that is unrecoverable by design.

Split the operation. `inspect_identity_backup` decrypts only to report whose
account the file holds and stores nothing; the destructive
`import_identity_backup` now runs from a separate confirmation step that names
both keys, and requires an explicit acknowledgement when an existing account
would be lost. Restoring the account already in use, or restoring onto a fresh
install, skips the warning because nothing is destroyed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…edgement

A confirmation prompt only warns; it does not make the mistake survivable.
Restoring a backup now archives the outgoing key to a second, dated keyring
entry before the new one is written, so replacement is a move rather than a
deletion. Replaced accounts appear under "Previous accounts on this computer"
with a switch-back button, which itself archives the key it displaces, so
switching in either direction destroys nothing.

Exporting a backup records which account the file covers. The confirmation step
reads that marker and states the true consequence: an account with its own
backup file is merely being set aside, while an account that has never been
exported will exist only in this computer's keychain afterwards. Each case
requires ticking the matching "I understand" box before the button enables.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
….2.0

# Conflicts:
#	src-tauri/src/lib.rs
#	src-tauri/src/network.rs
#	src-tauri/src/transfer.rs
#	src/routes/+page.svelte
@anthonyonazure
anthonyonazure marked this pull request as draft September 5, 2026 21:36
…ccounts

Three defects found by driving the restore flow by hand.

The dialog would not close after a successful restore. `closeBackupDialog`
refuses to run while an operation is in flight, which is right for a stray
backdrop click or Escape but wrong for completion, so the restore succeeded
while the interface reported nothing. It now takes an explicit force argument
that only the completion paths pass. The click handlers referenced the function
directly, which would have passed the mouse event as that argument and forced a
close mid-operation, so they now call it explicitly.

The archived-account list never appeared. It loaded once at mount, before the
first snapshot had established that a native bridge exists, so its own guard
skipped it and nothing retried. It now waits for that snapshot.

Restoring an account also blocked on reconnecting to every relay before
returning, leaving the dialog on "Replacing…" for tens of seconds. The identity
is already switched by then, so the reconnect now happens behind the returning
call.

The list also inherited the profile form's button indent, which pushed rows out
of view, and carried no count to distinguish "no archived accounts" from "not
loaded".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants