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:<payload>#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 <assistant@shakespeare.diy>
This commit is contained in:
shakespeare.diy 2026-07-07 19:35:00 +07:00
parent 9ffc961015
commit c30546f700

View file

@ -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:<payload>#Intent;scheme=nostrsigner;S.<key>=<value>;...;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:<payload>#Intent;scheme=nostrsigner;S.<key>=<val>;...;end
*/
function buildIntentUrl(payload: string, params: Record<string, string>): 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<NostrEvent, 'id' | 'sig'>): 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);
}