From 885116dac63f85163fac76d62efce8cbddac3e0b Mon Sep 17 00:00:00 2001
From: ssmithx
Date: Thu, 6 Aug 2026 17:51:13 +0000
Subject: [PATCH] =?UTF-8?q?Add=20'Login=20as=20Guest'=20=E2=80=94=20no=20e?=
=?UTF-8?q?xtension=20or=20nsec=20needed?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
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.
---
frontend/src/lib/guest.ts | 30 ++++++++++++++++++++++++++++++
frontend/src/stores/auth.ts | 18 ++++++++++++++++++
frontend/src/views/LoginView.vue | 24 ++++++++++++++++++++++++
3 files changed, 72 insertions(+)
create mode 100644 frontend/src/lib/guest.ts
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.
+