From 692ecb1452ccb04d314aa4c6f7a67fd6d048db8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nu=C5=A1a=20Puk=C5=A1i=C4=8D?= Date: Tue, 9 Dec 2025 18:24:55 +0100 Subject: [PATCH] Fix signer --- .../nostr/amber_connect_controller.js | 9 +- assets/controllers/nostr/logout_controller.js | 16 ++++ .../controllers/nostr/manual_nip46_session.js | 0 assets/controllers/nostr/signer_manager.js | 85 ++++++++++++++----- templates/components/UserMenu.html.twig | 4 +- templates/feedback/form.html.twig | 6 +- .../reading_list/reading_review.html.twig | 8 +- 7 files changed, 94 insertions(+), 34 deletions(-) create mode 100644 assets/controllers/nostr/logout_controller.js delete mode 100644 assets/controllers/nostr/manual_nip46_session.js diff --git a/assets/controllers/nostr/amber_connect_controller.js b/assets/controllers/nostr/amber_connect_controller.js index cf5f936..0988b59 100644 --- a/assets/controllers/nostr/amber_connect_controller.js +++ b/assets/controllers/nostr/amber_connect_controller.js @@ -1,7 +1,7 @@ import { Controller } from '@hotwired/stimulus'; import { getPublicKey, SimplePool } from 'nostr-tools'; import { BunkerSigner } from "nostr-tools/nip46"; -import { setRemoteSignerSession, clearRemoteSignerSession } from './signer_manager.js'; +import { setRemoteSignerSession } from './signer_manager.js'; export default class extends Controller { static targets = ['qr', 'status']; @@ -20,7 +20,8 @@ export default class extends Controller { disconnect() { try { this._signer?.close?.(); } catch (_) {} try { this._pool?.close?.([]); } catch (_) {} - clearRemoteSignerSession(); + // IMPORTANT: Don't clear session here - we want to reuse it after reload/navigation + // Session should only be cleared on explicit logout } async _init() { @@ -103,9 +104,11 @@ export default class extends Controller { }); if (resp.ok) { // Persist remote signer session for reuse after reload - // Note: Reconnection with Amber may require user approval each time + // Store the BunkerPointer (signer.bp) for proper reconnection using fromBunker() setRemoteSignerSession({ privkey: this._localSecretKey, + bunkerPointer: this._signer.bp, // BunkerPointer contains pubkey, relays, secret, perms + // Legacy fields for backward compatibility uri: this._uri, relays: this._relays, secret: this._secret diff --git a/assets/controllers/nostr/logout_controller.js b/assets/controllers/nostr/logout_controller.js new file mode 100644 index 0000000..71e7875 --- /dev/null +++ b/assets/controllers/nostr/logout_controller.js @@ -0,0 +1,16 @@ +import { Controller } from '@hotwired/stimulus'; +import { clearRemoteSignerSession } from './signer_manager.js'; + +/** + * Handles logout and clears remote signer session + * Usage: Add data-controller="nostr--logout" to logout link + * and data-action="click->nostr--logout#handleLogout" + */ +export default class extends Controller { + handleLogout(event) { + console.log('[logout] Clearing remote signer session'); + clearRemoteSignerSession(); + // Allow the default logout action to continue + } +} + diff --git a/assets/controllers/nostr/manual_nip46_session.js b/assets/controllers/nostr/manual_nip46_session.js deleted file mode 100644 index e69de29..0000000 diff --git a/assets/controllers/nostr/signer_manager.js b/assets/controllers/nostr/signer_manager.js index bfe51aa..4f6f9f4 100644 --- a/assets/controllers/nostr/signer_manager.js +++ b/assets/controllers/nostr/signer_manager.js @@ -3,15 +3,17 @@ import { SimplePool } from 'nostr-tools'; import { BunkerSigner } from 'nostr-tools/nip46'; const REMOTE_SIGNER_KEY = 'amber_remote_signer'; +const MAX_RETRIES = 3; +const RETRY_DELAY_MS = 2000; let remoteSigner = null; let remoteSignerPromise = null; let remoteSignerPool = null; -export async function getSigner(_retrying = 0) { +export async function getSigner(retryCount = 0) { // If remote signer session is active, use it const session = getRemoteSignerSession(); - console.log('[signer_manager] getSigner called, session exists:', !!session); + console.log('[signer_manager] getSigner called, session exists:', !!session, 'retry:', retryCount); if (session) { if (remoteSigner) { console.log('[signer_manager] Returning cached remote signer'); @@ -22,27 +24,28 @@ export async function getSigner(_retrying = 0) { return remoteSignerPromise; } - console.log('[signer_manager] Recreating BunkerSigner from stored session (no connect needed)...'); - // According to nostr-tools docs: BunkerSigner.fromURI() returns immediately - // After initial connect() during login, we can reuse the signer without reconnecting + console.log('[signer_manager] Recreating BunkerSigner from stored session...'); remoteSignerPromise = createRemoteSignerFromSession(session) .then(signer => { remoteSigner = signer; console.log('[signer_manager] Remote signer successfully recreated and cached'); return signer; }) - .catch((error) => { + .catch(async (error) => { console.error('[signer_manager] Remote signer creation failed:', error); remoteSignerPromise = null; - // Clear stale session - console.log('[signer_manager] Clearing stale remote signer session'); - clearRemoteSignerSession(); - // Fallback to browser extension if available - if (window.nostr && typeof window.nostr.signEvent === 'function') { - console.log('[signer_manager] Falling back to browser extension'); - return window.nostr; + + // Retry connection instead of clearing session + if (retryCount < MAX_RETRIES) { + console.log(`[signer_manager] Retrying connection (${retryCount + 1}/${MAX_RETRIES}) in ${RETRY_DELAY_MS}ms...`); + await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS)); + return getSigner(retryCount + 1); } - throw new Error('Remote signer unavailable. Please reconnect Amber or use a browser extension.'); + + // After all retries failed, throw error but DON'T clear session + // User can manually retry or reconnect + console.error('[signer_manager] All connection attempts failed. Remote signer may be offline.'); + throw new Error('Remote signer connection failed after ' + MAX_RETRIES + ' attempts. Please ensure Amber is running and try again.'); }); return remoteSignerPromise; } @@ -59,7 +62,13 @@ export function setRemoteSignerSession(session) { localStorage.setItem(REMOTE_SIGNER_KEY, JSON.stringify(session)); } +/** + * Clear the remote signer session from localStorage and close connections + * WARNING: Only call this on explicit logout - NOT on page navigation/disconnect + * The whole point of session persistence is to survive page reloads + */ export function clearRemoteSignerSession() { + console.log('[signer_manager] Clearing remote signer session'); localStorage.removeItem(REMOTE_SIGNER_KEY); remoteSigner = null; remoteSignerPromise = null; @@ -80,29 +89,59 @@ export function getRemoteSignerSession() { } // Create BunkerSigner from stored session -// According to nostr-tools: fromURI() returns immediately, no waiting for handshake -// The connect() was already done during initial login, so we can use the signer right away +// Uses fromBunker() for reconnection with stored BunkerPointer +// Falls back to fromURI() for legacy sessions async function createRemoteSignerFromSession(session) { console.log('[signer_manager] ===== Recreating BunkerSigner from session ====='); - console.log('[signer_manager] Session URI:', session.uri); - console.log('[signer_manager] Session relays:', session.relays); // Reuse existing pool if available, otherwise create new one if (!remoteSignerPool) { - console.log('[signer_manager] Creating new SimplePool for relays:', session.relays); + console.log('[signer_manager] Creating new SimplePool'); remoteSignerPool = new SimplePool(); } else { console.log('[signer_manager] Reusing existing SimplePool'); } try { - console.log('[signer_manager] Creating BunkerSigner from stored session...'); - // fromURI returns a Promise - await it to get the signer - const signer = await BunkerSigner.fromURI(session.privkey, session.uri, { pool: remoteSignerPool }); - console.log('[signer_manager] ✅ BunkerSigner created! Testing with getPublicKey...'); + let signer; + + // NEW PATTERN: Use fromBunker() with stored BunkerPointer (preferred) + if (session.bunkerPointer) { + console.log('[signer_manager] Using fromBunker() with stored BunkerPointer'); + console.log('[signer_manager] BunkerPointer pubkey:', session.bunkerPointer.pubkey); + console.log('[signer_manager] BunkerPointer relays:', session.bunkerPointer.relays); + + // fromBunker() is for reconnecting to an already-authorized bunker + // It doesn't wait for a new connect message like fromURI() does + signer = BunkerSigner.fromBunker( + session.privkey, + session.bunkerPointer, + { pool: remoteSignerPool } + ); + + console.log('[signer_manager] ✅ BunkerSigner created from pointer!'); + } + // LEGACY PATTERN: Fallback to fromURI() for old sessions (backward compatibility) + else if (session.uri) { + console.log('[signer_manager] ⚠️ Using legacy fromURI() pattern (session has no bunkerPointer)'); + console.log('[signer_manager] Session URI:', session.uri); + console.log('[signer_manager] Session relays:', session.relays); + + // fromURI returns a Promise - await it to get the signer + signer = await BunkerSigner.fromURI(session.privkey, session.uri, { pool: remoteSignerPool }); + console.log('[signer_manager] ✅ BunkerSigner created from URI!'); + + // With fromURI, we need to call connect() + console.log('[signer_manager] Calling connect() to establish relay connection...'); + await signer.connect(); + console.log('[signer_manager] ✅ Connected to remote signer!'); + } else { + throw new Error('Session missing both bunkerPointer and uri'); + } // Test the signer to make sure it works try { + console.log('[signer_manager] Testing signer with getPublicKey...'); const pubkey = await signer.getPublicKey(); console.log('[signer_manager] ✅ Signer verified! Pubkey:', pubkey); return signer; diff --git a/templates/components/UserMenu.html.twig b/templates/components/UserMenu.html.twig index e1e16df..423041e 100644 --- a/templates/components/UserMenu.html.twig +++ b/templates/components/UserMenu.html.twig @@ -25,7 +25,9 @@ {% endif %}
  • - {{ 'heading.logout'|trans }} + {{ 'heading.logout'|trans }}
  • {% else %} diff --git a/templates/feedback/form.html.twig b/templates/feedback/form.html.twig index 96dca42..8820bf4 100644 --- a/templates/feedback/form.html.twig +++ b/templates/feedback/form.html.twig @@ -22,13 +22,13 @@ {% endfor %}

    - +
    - +
    Preview
    -
    
    +            
    
             
         
     
    diff --git a/templates/reading_list/reading_review.html.twig b/templates/reading_list/reading_review.html.twig
    index 45b2f9f..ec53830 100644
    --- a/templates/reading_list/reading_review.html.twig
    +++ b/templates/reading_list/reading_review.html.twig
    @@ -38,7 +38,7 @@
       
     
       
    Cancel - +
    -
    +

    Event preview

    -
    
    +        
    
         
    {% endblock %}