feat: NIP-98 + JWT authentication with signer support

Replace insecure raw-pubkey auth with cryptographic NIP-98 signed
requests and server-issued JWT sessions. Logout now fully clears
all state including nsec. Add yellow "Use Nostr Signer" button
for Amber/NIP-07 remote signers.

- Server: JWT middleware (HMAC-SHA256, 24h expiry), NIP-98 verification
- Server: POST /api/auth/nostr/session endpoint
- Frontend: NIP-98 token builder + authFetch wrapper with JWT Bearer
- Frontend: All authenticated API calls use authFetch
- Security: logout clears JWT, pubkey, bot, nsec, and profile pic

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-09 12:31:25 +00:00
co-authored by Claude Opus 4.6
parent ad96d1158f
commit 3ba05a66b4
8 changed files with 532 additions and 105 deletions
+26 -3
View File
@@ -7,6 +7,7 @@ import SpritePreview from '../components/SpritePreview.vue'
import { ensureAudioContext } from '../game/audio'
import HumanPreview from '../components/HumanPreview.vue'
import WalletConnect from '../components/WalletConnect.vue'
import { authFetch } from '../lib/nostr-auth'
const router = useRouter()
const { pubkey, bot, profilePicUrl, isLoggedIn, hasExtension, hasStoredKey, isLoading, login, generateLogin, loginWithNsec, registerBot, registerHuman, getStoredNsec, logout } = useNostr()
@@ -184,6 +185,14 @@ async function handleLogin() {
}
}
function handleSignerLogin() {
if (!window.nostr) {
error.value = 'No Nostr signer detected. Install a NIP-07 extension (Nos2x, Alby) or use Amber on Android.'
return
}
handleLogin()
}
function handleGenerateLogin() {
error.value = ''
const { nsec } = generateLogin()
@@ -432,7 +441,7 @@ async function fightRanked() {
error.value = ''
try {
const paymentId = await payEntryFee(bot.value.id)
const res = await fetch(`/api/queue/join-ranked/${bot.value.id}`, {
const res = await authFetch(`/api/queue/join-ranked/${bot.value.id}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ paymentId, pubkey: pubkey.value }),
@@ -540,7 +549,7 @@ function handleSignOut() {
</div>
<div v-if="!showNsecBackup" class="space-y-3">
<!-- Sign in with extension -->
<!-- Sign in with extension (NIP-07) -->
<button
v-if="hasExtension"
class="w-full py-4 bg-neon-purple/10 border-2 border-neon-purple/50 text-neon-purple
@@ -552,7 +561,21 @@ function handleSignOut() {
@click="handleLogin"
>
<span v-if="isLoading" class="w-5 h-5 border-2 border-neon-purple/30 border-t-neon-purple rounded-full animate-spin" />
{{ isLoading ? 'CONNECTING...' : 'SIGN IN WITH NOSTR' }}
{{ isLoading ? 'CONNECTING...' : 'SIGN IN WITH EXTENSION' }}
</button>
<!-- Sign in with Nostr signer -->
<button
class="w-full py-4 bg-neon-yellow/10 border-2 border-neon-yellow/50 text-neon-yellow
font-display font-black text-base tracking-widest
hover:bg-neon-yellow/20 hover:border-neon-yellow transition-all
disabled:opacity-50 disabled:cursor-wait
flex items-center justify-center gap-3"
:disabled="isLoading"
@click="handleSignerLogin"
>
<span v-if="isLoading" class="w-5 h-5 border-2 border-neon-yellow/30 border-t-neon-yellow rounded-full animate-spin" />
{{ isLoading ? 'CONNECTING...' : 'USE NOSTR SIGNER' }}
</button>
<!-- Sign in with stored key -->