mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-12 08:27:27 +02:00
Add detailed planning documents for multi-account, multi-login method support in Grimoire: - MULTI_ACCOUNT_LOGIN_PLAN.md: Complete implementation plan covering read-only accounts (npub/nip-05/hex/nprofile), NIP-07 browser extensions, NIP-46 remote signers (bunker URLs), and NIP-55 Android signers. Includes current state analysis, architecture design, login flows, commands specification, UI components, and 4-phase implementation roadmap. - MULTI_ACCOUNT_IMPLEMENTATION_PHASES.md: Step-by-step implementation guide with specific files to create/modify for each phase, testing checklists, and timeline estimates. Breaks down into Phase 1 (read-only accounts), Phase 2 (management UI), Phase 3 (NIP-46), and Phase 4 (NIP-55). - MULTI_ACCOUNT_ARCHITECTURE.md: Visual architecture diagrams and data flow documentation. Covers system architecture, state management, persistence layer, component hierarchy, security boundaries, and performance considerations. The plan integrates with existing applesauce-accounts AccountManager and applesauce-signers library, with special focus on NIP-46 relay pool integration for remote signing.
21 KiB
21 KiB
Multi-Account Architecture Diagram
System Architecture
┌─────────────────────────────────────────────────────────────────────┐
│ User Interface Layer │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────┐ │
│ │ User Menu │ │ Login Dialog │ │ Account Manager Window │ │
│ │ │ │ │ │ │ │
│ │ • Avatar │ │ • Method │ │ • List all accounts │ │
│ │ • Accounts │ │ selection │ │ • Switch / Remove │ │
│ │ • Switch │ │ • Smart │ │ • Labels / Status │ │
│ │ • Add │ │ input │ │ • Connection indicators │ │
│ └──────────────┘ └──────────────┘ └──────────────────────────┘ │
│ │
└───────────────────────────────┬───────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Command Layer │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ /login [identifier] /accounts /logout [--all] │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ parseLoginCommand (opens window) parseLogoutCommand │
│ │
└───────────────────────────────┬───────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Account Management Layer │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ AccountManager (applesauce-accounts) │ │
│ │ │ │
│ │ • accounts$: Observable<Account[]> │ │
│ │ • active$: Observable<Account | null> │ │
│ │ • addAccount(account) │ │
│ │ • removeAccount(account) │ │
│ │ • setActive(account) │ │
│ │ • toJSON() / fromJSON() │ │
│ └────────────────────────┬────────────────────────────────────┘ │
│ │ │
│ ┌────────────┼────────────┬────────────┐ │
│ ▼ ▼ ▼ ▼ │
│ ┌────────────────┐ ┌─────────┐ ┌─────────┐ ┌──────────┐ │
│ │ ReadOnlyAccount│ │Extension│ │ Remote │ │ Android │ │
│ │ │ │ Account │ │ Signer │ │ Signer │ │
│ │ • pubkey │ │ │ │ Account │ │ Account │ │
│ │ • signer=null │ │ NIP-07 │ │ NIP-46 │ │ NIP-55 │ │
│ │ • metadata │ └─────────┘ └─────────┘ └──────────┘ │
│ │ - source │ │ │ │ │
│ │ - nip05 │ │ │ │ │
│ └────────────────┘ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌────────────────────────────────────┐ │
│ │ Signer Layer │ │
│ │ (applesauce-signers) │ │
│ │ │ │
│ │ • getPublicKey() │ │
│ │ • signEvent(event) │ │
│ │ • nip04/nip44 encrypt/decrypt │ │
│ └────────────┬───────────────────────┘ │
│ │ │
└─────────────────────────────────┼────────────────────────────────────┘
│
┌────────────────┼────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ window.nostr │ │ Relay Pool │ │Android Intent│
│ │ │ │ │ │
│ (NIP-07) │ │ (NIP-46) │ │ (NIP-55) │
└──────────────┘ └──────────────┘ └──────────────┘
Data Flow
1. Login Flow
User Input (npub, nip05, bunker://, etc.)
│
▼
detectLoginInputType()
│
▼
createAccountFromInput()
│
├─ npub ────────────────────► ReadOnlyAccount.fromNpub()
├─ nip05 ───────────────────► ReadOnlyAccount.fromNip05()
├─ hex ─────────────────────► ReadOnlyAccount.fromHex()
├─ nprofile ────────────────► ReadOnlyAccount.fromNprofile()
├─ bunker:// ───────────────► RemoteSignerAccount.fromBunkerUrl()
└─ (empty) ─────────────────► Show LoginDialog
│
▼
User selects method
│
▼
Create appropriate account
│
▼
accountManager.addAccount()
│
▼
accountManager.setActive()
│
▼
active$ emits new account
│
▼
useAccountSync() receives update
│
▼
Update GrimoireState.activeAccount
2. Account Switching Flow
User clicks account in menu
│
▼
accountManager.setActive(account)
│
▼
active$ emits account
│
▼
useAccountSync() hook listens
│
▼
Load relays for account (NIP-65 or cache)
│
▼
setActiveAccount({
pubkey,
relays,
accountType,
label
})
│
▼
Jotai state updates
│
▼
UI re-renders with new active account
3. Event Signing Flow
User wants to publish event
│
▼
Get active account from accountManager
│
├─ No account ──────────────► Show login prompt
│
▼
Check if account has signer
│
├─ No signer (read-only) ───► Show error + upgrade prompt
│
▼
account.signer.signEvent(event)
│
├─ ExtensionSigner ─────────► window.nostr.signEvent()
├─ Nip46Signer ─────────────► Send request via relay
└─ Nip55Signer ─────────────► Send Android intent
│
▼
User approves (if needed)
│
▼
Signed event returned
│
▼
Publish to relays
4. NIP-46 Connection Flow
RemoteSignerAccount.fromBunkerUrl(url)
│
▼
Parse bunker:// URL
• pubkey (remote signer)
• relays
• secret (optional)
│
▼
Create Nip46Signer({
remotePubkey,
relays,
pool: RelayPool (singleton)
})
│
▼
signer.connect()
│
├─ Connect to relays
├─ Subscribe to response events
└─ Send connect request
│
▼
Remote signer responds
│
▼
Connection established
│
▼
signer.getPublicKey()
│
▼
Create RemoteSignerAccount
│
▼
Monitor connection status
(connected/disconnected/connecting)
State Management
Jotai State (UI State)
GrimoireState {
activeAccount?: {
pubkey: string,
relays: RelayInfo[],
accountType: 'readonly' | 'extension' | 'remote' | 'android',
label?: string
}
}
AccountManager State (Account State)
AccountManager {
accounts$: Observable<Account[]>, // All accounts
active$: Observable<Account | null> // Active account
}
Sync Hook (Bridge)
useAccountSync() {
// Listens to: accountManager.active$
// Updates: grimoireState.activeAccount
// Loads: relays from NIP-65 or cache
}
Persistence
LocalStorage Keys
┌───────────────────────────────────────────────┐
│ localStorage │
├───────────────────────────────────────────────┤
│ │
│ "nostr-accounts": { │
│ accounts: [ │
│ { │
│ id: "readonly:abc123...", │
│ pubkey: "abc123...", │
│ metadata: { │
│ type: "readonly", │
│ source: "npub", │
│ originalInput: "npub1..." │
│ } │
│ }, │
│ { │
│ id: "remote:def456...", │
│ pubkey: "def456...", │
│ metadata: { │
│ type: "remote", │
│ relays: ["wss://..."], │
│ remotePubkey: "xyz789..." │
│ } │
│ } │
│ ] │
│ } │
│ │
│ "active-account": "readonly:abc123..." │
│ │
└───────────────────────────────────────────────┘
Initialization Sequence
1. App starts
▼
2. Load AccountManager from localStorage
▼
3. Register all account types
▼
4. Deserialize accounts from JSON
▼
5. For each RemoteSignerAccount:
• Recreate Nip46Signer
• Connect to relays (async)
▼
6. Set active account from localStorage
▼
7. useAccountSync() hook activates
▼
8. Load relays for active account
▼
9. Update GrimoireState
▼
10. UI renders with active account
Component Hierarchy
App
│
├─ UserMenu
│ ├─ AccountList (dropdown)
│ │ ├─ AccountItem (active)
│ │ ├─ AccountItem
│ │ └─ AccountItem
│ ├─ AddAccountButton
│ └─ LogoutButton
│
├─ LoginDialog (modal)
│ ├─ MethodSelector
│ │ ├─ ExtensionButton
│ │ ├─ ReadOnlyButton
│ │ ├─ RemoteSignerButton
│ │ └─ AndroidButton
│ └─ SmartInput (auto-detect format)
│
└─ Windows
└─ AccountManager (window app)
├─ AccountList
│ ├─ AccountCard
│ │ ├─ Avatar
│ │ ├─ Info (name, type, status)
│ │ └─ Actions (switch, remove, edit)
│ └─ ...
└─ AddAccountButton
Error Handling Strategy
┌──────────────────────────────────────────────────────┐
│ Error Type │ Handler │
├──────────────────────────────────────────────────────┤
│ Invalid input │ Toast + suggest valid format│
│ NIP-05 failed │ Toast + retry option │
│ Extension not found │ Toast + install link │
│ NIP-46 connect fail │ Toast + retry + manual │
│ Read-only sign │ Toast + upgrade prompt │
│ Signer rejected │ Toast + info │
│ Network error │ Toast + retry │
└──────────────────────────────────────────────────────┘
Security Boundaries
┌─────────────────────┐
│ Grimoire App │
│ (Trusted) │
└──────────┬──────────┘
│
┌─────────────────────┼─────────────────────┐
│ │ │
▼ ▼ ▼
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
│ Browser Ext │ │ Remote Signer │ │ Android App │
│ (Semi-Trusted) │ │ (Trusted) │ │ (Trusted) │
│ │ │ │ │ │
│ • Has keys │ │ • Has keys │ │ • Has keys │
│ • User approves│ │ • User approves│ │ • User approves│
│ • Sandboxed │ │ • Remote │ │ • Separate dev │
└────────────────┘ └────────────────┘ └────────────────┘
│ │ │
│ │ │
└─────────────────────┴─────────────────────┘
│
Never stored in Grimoire
(except read-only pubkeys)
Performance Considerations
Optimization Points
-
Account List Rendering:
- Use virtual scrolling for 100+ accounts
- Memoize account components
- Lazy load profile metadata
-
NIP-46 Connections:
- Maintain persistent connections
- Connection pooling for multiple accounts
- Reconnect with exponential backoff
-
Profile Loading:
- Cache profile metadata in Dexie
- Batch profile requests
- Use stale-while-revalidate pattern
-
Account Switching:
- Instant UI update (optimistic)
- Load relays in background
- Cancel in-flight requests from previous account
Observability
Events to Log
// Account lifecycle
logger.info("account.added", { accountId, type });
logger.info("account.removed", { accountId });
logger.info("account.switched", { fromId, toId });
// NIP-46 connection
logger.info("nip46.connecting", { accountId, relays });
logger.info("nip46.connected", { accountId, duration });
logger.error("nip46.failed", { accountId, error });
// Signing
logger.info("sign.requested", { accountId, kind });
logger.info("sign.completed", { accountId, eventId });
logger.error("sign.rejected", { accountId, reason });
Metrics to Track
- Account count by type
- Active account switch frequency
- NIP-46 connection success rate
- Sign request success rate
- Average connection duration
Migration Path
Existing Users
Current state (v1):
{
"active-account": "abc123...",
"nostr-accounts": {
"accounts": [
{
"id": "abc123...",
"pubkey": "abc123...",
"type": "extension" // Old format
}
]
}
}
After migration (v2):
{
"active-account": "extension:abc123...",
"nostr-accounts": {
"accounts": [
{
"id": "extension:abc123...", // New ID format
"pubkey": "abc123...",
"metadata": {
"type": "extension" // New metadata format
}
}
]
}
}
Migration code in src/services/accounts.ts:
// Detect old format and migrate
if (!account.metadata && account.type) {
account.metadata = { type: account.type };
delete account.type;
}
This architecture provides a clean separation of concerns, maintains security boundaries, and scales to support multiple login methods while keeping the implementation straightforward.