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
-
+
-
+
{% endblock %}