mirror of
https://github.com/dergigi/boris.git
synced 2025-12-18 23:24:22 +01:00
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:
@@ -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
|
||||
|
||||
110
src/App.tsx
110
src/App.tsx
@@ -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,12 +245,66 @@ 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)) {
|
||||
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)
|
||||
await reconnectBunkerSigner(account as Accounts.NostrConnectAccount<unknown>, pool)
|
||||
console.log('[bunker] 🎉 Signer ready for signing')
|
||||
} catch (error) {
|
||||
console.error('[bunker] ❌ Failed to open signer:', error)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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(
|
||||
decryptedContent = await (signerCandidate as { nip44: { decrypt: DecryptFn } }).nip44.decrypt(
|
||||
evt.pubkey,
|
||||
evt.content
|
||||
),
|
||||
6000,
|
||||
'nip44.decrypt'
|
||||
)
|
||||
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(
|
||||
decryptedContent = await (signerCandidate as { nip04: { decrypt: DecryptFn } }).nip04.decrypt(
|
||||
evt.pubkey,
|
||||
evt.content
|
||||
),
|
||||
6000,
|
||||
'nip04.decrypt'
|
||||
)
|
||||
console.log('[bunker] ✅ nip04 decrypt succeeded')
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('[bunker] ❌ nip04 decrypt failed (or timed out):', err)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user