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 <assistant@shakespeare.diy>
This commit is contained in:
shakespeare.diy 2026-07-09 12:34:10 +07:00
parent 0f758d7b1f
commit 9eda3d5c86

View file

@ -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<string> {
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<string> {
return this.pubkey;
}
/**
* signEvent(packageName, eventJson, id, npub)
* Returns { event: string } — signed event JSON
*/
async signEvent(event: Omit<NostrEvent, 'id' | 'sig'>): Promise<NostrEvent> {
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<string> => {
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<string> => {
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<string> => {
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<string> => {
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<string> {
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<boolean> {
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;