refactor(nostr): route signing + encryption through a Signer abstraction

Introduce a Signer interface (signEvent / nip44Encrypt / nip44Decrypt +
sync pubkey) with an in-process LocalSigner backed by an nsec, and route
every signing/encryption call site through it. Behaviour is unchanged —
LocalSigner wraps the same MachineIdentity the code used directly before.

This is Phase A of the bunker migration (aiolabs/bitspire#52): it puts the
seam in place so Phase B can drop in a NIP-46 BunkerSigner at the bootstrap
without touching any call site. The whole chain becomes async (the bunker
path is a relay round-trip; LocalSigner resolves immediately).

Sites moved onto the signer:
- packages/nostr-client: createSignedEvent / createAuthEvent (now async),
  NostrClient config (signer not identity), AUTH challenge handler.
- packages/lnbits: LnbitsClient.initialize(nostr, signer); kind-21000 RPC
  encrypt + sign + reply-decrypt; handleReply is now async (event-id dedup
  still runs synchronously before the awaited decrypt, so replay safety and
  per-subscription hash dedup are preserved).
- apps/machine: lightning.ts builds a LocalSigner and exposes it on
  LightningServices; operator-config / operator-fees / availability beacon /
  maintenance beacon / fund-atm all sign + encrypt via the signer.

NIP-42 auth (kind 22242) is included — under the bunker it must be in the
spire policy (aiolabs/spirekeeper#26, already merged).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-06-18 19:56:35 +02:00
commit d6b22e1156
16 changed files with 300 additions and 260 deletions

View file

@ -7,14 +7,7 @@
* - Automatic reconnection
*/
import {
type Event,
type Filter,
type VerifiedEvent,
Relay,
SimplePool,
verifyEvent,
} from 'nostr-tools'
import { type Event, type Filter, Relay, SimplePool, verifyEvent, nip19 } from 'nostr-tools'
import { createAuthEvent } from './events.js'
import type {
NostrClientConfig,
@ -156,10 +149,15 @@ export class NostrClient {
// We need to extract the challenge and create our auth response
const challenge =
evt.tags?.find((t): t is [string, string] => t[0] === 'challenge')?.[1] ?? ''
const authEvent = createAuthEvent(this.config.identity, connection.config.url, challenge)
// Verify the event to get a VerifiedEvent type
const authEvent = await createAuthEvent(
this.config.signer,
connection.config.url,
challenge
)
// The signer returns a fully-signed event; re-verify defensively
// (a remote bunker could in principle return a malformed reply).
if (verifyEvent(authEvent)) {
return authEvent as VerifiedEvent
return authEvent
}
throw new Error('Failed to create valid auth event')
})
@ -393,13 +391,13 @@ export class NostrClient {
* Get the machine's public key
*/
get publicKey(): string {
return this.config.identity.publicKey
return this.config.signer.pubkey
}
/**
* Get the machine's npub
*/
get npub(): string {
return this.config.identity.npub
return nip19.npubEncode(this.config.signer.pubkey)
}
}