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
@@ -0,0 +1,76 @@
|
||||
// Bot authentication helper.
|
||||
// Verifies bot identity via bot_id + secret (SHA256 hash comparison).
|
||||
// Supports: Authorization header or query params.
|
||||
|
||||
import { createHash } from 'crypto'
|
||||
import { db, schema } from '../db/index.js'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import type { Context } from 'hono'
|
||||
|
||||
export interface BotAuthContext {
|
||||
botId: string
|
||||
botName: string
|
||||
webhookUrl: string
|
||||
}
|
||||
|
||||
/** Extract and verify bot credentials from request. Returns bot context or error response. */
|
||||
export async function authenticateBot(c: Context): Promise<BotAuthContext | Response> {
|
||||
let botId: string | undefined
|
||||
let secret: string | undefined
|
||||
|
||||
// 1. Authorization: Bot <bot_id>:<secret>
|
||||
const auth = c.req.header('Authorization')
|
||||
if (auth?.startsWith('Bot ')) {
|
||||
const parts = auth.slice(4).split(':')
|
||||
if (parts.length === 2 && parts[0] && parts[1]) {
|
||||
botId = parts[0]
|
||||
secret = parts[1]
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Query params: ?bot_id=xxx&secret=yyy
|
||||
if (!botId || !secret) {
|
||||
const qBotId = c.req.query('bot_id')
|
||||
const qSecret = c.req.query('secret')
|
||||
if (qBotId && qSecret) {
|
||||
botId = qBotId
|
||||
secret = qSecret
|
||||
}
|
||||
}
|
||||
|
||||
if (!botId || !secret) {
|
||||
return c.json({ error: 'Authentication required. Use Authorization: Bot <bot_id>:<secret> or query params ?bot_id=...&secret=...' }, 401)
|
||||
}
|
||||
|
||||
const hash = createHash('sha256').update(secret).digest('hex')
|
||||
|
||||
const rows = await db.select({
|
||||
id: schema.bots.id,
|
||||
name: schema.bots.name,
|
||||
secretHash: schema.bots.secretHash,
|
||||
webhookUrl: schema.bots.webhookUrl,
|
||||
}).from(schema.bots).where(eq(schema.bots.id, botId)).limit(1)
|
||||
|
||||
if (rows.length === 0) {
|
||||
return c.json({ error: 'Invalid bot_id or secret.' }, 401)
|
||||
}
|
||||
|
||||
// Constant-time comparison
|
||||
const expected = rows[0].secretHash
|
||||
if (hash.length !== expected.length) {
|
||||
return c.json({ error: 'Invalid bot_id or secret.' }, 401)
|
||||
}
|
||||
let mismatch = 0
|
||||
for (let i = 0; i < hash.length; i++) {
|
||||
mismatch |= hash.charCodeAt(i) ^ expected.charCodeAt(i)
|
||||
}
|
||||
if (mismatch !== 0) {
|
||||
return c.json({ error: 'Invalid bot_id or secret.' }, 401)
|
||||
}
|
||||
|
||||
return {
|
||||
botId: rows[0].id,
|
||||
botName: rows[0].name,
|
||||
webhookUrl: rows[0].webhookUrl,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user