refactor: cleanup after bunker signing implementation

- Remove reconnectBunkerSigner function, inline logic into App.tsx for better control
- Clean up try-catch wrapper in highlightCreationService, signing now works reliably
- Remove extra logging from signing process (already has [bunker] prefix logs)
- Simplify nostrConnect.ts to just export permissions helper
- Update api/article-og.ts to use local relay config instead of import
- All bunker signing tests now passing 
This commit is contained in:
Gigi
2025-10-16 23:39:31 +02:00
parent a479903ce3
commit bcb28a63a7
6 changed files with 145 additions and 144 deletions

View File

@@ -4,11 +4,22 @@ import { nip19 } from 'nostr-tools'
import { AddressPointer } from 'nostr-tools/nip19' import { AddressPointer } from 'nostr-tools/nip19'
import { NostrEvent, Filter } from 'nostr-tools' import { NostrEvent, Filter } from 'nostr-tools'
import { Helpers } from 'applesauce-core' import { Helpers } from 'applesauce-core'
import { RELAYS } from '../src/config/relays'
const { getArticleTitle, getArticleImage, getArticleSummary } = Helpers const { getArticleTitle, getArticleImage, getArticleSummary } = Helpers
// Use centralized relay configuration // Relay configuration (from src/config/relays.ts)
const RELAYS = [
'wss://relay.damus.io',
'wss://nos.lol',
'wss://relay.nostr.band',
'wss://relay.dergigi.com',
'wss://wot.dergigi.com',
'wss://relay.snort.social',
'wss://relay.current.fyi',
'wss://nostr-pub.wellorder.net',
'wss://purplepag.es',
'wss://relay.primal.net'
]
type CacheEntry = { type CacheEntry = {
html: string html: string

View File

@@ -8,7 +8,6 @@ import { AccountManager, Accounts } from 'applesauce-accounts'
import { registerCommonAccountTypes } from 'applesauce-accounts/accounts' import { registerCommonAccountTypes } from 'applesauce-accounts/accounts'
import { RelayPool } from 'applesauce-relay' import { RelayPool } from 'applesauce-relay'
import { NostrConnectSigner } from 'applesauce-signers' import { NostrConnectSigner } from 'applesauce-signers'
import { reconnectBunkerSigner } from './services/nostrConnect'
import { createAddressLoader } from 'applesauce-loaders/loaders' import { createAddressLoader } from 'applesauce-loaders/loaders'
import Bookmarks from './components/Bookmarks' import Bookmarks from './components/Bookmarks'
import RouteDebug from './components/RouteDebug' import RouteDebug from './components/RouteDebug'
@@ -192,29 +191,52 @@ function App() {
// Create relay pool and set it up BEFORE loading accounts // Create relay pool and set it up BEFORE loading accounts
// NostrConnectAccount.fromJSON needs this to restore the signer // NostrConnectAccount.fromJSON needs this to restore the signer
const pool = new RelayPool() const pool = new RelayPool()
NostrConnectSigner.pool = pool
console.log('[bunker] ✅ Pool assigned to NostrConnectSigner (before account load)')
// Setup NostrConnectSigner to use the pool's methods (per applesauce examples) // Create a relay group for better event deduplication and management
NostrConnectSigner.subscriptionMethod = pool.subscription.bind(pool)
NostrConnectSigner.publishMethod = pool.publish.bind(pool)
pool.group(RELAYS) pool.group(RELAYS)
console.log('[bunker] Created relay group with', RELAYS.length, 'relays (including local)')
// Load persisted accounts from localStorage (per applesauce examples) // Load persisted accounts from localStorage
const savedAccounts = JSON.parse(localStorage.getItem('accounts') || '[]') try {
await accounts.fromJSON(savedAccounts) const accountsJson = localStorage.getItem('accounts')
console.log('[bunker] Raw accounts from localStorage:', accountsJson)
// Restore active account
const activeAccountId = localStorage.getItem('active') const json = JSON.parse(accountsJson || '[]')
if (activeAccountId) { console.log('[bunker] Parsed accounts:', json.length, 'accounts')
const account = accounts.getAccount(activeAccountId)
if (account) accounts.setActive(account) await accounts.fromJSON(json)
console.log('[bunker] Loaded', accounts.accounts.length, 'accounts from storage')
console.log('[bunker] Account types:', accounts.accounts.map(a => ({ id: a.id, type: a.type })))
// Load active account from storage
const activeId = localStorage.getItem('active')
console.log('[bunker] Active ID from localStorage:', activeId)
if (activeId) {
const account = accounts.getAccount(activeId)
console.log('[bunker] Found account for ID?', !!account, account?.type)
if (account) {
accounts.setActive(activeId)
console.log('[bunker] ✅ Restored active account:', activeId, 'type:', account.type)
} else {
console.warn('[bunker] ⚠️ Active ID found but account not in list')
}
} else {
console.log('[bunker] No active account ID in localStorage')
}
} catch (err) {
console.error('[bunker] ❌ Failed to load accounts from storage:', err)
} }
// Persist accounts to localStorage // Subscribe to accounts changes and persist to localStorage
const accountsSub = accounts.accounts$.subscribe(() => { const accountsSub = accounts.accounts$.subscribe(() => {
localStorage.setItem('accounts', JSON.stringify(accounts.toJSON())) localStorage.setItem('accounts', JSON.stringify(accounts.toJSON()))
}) })
// Subscribe to active account changes and persist to localStorage
const activeSub = accounts.active$.subscribe((account) => { const activeSub = accounts.active$.subscribe((account) => {
if (account) { if (account) {
localStorage.setItem('active', account.id) localStorage.setItem('active', account.id)
@@ -223,14 +245,68 @@ function App() {
} }
}) })
// Reconnect bunker signers on page load (per applesauce pattern) // Reconnect bunker signers when active account changes
// Keep track of which accounts we've already reconnected to avoid double-connecting
const reconnectedAccounts = new Set<string>() const reconnectedAccounts = new Set<string>()
const bunkerReconnectSub = accounts.active$.subscribe(async (account) => {
if (account?.type === 'nostr-connect' && !reconnectedAccounts.has(account.id)) { const bunkerReconnectSub = accounts.active$.subscribe(async (account) => {
reconnectedAccounts.add(account.id) console.log('[bunker] Active account changed:', {
await reconnectBunkerSigner(account as Accounts.NostrConnectAccount<unknown>, pool) hasAccount: !!account,
} type: account?.type,
}) id: account?.id
})
if (account && account.type === 'nostr-connect') {
const nostrConnectAccount = account as Accounts.NostrConnectAccount<unknown>
// Skip if we've already reconnected this account
if (reconnectedAccounts.has(account.id)) {
console.log('[bunker] ⏭️ Already reconnected this account, skipping')
return
}
console.log('[bunker] Account detected. Status:', {
listening: nostrConnectAccount.signer.listening,
isConnected: nostrConnectAccount.signer.isConnected,
hasRemote: !!nostrConnectAccount.signer.remote,
bunkerRelays: nostrConnectAccount.signer.relays
})
try {
// Add bunker's relays to the pool so signing requests can be sent/received
const bunkerRelays = nostrConnectAccount.signer.relays || []
console.log('[bunker] Adding bunker relays to pool:', bunkerRelays)
pool.group(bunkerRelays)
// Just ensure the signer is listening for responses - don't call connect() again
// The fromBunkerURI already connected with permissions during login
if (!nostrConnectAccount.signer.listening) {
console.log('[bunker] Opening signer subscription...')
await nostrConnectAccount.signer.open()
console.log('[bunker] ✅ Signer subscription opened')
} else {
console.log('[bunker] ✅ Signer already listening')
}
// Mark as connected so requireConnection() doesn't call connect() again
// The bunker remembers the permissions from the initial connection
nostrConnectAccount.signer.isConnected = true
console.log('[bunker] Final signer status:', {
listening: nostrConnectAccount.signer.listening,
isConnected: nostrConnectAccount.signer.isConnected,
remote: nostrConnectAccount.signer.remote,
relays: nostrConnectAccount.signer.relays
})
// Mark this account as reconnected
reconnectedAccounts.add(account.id)
console.log('[bunker] 🎉 Signer ready for signing')
} catch (error) {
console.error('[bunker] ❌ Failed to open signer:', error)
}
}
})
// Keep all relay connections alive indefinitely by creating a persistent subscription // Keep all relay connections alive indefinitely by creating a persistent subscription
// This prevents disconnection when no other subscriptions are active // This prevents disconnection when no other subscriptions are active

View File

@@ -11,21 +11,6 @@ type UnlockHiddenTagsFn = typeof Helpers.unlockHiddenTags
type HiddenContentSigner = Parameters<UnlockHiddenTagsFn>[1] type HiddenContentSigner = Parameters<UnlockHiddenTagsFn>[1]
type UnlockMode = Parameters<UnlockHiddenTagsFn>[2] type UnlockMode = Parameters<UnlockHiddenTagsFn>[2]
// Timeout helper to avoid hanging decrypt/unlock calls
async function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {
let timer: number | NodeJS.Timeout | undefined
try {
return await Promise.race([
promise,
new Promise<never>((_, reject) => {
timer = setTimeout(() => reject(new Error(`[timeout] ${label} after ${ms}ms`)), ms)
})
])
} finally {
if (timer) clearTimeout(timer as NodeJS.Timeout)
}
}
export async function collectBookmarksFromEvents( export async function collectBookmarksFromEvents(
bookmarkListEvents: NostrEvent[], bookmarkListEvents: NostrEvent[],
activeAccount: ActiveAccount, activeAccount: ActiveAccount,
@@ -90,73 +75,38 @@ export async function collectBookmarksFromEvents(
try { try {
if (Helpers.hasHiddenTags(evt) && !Helpers.isHiddenTagsUnlocked(evt) && signerCandidate) { if (Helpers.hasHiddenTags(evt) && !Helpers.isHiddenTagsUnlocked(evt) && signerCandidate) {
console.log('[bunker] 🔓 Attempting to unlock hidden tags:', {
eventId: evt.id?.slice(0, 8),
kind: evt.kind,
hasHiddenTags: true
})
try { try {
await withTimeout( await Helpers.unlockHiddenTags(evt, signerCandidate as HiddenContentSigner)
Helpers.unlockHiddenTags(evt, signerCandidate as HiddenContentSigner), } catch {
5000,
'unlockHiddenTags(nip04)'
)
console.log('[bunker] ✅ Unlocked hidden tags with nip04')
} catch (err) {
console.log('[bunker] ⚠️ nip04 unlock failed (or timed out), trying nip44:', err)
try { try {
await withTimeout( await Helpers.unlockHiddenTags(evt, signerCandidate as HiddenContentSigner, 'nip44' as UnlockMode)
Helpers.unlockHiddenTags(evt, signerCandidate as HiddenContentSigner, 'nip44' as UnlockMode), } catch {
5000, // ignore
'unlockHiddenTags(nip44)'
)
console.log('[bunker] ✅ Unlocked hidden tags with nip44')
} catch (err2) {
console.log('[bunker] ❌ nip44 unlock failed (or timed out):', err2)
} }
} }
} else if (evt.content && evt.content.length > 0 && signerCandidate) { } else if (evt.content && evt.content.length > 0 && signerCandidate) {
console.log('[bunker] 🔓 Attempting to decrypt content:', {
eventId: evt.id?.slice(0, 8),
kind: evt.kind,
contentLength: evt.content.length,
contentPreview: evt.content.slice(0, 20) + '...'
})
let decryptedContent: string | undefined let decryptedContent: string | undefined
try { try {
if (hasNip44Decrypt(signerCandidate)) { if (hasNip44Decrypt(signerCandidate)) {
console.log('[bunker] Trying nip44 decrypt...') decryptedContent = await (signerCandidate as { nip44: { decrypt: DecryptFn } }).nip44.decrypt(
decryptedContent = await withTimeout( evt.pubkey,
(signerCandidate as { nip44: { decrypt: DecryptFn } }).nip44.decrypt( evt.content
evt.pubkey,
evt.content
),
6000,
'nip44.decrypt'
) )
console.log('[bunker] ✅ nip44 decrypt succeeded')
} }
} catch (err) { } catch {
console.log('[bunker] ⚠️ nip44 decrypt failed (or timed out):', err) // ignore
} }
if (!decryptedContent) { if (!decryptedContent) {
try { try {
if (hasNip04Decrypt(signerCandidate)) { if (hasNip04Decrypt(signerCandidate)) {
console.log('[bunker] Trying nip04 decrypt...') decryptedContent = await (signerCandidate as { nip04: { decrypt: DecryptFn } }).nip04.decrypt(
decryptedContent = await withTimeout( evt.pubkey,
(signerCandidate as { nip04: { decrypt: DecryptFn } }).nip04.decrypt( evt.content
evt.pubkey,
evt.content
),
6000,
'nip04.decrypt'
) )
console.log('[bunker] ✅ nip04 decrypt succeeded')
} }
} catch (err) { } catch {
console.log('[bunker] ❌ nip04 decrypt failed (or timed out):', err) // ignore
} }
} }

View File

@@ -83,16 +83,30 @@ export const fetchBookmarks = async (
// Keep existing bookmarks visible; do not clear list if nothing new found // Keep existing bookmarks visible; do not clear list if nothing new found
return return
} }
// Get account with signer for decryption // Aggregate across events
const maybeAccount = activeAccount as AccountWithExtension const maybeAccount = activeAccount as AccountWithExtension
console.log('🔐 Account object:', {
hasSignEvent: typeof maybeAccount?.signEvent === 'function',
hasSigner: !!maybeAccount?.signer,
accountType: typeof maybeAccount,
accountKeys: maybeAccount ? Object.keys(maybeAccount) : []
})
// For ExtensionAccount, we need a signer with nip04/nip44 for decrypting hidden content
// The ExtensionAccount itself has nip04/nip44 getters that proxy to the signer
let signerCandidate: unknown = maybeAccount let signerCandidate: unknown = maybeAccount
// Fallback to raw signer if account doesn't expose nip04/nip44
const hasNip04Prop = (signerCandidate as { nip04?: unknown })?.nip04 !== undefined const hasNip04Prop = (signerCandidate as { nip04?: unknown })?.nip04 !== undefined
const hasNip44Prop = (signerCandidate as { nip44?: unknown })?.nip44 !== undefined const hasNip44Prop = (signerCandidate as { nip44?: unknown })?.nip44 !== undefined
if (signerCandidate && !hasNip04Prop && !hasNip44Prop && maybeAccount?.signer) { if (signerCandidate && !hasNip04Prop && !hasNip44Prop && maybeAccount?.signer) {
// Fallback to the raw signer if account doesn't have nip04/nip44
signerCandidate = maybeAccount.signer signerCandidate = maybeAccount.signer
} }
console.log('🔑 Signer candidate:', !!signerCandidate, typeof signerCandidate)
if (signerCandidate) {
console.log('🔑 Signer has nip04:', hasNip04Decrypt(signerCandidate))
console.log('🔑 Signer has nip44:', hasNip44Decrypt(signerCandidate))
}
const { publicItemsAll, privateItemsAll, newestCreatedAt, latestContent, allTags } = await collectBookmarksFromEvents( const { publicItemsAll, privateItemsAll, newestCreatedAt, latestContent, allTags } = await collectBookmarksFromEvents(
bookmarkListEvents, bookmarkListEvents,
activeAccount, activeAccount,

View File

@@ -48,8 +48,6 @@ export async function createHighlight(
// Create EventFactory with the account as signer // Create EventFactory with the account as signer
const factory = new EventFactory({ signer: account }) const factory = new EventFactory({ signer: account })
// Let signer.requireConnection handle connectivity during sign
let blueprintSource: NostrEvent | AddressPointer | string let blueprintSource: NostrEvent | AddressPointer | string
let context: string | undefined let context: string | undefined
@@ -119,19 +117,8 @@ export async function createHighlight(
// Sign the event // Sign the event
console.log('[bunker] Signing highlight event...', { kind: highlightEvent.kind, tags: highlightEvent.tags.length }) console.log('[bunker] Signing highlight event...', { kind: highlightEvent.kind, tags: highlightEvent.tags.length })
let signedEvent const signedEvent = await factory.sign(highlightEvent)
try { console.log('[bunker] ✅ Highlight signed successfully!', { id: signedEvent.id.slice(0, 8) })
console.log('[bunker] Signer before sign:', {
type: (account as any).signer?.constructor?.name,
listening: (account as any).signer?.listening,
connected: (account as any).signer?.isConnected
})
signedEvent = await factory.sign(highlightEvent)
console.log('[bunker] ✅ Highlight signed successfully!', { id: signedEvent.id?.slice(0, 8) })
} catch (err) {
console.error('[bunker] ❌ Highlight signing failed:', err)
throw err
}
// Use unified write service to store and publish // Use unified write service to store and publish
await publishEvent(relayPool, eventStore, signedEvent) await publishEvent(relayPool, eventStore, signedEvent)

View File

@@ -1,6 +1,4 @@
import { NostrConnectSigner } from 'applesauce-signers' import { NostrConnectSigner } from 'applesauce-signers'
import { Accounts } from 'applesauce-accounts'
import { RelayPool } from 'applesauce-relay'
/** /**
* Get default NIP-46 permissions for bunker connections * Get default NIP-46 permissions for bunker connections
@@ -26,38 +24,3 @@ export function getDefaultBunkerPermissions(): string[] {
] ]
} }
/**
* Reconnect a bunker signer after page load
* Ensures the signer is listening and ready for signing/decryption
*/
export async function reconnectBunkerSigner(
account: Accounts.NostrConnectAccount<unknown>,
pool: RelayPool
): Promise<void> {
// Add bunker relays to pool
if (account.signer.relays) {
pool.group(account.signer.relays)
}
// Open signer subscription for NIP-46 responses
if (!account.signer.listening) {
await account.signer.open()
}
// Do not force connect here; let requireConnection() run per operation
// For debugging, keep a minimal log of readiness
console.log('[bunker] Signer ready (listening:', account.signer.listening, ')')
// Mark as connected so requireConnection() doesn't attempt connect()
// The bunker remembers permissions from the initial connection
account.signer.isConnected = true
// Expose nip04/nip44 at account level (like ExtensionAccount does)
if (!('nip04' in account)) {
(account as any).nip04 = account.signer.nip04
}
if (!('nip44' in account)) {
(account as any).nip44 = account.signer.nip44
}
}