From 40239aa0758c8441e37d422ef73b922148e30d12 Mon Sep 17 00:00:00 2001 From: Padreug Date: Fri, 19 Jun 2026 00:15:17 +0200 Subject: [PATCH] refactor(clink): route CLINK signing + encryption through the Signer Swap CLINKClient's MachineIdentity for the Signer abstraction: sign_event / nip44 now go through the signer (async), so the spire identity can live in a NIP-46 bunker. The kind-21003 management path (operator-driven manual dispense, the one live CLINK path on dev) decrypts as the spire via the bunker; the dormant offer/debit paths are migrated too so they're bunker-ready when CLINK is re-implemented for the upcoming ndebit/k1 spec (shocknet/CLINK#7, #8). Part of Phase C, aiolabs/bitspire#52. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/clink/src/client.ts | 91 ++++++++++++++++-------------------- 1 file changed, 41 insertions(+), 50 deletions(-) diff --git a/packages/clink/src/client.ts b/packages/clink/src/client.ts index 333461f..e2b2e88 100644 --- a/packages/clink/src/client.ts +++ b/packages/clink/src/client.ts @@ -10,34 +10,30 @@ * Uses NIP-44v2 encryption for all messages. */ -import type { Event, UnsignedEvent } from 'nostr-tools' -import { finalizeEvent } from 'nostr-tools' -import type { MachineIdentity, NostrClient } from '@bitSpire/nostr-client' -import { encryptContentV2, decryptContentV2 } from '@bitSpire/nostr-client' +import type { Event, EventTemplate } from 'nostr-tools' +import type { NostrClient, Signer } from '@bitSpire/nostr-client' /** CLINK protocol version tag (mandatory per CLINK spec) */ const CLINK_VERSION_TAG: [string, string] = ['clink_version', '1'] /** - * Encrypt content using NIP-44 v2 (required for CLINK events) + * Encrypt content using NIP-44 v2 (required for CLINK events). + * Routes through the Signer so the spire identity can live in a bunker. */ -function encryptCLINK( - identity: MachineIdentity, - recipientPubkey: string, - content: unknown -): string { - return encryptContentV2(identity, recipientPubkey, content) +function encryptCLINK(signer: Signer, recipientPubkey: string, content: unknown): Promise { + const plaintext = typeof content === 'string' ? content : JSON.stringify(content) + return signer.nip44Encrypt(recipientPubkey, plaintext) } /** - * Decrypt and parse JSON content using NIP-44 v2 + * Decrypt and parse JSON content using NIP-44 v2. */ -function decryptCLINKJSON( - identity: MachineIdentity, +async function decryptCLINKJSON( + signer: Signer, senderPubkey: string, ciphertext: string -): T { - const plaintext = decryptContentV2(identity, senderPubkey, ciphertext) +): Promise { + const plaintext = await signer.nip44Decrypt(senderPubkey, ciphertext) return JSON.parse(plaintext) as T } import { @@ -67,8 +63,8 @@ import { encodeNoffer, decodeNoffer } from './noffer.js' export interface CLINKClientOptions { /** Nostr client for communication */ nostrClient: NostrClient - /** Machine identity */ - identity: MachineIdentity + /** Signer for the spire identity (local nsec or remote bunker) */ + signer: Signer /** Operator pubkey(s) for management commands */ operatorPubkey: string | string[] /** Relays to use for offers */ @@ -102,7 +98,7 @@ export type ManagementHandler = ( */ export class CLINKClient { private nostrClient: NostrClient - private identity: MachineIdentity + private signer: Signer private operatorPubkeys: string[] private relays: string[] private generateInvoice?: GenerateInvoice @@ -120,7 +116,7 @@ export class CLINKClient { constructor(options: CLINKClientOptions) { this.nostrClient = options.nostrClient - this.identity = options.identity + this.signer = options.signer this.operatorPubkeys = Array.isArray(options.operatorPubkey) ? options.operatorPubkey : [options.operatorPubkey] @@ -144,7 +140,7 @@ export class CLINKClient { currency?: string }): string { const offer: CLINKOffer = { - pubkey: this.identity.publicKey, + pubkey: this.signer.pubkey, relays: this.relays, priceType: options.priceType, offerId: options.offerId, @@ -193,7 +189,7 @@ export class CLINKClient { [ { kinds: [CLINKEventKind.Offer, CLINKEventKind.Debit, CLINKEventKind.Manage], - '#p': [this.identity.publicKey], + '#p': [this.signer.pubkey], }, ], { @@ -234,9 +230,9 @@ export class CLINKClient { expires_in_seconds: options?.expiresInSeconds, } - const content = encryptCLINK(this.identity, offer.pubkey, request) + const content = await encryptCLINK(this.signer, offer.pubkey, request) - const event = this.createSignedEvent({ + const event = await this.createSignedEvent({ kind: CLINKEventKind.Offer, content, tags: [['p', offer.pubkey], CLINK_VERSION_TAG], @@ -269,9 +265,9 @@ export class CLINKClient { description: options?.description, } - const content = encryptCLINK(this.identity, targetPubkey, request) + const content = await encryptCLINK(this.signer, targetPubkey, request) - const event = this.createSignedEvent({ + const event = await this.createSignedEvent({ kind: CLINKEventKind.Debit, content, tags: [['p', targetPubkey], CLINK_VERSION_TAG], @@ -303,9 +299,9 @@ export class CLINKClient { description: options?.description, } - const content = encryptCLINK(this.identity, targetPubkey, request) + const content = await encryptCLINK(this.signer, targetPubkey, request) - const event = this.createSignedEvent({ + const event = await this.createSignedEvent({ kind: CLINKEventKind.Debit, content, tags: [['p', targetPubkey], CLINK_VERSION_TAG], @@ -324,9 +320,9 @@ export class CLINKClient { targetPubkey: string, request: ManagementRequest ): Promise { - const content = encryptCLINK(this.identity, targetPubkey, request) + const content = await encryptCLINK(this.signer, targetPubkey, request) - const event = this.createSignedEvent({ + const event = await this.createSignedEvent({ kind: CLINKEventKind.Manage, content, tags: [['p', targetPubkey], CLINK_VERSION_TAG], @@ -394,15 +390,15 @@ export class CLINKClient { return } - const request = decryptCLINKJSON(this.identity, event.pubkey, event.content) + const request = await decryptCLINKJSON(this.signer, event.pubkey, event.content) const response = await this.offerHandler(request, event.pubkey) if (!response) return // Send encrypted response with clink_version tag - const content = encryptCLINK(this.identity, event.pubkey, response) + const content = await encryptCLINK(this.signer, event.pubkey, response) - const responseEvent = this.createSignedEvent({ + const responseEvent = await this.createSignedEvent({ kind: CLINKEventKind.Offer, content, tags: [['p', event.pubkey], ['e', event.id], CLINK_VERSION_TAG], @@ -425,14 +421,14 @@ export class CLINKClient { return } - const request = decryptCLINKJSON(this.identity, event.pubkey, event.content) + const request = await decryptCLINKJSON(this.signer, event.pubkey, event.content) const response = await this.debitHandler(request, event.pubkey) // Send encrypted response with clink_version tag - const content = encryptCLINK(this.identity, event.pubkey, response) + const content = await encryptCLINK(this.signer, event.pubkey, response) - const responseEvent = this.createSignedEvent({ + const responseEvent = await this.createSignedEvent({ kind: CLINKEventKind.Debit, content, tags: [['p', event.pubkey], ['e', event.id], CLINK_VERSION_TAG], @@ -491,15 +487,15 @@ export class CLINKClient { if (first) this.processedManageEvents.delete(first) } - const request = decryptCLINKJSON(this.identity, event.pubkey, event.content) + const request = await decryptCLINKJSON(this.signer, event.pubkey, event.content) const response = await this.managementHandler(request, event.pubkey) if (!response) return // Send encrypted response with clink_version tag - const content = encryptCLINK(this.identity, event.pubkey, response) + const content = await encryptCLINK(this.signer, event.pubkey, response) - const responseEvent = this.createSignedEvent({ + const responseEvent = await this.createSignedEvent({ kind: CLINKEventKind.Manage, content, tags: [['p', event.pubkey], ['e', event.id], CLINK_VERSION_TAG], @@ -524,7 +520,7 @@ export class CLINKClient { { kinds: [kind], authors: [fromPubkey], - '#p': [this.identity.publicKey], + '#p': [this.signer.pubkey], '#e': [requestEventId], since: Math.floor(Date.now() / 1000) - 5, }, @@ -540,12 +536,7 @@ export class CLINKClient { clearTimeout(timeout) this.nostrClient.unsubscribe(subId) - try { - const response = decryptCLINKJSON(this.identity, fromPubkey, event.content) - resolve(response) - } catch (e) { - reject(e) - } + decryptCLINKJSON(this.signer, fromPubkey, event.content).then(resolve).catch(reject) }, } ) @@ -553,11 +544,11 @@ export class CLINKClient { } /** - * Create a signed event + * Create a signed event via the signer (sets pubkey/id/sig). Async because + * a BunkerSigner is a relay round-trip. */ - private createSignedEvent(event: Omit): Event { - // finalizeEvent derives pubkey from the secret key - return finalizeEvent(event, this.identity.privateKey) + private createSignedEvent(template: EventTemplate): Promise { + return this.signer.signEvent(template) } }