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
+21 -13
View File
@@ -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,