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
+34
-22
@@ -153,18 +153,21 @@ authRouter.post('/register', rateLimit(600_000, 10), async (c) => {
|
||||
|
||||
const normalizedName = name.toLowerCase()
|
||||
|
||||
if (!webhookUrl || typeof webhookUrl !== 'string') {
|
||||
return c.json({ error: 'webhookUrl is required.' }, 400)
|
||||
}
|
||||
// Poll mode: if no webhookUrl provided, bot will poll for challenges
|
||||
const isPollMode = !webhookUrl || webhookUrl === ''
|
||||
|
||||
try {
|
||||
new URL(webhookUrl)
|
||||
} catch {
|
||||
return c.json({ error: 'webhookUrl must be a valid URL.' }, 400)
|
||||
}
|
||||
|
||||
if (!isAllowedWebhookUrl(webhookUrl)) {
|
||||
return c.json({ error: 'webhookUrl must not point to private/internal addresses.' }, 400)
|
||||
if (!isPollMode) {
|
||||
if (typeof webhookUrl !== 'string') {
|
||||
return c.json({ error: 'webhookUrl must be a string.' }, 400)
|
||||
}
|
||||
try {
|
||||
new URL(webhookUrl)
|
||||
} catch {
|
||||
return c.json({ error: 'webhookUrl must be a valid URL.' }, 400)
|
||||
}
|
||||
if (!isAllowedWebhookUrl(webhookUrl)) {
|
||||
return c.json({ error: 'webhookUrl must not point to private/internal addresses.' }, 400)
|
||||
}
|
||||
}
|
||||
|
||||
// Check pubkey not already used
|
||||
@@ -187,18 +190,23 @@ authRouter.post('/register', rateLimit(600_000, 10), async (c) => {
|
||||
return c.json({ error: 'A bot with that name already exists.' }, 409)
|
||||
}
|
||||
|
||||
// Test the webhook
|
||||
const testResult = await testWebhook(webhookUrl)
|
||||
if (!testResult.reachable || !testResult.validResponse) {
|
||||
return c.json({
|
||||
error: 'Webhook verification failed.',
|
||||
details: testResult.error || 'Webhook must return {"answer": "..."} as JSON.',
|
||||
latencyMs: testResult.latencyMs,
|
||||
}, 422)
|
||||
// Test the webhook (skip for poll mode)
|
||||
let testResult: { latencyMs: number } | null = null
|
||||
if (!isPollMode) {
|
||||
const result = await testWebhook(webhookUrl)
|
||||
if (!result.reachable || !result.validResponse) {
|
||||
return c.json({
|
||||
error: 'Webhook verification failed.',
|
||||
details: result.error || 'Webhook must return {"answer": "..."} as JSON.',
|
||||
latencyMs: result.latencyMs,
|
||||
}, 422)
|
||||
}
|
||||
testResult = result
|
||||
}
|
||||
|
||||
const id = nanoid(12)
|
||||
const secret = randomBytes(32).toString('hex')
|
||||
const effectiveWebhookUrl = isPollMode ? 'http://poll.local/' : webhookUrl
|
||||
|
||||
const baseArchetype = custResult.data.archetype || archetype || 'standard'
|
||||
const effectiveArchetype = isCreatorPubkey(pubkey) ? 'the_creator' : baseArchetype
|
||||
@@ -207,7 +215,7 @@ authRouter.post('/register', rateLimit(600_000, 10), async (c) => {
|
||||
await db.insert(schema.bots).values({
|
||||
id,
|
||||
name: normalizedName,
|
||||
webhookUrl,
|
||||
webhookUrl: effectiveWebhookUrl,
|
||||
avatarSeed: normalizedName,
|
||||
archetype: effectiveArchetype,
|
||||
secretHash: createHash('sha256').update(secret).digest('hex'),
|
||||
@@ -220,10 +228,14 @@ authRouter.post('/register', rateLimit(600_000, 10), async (c) => {
|
||||
return c.json({
|
||||
id,
|
||||
name: normalizedName,
|
||||
secret,
|
||||
mode: isPollMode ? 'poll' : 'webhook',
|
||||
archetype: effectiveArchetype,
|
||||
customization: custResult.data,
|
||||
webhookLatencyMs: testResult.latencyMs,
|
||||
message: 'Bot registered. Webhook verified.',
|
||||
webhookLatencyMs: testResult?.latencyMs ?? null,
|
||||
message: isPollMode
|
||||
? 'Bot registered in poll mode. No public URL needed. Use GET /api/fights/poll to receive challenges.'
|
||||
: 'Bot registered. Webhook verified.',
|
||||
}, 201)
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user