From c30546f7001791d3751cf0338339dd051fa9dc2a Mon Sep 17 00:00:00 2001 From: "shakespeare.diy" Date: Tue, 7 Jul 2026 19:35:00 +0700 Subject: [PATCH] fix: use Android intent: URL format for Amber (not nostrsigner: directly) The nostrsigner: scheme used directly caused "malformed request" in Amber when opened from a browser context. The correct format per Amber's README for web clients is the Android intent: URL format: intent:#Intent;scheme=nostrsigner;S.type=...;S.callbackUrl=...;end Updated all Amber operations (login, sign, nip04/nip44 encrypt/decrypt) to use buildIntentUrl() helper instead of raw nostrsigner: URLs. Co-authored-by: shakespeare.diy --- src/lib/amber.ts | 97 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 73 insertions(+), 24 deletions(-) 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); }