From 58c7e410b0db135d1b4b915c1dc36eac74fbc66b Mon Sep 17 00:00:00 2001 From: HolgerHatGarKeineNode <123783602+HolgerHatGarKeineNode@users.noreply.github.com> Date: Thu, 11 Jun 2026 22:08:17 +0200 Subject: [PATCH] Add headless Nostr launcher page for the mobile app A direct ACTION_VIEW intent to nostrsigner: (Browser::open from the app) lacks category.BROWSABLE, so Amber routes it into its app-to-app path and rejects it as malformed. The app instead opens /auth/mobile/nostr in an in-app browser; that page fires the signer via window.location, so the intent carries BROWSABLE and Amber uses its web-signing flow. No visible login UI, local signing, token returned via the App Link. --- app/Http/Controllers/MobileAuthController.php | 47 +++++++++++++++++++ .../views/auth/mobile-nostr-launch.blade.php | 29 ++++++++++++ routes/auth.php | 6 +++ 3 files changed, 82 insertions(+) create mode 100644 resources/views/auth/mobile-nostr-launch.blade.php diff --git a/app/Http/Controllers/MobileAuthController.php b/app/Http/Controllers/MobileAuthController.php index 46ee9bb..c4eb978 100644 --- a/app/Http/Controllers/MobileAuthController.php +++ b/app/Http/Controllers/MobileAuthController.php @@ -125,6 +125,53 @@ final class MobileAuthController extends Controller ]); } + /** + * Headless Nostr launcher for the mobile app. + * + * The app opens this page in an in-app browser (Chrome Custom Tab). It + * immediately launches the NIP-55 signer (e.g. Amber) via window.location + * so the intent carries category.BROWSABLE — which routes Amber into its + * web-signing flow (a direct ACTION_VIEW intent without that category + * lands in Amber's app-to-app path and is rejected as malformed). + * + * The signer signs the kind-22242 challenge locally and opens the + * /auth/mobile/signed callback, which issues the token and hands it back + * to the app via the verified App Link. No relay, no visible login UI. + */ + public function nostrLauncher(Request $request) + { + $deviceName = str((string) $request->query('device_name', self::DEFAULT_DEVICE_NAME)) + ->limit(64, '') + ->whenEmpty(fn () => str(self::DEFAULT_DEVICE_NAME)) + ->value(); + + // The signed callback issues the token and reads the device name + // from this session (the callback shares the Custom Tab's cookies). + $request->session()->put('mobile_auth', [ + 'redirect_uri' => self::ALLOWED_REDIRECT_URIS[0], + 'device_name' => $deviceName, + ]); + + $k1 = bin2hex(random_bytes(32)); + + $event = [ + 'kind' => 22242, + 'created_at' => now()->timestamp, + 'content' => '', + 'tags' => [['challenge', $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.'/'), + ]); + + return view('auth.mobile-nostr-launch', ['signerUri' => $signerUri]); + } + /** * Browser fallback for the app handoff URL (/app/auth?token=…). * diff --git a/resources/views/auth/mobile-nostr-launch.blade.php b/resources/views/auth/mobile-nostr-launch.blade.php new file mode 100644 index 0000000..f1be333 --- /dev/null +++ b/resources/views/auth/mobile-nostr-launch.blade.php @@ -0,0 +1,29 @@ + + + + + + {{ __('Anmeldung mit Nostr') }} — Einundzwanzig + + + +
+

{{ __('Anmeldung mit Nostr') }}

+

{{ __('Dein Nostr-Signer (z. B. Amber) öffnet sich gleich. Falls nicht, tippe auf den Button.') }}

+ {{ __('Signer öffnen') }} +
+ + + diff --git a/routes/auth.php b/routes/auth.php index c2e91e2..cb5ccee 100644 --- a/routes/auth.php +++ b/routes/auth.php @@ -44,6 +44,12 @@ Route::livewire('/auth/mobile', 'auth.mobile-login') ->middleware('throttle:30,1') ->name('auth.mobile'); +// Headless Nostr launcher: opened by the app in an in-app browser, fires +// the NIP-55 signer (Amber) with category.BROWSABLE via window.location. +Route::get('/auth/mobile/nostr', [MobileAuthController::class, 'nostrLauncher']) + ->middleware('throttle:30,1') + ->name('auth.mobile.nostr'); + Route::get('/auth/mobile/complete/{k1}', [MobileAuthController::class, 'complete']) ->where('k1', '[a-f0-9]{64}') ->middleware('throttle:30,1')