From 6f303d195610066ff39ac3f65b993ab2944dc4d0 Mon Sep 17 00:00:00 2001 From: "shakespeare.diy" Date: Wed, 8 Jul 2026 11:26:42 +0700 Subject: [PATCH] fix: revert to nostrsigner: format, fix double-encoding of callbackUrl Root cause of "malformed request": - buildIntentUrl() was calling encodeURIComponent() on ALL values including callbackUrl, which was ALREADY a full URL with special chars - This double-encoded the callbackUrl, arriving at Amber as garbage - The intent: wrapper format was also unnecessary Fix: back to clean nostrsigner: format per NIP-55 spec - Only callbackUrl gets encodeURIComponent (once) - All other params are plain query string values - Matches exactly what NIP-55 spec shows in its example code Co-authored-by: shakespeare.diy --- package.json | 2 +- release-notes.txt | 7 +++-- src/lib/amber.ts | 67 +++++++++++++++++++++++++---------------------- 3 files changed, 40 insertions(+), 36 deletions(-) diff --git a/package.json b/package.json index e8cd4ad..7fac79a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "skate-spots", "private": true, - "version": "1.0.28", + "version": "1.0.29", "type": "module", "homepage": "https://skate-spots.com", "scripts": { diff --git a/release-notes.txt b/release-notes.txt index f31d314..acfb2e6 100644 --- a/release-notes.txt +++ b/release-notes.txt @@ -1,5 +1,4 @@ -v1.0.28 — Amber login fixed for real +v1.0.29 — Amber login fixed (properly this time) -- Fixed "malformed request" error when logging in with Amber (NIP-55) -- Switched to correct Android intent: URL format that browsers require -- Previous versions used nostrsigner: directly which only works for native apps, not web/PWA +- Root cause found: callbackUrl was being double-encoded inside the URL builder, arriving garbled at Amber and causing "malformed request" +- Reverted to clean nostrsigner: format per NIP-55 spec with only callbackUrl encoded once diff --git a/src/lib/amber.ts b/src/lib/amber.ts index 23ef2fc..dd06153 100644 --- a/src/lib/amber.ts +++ b/src/lib/amber.ts @@ -300,31 +300,36 @@ function buildCallbackUrl(): string { /** * Navigate to an Amber nostrsigner: URL. * - * For web apps the correct Android intent format is: - * intent:#Intent;scheme=nostrsigner;S.=;...;end + * Per NIP-55 spec and Amber README, web clients use window.location.href + * with a nostrsigner: URI. The params go in the query string unencoded + * (except the callbackUrl which must be encodeURIComponent'd so Amber + * can parse it as a single value). * - * 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) + * Correct format per NIP-55 spec: + * nostrsigner:?type=get_public_key&compressionType=none&returnType=signature&callbackUrl=https://.../?event= */ 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. + * Build a nostrsigner: URL for Amber web usage. * - * Format: intent:#Intent;scheme=nostrsigner;S.=;...;end + * Per NIP-55 spec the format is: + * nostrsigner:?type=&compressionType=none&returnType=signature&callbackUrl= + * + * The callbackUrl is the ONLY value that needs encodeURIComponent. + * All other params are plain query string values. */ -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`; +function buildAmberUrl(payload: string, params: Record): string { + const query = Object.entries(params) + .map(([k, v]) => { + // callbackUrl must be encoded so Amber treats it as a single value + if (k === 'callbackUrl') return `${k}=${encodeURIComponent(v)}`; + return `${k}=${v}`; + }) + .join('&'); + return `nostrsigner:${payload}?${query}`; } /** @@ -333,10 +338,10 @@ function buildIntentUrl(payload: string, params: Record): string * the app reads it on visibilitychange. */ export function openAmberForLoginNoCallback(): void { - const amberUrl = buildIntentUrl('', { + const amberUrl = buildAmberUrl('', { + type: 'get_public_key', compressionType: 'none', returnType: 'signature', - type: 'get_public_key', }); openAmberUrl(amberUrl); } @@ -363,11 +368,11 @@ export function openAmberForLogin(): void { const currentPath = window.location.hash.replace(/^#/, '') || '/'; localStorage.setItem('amber_return_path', currentPath); - // Android intent format for web → Amber communication (per Amber README) - const amberUrl = buildIntentUrl('', { + // NIP-55 nostrsigner: URL — Amber opens, user approves, Amber calls callbackUrl + const amberUrl = buildAmberUrl('', { + type: 'get_public_key', compressionType: 'none', returnType: 'signature', - type: 'get_public_key', callbackUrl: callbackUrl, }); @@ -411,10 +416,10 @@ export function openAmberForSignEvent(event: Omit): vo timestamp: Date.now(), }); - const amberUrl = buildIntentUrl(encodeURIComponent(eventJson), { + const amberUrl = buildAmberUrl(encodeURIComponent(eventJson), { + type: 'sign_event', compressionType: 'none', returnType: 'event', - type: 'sign_event', callbackUrl: callbackUrl, }); @@ -436,11 +441,11 @@ export function openAmberForNip04Encrypt(pubkey: string, plaintext: string): voi timestamp: Date.now(), }); - const amberUrl = buildIntentUrl(encodeURIComponent(plaintext), { + const amberUrl = buildAmberUrl(encodeURIComponent(plaintext), { + type: 'nip04_encrypt', pubKey: pubkey, compressionType: 'none', returnType: 'signature', - type: 'nip04_encrypt', callbackUrl: callbackUrl, }); @@ -462,11 +467,11 @@ export function openAmberForNip04Decrypt(pubkey: string, ciphertext: string): vo timestamp: Date.now(), }); - const amberUrl = buildIntentUrl(encodeURIComponent(ciphertext), { + const amberUrl = buildAmberUrl(encodeURIComponent(ciphertext), { + type: 'nip04_decrypt', pubKey: pubkey, compressionType: 'none', returnType: 'signature', - type: 'nip04_decrypt', callbackUrl: callbackUrl, }); @@ -488,11 +493,11 @@ export function openAmberForNip44Encrypt(pubkey: string, plaintext: string): voi timestamp: Date.now(), }); - const amberUrl = buildIntentUrl(encodeURIComponent(plaintext), { + const amberUrl = buildAmberUrl(encodeURIComponent(plaintext), { + type: 'nip44_encrypt', pubKey: pubkey, compressionType: 'none', returnType: 'signature', - type: 'nip44_encrypt', callbackUrl: callbackUrl, }); @@ -514,11 +519,11 @@ export function openAmberForNip44Decrypt(pubkey: string, ciphertext: string): vo timestamp: Date.now(), }); - const amberUrl = buildIntentUrl(encodeURIComponent(ciphertext), { + const amberUrl = buildAmberUrl(encodeURIComponent(ciphertext), { + type: 'nip44_decrypt', pubKey: pubkey, compressionType: 'none', returnType: 'signature', - type: 'nip44_decrypt', callbackUrl: callbackUrl, });