clone of github.com/decent-newsroom/newsroom
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

166 lines
6.3 KiB

// Shared signer manager for Nostr signers (remote and extension)
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(retryCount = 0) {
// If remote signer session is active, use it
const session = getRemoteSignerSession();
console.log('[signer_manager] getSigner called, session exists:', !!session, 'retry:', retryCount);
if (session) {
if (remoteSigner) {
console.log('[signer_manager] Returning cached remote signer');
return remoteSigner;
}
if (remoteSignerPromise) {
console.log('[signer_manager] Returning existing connection promise');
return remoteSignerPromise;
}
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(async (error) => {
console.error('[signer_manager] Remote signer creation failed:', error);
remoteSignerPromise = null;
// 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);
}
// 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;
}
// Fallback to browser extension ONLY if no remote session
console.log('[signer_manager] No remote session, checking for browser extension');
if (window.nostr && typeof window.nostr.signEvent === 'function') {
console.log('[signer_manager] Using browser extension');
return window.nostr;
}
throw new Error('No signer available');
}
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;
if (remoteSignerPool) {
try { remoteSignerPool.close?.([]); } catch (_) {}
remoteSignerPool = null;
}
}
export function getRemoteSignerSession() {
const raw = localStorage.getItem(REMOTE_SIGNER_KEY);
if (!raw) return null;
try {
return JSON.parse(raw);
} catch {
return null;
}
}
// Create BunkerSigner from stored session
// 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 =====');
// Reuse existing pool if available, otherwise create new one
if (!remoteSignerPool) {
console.log('[signer_manager] Creating new SimplePool');
remoteSignerPool = new SimplePool();
} else {
console.log('[signer_manager] Reusing existing SimplePool');
}
try {
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;
} catch (testError) {
console.error('[signer_manager] ❌ Signer test failed:', testError);
throw new Error('Signer created but failed verification: ' + testError.message);
}
} catch (error) {
console.error('[signer_manager] ❌ Failed to create signer:', error);
// Clean up on error
if (remoteSignerPool) {
try {
console.log('[signer_manager] Closing pool after error');
remoteSignerPool.close?.([]);
} catch (_) {}
remoteSignerPool = null;
}
remoteSigner = null;
remoteSignerPromise = null;
throw error;
}
}