Add 'Login as Guest' — no extension or nsec needed
Generates a fresh random nostr keypair client-side and signs the NIP-98 login with it, same mechanism as the existing nsec path but nothing is typed by a human and nothing is ever displayed/saved — the key exists in page memory only long enough to sign the one login request (lib/guest.ts). Visible and clearly labeled on the login screen (unlike the de-emphasized 'paste an nsec' option), with an explicit note that it's disposable: if the session is lost there's no way to recover the same identity, since nothing was saved for the player to do it with. Verified end-to-end locally with a real generated key before deploying.
This commit is contained in:
@@ -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))}`;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
</p>
|
||||
|
||||
<button
|
||||
class="rounded border border-cyan-neon/50 px-5 py-2 font-display uppercase tracking-wider text-cyan-neon/90 transition hover:border-cyan-neon hover:text-cyan-neon disabled:opacity-50"
|
||||
:disabled="loggingIn"
|
||||
@click="handleGuestLogin"
|
||||
>
|
||||
{{ loggingIn ? 'Authenticating…' : 'Login as Guest' }}
|
||||
</button>
|
||||
<p class="max-w-sm text-xs text-cyan-neon/60">
|
||||
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.
|
||||
</p>
|
||||
|
||||
<button class="text-xs text-cyan-neon/60 underline hover:text-magenta-neon" @click="showNsecForm = !showNsecForm">
|
||||
{{ showNsecForm ? 'Hide' : 'Or paste an nsec (unsafe)' }}
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user