Fix NIP-46 signing across pages by auto-reconnecting BunkerSigner
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
* Two-file architecture:
|
||||
* 1. Load nostr.bundle.js (official nostr-tools bundle)
|
||||
* 2. Load nostr-lite.js (this file - NOSTR_LOGIN_LITE library with CSS-only themes)
|
||||
* Generated on: 2025-11-14T18:40:05.334Z
|
||||
* Generated on: 2026-04-04T13:40:23.265Z
|
||||
*/
|
||||
|
||||
// Verify dependencies are loaded
|
||||
@@ -436,7 +436,7 @@ class Modal {
|
||||
modalContent.appendChild(modalHeader);
|
||||
// Add version element in bottom-right corner aligned with modal body
|
||||
const versionElement = document.createElement('div');
|
||||
versionElement.textContent = 'v0.1.12';
|
||||
versionElement.textContent = 'v0.1.13';
|
||||
versionElement.style.cssText = `
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
@@ -492,7 +492,10 @@ class Modal {
|
||||
// No manual styling needed - the CSS variables handle everything
|
||||
}
|
||||
|
||||
open(opts = {}) {
|
||||
open(opts = {}) {
|
||||
// If a NIP-46 connect is in progress, don't overwrite the connecting UI
|
||||
if (this._nip46Connecting) return;
|
||||
|
||||
this.currentScreen = opts.startScreen;
|
||||
this.isVisible = true;
|
||||
this.container.style.display = 'block';
|
||||
@@ -1707,14 +1710,20 @@ class Modal {
|
||||
}
|
||||
}
|
||||
|
||||
_handleNip46Connect(bunkerPubkey) {
|
||||
_handleNip46Connect(bunkerPubkey) {
|
||||
if (!bunkerPubkey || !bunkerPubkey.length) {
|
||||
this._showError('Bunker pubkey is required');
|
||||
return;
|
||||
}
|
||||
|
||||
this._showNip46Connecting(bunkerPubkey);
|
||||
this._performNip46Connect(bunkerPubkey);
|
||||
|
||||
// While connecting, prevent modal.open() from overwriting the "Connecting..."
|
||||
// state (e.g. when init-ndk's auth timeout fires and calls launch() again).
|
||||
this._nip46Connecting = true;
|
||||
this._performNip46Connect(bunkerPubkey).finally(() => {
|
||||
this._nip46Connecting = false;
|
||||
});
|
||||
}
|
||||
|
||||
_showNip46Connecting(bunkerPubkey) {
|
||||
@@ -1753,51 +1762,109 @@ class Modal {
|
||||
this.modalBody.appendChild(connectingDiv);
|
||||
}
|
||||
|
||||
async _performNip46Connect(bunkerPubkey) {
|
||||
try {
|
||||
// console.log('Starting NIP-46 connection to bunker:', bunkerPubkey);
|
||||
async _performNip46Connect(bunkerPubkey) {
|
||||
// Cancel any previous in-flight connect attempt
|
||||
if (this._nip46CancelToken) this._nip46CancelToken.cancelled = true;
|
||||
const cancelToken = { cancelled: false };
|
||||
this._nip46CancelToken = cancelToken;
|
||||
|
||||
try {
|
||||
// Check if nostr-tools NIP-46 is available
|
||||
if (!window.NostrTools?.nip46) {
|
||||
throw new Error('nostr-tools NIP-46 module not available');
|
||||
}
|
||||
|
||||
// Use nostr-tools to parse bunker input - this handles all formats correctly
|
||||
// console.log('Parsing bunker input with nostr-tools...');
|
||||
const bunkerPointer = await window.NostrTools.nip46.parseBunkerInput(bunkerPubkey);
|
||||
|
||||
if (!bunkerPointer) {
|
||||
throw new Error('Unable to parse bunker connection string or resolve NIP-05 identifier');
|
||||
}
|
||||
|
||||
// console.log('Parsed bunker pointer:', bunkerPointer);
|
||||
|
||||
// Create local client keypair for this session
|
||||
const localSecretKey = window.NostrTools.generateSecretKey();
|
||||
// console.log('Generated local client keypair for NIP-46 session');
|
||||
|
||||
// Use nostr-tools BunkerSigner factory method (not constructor - it's private)
|
||||
// console.log('Creating nip46 BunkerSigner...');
|
||||
// Create a SimplePool with a patched subscribe that filters out stale cached
|
||||
// NIP-46 response events. Relays like primal.net store and replay previous
|
||||
// "unauthorized" responses before we've even sent our request.
|
||||
// We use a shared object so the flag can be set synchronously from outside
|
||||
// the closure at exactly the right moment.
|
||||
const sessionStart = Math.floor(Date.now() / 1000);
|
||||
const gate = { open: false };
|
||||
const signerRef = { current: null }; // forward ref so the closure can access signer after it's created
|
||||
const pool = new window.NostrTools.SimplePool();
|
||||
const origSubscribe = pool.subscribe.bind(pool);
|
||||
pool.subscribe = (relays, filter, subParams) => {
|
||||
const origOnevent = subParams.onevent;
|
||||
subParams.onevent = async (event) => {
|
||||
if (!gate.open) return; // drop events arriving before we open the gate
|
||||
// Also skip if we can decrypt and see it's a response to a request id
|
||||
// that we haven't sent yet (i.e. its id doesn't match any pending listener)
|
||||
try {
|
||||
const s = signerRef.current;
|
||||
if (!s) return; // signer not yet created, drop
|
||||
const decrypted = JSON.parse(window.NostrTools.nip44.decrypt(event.content, s.conversationKey));
|
||||
if (decrypted.id && !s.listeners[decrypted.id]) {
|
||||
// No pending listener for this id — it's stale, drop it
|
||||
return;
|
||||
}
|
||||
} catch (_) {
|
||||
// Can't decrypt — not for us, drop it
|
||||
return;
|
||||
}
|
||||
return origOnevent(event);
|
||||
};
|
||||
return origSubscribe(relays, { ...filter, since: sessionStart }, subParams);
|
||||
};
|
||||
|
||||
// Use nostr-tools BunkerSigner factory method, passing our patched pool
|
||||
const signer = window.NostrTools.nip46.BunkerSigner.fromBunker(localSecretKey, bunkerPointer, {
|
||||
pool,
|
||||
onauth: (url) => {
|
||||
// console.log('Received auth URL from bunker:', url);
|
||||
// Open auth URL in popup or redirect
|
||||
window.open(url, '_blank', 'width=600,height=800');
|
||||
}
|
||||
});
|
||||
signerRef.current = signer; // allow the onevent closure to access the signer
|
||||
|
||||
// console.log('NIP-46 BunkerSigner created successfully');
|
||||
// NIP-46 connect requires 3 params: [remotePubkey, secret, perms]
|
||||
// The nostr-tools BunkerSigner.connect() only sends [pubkey, secret] (2 params),
|
||||
// which causes strict bunkers to respond "unauthorized" for subsequent calls
|
||||
// because no permissions were explicitly requested.
|
||||
// We call sendRequest directly to include the required perms in the connect call.
|
||||
const perms = bunkerPointer.perms && bunkerPointer.perms.length
|
||||
? bunkerPointer.perms
|
||||
: ['get_public_key', 'sign_event', 'nip04_encrypt', 'nip04_decrypt', 'nip44_encrypt', 'nip44_decrypt'];
|
||||
|
||||
// Skip ping test - NIP-46 works through relays, not direct connection
|
||||
// Try to connect directly (this may trigger auth flow)
|
||||
// console.log('Attempting NIP-46 connect...');
|
||||
await signer.connect();
|
||||
// console.log('NIP-46 connect successful');
|
||||
// Some signers (e.g. Amber) require the user to approve the connection on their
|
||||
// device before responding with "ack". They respond "unauthorized" immediately
|
||||
// and send "ack" once the user taps approve. We retry the connect request on
|
||||
// "unauthorized" until the user approves or the overall timeout is reached.
|
||||
const CONNECT_TIMEOUT_MS = 120000; // 2 minutes for user to approve on device
|
||||
const RETRY_DELAY_MS = 3000;
|
||||
const deadline = Date.now() + CONNECT_TIMEOUT_MS;
|
||||
let connectError = null;
|
||||
let connectDone = false;
|
||||
while (!connectDone) {
|
||||
try {
|
||||
gate.open = true;
|
||||
await signer.sendRequest('connect', [bunkerPointer.pubkey, bunkerPointer.secret || '', perms.join(',')]);
|
||||
connectDone = true;
|
||||
} catch (err) {
|
||||
if (String(err) === 'unauthorized' && Date.now() < deadline && !cancelToken.cancelled) {
|
||||
connectError = err;
|
||||
await new Promise(r => setTimeout(r, RETRY_DELAY_MS));
|
||||
// Only cancel during the retry wait, not after a successful connect
|
||||
if (cancelToken.cancelled) return;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get the user's public key from the bunker
|
||||
// console.log('Getting public key from bunker...');
|
||||
console.log('[NIP-46] connect done, requesting getPublicKey...');
|
||||
const userPubkey = await signer.getPublicKey();
|
||||
// console.log('NIP-46 user public key:', userPubkey);
|
||||
console.log('[NIP-46] getPublicKey returned:', userPubkey);
|
||||
|
||||
// Store the NIP-46 authentication info
|
||||
const nip46Info = {
|
||||
@@ -1807,19 +1874,20 @@ class Modal {
|
||||
remotePubkey: bunkerPointer.pubkey,
|
||||
bunkerSigner: signer,
|
||||
secret: bunkerPointer.secret,
|
||||
relays: bunkerPointer.relays
|
||||
relays: bunkerPointer.relays,
|
||||
localSecretKey: window.NostrTools.nip19.nsecEncode(localSecretKey)
|
||||
}
|
||||
};
|
||||
|
||||
// console.log('NOSTR_LOGIN_LITE NIP-46 connection established successfully!');
|
||||
|
||||
// Set as current auth method
|
||||
console.log('[NIP-46] connect success, pubkey:', userPubkey, 'calling _setAuthMethod...');
|
||||
this._setAuthMethod('nip46', nip46Info);
|
||||
console.log('[NIP-46] _setAuthMethod returned, modal.isVisible:', this.isVisible);
|
||||
return;
|
||||
|
||||
} catch (error) {
|
||||
console.error('NIP-46 connection failed:', error);
|
||||
this._showNip46Error(error.message);
|
||||
console.error('[NIP-46] connection failed:', error);
|
||||
this._showNip46Error(error?.message || String(error));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3438,12 +3506,13 @@ class AuthManager {
|
||||
break;
|
||||
|
||||
case 'nip46':
|
||||
// For NIP-46, store connection parameters (no secrets)
|
||||
// For NIP-46, store connection parameters including secret to allow auto-reconnect
|
||||
if (authData.signer) {
|
||||
authState.nip46 = {
|
||||
remotePubkey: authData.signer.remotePubkey,
|
||||
relays: authData.signer.relays,
|
||||
// Don't store secret - user will need to reconnect
|
||||
secret: authData.signer.secret,
|
||||
localSecretKey: authData.signer.localSecretKey // We need to store this too
|
||||
};
|
||||
|
||||
}
|
||||
@@ -3726,10 +3795,88 @@ class AuthManager {
|
||||
return null;
|
||||
}
|
||||
|
||||
// For NIP-46, we can't automatically restore the connection
|
||||
// because it requires the user to re-authenticate with the remote signer
|
||||
// Instead, we return the connection parameters so the UI can prompt for reconnection
|
||||
// If we have the local secret key and remote pubkey, we can auto-reconnect
|
||||
if (authState.nip46.localSecretKey && authState.nip46.remotePubkey) {
|
||||
try {
|
||||
if (!window.NostrTools?.nip46) {
|
||||
throw new Error('nostr-tools NIP-46 module not available');
|
||||
}
|
||||
|
||||
const localSecretKey = window.NostrTools.nip19.decode(authState.nip46.localSecretKey).data;
|
||||
|
||||
// Reconstruct bunker pointer
|
||||
const bunkerPointer = {
|
||||
pubkey: authState.nip46.remotePubkey,
|
||||
relays: authState.nip46.relays || [],
|
||||
secret: authState.nip46.secret
|
||||
};
|
||||
|
||||
// Create a SimplePool with a patched subscribe that filters out stale cached
|
||||
// NIP-46 response events. Relays like primal.net store and replay previous
|
||||
// "unauthorized" responses before we've even sent our request.
|
||||
const sessionStart = Math.floor(Date.now() / 1000);
|
||||
const gate = { open: false };
|
||||
const signerRef = { current: null }; // forward ref so the closure can access signer after it's created
|
||||
const pool = new window.NostrTools.SimplePool();
|
||||
const origSubscribe = pool.subscribe.bind(pool);
|
||||
pool.subscribe = (relays, filter, subParams) => {
|
||||
const origOnevent = subParams.onevent;
|
||||
subParams.onevent = async (event) => {
|
||||
if (!gate.open) return; // drop events arriving before we open the gate
|
||||
// Also skip if we can decrypt and see it's a response to a request id
|
||||
// that we haven't sent yet (i.e. its id doesn't match any pending listener)
|
||||
try {
|
||||
const s = signerRef.current;
|
||||
if (!s) return; // signer not yet created, drop
|
||||
const decrypted = JSON.parse(window.NostrTools.nip44.decrypt(event.content, s.conversationKey));
|
||||
if (decrypted.id && !s.listeners[decrypted.id]) {
|
||||
// No pending listener for this id — it's stale, drop it
|
||||
return;
|
||||
}
|
||||
} catch (_) {
|
||||
// Can't decrypt — not for us, drop it
|
||||
return;
|
||||
}
|
||||
return origOnevent(event);
|
||||
};
|
||||
return origSubscribe(relays, { ...filter, since: sessionStart }, subParams);
|
||||
};
|
||||
|
||||
// Use nostr-tools BunkerSigner factory method, passing our patched pool
|
||||
const signer = window.NostrTools.nip46.BunkerSigner.fromBunker(localSecretKey, bunkerPointer, {
|
||||
pool,
|
||||
onauth: (url) => {
|
||||
window.open(url, '_blank', 'width=600,height=800');
|
||||
}
|
||||
});
|
||||
signerRef.current = signer; // allow the onevent closure to access the signer
|
||||
|
||||
// Open the gate to allow events through
|
||||
gate.open = true;
|
||||
|
||||
// We don't need to call connect() again if we already have the pubkey and secret,
|
||||
// but we do need to ensure the signer is ready to sign.
|
||||
// The BunkerSigner will automatically subscribe to the relays when we call signEvent.
|
||||
|
||||
return {
|
||||
method: 'nip46',
|
||||
pubkey: authState.pubkey,
|
||||
signer: {
|
||||
method: 'nip46',
|
||||
remotePubkey: authState.nip46.remotePubkey,
|
||||
bunkerSigner: signer,
|
||||
secret: authState.nip46.secret,
|
||||
relays: authState.nip46.relays
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('🔍 AuthManager: Failed to auto-reconnect NIP-46:', error);
|
||||
// Fall back to requiring reconnection
|
||||
}
|
||||
}
|
||||
|
||||
// For NIP-46, if we can't auto-reconnect, we return the connection parameters
|
||||
// so the UI can prompt for reconnection
|
||||
return {
|
||||
method: 'nip46',
|
||||
pubkey: authState.pubkey,
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.1.12
|
||||
0.1.13
|
||||
|
||||
89
src/build.js
89
src/build.js
@@ -1427,12 +1427,13 @@ class AuthManager {
|
||||
break;
|
||||
|
||||
case 'nip46':
|
||||
// For NIP-46, store connection parameters (no secrets)
|
||||
// For NIP-46, store connection parameters including secret to allow auto-reconnect
|
||||
if (authData.signer) {
|
||||
authState.nip46 = {
|
||||
remotePubkey: authData.signer.remotePubkey,
|
||||
relays: authData.signer.relays,
|
||||
// Don't store secret - user will need to reconnect
|
||||
secret: authData.signer.secret,
|
||||
localSecretKey: authData.signer.localSecretKey // We need to store this too
|
||||
};
|
||||
|
||||
}
|
||||
@@ -1715,10 +1716,88 @@ class AuthManager {
|
||||
return null;
|
||||
}
|
||||
|
||||
// For NIP-46, we can't automatically restore the connection
|
||||
// because it requires the user to re-authenticate with the remote signer
|
||||
// Instead, we return the connection parameters so the UI can prompt for reconnection
|
||||
// If we have the local secret key and remote pubkey, we can auto-reconnect
|
||||
if (authState.nip46.localSecretKey && authState.nip46.remotePubkey) {
|
||||
try {
|
||||
if (!window.NostrTools?.nip46) {
|
||||
throw new Error('nostr-tools NIP-46 module not available');
|
||||
}
|
||||
|
||||
const localSecretKey = window.NostrTools.nip19.decode(authState.nip46.localSecretKey).data;
|
||||
|
||||
// Reconstruct bunker pointer
|
||||
const bunkerPointer = {
|
||||
pubkey: authState.nip46.remotePubkey,
|
||||
relays: authState.nip46.relays || [],
|
||||
secret: authState.nip46.secret
|
||||
};
|
||||
|
||||
// Create a SimplePool with a patched subscribe that filters out stale cached
|
||||
// NIP-46 response events. Relays like primal.net store and replay previous
|
||||
// "unauthorized" responses before we've even sent our request.
|
||||
const sessionStart = Math.floor(Date.now() / 1000);
|
||||
const gate = { open: false };
|
||||
const signerRef = { current: null }; // forward ref so the closure can access signer after it's created
|
||||
const pool = new window.NostrTools.SimplePool();
|
||||
const origSubscribe = pool.subscribe.bind(pool);
|
||||
pool.subscribe = (relays, filter, subParams) => {
|
||||
const origOnevent = subParams.onevent;
|
||||
subParams.onevent = async (event) => {
|
||||
if (!gate.open) return; // drop events arriving before we open the gate
|
||||
// Also skip if we can decrypt and see it's a response to a request id
|
||||
// that we haven't sent yet (i.e. its id doesn't match any pending listener)
|
||||
try {
|
||||
const s = signerRef.current;
|
||||
if (!s) return; // signer not yet created, drop
|
||||
const decrypted = JSON.parse(window.NostrTools.nip44.decrypt(event.content, s.conversationKey));
|
||||
if (decrypted.id && !s.listeners[decrypted.id]) {
|
||||
// No pending listener for this id — it's stale, drop it
|
||||
return;
|
||||
}
|
||||
} catch (_) {
|
||||
// Can't decrypt — not for us, drop it
|
||||
return;
|
||||
}
|
||||
return origOnevent(event);
|
||||
};
|
||||
return origSubscribe(relays, { ...filter, since: sessionStart }, subParams);
|
||||
};
|
||||
|
||||
// Use nostr-tools BunkerSigner factory method, passing our patched pool
|
||||
const signer = window.NostrTools.nip46.BunkerSigner.fromBunker(localSecretKey, bunkerPointer, {
|
||||
pool,
|
||||
onauth: (url) => {
|
||||
window.open(url, '_blank', 'width=600,height=800');
|
||||
}
|
||||
});
|
||||
signerRef.current = signer; // allow the onevent closure to access the signer
|
||||
|
||||
// Open the gate to allow events through
|
||||
gate.open = true;
|
||||
|
||||
// We don't need to call connect() again if we already have the pubkey and secret,
|
||||
// but we do need to ensure the signer is ready to sign.
|
||||
// The BunkerSigner will automatically subscribe to the relays when we call signEvent.
|
||||
|
||||
return {
|
||||
method: 'nip46',
|
||||
pubkey: authState.pubkey,
|
||||
signer: {
|
||||
method: 'nip46',
|
||||
remotePubkey: authState.nip46.remotePubkey,
|
||||
bunkerSigner: signer,
|
||||
secret: authState.nip46.secret,
|
||||
relays: authState.nip46.relays
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('🔍 AuthManager: Failed to auto-reconnect NIP-46:', error);
|
||||
// Fall back to requiring reconnection
|
||||
}
|
||||
}
|
||||
|
||||
// For NIP-46, if we can't auto-reconnect, we return the connection parameters
|
||||
// so the UI can prompt for reconnection
|
||||
return {
|
||||
method: 'nip46',
|
||||
pubkey: authState.pubkey,
|
||||
|
||||
122
src/ui/modal.js
122
src/ui/modal.js
@@ -180,7 +180,10 @@ class Modal {
|
||||
// No manual styling needed - the CSS variables handle everything
|
||||
}
|
||||
|
||||
open(opts = {}) {
|
||||
open(opts = {}) {
|
||||
// If a NIP-46 connect is in progress, don't overwrite the connecting UI
|
||||
if (this._nip46Connecting) return;
|
||||
|
||||
this.currentScreen = opts.startScreen;
|
||||
this.isVisible = true;
|
||||
this.container.style.display = 'block';
|
||||
@@ -1395,14 +1398,20 @@ class Modal {
|
||||
}
|
||||
}
|
||||
|
||||
_handleNip46Connect(bunkerPubkey) {
|
||||
_handleNip46Connect(bunkerPubkey) {
|
||||
if (!bunkerPubkey || !bunkerPubkey.length) {
|
||||
this._showError('Bunker pubkey is required');
|
||||
return;
|
||||
}
|
||||
|
||||
this._showNip46Connecting(bunkerPubkey);
|
||||
this._performNip46Connect(bunkerPubkey);
|
||||
|
||||
// While connecting, prevent modal.open() from overwriting the "Connecting..."
|
||||
// state (e.g. when init-ndk's auth timeout fires and calls launch() again).
|
||||
this._nip46Connecting = true;
|
||||
this._performNip46Connect(bunkerPubkey).finally(() => {
|
||||
this._nip46Connecting = false;
|
||||
});
|
||||
}
|
||||
|
||||
_showNip46Connecting(bunkerPubkey) {
|
||||
@@ -1441,51 +1450,109 @@ class Modal {
|
||||
this.modalBody.appendChild(connectingDiv);
|
||||
}
|
||||
|
||||
async _performNip46Connect(bunkerPubkey) {
|
||||
try {
|
||||
// console.log('Starting NIP-46 connection to bunker:', bunkerPubkey);
|
||||
async _performNip46Connect(bunkerPubkey) {
|
||||
// Cancel any previous in-flight connect attempt
|
||||
if (this._nip46CancelToken) this._nip46CancelToken.cancelled = true;
|
||||
const cancelToken = { cancelled: false };
|
||||
this._nip46CancelToken = cancelToken;
|
||||
|
||||
try {
|
||||
// Check if nostr-tools NIP-46 is available
|
||||
if (!window.NostrTools?.nip46) {
|
||||
throw new Error('nostr-tools NIP-46 module not available');
|
||||
}
|
||||
|
||||
// Use nostr-tools to parse bunker input - this handles all formats correctly
|
||||
// console.log('Parsing bunker input with nostr-tools...');
|
||||
const bunkerPointer = await window.NostrTools.nip46.parseBunkerInput(bunkerPubkey);
|
||||
|
||||
if (!bunkerPointer) {
|
||||
throw new Error('Unable to parse bunker connection string or resolve NIP-05 identifier');
|
||||
}
|
||||
|
||||
// console.log('Parsed bunker pointer:', bunkerPointer);
|
||||
|
||||
// Create local client keypair for this session
|
||||
const localSecretKey = window.NostrTools.generateSecretKey();
|
||||
// console.log('Generated local client keypair for NIP-46 session');
|
||||
|
||||
// Use nostr-tools BunkerSigner factory method (not constructor - it's private)
|
||||
// console.log('Creating nip46 BunkerSigner...');
|
||||
// Create a SimplePool with a patched subscribe that filters out stale cached
|
||||
// NIP-46 response events. Relays like primal.net store and replay previous
|
||||
// "unauthorized" responses before we've even sent our request.
|
||||
// We use a shared object so the flag can be set synchronously from outside
|
||||
// the closure at exactly the right moment.
|
||||
const sessionStart = Math.floor(Date.now() / 1000);
|
||||
const gate = { open: false };
|
||||
const signerRef = { current: null }; // forward ref so the closure can access signer after it's created
|
||||
const pool = new window.NostrTools.SimplePool();
|
||||
const origSubscribe = pool.subscribe.bind(pool);
|
||||
pool.subscribe = (relays, filter, subParams) => {
|
||||
const origOnevent = subParams.onevent;
|
||||
subParams.onevent = async (event) => {
|
||||
if (!gate.open) return; // drop events arriving before we open the gate
|
||||
// Also skip if we can decrypt and see it's a response to a request id
|
||||
// that we haven't sent yet (i.e. its id doesn't match any pending listener)
|
||||
try {
|
||||
const s = signerRef.current;
|
||||
if (!s) return; // signer not yet created, drop
|
||||
const decrypted = JSON.parse(window.NostrTools.nip44.decrypt(event.content, s.conversationKey));
|
||||
if (decrypted.id && !s.listeners[decrypted.id]) {
|
||||
// No pending listener for this id — it's stale, drop it
|
||||
return;
|
||||
}
|
||||
} catch (_) {
|
||||
// Can't decrypt — not for us, drop it
|
||||
return;
|
||||
}
|
||||
return origOnevent(event);
|
||||
};
|
||||
return origSubscribe(relays, { ...filter, since: sessionStart }, subParams);
|
||||
};
|
||||
|
||||
// Use nostr-tools BunkerSigner factory method, passing our patched pool
|
||||
const signer = window.NostrTools.nip46.BunkerSigner.fromBunker(localSecretKey, bunkerPointer, {
|
||||
pool,
|
||||
onauth: (url) => {
|
||||
// console.log('Received auth URL from bunker:', url);
|
||||
// Open auth URL in popup or redirect
|
||||
window.open(url, '_blank', 'width=600,height=800');
|
||||
}
|
||||
});
|
||||
signerRef.current = signer; // allow the onevent closure to access the signer
|
||||
|
||||
// console.log('NIP-46 BunkerSigner created successfully');
|
||||
// NIP-46 connect requires 3 params: [remotePubkey, secret, perms]
|
||||
// The nostr-tools BunkerSigner.connect() only sends [pubkey, secret] (2 params),
|
||||
// which causes strict bunkers to respond "unauthorized" for subsequent calls
|
||||
// because no permissions were explicitly requested.
|
||||
// We call sendRequest directly to include the required perms in the connect call.
|
||||
const perms = bunkerPointer.perms && bunkerPointer.perms.length
|
||||
? bunkerPointer.perms
|
||||
: ['get_public_key', 'sign_event', 'nip04_encrypt', 'nip04_decrypt', 'nip44_encrypt', 'nip44_decrypt'];
|
||||
|
||||
// Skip ping test - NIP-46 works through relays, not direct connection
|
||||
// Try to connect directly (this may trigger auth flow)
|
||||
// console.log('Attempting NIP-46 connect...');
|
||||
await signer.connect();
|
||||
// console.log('NIP-46 connect successful');
|
||||
// Some signers (e.g. Amber) require the user to approve the connection on their
|
||||
// device before responding with "ack". They respond "unauthorized" immediately
|
||||
// and send "ack" once the user taps approve. We retry the connect request on
|
||||
// "unauthorized" until the user approves or the overall timeout is reached.
|
||||
const CONNECT_TIMEOUT_MS = 120000; // 2 minutes for user to approve on device
|
||||
const RETRY_DELAY_MS = 3000;
|
||||
const deadline = Date.now() + CONNECT_TIMEOUT_MS;
|
||||
let connectError = null;
|
||||
let connectDone = false;
|
||||
while (!connectDone) {
|
||||
try {
|
||||
gate.open = true;
|
||||
await signer.sendRequest('connect', [bunkerPointer.pubkey, bunkerPointer.secret || '', perms.join(',')]);
|
||||
connectDone = true;
|
||||
} catch (err) {
|
||||
if (String(err) === 'unauthorized' && Date.now() < deadline && !cancelToken.cancelled) {
|
||||
connectError = err;
|
||||
await new Promise(r => setTimeout(r, RETRY_DELAY_MS));
|
||||
// Only cancel during the retry wait, not after a successful connect
|
||||
if (cancelToken.cancelled) return;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get the user's public key from the bunker
|
||||
// console.log('Getting public key from bunker...');
|
||||
console.log('[NIP-46] connect done, requesting getPublicKey...');
|
||||
const userPubkey = await signer.getPublicKey();
|
||||
// console.log('NIP-46 user public key:', userPubkey);
|
||||
console.log('[NIP-46] getPublicKey returned:', userPubkey);
|
||||
|
||||
// Store the NIP-46 authentication info
|
||||
const nip46Info = {
|
||||
@@ -1495,19 +1562,20 @@ class Modal {
|
||||
remotePubkey: bunkerPointer.pubkey,
|
||||
bunkerSigner: signer,
|
||||
secret: bunkerPointer.secret,
|
||||
relays: bunkerPointer.relays
|
||||
relays: bunkerPointer.relays,
|
||||
localSecretKey: window.NostrTools.nip19.nsecEncode(localSecretKey)
|
||||
}
|
||||
};
|
||||
|
||||
// console.log('NOSTR_LOGIN_LITE NIP-46 connection established successfully!');
|
||||
|
||||
// Set as current auth method
|
||||
console.log('[NIP-46] connect success, pubkey:', userPubkey, 'calling _setAuthMethod...');
|
||||
this._setAuthMethod('nip46', nip46Info);
|
||||
console.log('[NIP-46] _setAuthMethod returned, modal.isVisible:', this.isVisible);
|
||||
return;
|
||||
|
||||
} catch (error) {
|
||||
console.error('NIP-46 connection failed:', error);
|
||||
this._showNip46Error(error.message);
|
||||
console.error('[NIP-46] connection failed:', error);
|
||||
this._showNip46Error(error?.message || String(error));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user