diff --git a/src/lib/amber.ts b/src/lib/amber.ts index 1c61c96..23ef2fc 100644 --- a/src/lib/amber.ts +++ b/src/lib/amber.ts @@ -300,24 +300,44 @@ function buildCallbackUrl(): string { /** * Navigate to an Amber nostrsigner: URL. * - * Per NIP-55, web clients must navigate directly to the nostrsigner: URI - * using window.location.href. This fires an Android intent that Amber - * intercepts via its registered nostrsigner: scheme handler. + * For web apps the correct Android intent format is: + * intent:#Intent;scheme=nostrsigner;S.=;...;end * - * DO NOT use window.open(..., '_blank') — that opens a new tab/window - * instead of firing the intent, which is what caused "malformed request". + * This is the format Amber's GitHub README documents for web clients. + * The nostrsigner: URL format (without intent: wrapper) causes the + * "malformed request" error because Android doesn't route it correctly + * from a browser context. + * + * Reference: https://github.com/greenart7c3/Amber (Web Applications section) */ function openAmberUrl(url: string): void { window.location.href = url; } +/** + * Build an Android intent URL for Amber web usage. + * Converts nostrsigner: params into the intent: format Amber expects from browsers. + * + * Format: intent:#Intent;scheme=nostrsigner;S.=;...;end + */ +function buildIntentUrl(payload: string, params: Record): string { + const extras = Object.entries(params) + .map(([k, v]) => `S.${k}=${encodeURIComponent(v)}`) + .join(';'); + return `intent:${payload}#Intent;scheme=nostrsigner;${extras};end`; +} + /** * Open Amber for get_public_key WITHOUT a callback URL. * Used in PWA/Capacitor mode: Amber copies pubkey to clipboard, * the app reads it on visibilitychange. */ export function openAmberForLoginNoCallback(): void { - const amberUrl = 'nostrsigner:?compressionType=none&returnType=signature&type=get_public_key'; + const amberUrl = buildIntentUrl('', { + compressionType: 'none', + returnType: 'signature', + type: 'get_public_key', + }); openAmberUrl(amberUrl); } @@ -343,10 +363,15 @@ export function openAmberForLogin(): void { const currentPath = window.location.hash.replace(/^#/, '') || '/'; localStorage.setItem('amber_return_path', currentPath); - // NIP-55 nostrsigner: URI — Amber opens, user approves, Amber calls callbackUrl - const amberUrl = `nostrsigner:?compressionType=none&returnType=signature&type=get_public_key&callbackUrl=${encodeURIComponent(callbackUrl)}`; + // Android intent format for web → Amber communication (per Amber README) + const amberUrl = buildIntentUrl('', { + compressionType: 'none', + returnType: 'signature', + type: 'get_public_key', + callbackUrl: callbackUrl, + }); - console.log('[Amber] Opening Amber for login, callback:', callbackUrl); + console.log('[Amber] Opening Amber for login, intent:', amberUrl); openAmberUrl(amberUrl); } @@ -386,9 +411,13 @@ export function openAmberForSignEvent(event: Omit): vo timestamp: Date.now(), }); - const encodedEvent = encodeURIComponent(eventJson); - const amberUrl = `nostrsigner:${encodedEvent}?compressionType=none&returnType=event&type=sign_event&callbackUrl=${encodeURIComponent(callbackUrl)}`; - + const amberUrl = buildIntentUrl(encodeURIComponent(eventJson), { + compressionType: 'none', + returnType: 'event', + type: 'sign_event', + callbackUrl: callbackUrl, + }); + openAmberUrl(amberUrl); } @@ -407,9 +436,14 @@ export function openAmberForNip04Encrypt(pubkey: string, plaintext: string): voi timestamp: Date.now(), }); - const encodedText = encodeURIComponent(plaintext); - const amberUrl = `nostrsigner:${encodedText}?pubkey=${pubkey}&compressionType=none&returnType=signature&type=nip04_encrypt&callbackUrl=${encodeURIComponent(callbackUrl)}`; - + const amberUrl = buildIntentUrl(encodeURIComponent(plaintext), { + pubKey: pubkey, + compressionType: 'none', + returnType: 'signature', + type: 'nip04_encrypt', + callbackUrl: callbackUrl, + }); + openAmberUrl(amberUrl); } @@ -428,9 +462,14 @@ export function openAmberForNip04Decrypt(pubkey: string, ciphertext: string): vo timestamp: Date.now(), }); - const encodedText = encodeURIComponent(ciphertext); - const amberUrl = `nostrsigner:${encodedText}?pubkey=${pubkey}&compressionType=none&returnType=signature&type=nip04_decrypt&callbackUrl=${encodeURIComponent(callbackUrl)}`; - + const amberUrl = buildIntentUrl(encodeURIComponent(ciphertext), { + pubKey: pubkey, + compressionType: 'none', + returnType: 'signature', + type: 'nip04_decrypt', + callbackUrl: callbackUrl, + }); + openAmberUrl(amberUrl); } @@ -449,9 +488,14 @@ export function openAmberForNip44Encrypt(pubkey: string, plaintext: string): voi timestamp: Date.now(), }); - const encodedText = encodeURIComponent(plaintext); - const amberUrl = `nostrsigner:${encodedText}?pubkey=${pubkey}&compressionType=none&returnType=signature&type=nip44_encrypt&callbackUrl=${encodeURIComponent(callbackUrl)}`; - + const amberUrl = buildIntentUrl(encodeURIComponent(plaintext), { + pubKey: pubkey, + compressionType: 'none', + returnType: 'signature', + type: 'nip44_encrypt', + callbackUrl: callbackUrl, + }); + openAmberUrl(amberUrl); } @@ -470,9 +514,14 @@ export function openAmberForNip44Decrypt(pubkey: string, ciphertext: string): vo timestamp: Date.now(), }); - const encodedText = encodeURIComponent(ciphertext); - const amberUrl = `nostrsigner:${encodedText}?pubkey=${pubkey}&compressionType=none&returnType=signature&type=nip44_decrypt&callbackUrl=${encodeURIComponent(callbackUrl)}`; - + const amberUrl = buildIntentUrl(encodeURIComponent(ciphertext), { + pubKey: pubkey, + compressionType: 'none', + returnType: 'signature', + type: 'nip44_decrypt', + callbackUrl: callbackUrl, + }); + openAmberUrl(amberUrl); }