Refactor Nostr auth service
This commit is contained in:
@@ -12,6 +12,8 @@ const STORAGE_KEY_CONNECT_LOCAL_KEY = 'marco:nostr_connect_local_key';
|
||||
const STORAGE_KEY_CONNECT_REMOTE_PUBKEY = 'marco:nostr_connect_remote_pubkey';
|
||||
const STORAGE_KEY_CONNECT_RELAY = 'marco:nostr_connect_relay';
|
||||
|
||||
const DEFAULT_CONNECT_RELAY = 'wss://relay.nsec.app';
|
||||
|
||||
export default class NostrAuthService extends Service {
|
||||
@service nostrRelay;
|
||||
|
||||
@@ -44,7 +46,7 @@ export default class NostrAuthService extends Service {
|
||||
async _verifyPubkey() {
|
||||
if (this.signerType === 'extension') {
|
||||
if (typeof window.nostr === 'undefined') {
|
||||
this.logout();
|
||||
this.disconnect();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -56,14 +58,14 @@ export default class NostrAuthService extends Service {
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to verify extension nostr pubkey, logging out', e);
|
||||
this.logout();
|
||||
this.disconnect();
|
||||
}
|
||||
} else if (this.signerType === 'connect') {
|
||||
try {
|
||||
await this._initConnectSigner();
|
||||
} catch (e) {
|
||||
console.warn('Failed to verify connect nostr pubkey, logging out', e);
|
||||
this.logout();
|
||||
this.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -99,129 +101,132 @@ export default class NostrAuthService extends Service {
|
||||
return null;
|
||||
}
|
||||
|
||||
async login(type = 'extension') {
|
||||
if (type === 'extension') {
|
||||
if (typeof window.nostr === 'undefined') {
|
||||
throw new Error('No NIP-07 Nostr extension found (e.g., Alby, nos2x).');
|
||||
async connectWithExtension() {
|
||||
if (typeof window.nostr === 'undefined') {
|
||||
throw new Error('No NIP-07 Nostr extension found (e.g., Alby, nos2x).');
|
||||
}
|
||||
|
||||
try {
|
||||
this._signerInstance = new ExtensionSigner();
|
||||
this.pubkey = await this._signerInstance.getPublicKey();
|
||||
this.signerType = 'extension';
|
||||
localStorage.setItem(STORAGE_KEY, this.pubkey);
|
||||
localStorage.setItem(STORAGE_KEY_TYPE, 'extension');
|
||||
return this.pubkey;
|
||||
} catch (error) {
|
||||
console.error('Failed to get public key from extension:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
_getLocalSigner() {
|
||||
let localKeyHex = localStorage.getItem(STORAGE_KEY_CONNECT_LOCAL_KEY);
|
||||
let localSigner;
|
||||
if (localKeyHex) {
|
||||
localSigner = PrivateKeySigner.fromKey(localKeyHex);
|
||||
} else {
|
||||
localSigner = new PrivateKeySigner();
|
||||
// Store the raw Uint8Array as hex string
|
||||
localKeyHex = Array.from(localSigner.key)
|
||||
.map((b) => b.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
localStorage.setItem(STORAGE_KEY_CONNECT_LOCAL_KEY, localKeyHex);
|
||||
}
|
||||
return localSigner;
|
||||
}
|
||||
|
||||
async connectWithApp() {
|
||||
this.connectStatus = 'waiting';
|
||||
|
||||
try {
|
||||
const localSigner = this._getLocalSigner();
|
||||
|
||||
// We use a specific relay for the connection handshake.
|
||||
const relay = DEFAULT_CONNECT_RELAY;
|
||||
localStorage.setItem(STORAGE_KEY_CONNECT_RELAY, relay);
|
||||
|
||||
// Override aggressive 10s EOSE timeout to allow time for QR scanning
|
||||
this.nostrRelay.pool.relay(relay).eoseTimeout = 180000; // 3 minutes
|
||||
|
||||
this._signerInstance = new NostrConnectSigner({
|
||||
pool: this.nostrRelay.pool,
|
||||
relays: [relay],
|
||||
signer: localSigner,
|
||||
onAuth: async (url) => {
|
||||
// NIP-46 auth callback. Normally the signer app does this natively via notification.
|
||||
// But if it requires an explicit browser window:
|
||||
if (
|
||||
confirm(
|
||||
`Your signer app requests authentication via a web page. Open it now?\n\nURL: ${url}`
|
||||
)
|
||||
) {
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Set the uri for display (e.g., to redirect via intent)
|
||||
this.connectUri = this._signerInstance.getNostrConnectURI({
|
||||
name: 'Marco',
|
||||
url: window.location.origin,
|
||||
description: 'An unhosted maps application.',
|
||||
icons: [],
|
||||
});
|
||||
|
||||
// Trigger the deep link intent immediately for the user if on mobile
|
||||
if (this.isMobile) {
|
||||
console.debug('Mobile detected, triggering deep link intent.');
|
||||
window.location.href = this.connectUri;
|
||||
}
|
||||
|
||||
// Start listening to the relay
|
||||
console.debug('Opening signer connection to relay...');
|
||||
await this._signerInstance.open();
|
||||
console.debug('Signer connection opened successfully.');
|
||||
|
||||
// Wait for the remote signer to reply with their pubkey
|
||||
console.debug('Waiting for remote signer to ack via relay...');
|
||||
try {
|
||||
this._signerInstance = new ExtensionSigner();
|
||||
this.pubkey = await this._signerInstance.getPublicKey();
|
||||
this.signerType = 'extension';
|
||||
localStorage.setItem(STORAGE_KEY, this.pubkey);
|
||||
localStorage.setItem(STORAGE_KEY_TYPE, 'extension');
|
||||
return this.pubkey;
|
||||
} catch (error) {
|
||||
console.error('Failed to get public key from extension:', error);
|
||||
throw error;
|
||||
await this._signerInstance.waitForSigner();
|
||||
console.debug('Remote signer ack received!');
|
||||
} catch (waitErr) {
|
||||
console.error('Error while waiting for remote signer ack:', waitErr);
|
||||
throw waitErr;
|
||||
}
|
||||
} else if (type === 'connect') {
|
||||
this.connectStatus = 'waiting';
|
||||
|
||||
try {
|
||||
// Generate or retrieve a local ephemeral keypair
|
||||
let localKeyHex = localStorage.getItem(STORAGE_KEY_CONNECT_LOCAL_KEY);
|
||||
let localSigner;
|
||||
if (localKeyHex) {
|
||||
localSigner = PrivateKeySigner.fromKey(localKeyHex);
|
||||
} else {
|
||||
localSigner = new PrivateKeySigner();
|
||||
// Store the raw Uint8Array as hex string
|
||||
localKeyHex = Array.from(localSigner.key)
|
||||
.map((b) => b.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
localStorage.setItem(STORAGE_KEY_CONNECT_LOCAL_KEY, localKeyHex);
|
||||
}
|
||||
// Once connected, get the actual user pubkey
|
||||
this.pubkey = await this._signerInstance.getPublicKey();
|
||||
this.signerType = 'connect';
|
||||
this.connectStatus = 'connected';
|
||||
|
||||
// We use a specific relay for the connection handshake.
|
||||
const relay = 'wss://relay.nsec.app';
|
||||
localStorage.setItem(STORAGE_KEY_CONNECT_RELAY, relay);
|
||||
// Save connection state
|
||||
localStorage.setItem(STORAGE_KEY, this.pubkey);
|
||||
localStorage.setItem(STORAGE_KEY_TYPE, 'connect');
|
||||
localStorage.setItem(
|
||||
STORAGE_KEY_CONNECT_REMOTE_PUBKEY,
|
||||
this._signerInstance.remote
|
||||
);
|
||||
|
||||
// Override aggressive 10s EOSE timeout to allow time for QR scanning
|
||||
this.nostrRelay.pool.relay(relay).eoseTimeout = 180000; // 3 minutes
|
||||
|
||||
this._signerInstance = new NostrConnectSigner({
|
||||
pool: this.nostrRelay.pool,
|
||||
relays: [relay],
|
||||
signer: localSigner,
|
||||
onAuth: async (url) => {
|
||||
// NIP-46 auth callback. Normally the signer app does this natively via notification.
|
||||
// But if it requires an explicit browser window:
|
||||
if (
|
||||
confirm(
|
||||
`Your signer app requests authentication via a web page. Open it now?\n\nURL: ${url}`
|
||||
)
|
||||
) {
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Set the uri for display (e.g., to redirect via intent)
|
||||
this.connectUri = this._signerInstance.getNostrConnectURI({
|
||||
name: 'Marco',
|
||||
url: window.location.origin,
|
||||
description: 'An unhosted maps application.',
|
||||
icons: [],
|
||||
});
|
||||
|
||||
// Trigger the deep link intent immediately for the user if on mobile
|
||||
if (this.isMobile) {
|
||||
console.debug('Mobile detected, triggering deep link intent.');
|
||||
window.location.href = this.connectUri;
|
||||
}
|
||||
|
||||
// Start listening to the relay
|
||||
console.debug('Opening signer connection to relay...');
|
||||
await this._signerInstance.open();
|
||||
console.debug('Signer connection opened successfully.');
|
||||
|
||||
// Wait for the remote signer to reply with their pubkey
|
||||
console.debug('Waiting for remote signer to ack via relay...');
|
||||
try {
|
||||
await this._signerInstance.waitForSigner();
|
||||
console.debug('Remote signer ack received!');
|
||||
} catch (waitErr) {
|
||||
console.error('Error while waiting for remote signer ack:', waitErr);
|
||||
throw waitErr;
|
||||
}
|
||||
|
||||
// Once connected, get the actual user pubkey
|
||||
this.pubkey = await this._signerInstance.getPublicKey();
|
||||
this.signerType = 'connect';
|
||||
this.connectStatus = 'connected';
|
||||
|
||||
// Save connection state
|
||||
localStorage.setItem(STORAGE_KEY, this.pubkey);
|
||||
localStorage.setItem(STORAGE_KEY_TYPE, 'connect');
|
||||
localStorage.setItem(
|
||||
STORAGE_KEY_CONNECT_REMOTE_PUBKEY,
|
||||
this._signerInstance.remote
|
||||
);
|
||||
|
||||
return this.pubkey;
|
||||
} catch (error) {
|
||||
this.connectStatus = null;
|
||||
console.error('Failed to connect via Nostr Connect:', error);
|
||||
throw error;
|
||||
}
|
||||
return this.pubkey;
|
||||
} catch (error) {
|
||||
this.connectStatus = null;
|
||||
console.error('Failed to connect via Nostr Connect:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async _initConnectSigner() {
|
||||
const localKeyHex = localStorage.getItem(STORAGE_KEY_CONNECT_LOCAL_KEY);
|
||||
const remotePubkey = localStorage.getItem(
|
||||
STORAGE_KEY_CONNECT_REMOTE_PUBKEY
|
||||
);
|
||||
const relay =
|
||||
localStorage.getItem(STORAGE_KEY_CONNECT_RELAY) || 'wss://relay.nsec.app';
|
||||
localStorage.getItem(STORAGE_KEY_CONNECT_RELAY) || DEFAULT_CONNECT_RELAY;
|
||||
|
||||
if (!localKeyHex || !remotePubkey) {
|
||||
throw new Error('Missing Nostr Connect local state.');
|
||||
if (!remotePubkey) {
|
||||
throw new Error('Missing Nostr Connect remote pubkey.');
|
||||
}
|
||||
|
||||
const localSigner = PrivateKeySigner.fromKey(localKeyHex);
|
||||
const localSigner = this._getLocalSigner();
|
||||
|
||||
// Override aggressive 10s EOSE timeout to allow time for QR scanning
|
||||
this.nostrRelay.pool.relay(relay).eoseTimeout = 180000; // 3 minutes
|
||||
@@ -259,7 +264,7 @@ export default class NostrAuthService extends Service {
|
||||
return await this.signer.signEvent(event);
|
||||
}
|
||||
|
||||
async logout() {
|
||||
async disconnect() {
|
||||
this.pubkey = null;
|
||||
this.signerType = null;
|
||||
this.connectStatus = null;
|
||||
|
||||
Reference in New Issue
Block a user