Build the Nostr signer URI in the browser, not server-side

Server-side percent-encoding (rawurlencode/http_build_query) produced a
nostrsigner: URI that Amber rejected as malformed. The launcher view now
assembles it in JS with encodeURIComponent(JSON.stringify(event)) — the
exact encoding Amber accepts (verified working earlier in the session).
The controller only passes k1 and the callback URL.
This commit is contained in:
HolgerHatGarKeineNode
2026-06-11 22:40:57 +02:00
parent 58c7e410b0
commit 7e491326a9
2 changed files with 26 additions and 20 deletions
+6 -14
View File
@@ -154,22 +154,14 @@ final class MobileAuthController extends Controller
$k1 = bin2hex(random_bytes(32)); $k1 = bin2hex(random_bytes(32));
$event = [ // The signer URI is assembled in the browser (see the view) with
'kind' => 22242, // encodeURIComponent(JSON.stringify(event)) — the exact encoding
'created_at' => now()->timestamp, // Amber accepts. Building it server-side produced subtly different
'content' => '', // percent-encoding that Amber rejected as malformed.
'tags' => [['challenge', $k1]], return view('auth.mobile-nostr-launch', [
]; 'k1' => $k1,
$signerUri = 'nostrsigner:'.rawurlencode(json_encode($event)).'?'.http_build_query([
'compressionType' => 'none',
'returnType' => 'event',
'type' => 'sign_event',
'appName' => 'Einundzwanzig',
'callbackUrl' => url('/auth/mobile/signed/'.$k1.'/'), 'callbackUrl' => url('/auth/mobile/signed/'.$k1.'/'),
]); ]);
return view('auth.mobile-nostr-launch', ['signerUri' => $signerUri]);
} }
/** /**
@@ -10,20 +10,34 @@
.card { text-align: center; padding: 2rem; max-width: 22rem; } .card { text-align: center; padding: 2rem; max-width: 22rem; }
h1 { font-size: 1.25rem; margin: 1rem 0 .5rem; } h1 { font-size: 1.25rem; margin: 1rem 0 .5rem; }
p { color: #a1a1aa; line-height: 1.5; } p { color: #a1a1aa; line-height: 1.5; }
a.button { display: inline-block; margin-top: 1.5rem; padding: .875rem 1.25rem; border-radius: .75rem; button.launch { margin-top: 1.5rem; padding: .875rem 1.25rem; border: 0; border-radius: .75rem;
background: #f7931a; color: #09090b; font-weight: 600; text-decoration: none; } background: #f7931a; color: #09090b; font-weight: 600; font-size: 1rem; cursor: pointer; }
</style> </style>
</head> </head>
<body> <body>
<div class="card"> <div class="card">
<h1>{{ __('Anmeldung mit Nostr') }}</h1> <h1>{{ __('Anmeldung mit Nostr') }}</h1>
<p>{{ __('Dein Nostr-Signer (z. B. Amber) öffnet sich gleich. Falls nicht, tippe auf den Button.') }}</p> <p>{{ __('Dein Nostr-Signer (z. B. Amber) öffnet sich gleich. Falls nicht, tippe auf den Button.') }}</p>
<a class="button" href="{{ $signerUri }}">{{ __('Signer öffnen') }}</a> <button class="launch" onclick="launchSigner()">{{ __('Signer öffnen') }}</button>
</div> </div>
<script> <script>
// Launch via window.location so the intent carries category.BROWSABLE // Build the NIP-55 signer URI in the browser with
// and Amber routes it into its web-signing flow. // encodeURIComponent(JSON.stringify(event)) — the exact encoding
window.location.href = @js($signerUri); // Amber accepts. Launch via window.location so the intent carries
// category.BROWSABLE and Amber uses its web-signing flow.
function launchSigner() {
const event = {
kind: 22242,
created_at: Math.floor(Date.now() / 1000),
content: '',
tags: [['challenge', @js($k1)]],
};
window.location.href = 'nostrsigner:' + encodeURIComponent(JSON.stringify(event))
+ '?compressionType=none&returnType=event&type=sign_event&appName=Einundzwanzig'
+ '&callbackUrl=' + encodeURIComponent(@js($callbackUrl));
}
launchSigner();
</script> </script>
</body> </body>
</html> </html>