feat: polling API, HMAC webhook signing, session-only keys, prod audio fix
- Add polling API (GET/POST /api/fights/poll) so bots don't need public URLs - Add HMAC-SHA256 webhook signing (X-Botfights-Signature header) - Stop auto-persisting nsec keys — session-only by default with opt-in "Remember on this device" - Fix production TTS: add wav/mp3/ogg MIME types, /audio/* route, SPA blocklist - Overhaul docs: mode selector (poll vs webhook), AI-first bot examples, security tab - Fix duplicate sign-in buttons, login flow bugs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
150ce7447d
commit
95ed80335a
@@ -86,6 +86,7 @@ function clearAllState() {
|
||||
store('bf_bot', null)
|
||||
store('bf_pic', null)
|
||||
setToken(null)
|
||||
sessionNsec = null
|
||||
localStorage.removeItem('bf_nsec')
|
||||
}
|
||||
|
||||
@@ -98,6 +99,8 @@ const isLoading = ref(false)
|
||||
let autoRestoreRan = false
|
||||
// Flag: skip relay pic fetch for freshly generated keys (no profile exists)
|
||||
let freshlyGenerated = false
|
||||
// In-memory nsec for current session (never auto-persisted to localStorage)
|
||||
let sessionNsec: string | null = null
|
||||
|
||||
// Sync in-memory auth state when tab regains focus (handles external localStorage clearing)
|
||||
if (typeof document !== 'undefined') {
|
||||
@@ -196,8 +199,8 @@ export function useNostr() {
|
||||
// Mobile signers (Amber) inject window.nostr late — poll for up to 3s
|
||||
const found = await waitForSigner(3000)
|
||||
if (!found) {
|
||||
// Fall back to stored nsec if available
|
||||
const storedNsec = localStorage.getItem('bf_nsec')
|
||||
// Fall back to session or persisted nsec if available
|
||||
const storedNsec = sessionNsec || localStorage.getItem('bf_nsec')
|
||||
if (storedNsec) {
|
||||
return loginWithNsec(storedNsec)
|
||||
}
|
||||
@@ -247,16 +250,16 @@ export function useNostr() {
|
||||
// Mark as freshly generated so login() skips relay pic fetch
|
||||
freshlyGenerated = true
|
||||
|
||||
// Store new key (will be cleared on logout)
|
||||
localStorage.setItem('bf_nsec', nsecHex)
|
||||
// Hold key in session memory only — user must opt in to persist
|
||||
sessionNsec = nsecHex
|
||||
pubkey.value = pk
|
||||
store('bf_pubkey', pk)
|
||||
|
||||
return { pubkey: pk, nsec: nsecBech32 }
|
||||
}
|
||||
|
||||
/** Login with an existing nsec (hex). Signs NIP-98 locally. */
|
||||
async function loginWithNsec(nsecHex: string): Promise<{ pubkey: string; bot: BotData | null }> {
|
||||
/** Login with an existing nsec (hex). Signs NIP-98 locally. If persist=true, saves to localStorage. */
|
||||
async function loginWithNsec(nsecHex: string, persist = false): Promise<{ pubkey: string; bot: BotData | null }> {
|
||||
let secretKey: Uint8Array
|
||||
try {
|
||||
secretKey = hexToBytes(nsecHex)
|
||||
@@ -271,7 +274,8 @@ export function useNostr() {
|
||||
store('bf_bot', null)
|
||||
store('bf_pic', null)
|
||||
|
||||
localStorage.setItem('bf_nsec', nsecHex)
|
||||
sessionNsec = nsecHex
|
||||
if (persist) localStorage.setItem('bf_nsec', nsecHex)
|
||||
|
||||
isLoading.value = true
|
||||
try {
|
||||
@@ -332,8 +336,7 @@ export function useNostr() {
|
||||
|
||||
// Re-authenticate to get fresh JWT with botId
|
||||
try {
|
||||
const nsec = localStorage.getItem('bf_nsec')
|
||||
await authenticateSession(nsec)
|
||||
await authenticateSession(sessionNsec)
|
||||
} catch {
|
||||
// Non-critical: existing JWT still works, just missing botId
|
||||
}
|
||||
@@ -426,8 +429,7 @@ export function useNostr() {
|
||||
|
||||
// Re-authenticate to get fresh JWT with botId
|
||||
try {
|
||||
const nsec = localStorage.getItem('bf_nsec')
|
||||
await authenticateSession(nsec)
|
||||
await authenticateSession(sessionNsec)
|
||||
} catch {
|
||||
// Non-critical
|
||||
}
|
||||
@@ -443,9 +445,14 @@ export function useNostr() {
|
||||
/** Check if user has a locally stored key (no extension needed) */
|
||||
const hasStoredKey = computed(() => !!localStorage.getItem('bf_nsec'))
|
||||
|
||||
/** Get the stored nsec hex for backup display */
|
||||
/** Get the current nsec hex (session memory first, then localStorage) */
|
||||
function getStoredNsec(): string | null {
|
||||
return localStorage.getItem('bf_nsec')
|
||||
return sessionNsec || localStorage.getItem('bf_nsec')
|
||||
}
|
||||
|
||||
/** Persist the current session key to localStorage (opt-in) */
|
||||
function persistKey(): void {
|
||||
if (sessionNsec) localStorage.setItem('bf_nsec', sessionNsec)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -539,6 +546,7 @@ export function useNostr() {
|
||||
updateCustomization,
|
||||
updateWebhook,
|
||||
getStoredNsec,
|
||||
persistKey,
|
||||
logout,
|
||||
fetchNostrProfile,
|
||||
initiateNip55Login,
|
||||
|
||||
+689
-359
File diff suppressed because it is too large
Load Diff
@@ -10,10 +10,12 @@ import WalletConnect from '../components/WalletConnect.vue'
|
||||
import { authFetch } from '../lib/nostr-auth'
|
||||
|
||||
const router = useRouter()
|
||||
const { pubkey, bot, profilePicUrl, isLoggedIn, hasExtension, hasStoredKey, isLoading, login, waitForSigner, generateLogin, loginWithNsec, registerBot, registerHuman, getStoredNsec, logout, initiateNip55Login, processNip55Return, hasAndroidSigner } = useNostr()
|
||||
const { pubkey, bot, profilePicUrl, isLoggedIn, hasExtension, hasStoredKey, isLoading, login, waitForSigner, generateLogin, loginWithNsec, registerBot, registerHuman, getStoredNsec, persistKey, logout, initiateNip55Login, processNip55Return, hasAndroidSigner } = useNostr()
|
||||
const showNsecBackup = ref(false)
|
||||
const generatedNsec = ref('')
|
||||
const generatedNsecHex = ref('')
|
||||
const nsecInput = ref('')
|
||||
const rememberKey = ref(false)
|
||||
const { isWalletConnected, payEntryFee, paymentStatus } = useWallet()
|
||||
|
||||
// Steps: 'login' | 'choose-mode' | 'pick-character' | 'name-bot' | 'bot-setup' | 'add-webhook' |
|
||||
@@ -265,18 +267,46 @@ async function handleSignerLogin() {
|
||||
handleLogin()
|
||||
}
|
||||
|
||||
async function handleStoredKeyLogin() {
|
||||
error.value = ''
|
||||
const storedNsec = localStorage.getItem('bf_nsec')
|
||||
if (!storedNsec) {
|
||||
error.value = 'No saved key found. Generate a new identity.'
|
||||
return
|
||||
}
|
||||
try {
|
||||
const result = await loginWithNsec(storedNsec)
|
||||
if (result.bot) {
|
||||
isHumanMode.value = !!result.bot.isHuman
|
||||
step.value = 'ready'
|
||||
} else {
|
||||
step.value = 'choose-mode'
|
||||
}
|
||||
} catch (e) {
|
||||
handleError(e, 'Login failed.')
|
||||
}
|
||||
}
|
||||
|
||||
function handleGenerateLogin() {
|
||||
error.value = ''
|
||||
const { nsec } = generateLogin()
|
||||
generatedNsec.value = nsec
|
||||
generatedNsecHex.value = getStoredNsec() || ''
|
||||
showNsecBackup.value = true
|
||||
rememberKey.value = false
|
||||
}
|
||||
|
||||
async function handleNsecBackupDone() {
|
||||
if (isLoading.value) return
|
||||
showNsecBackup.value = false
|
||||
try {
|
||||
const result = await login()
|
||||
const nsecHex = generatedNsecHex.value || getStoredNsec()
|
||||
if (!nsecHex) {
|
||||
error.value = 'Key not found. Generate a new identity.'
|
||||
return
|
||||
}
|
||||
if (rememberKey.value) persistKey()
|
||||
const result = await loginWithNsec(nsecHex, rememberKey.value)
|
||||
if (result.bot) {
|
||||
isHumanMode.value = !!result.bot.isHuman
|
||||
step.value = 'ready'
|
||||
@@ -570,6 +600,15 @@ async function practice() {
|
||||
|
||||
function handleSignOut() {
|
||||
logout()
|
||||
// Reset all UI state to prevent stale data
|
||||
error.value = ''
|
||||
generatedNsec.value = ''
|
||||
generatedNsecHex.value = ''
|
||||
showNsecBackup.value = false
|
||||
nsecInput.value = ''
|
||||
rememberKey.value = false
|
||||
isHumanMode.value = false
|
||||
activeFightLink.value = ''
|
||||
step.value = 'login'
|
||||
}
|
||||
</script>
|
||||
@@ -617,6 +656,14 @@ function handleSignOut() {
|
||||
{{ nsecCopied ? 'COPIED' : 'COPY' }}
|
||||
</button>
|
||||
</div>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
v-model="rememberKey"
|
||||
type="checkbox"
|
||||
class="w-4 h-4 accent-neon-purple"
|
||||
/>
|
||||
<span class="font-mono text-xs text-text-muted">Remember on this device</span>
|
||||
</label>
|
||||
<button
|
||||
class="w-full py-3 bg-neon-green/10 border-2 border-neon-green/50 text-neon-green
|
||||
font-display font-black text-sm tracking-widest
|
||||
@@ -630,7 +677,7 @@ function handleSignOut() {
|
||||
<div v-if="!showNsecBackup" class="space-y-3">
|
||||
<!-- Sign in with extension (NIP-07) -->
|
||||
<button
|
||||
v-if="hasExtension"
|
||||
v-if="hasExtension && !hasStoredKey"
|
||||
class="w-full py-4 bg-neon-purple/10 border-2 border-neon-purple/50 text-neon-purple
|
||||
font-display font-black text-base tracking-widest
|
||||
hover:bg-neon-purple/20 hover:border-neon-purple transition-all
|
||||
@@ -643,8 +690,9 @@ function handleSignOut() {
|
||||
{{ isLoading ? 'CONNECTING...' : 'SIGN IN WITH EXTENSION' }}
|
||||
</button>
|
||||
|
||||
<!-- Sign in with Nostr signer -->
|
||||
<!-- Sign in with Nostr signer (Android / mobile signers) -->
|
||||
<button
|
||||
v-if="hasAndroidSigner"
|
||||
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
|
||||
@@ -654,22 +702,22 @@ function handleSignOut() {
|
||||
@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...' : hasAndroidSigner ? 'SIGN IN WITH AMBER / PRIMAL' : 'USE NOSTR SIGNER' }}
|
||||
{{ isLoading ? 'CONNECTING...' : 'SIGN IN WITH AMBER / PRIMAL' }}
|
||||
</button>
|
||||
|
||||
<!-- Sign in with stored key -->
|
||||
<!-- Sign in with stored key (always show if key exists) -->
|
||||
<button
|
||||
v-if="!hasExtension && hasStoredKey"
|
||||
v-if="hasStoredKey"
|
||||
class="w-full py-4 bg-neon-purple/10 border-2 border-neon-purple/50 text-neon-purple
|
||||
font-display font-black text-base tracking-widest
|
||||
hover:bg-neon-purple/20 hover:border-neon-purple transition-all
|
||||
disabled:opacity-50 disabled:cursor-wait
|
||||
flex items-center justify-center gap-3"
|
||||
:disabled="isLoading"
|
||||
@click="handleLogin"
|
||||
@click="handleStoredKeyLogin"
|
||||
>
|
||||
<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' }}
|
||||
{{ isLoading ? 'CONNECTING...' : 'SIGN IN WITH EXTENSION' }}
|
||||
</button>
|
||||
|
||||
<!-- Always show Generate Login -->
|
||||
|
||||
Reference in New Issue
Block a user