Research report on the most frictionless "sign in with your Nostr signer" flow for node login (NIP-07 extension + NIP-46 QR scan with Amber, password always kept as fallback), and a plan for adding existing Nostr identities (nsec import / npub watch-only / browser extension) to the Nostr Identities screen. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
83 lines
4.3 KiB
Markdown
83 lines
4.3 KiB
Markdown
# Add an existing Nostr identity to the node — UX & implementation plan
|
|
|
|
**Status:** plan only (2026-07-16), no code. Companion research: `docs/nostr-signer-login-research.md`.
|
|
|
|
## Where it lives
|
|
|
|
The **Nostr Identities** screen (`Web5Identities.vue`, backed by `identity.list` /
|
|
`identity.create`). Today every identity is **seed-derived** (`identity_manager.rs`
|
|
derives ed25519 + nostr keys from the BIP-39 master seed at an index). "Add existing"
|
|
introduces a second class of identity: one whose key material comes from *outside* the
|
|
seed.
|
|
|
|
## Two import kinds (both needed, different guarantees)
|
|
|
|
1. **Full import (nsec)** — the node holds the secret key. The identity behaves exactly
|
|
like a seed-derived one (can sign in embedded apps, publish, encrypt). NOT covered by
|
|
seed backup — flag it visibly and include it in the encrypted node backup.
|
|
2. **Linked signer (npub only)** — the node stores just the public key; signing is
|
|
delegated to the user's own signer (browser extension NIP-07, or a NIP-46 remote
|
|
signer later). Zero key custody; some features (background publishing) unavailable —
|
|
the UI should badge what works.
|
|
|
|
## The UX (matching the house style)
|
|
|
|
**Entry point:** next to "Create identity" on Nostr Identities, an **"Add existing"**
|
|
glass-button. Opens a modal with three tabs (same tab pattern as the send/receive
|
|
modals):
|
|
|
|
1. **Browser extension** (default when `window.nostr` exists)
|
|
- One button: "Connect with extension". Flow: `getPublicKey()` → show the npub +
|
|
resolved profile (kind-0 fetched via the node's relays: avatar, name — instant
|
|
recognition) → "Add this identity".
|
|
- Creates a **linked signer** identity. A challenge signature
|
|
(`signEvent` on a throwaway event) proves key possession before adding — never add
|
|
an unverified npub as "yours".
|
|
2. **Secret key (nsec)**
|
|
- Paste field (masked, `nsec1…` or hex), inline validation + derived npub preview
|
|
with the same kind-0 profile card before confirming.
|
|
- Scary-clear copy: "Your key will be stored on this node, encrypted at rest. It is
|
|
NOT part of your seed backup — back it up separately." Confirm step requires the
|
|
profile card to load or an explicit "add anyway".
|
|
- Creates a **full** identity.
|
|
3. **Public key (npub)** — watch-only
|
|
- Paste an npub for a linked identity without any signer attached yet (useful to
|
|
reserve the profile, upgrade to extension/NIP-46 signing later).
|
|
|
|
**After adding:** the identity appears in the same grid with a small origin badge —
|
|
`seed` / `imported` / `linked` — and the imported profile picture/name pulled from
|
|
relays. Everything else (picker in apps, rename, avatar) behaves uniformly.
|
|
|
|
**Removal:** existing delete flow; for `imported` identities the confirm dialog warns
|
|
the key is destroyed unless exported first (offer "Export nsec" in the identity's detail
|
|
sheet, gated behind password re-entry).
|
|
|
|
## Backend work
|
|
|
|
- `identity_manager.rs`: identity records gain `origin: Seed { index } | Imported |
|
|
Linked`, optional `nostr_secret_hex` absent for Linked. Storage: reuse the existing
|
|
encrypted identity file; imported secrets included in node backup.
|
|
- New RPCs:
|
|
- `identity.import-nostr` `{ nsec | npub, name?, verify_sig? }` → validates, derives
|
|
npub, rejects duplicates (same pubkey as any existing identity), returns the new
|
|
identity.
|
|
- `identity.fetch-profile` `{ pubkey }` → kind-0 lookup via `nostr_relays.rs` for the
|
|
preview card (frontend could also do this, but the node already has relay plumbing
|
|
and avoids CORS).
|
|
- `identity.nostr-sign` (used by the iframe NIP-07 bridge): for `Linked` identities
|
|
return a typed error the bridge translates into "ask the user's extension instead" —
|
|
phase 2; phase 1 simply hides linked identities from the in-app signer picker.
|
|
|
|
## Demo mode
|
|
|
|
Mock `identity.import-nostr` + `identity.fetch-profile` in mock-backend.js (canned
|
|
profile: picture + name for any pasted npub) so the whole add-existing flow is
|
|
demoable without real relays.
|
|
|
|
## Phasing
|
|
|
|
1. **Phase 1 (small):** nsec + npub tabs, origin badges, backup inclusion, mock.
|
|
2. **Phase 2:** extension tab with possession-proof + kind-0 preview cards everywhere.
|
|
3. **Phase 3:** NIP-46 remote-signer identities + login integration (shares the QR
|
|
plumbing from the signer-login work).
|