diff --git a/frontend/src/lib/guest.ts b/frontend/src/lib/guest.ts new file mode 100644 index 0000000..6da154b --- /dev/null +++ b/frontend/src/lib/guest.ts @@ -0,0 +1,30 @@ +// Guest login: a freshly generated, throwaway nostr identity, signed +// client-side — no NIP-07 extension needed, no nsec to paste. Same +// never-persisted-key posture as lib/nsec.ts (the key exists in page memory +// only long enough to sign this one login event), except here nothing was +// ever typed by a human, so there's no "don't paste your real key" risk to +// warn about — it's random and disposable by construction. +// +// Tradeoff worth knowing: since the key is never saved anywhere (not even +// as an nsec the player could copy down), losing the session cookie means +// losing this identity for good — a fresh "Login as Guest" click makes a +// brand new one, it can't recover the old pubkey's claims. +import { finalizeEvent, generateSecretKey } from 'nostr-tools/pure'; + +/** Generates a random identity and returns a signed NIP-98 Authorization header for it. */ +export function buildGuestNip98Header(url: string, method: string): string { + const sk = generateSecretKey(); + const event = finalizeEvent( + { + kind: 27235, + created_at: Math.floor(Date.now() / 1000), + content: '', + tags: [ + ['u', url], + ['method', method], + ], + }, + sk, + ); + return `Nostr ${btoa(JSON.stringify(event))}`; +} diff --git a/frontend/src/stores/auth.ts b/frontend/src/stores/auth.ts index e4fc4a7..9993093 100644 --- a/frontend/src/stores/auth.ts +++ b/frontend/src/stores/auth.ts @@ -2,6 +2,7 @@ import { defineStore } from 'pinia'; import { api, apiUrl, ApiError } from '../lib/api'; import { buildNip98Header, hasNip07 } from '../lib/nip07'; import { buildNip98HeaderWithNsec, isValidNsec } from '../lib/nsec'; +import { buildGuestNip98Header } from '../lib/guest'; export type Team = 'orange' | 'green'; @@ -63,6 +64,23 @@ export const useAuthStore = defineStore('auth', { this.pubkey = res.pubkey; this.team = res.team; }, + /** + * A freshly generated, throwaway identity — no extension, no key to + * paste. The key only exists in page memory for this one signing call; + * losing the session means losing this identity for good, since nothing + * is saved for the player to recover it with. + */ + async loginAsGuest() { + const url = apiUrl('/api/auth/login'); + const header = buildGuestNip98Header(url, 'POST'); + const res = await api.post<{ pubkey: string; team: Team | null }>( + '/api/auth/login', + undefined, + { authorization: header }, + ); + this.pubkey = res.pubkey; + this.team = res.team; + }, async logout() { await api.post('/api/auth/logout'); this.pubkey = null; diff --git a/frontend/src/views/LoginView.vue b/frontend/src/views/LoginView.vue index e6e9edb..130c5ee 100644 --- a/frontend/src/views/LoginView.vue +++ b/frontend/src/views/LoginView.vue @@ -21,6 +21,18 @@ async function handleLogin() { } } +async function handleGuestLogin() { + error.value = null; + loggingIn.value = true; + try { + await auth.loginAsGuest(); + } catch (err) { + error.value = err instanceof Error ? err.message : String(err); + } finally { + loggingIn.value = false; + } +} + async function handleNsecLogin() { error.value = null; loggingIn.value = true; @@ -57,6 +69,18 @@ async function handleNsecLogin() { No NIP-07 extension detected — install Alby or nos2x, or open this app from the Archipelago dashboard.
+ ++ Jumps in with a fresh throwaway identity — no extension, nothing to install. It's disposable: if you lose this + session there's no way to get the same identity back, so don't rely on it for territory you care about keeping. +
+