Browse Source

Fix signer

imwald
Nuša Pukšič 1 month ago
parent
commit
692ecb1452
  1. 9
      assets/controllers/nostr/amber_connect_controller.js
  2. 16
      assets/controllers/nostr/logout_controller.js
  3. 0
      assets/controllers/nostr/manual_nip46_session.js
  4. 85
      assets/controllers/nostr/signer_manager.js
  5. 4
      templates/components/UserMenu.html.twig
  6. 6
      templates/feedback/form.html.twig
  7. 8
      templates/reading_list/reading_review.html.twig

9
assets/controllers/nostr/amber_connect_controller.js

@ -1,7 +1,7 @@ @@ -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 { @@ -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 { @@ -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

16
assets/controllers/nostr/logout_controller.js

@ -0,0 +1,16 @@ @@ -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
}
}

0
assets/controllers/nostr/manual_nip46_session.js

85
assets/controllers/nostr/signer_manager.js

@ -3,15 +3,17 @@ import { SimplePool } from 'nostr-tools'; @@ -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) { @@ -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) { @@ -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() { @@ -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;

4
templates/components/UserMenu.html.twig

@ -25,7 +25,9 @@ @@ -25,7 +25,9 @@
</li>
{% endif %}
<li>
<a href="/logout" data-action="live#$render">{{ 'heading.logout'|trans }}</a>
<a href="/logout"
data-controller="nostr--logout"
data-action="click->nostr--logout#handleLogout live#$render">{{ 'heading.logout'|trans }}</a>
</li>
</ul>
{% else %}

6
templates/feedback/form.html.twig

@ -22,13 +22,13 @@ @@ -22,13 +22,13 @@
{% endfor %}
</p>
<label for="feedback-message" class="form-label">Your Message</label>
<textarea id="feedback-message" class="form-control" rows="5" required data-action="input->nostr-single-sign#preparePreview"></textarea>
<textarea id="feedback-message" class="form-control" rows="5" required data-action="input->nostr--nostr-single-sign#preparePreview"></textarea>
</div>
<div class="actions">
<button type="submit" class="btn btn-primary" data-nostr-single-sign-target="publishButton">Send Feedback</button>
<button type="submit" class="btn btn-primary" data-nostr--nostr-single-sign-target="publishButton">Send Feedback</button>
</div>
<h5 class="mt-4">Preview</h5>
<pre data-nostr-single-sign-target="computedPreview" class="bg-light p-2"></pre>
<pre data-nostr--nostr-single-sign-target="computedPreview" class="bg-light p-2"></pre>
</form>
</section>
</div>

8
templates/reading_list/reading_review.html.twig

@ -38,7 +38,7 @@ @@ -38,7 +38,7 @@
</section>
<div
{{ stimulus_controller('nostr-single-sign', {
{{ stimulus_controller('nostr--nostr-single-sign', {
event: eventJson,
publishUrl: path('api-index-publish'),
csrfToken: csrfToken
@ -46,12 +46,12 @@ @@ -46,12 +46,12 @@
>
<div class="d-flex flex-row gap-2">
<a class="btn btn-secondary" href="{{ path('read_wizard_cancel') }}">Cancel</a>
<button class="btn btn-primary" data-nostr-single-sign-target="publishButton" data-action="click->nostr-single-sign#signAndPublish">Sign & Publish</button>
<button class="btn btn-primary" data-nostr--nostr-single-sign-target="publishButton" data-action="click->nostr--nostr-single-sign#signAndPublish">Sign & Publish</button>
</div>
<div class="mt-3" data-nostr-single-sign-target="status"></div>
<div class="mt-3" data-nostr--nostr-single-sign-target="status"></div>
<section class="mb-3">
<h3>Event preview</h3>
<pre class="small" data-nostr-single-sign-target="computedPreview"></pre>
<pre class="small" data-nostr--nostr-single-sign-target="computedPreview"></pre>
</section>
</div>
{% endblock %}

Loading…
Cancel
Save