From 9eda3d5c86a9f75a541cad6efe31b448ff8804b7 Mon Sep 17 00:00:00 2001 From: "shakespeare.diy" Date: Thu, 9 Jul 2026 12:34:10 +0700 Subject: [PATCH] Fix capacitorAmberSigner: rewrite with correct positional API The actual plugin source uses positional args, not object params: - getPublicKey(packageName) - signEvent(packageName, eventJson, id, npub) - nip04Encrypt/Decrypt(packageName, text, id, pubKey, npub) - nip44Encrypt/Decrypt(packageName, text, id, pubKey, npub) - isExternalSignerInstalled(packageName) Previous implementation was based on wrong README docs. Now based on actual dist/esm/index.js source. Co-authored-by: shakespeare.diy --- src/lib/capacitorAmberSigner.ts | 127 +++++++++++++++++++++----------- 1 file changed, 84 insertions(+), 43 deletions(-) diff --git a/src/lib/capacitorAmberSigner.ts b/src/lib/capacitorAmberSigner.ts index d992e06..67e69e6 100644 --- a/src/lib/capacitorAmberSigner.ts +++ b/src/lib/capacitorAmberSigner.ts @@ -1,37 +1,30 @@ /** * CapacitorAmberSigner * - * A NostrSigner implementation that delegates all cryptographic operations - * to the nostr-signer-capacitor-plugin, which communicates with Amber - * (or any NIP-55 compatible signer app) via native Android intents. + * Wraps nostr-signer-capacitor-plugin with the CORRECT API as per the + * actual compiled source (dist/esm/index.js) — NOT the README docs. * - * This ONLY works inside a Capacitor native app on Android. - * On web/PWA the AmberSigner (URL redirect) is used instead. + * Every method takes packageName as the FIRST positional argument. + * signEvent/encrypt/decrypt also require id (unique op id) and npub. */ import type { NostrEvent, NostrSigner } from '@nostrify/nostrify'; import { nip19 } from 'nostr-tools'; -// Amber's package name on Android -const AMBER_PACKAGE_NAME = 'com.greenart7c3.nostrsigner'; +const AMBER_PACKAGE = 'com.greenart7c3.nostrsigner'; + +function makeId(): string { + return `${Date.now()}_${Math.random().toString(36).slice(2, 9)}`; +} -/** - * Load the plugin using its correct named export: NostrSignerPlugin. - * The plugin calls ensureAndroid() internally — if not on Android it throws - * with code ANDROID_ONLY. We catch that and return null so the web/PWA - * path is used instead. - */ async function getPlugin() { try { - // Named export — NOT a default export const { NostrSignerPlugin } = await import('nostr-signer-capacitor-plugin'); if (!NostrSignerPlugin) return null; - // setPackageName takes a plain string, not { packageName: string } - await NostrSignerPlugin.setPackageName(AMBER_PACKAGE_NAME); + // setPackageName(packageName: string) — plain string, first arg + await NostrSignerPlugin.setPackageName(AMBER_PACKAGE); return NostrSignerPlugin; } catch (e: unknown) { - // ANDROID_ONLY error = not on Android (web/PWA) — silent fallback - // Any other error = plugin not loaded correctly — log it const code = (e as { code?: string })?.code; if (code !== 'ANDROID_ONLY') { console.error('[CapacitorAmberSigner] getPlugin failed:', e); @@ -40,81 +33,129 @@ async function getPlugin() { } } +/** + * Login: get public key from Amber. + * getPublicKey(packageName, permissions?) + * Returns { npub: string } + */ +export async function getAmberPublicKeyViaPlugin(): Promise { + const plugin = await getPlugin(); + if (!plugin) throw new Error('Capacitor Amber plugin not available — are you on a native build?'); + + const { npub } = await plugin.getPublicKey(AMBER_PACKAGE); + + const decoded = nip19.decode(npub); + if (decoded.type !== 'npub') throw new Error('Unexpected key format from Amber'); + return decoded.data as string; +} + export class CapacitorAmberSigner implements NostrSigner { readonly pubkey: string; + // Store the npub for use in signing calls + private readonly npub: string; constructor(pubkeyHex: string) { this.pubkey = pubkeyHex; + this.npub = nip19.npubEncode(pubkeyHex); } async getPublicKey(): Promise { return this.pubkey; } + /** + * signEvent(packageName, eventJson, id, npub) + * Returns { event: string } — signed event JSON + */ async signEvent(event: Omit): Promise { const plugin = await getPlugin(); if (!plugin) throw new Error('Capacitor Amber plugin not available'); - const { event: signedJson } = await plugin.signEvent({ - eventJson: JSON.stringify(event), - }); + + const id = makeId(); + const { event: signedJson } = await plugin.signEvent( + AMBER_PACKAGE, + JSON.stringify(event), + id, + this.npub, + ); return JSON.parse(signedJson) as NostrEvent; } readonly nip04 = { + /** + * nip04Encrypt(packageName, plainText, id, pubKey, npub) + */ encrypt: async (pubkey: string, plaintext: string): Promise => { const plugin = await getPlugin(); if (!plugin) throw new Error('Capacitor Amber plugin not available'); - const { result } = await plugin.nip04Encrypt({ pubKey: pubkey, plainText: plaintext }); + const { result } = await plugin.nip04Encrypt( + AMBER_PACKAGE, + plaintext, + makeId(), + pubkey, + this.npub, + ); return result; }, + /** + * nip04Decrypt(packageName, encryptedText, id, pubKey, npub) + */ decrypt: async (pubkey: string, ciphertext: string): Promise => { const plugin = await getPlugin(); if (!plugin) throw new Error('Capacitor Amber plugin not available'); - const { result } = await plugin.nip04Decrypt({ pubKey: pubkey, encryptedText: ciphertext }); + const { result } = await plugin.nip04Decrypt( + AMBER_PACKAGE, + ciphertext, + makeId(), + pubkey, + this.npub, + ); return result; }, }; readonly nip44 = { + /** + * nip44Encrypt(packageName, plainText, id, pubKey, npub) + */ encrypt: async (pubkey: string, plaintext: string): Promise => { const plugin = await getPlugin(); if (!plugin) throw new Error('Capacitor Amber plugin not available'); - const { result } = await plugin.nip44Encrypt({ pubKey: pubkey, plainText: plaintext }); + const { result } = await plugin.nip44Encrypt( + AMBER_PACKAGE, + plaintext, + makeId(), + pubkey, + this.npub, + ); return result; }, + /** + * nip44Decrypt(packageName, encryptedText, id, pubKey, npub) + */ decrypt: async (pubkey: string, ciphertext: string): Promise => { const plugin = await getPlugin(); if (!plugin) throw new Error('Capacitor Amber plugin not available'); - const { result } = await plugin.nip44Decrypt({ pubKey: pubkey, encryptedText: ciphertext }); + const { result } = await plugin.nip44Decrypt( + AMBER_PACKAGE, + ciphertext, + makeId(), + pubkey, + this.npub, + ); return result; }, }; } /** - * Request the public key from Amber via the Capacitor plugin. - * Returns hex pubkey string. Throws on failure or cancellation. - */ -export async function getAmberPublicKeyViaPlugin(): Promise { - const plugin = await getPlugin(); - if (!plugin) throw new Error('Capacitor Amber plugin not available — are you on a native build?'); - - const { npub } = await plugin.getPublicKey(); - - // Plugin returns npub — decode to hex - const decoded = nip19.decode(npub); - if (decoded.type !== 'npub') throw new Error('Unexpected key format from Amber plugin'); - return decoded.data as string; -} - -/** - * Check if Amber is installed. Returns false on web/PWA. + * isExternalSignerInstalled(packageName) */ export async function isAmberInstalled(): Promise { const plugin = await getPlugin(); if (!plugin) return false; try { - const { installed } = await plugin.isExternalSignerInstalled(AMBER_PACKAGE_NAME); + const { installed } = await plugin.isExternalSignerInstalled(AMBER_PACKAGE); return installed; } catch { return false;