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 { NostrEvent, Filter } from 'nostr-tools'
import { Helpers } from 'applesauce-core'
import { RELAYS } from '../src/config/relays'
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 = {
html: string

View File

@@ -8,7 +8,6 @@ import { AccountManager, Accounts } from 'applesauce-accounts'
import { registerCommonAccountTypes } from 'applesauce-accounts/accounts'
import { RelayPool } from 'applesauce-relay'
import { NostrConnectSigner } from 'applesauce-signers'
import { reconnectBunkerSigner } from './services/nostrConnect'
import { createAddressLoader } from 'applesauce-loaders/loaders'
import Bookmarks from './components/Bookmarks'
import RouteDebug from './components/RouteDebug'
@@ -192,29 +191,52 @@ function App() {
// Create relay pool and set it up BEFORE loading accounts
// NostrConnectAccount.fromJSON needs this to restore the signer
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)
NostrConnectSigner.subscriptionMethod = pool.subscription.bind(pool)
NostrConnectSigner.publishMethod = pool.publish.bind(pool)
// Create a relay group for better event deduplication and management
pool.group(RELAYS)
console.log('[bunker] Created relay group with', RELAYS.length, 'relays (including local)')
// Load persisted accounts from localStorage (per applesauce examples)
const savedAccounts = JSON.parse(localStorage.getItem('accounts') || '[]')
await accounts.fromJSON(savedAccounts)
// Load persisted accounts from localStorage
try {
const accountsJson = localStorage.getItem('accounts')
console.log('[bunker] Raw accounts from localStorage:', accountsJson)
// Restore active account
const activeAccountId = localStorage.getItem('active')
if (activeAccountId) {
const account = accounts.getAccount(activeAccountId)
if (account) accounts.setActive(account)
const json = JSON.parse(accountsJson || '[]')
console.log('[bunker] Parsed accounts:', json.length, 'accounts')
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(() => {
localStorage.setItem('accounts', JSON.stringify(accounts.toJSON()))
})
// Subscribe to active account changes and persist to localStorage
const activeSub = accounts.active$.subscribe((account) => {
if (account) {
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 bunkerReconnectSub = accounts.active$.subscribe(async (account) => {
if (account?.type === 'nostr-connect' && !reconnectedAccounts.has(account.id)) {
reconnectedAccounts.add(account.id)
await reconnectBunkerSigner(account as Accounts.NostrConnectAccount<unknown>, pool)
}
})
const bunkerReconnectSub = accounts.active$.subscribe(async (account) => {
console.log('[bunker] Active account changed:', {
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
// 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 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(
bookmarkListEvents: NostrEvent[],
activeAccount: ActiveAccount,
@@ -90,73 +75,38 @@ export async function collectBookmarksFromEvents(
try {
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 {
await withTimeout(
Helpers.unlockHiddenTags(evt, signerCandidate as HiddenContentSigner),
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)
await Helpers.unlockHiddenTags(evt, signerCandidate as HiddenContentSigner)
} catch {
try {
await withTimeout(
Helpers.unlockHiddenTags(evt, signerCandidate as HiddenContentSigner, 'nip44' as UnlockMode),
5000,
'unlockHiddenTags(nip44)'
)
console.log('[bunker] ✅ Unlocked hidden tags with nip44')
} catch (err2) {
console.log('[bunker] ❌ nip44 unlock failed (or timed out):', err2)
await Helpers.unlockHiddenTags(evt, signerCandidate as HiddenContentSigner, 'nip44' as UnlockMode)
} catch {
// ignore
}
}
} 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
try {
if (hasNip44Decrypt(signerCandidate)) {
console.log('[bunker] Trying nip44 decrypt...')
decryptedContent = await withTimeout(
(signerCandidate as { nip44: { decrypt: DecryptFn } }).nip44.decrypt(
evt.pubkey,
evt.content
),
6000,
'nip44.decrypt'
decryptedContent = await (signerCandidate as { nip44: { decrypt: DecryptFn } }).nip44.decrypt(
evt.pubkey,
evt.content
)
console.log('[bunker] ✅ nip44 decrypt succeeded')
}
} catch (err) {
console.log('[bunker] ⚠️ nip44 decrypt failed (or timed out):', err)
} catch {
// ignore
}
if (!decryptedContent) {
try {
if (hasNip04Decrypt(signerCandidate)) {
console.log('[bunker] Trying nip04 decrypt...')
decryptedContent = await withTimeout(
(signerCandidate as { nip04: { decrypt: DecryptFn } }).nip04.decrypt(
evt.pubkey,
evt.content
),
6000,
'nip04.decrypt'
decryptedContent = await (signerCandidate as { nip04: { decrypt: DecryptFn } }).nip04.decrypt(
evt.pubkey,
evt.content
)
console.log('[bunker] ✅ nip04 decrypt succeeded')
}
} catch (err) {
console.log('[bunker] ❌ nip04 decrypt failed (or timed out):', err)
} catch {
// ignore
}
}

View File

@@ -83,16 +83,30 @@ export const fetchBookmarks = async (
// Keep existing bookmarks visible; do not clear list if nothing new found
return
}
// Get account with signer for decryption
// Aggregate across events
const maybeAccount = activeAccount as AccountWithExtension
let signerCandidate: unknown = maybeAccount
console.log('🔐 Account object:', {
hasSignEvent: typeof maybeAccount?.signEvent === 'function',
hasSigner: !!maybeAccount?.signer,
accountType: typeof maybeAccount,
accountKeys: maybeAccount ? Object.keys(maybeAccount) : []
})
// Fallback to raw signer if account doesn't expose nip04/nip44
// 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
const hasNip04Prop = (signerCandidate as { nip04?: unknown })?.nip04 !== undefined
const hasNip44Prop = (signerCandidate as { nip44?: unknown })?.nip44 !== undefined
if (signerCandidate && !hasNip04Prop && !hasNip44Prop && maybeAccount?.signer) {
// Fallback to the raw signer if account doesn't have nip04/nip44
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(
bookmarkListEvents,
activeAccount,

View File

@@ -48,8 +48,6 @@ export async function createHighlight(
// Create EventFactory with the account as signer
const factory = new EventFactory({ signer: account })
// Let signer.requireConnection handle connectivity during sign
let blueprintSource: NostrEvent | AddressPointer | string
let context: string | undefined
@@ -119,19 +117,8 @@ export async function createHighlight(
// Sign the event
console.log('[bunker] Signing highlight event...', { kind: highlightEvent.kind, tags: highlightEvent.tags.length })
let signedEvent
try {
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
}
const signedEvent = await factory.sign(highlightEvent)
console.log('[bunker] ✅ Highlight signed successfully!', { id: signedEvent.id.slice(0, 8) })
// Use unified write service to store and publish
await publishEvent(relayPool, eventStore, signedEvent)

View File

@@ -1,6 +1,4 @@
import { NostrConnectSigner } from 'applesauce-signers'
import { Accounts } from 'applesauce-accounts'
import { RelayPool } from 'applesauce-relay'
/**
* 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
}
}