feat: v4 — TUI fight loop, rate limiting, webhook tooling, expanded choreographies

- Fight loop CLI with TUI renderer (ink-style terminal UI)
- Rate limiting middleware for API routes
- Queue cooldowns wired into orchestrator after fights
- Webhook test utility for bot debugging
- API docs route
- Expanded FightScene choreographies and weapon props
- Fix Drizzle transaction execution in orchestrator
- Schema additions, scoring/challenge/mock expansions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-07 00:14:46 +00:00
co-authored by Claude Opus 4.6
parent 2c0323d5fb
commit 4d8b18a58a
24 changed files with 2973 additions and 721 deletions
+51
View File
@@ -0,0 +1,51 @@
import type { Context, Next } from 'hono'
const hitCounts = new Map<string, { count: number; resetAt: number }>()
// Cleanup stale entries every 5 minutes
setInterval(() => {
const now = Date.now()
for (const [key, entry] of hitCounts) {
if (now > entry.resetAt) hitCounts.delete(key)
}
}, 5 * 60 * 1000)
export function rateLimit(windowMs: number, maxHits: number) {
return async (c: Context, next: Next) => {
const key = c.req.header('x-forwarded-for') || c.req.header('cf-connecting-ip') || 'unknown'
const now = Date.now()
const entry = hitCounts.get(key)
if (!entry || now > entry.resetAt) {
hitCounts.set(key, { count: 1, resetAt: now + windowMs })
} else {
entry.count++
if (entry.count > maxHits) {
return c.json({ error: 'Too many requests. Slow down.' }, 429)
}
}
await next()
}
}
// Per-bot rate limiter (uses bot ID instead of IP)
const botHitCounts = new Map<string, number>()
export function botRateLimit(cooldownMs: number) {
return async (c: Context, next: Next) => {
const botId = c.req.param('botId')
if (!botId) return next()
const lastHit = botHitCounts.get(botId) || 0
const now = Date.now()
if (now - lastHit < cooldownMs) {
const waitSec = Math.ceil((cooldownMs - (now - lastHit)) / 1000)
return c.json({ error: `Cooldown active. Wait ${waitSec}s.` }, 429)
}
botHitCounts.set(botId, now)
await next()
}
}