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:
Dorian
2026-03-09 18:34:22 +00:00
co-authored by Claude Opus 4.6
parent 150ce7447d
commit 95ed80335a
12 changed files with 1143 additions and 433 deletions
File diff suppressed because it is too large Load Diff
+57 -9
View File
@@ -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 -->