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
+6 -1
View File
@@ -122,6 +122,9 @@ if (process.env.NODE_ENV === 'production' && existsSync(publicDir)) {
woff2: 'font/woff2',
webp: 'image/webp',
webmanifest: 'application/manifest+json',
wav: 'audio/wav',
mp3: 'audio/mpeg',
ogg: 'audio/ogg',
}
function serveFile(c: Context, reqPath: string, cacheControl: string) {
@@ -160,12 +163,14 @@ if (process.env.NODE_ENV === 'production' && existsSync(publicDir)) {
app.get('/icon.svg', (c) => serveFile(c, '/icon.svg', 'public, max-age=86400'))
app.get('/apple-touch-icon.png', (c) => serveFile(c, '/apple-touch-icon.png', 'public, max-age=86400'))
// Audio files (pre-generated TTS, SFX)
app.get('/audio/*', (c) => serveFile(c, c.req.path, 'public, max-age=86400'))
// SPA fallback: only for navigation requests (not JS/CSS/asset files)
app.get('*', (c) => {
if (c.req.path.startsWith('/api/')) return c.notFound()
// Don't serve index.html for asset requests — return 404 so the browser gets a proper error
const ext = c.req.path.split('.').pop()
if (ext && ext !== c.req.path && ['js', 'css', 'map', 'json', 'png', 'jpg', 'svg', 'woff', 'woff2', 'webp', 'ico'].includes(ext)) {
if (ext && ext !== c.req.path && ['js', 'css', 'map', 'json', 'png', 'jpg', 'svg', 'woff', 'woff2', 'webp', 'ico', 'wav', 'mp3', 'ogg'].includes(ext)) {
return c.notFound()
}
const indexPath = join(publicDir, 'index.html')