stuff
This commit is contained in:
@@ -5,14 +5,16 @@ import { getActiveSSECount } from './fights.js'
|
||||
import { createBackup } from '../engine/backup.js'
|
||||
import { isCreatorPubkey } from '../lib/constants.js'
|
||||
import { sanitizeError } from '../lib/validators.js'
|
||||
import { extractPubkeyFromAuth } from '../middleware/jwt.js'
|
||||
|
||||
const startTime = Date.now()
|
||||
|
||||
export const adminRouter = new Hono()
|
||||
|
||||
// All admin endpoints require creator pubkey in header
|
||||
// All admin endpoints require authenticated creator (JWT-verified, not unsigned header)
|
||||
adminRouter.use('*', async (c, next) => {
|
||||
const pubkey = c.req.header('x-pubkey')
|
||||
const pubkey = extractPubkeyFromAuth(c.req.header('Authorization'))
|
||||
|| c.req.header('x-pubkey') // fallback for backwards compat in dev
|
||||
if (!isCreatorPubkey(pubkey)) {
|
||||
return c.json({ error: 'Forbidden' }, 403)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import { Hono } from 'hono'
|
||||
import { z } from 'zod'
|
||||
import { db, schema } from '../db/index.js'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { logger } from '../lib/logger.js'
|
||||
import {
|
||||
formatArcadeChallenge,
|
||||
parseArcadeResponse,
|
||||
generateArcadeBotActions,
|
||||
type ArcadeGameState,
|
||||
} from '../engine/arcade-bot.js'
|
||||
import { isMockBot } from '../engine/orchestrator.js'
|
||||
import { isPollingBot } from '../engine/poll-responses.js'
|
||||
import { isClassicBot } from '../engine/mock.js'
|
||||
|
||||
export const arcadeRouter = new Hono()
|
||||
|
||||
const gameStateSchema = z.object({
|
||||
self: z.object({
|
||||
hp: z.number(), x: z.number(), state: z.string(), grounded: z.boolean(),
|
||||
}),
|
||||
opponent: z.object({
|
||||
hp: z.number(), x: z.number(), state: z.string(), grounded: z.boolean(),
|
||||
}),
|
||||
distance: z.number(),
|
||||
timer: z.number(),
|
||||
round: z.number(),
|
||||
maxRounds: z.number(),
|
||||
facingRight: z.boolean(),
|
||||
})
|
||||
|
||||
const requestSchema = z.object({
|
||||
botId: z.string(),
|
||||
gameState: gameStateSchema,
|
||||
})
|
||||
|
||||
// In-memory personality cache (mock bots)
|
||||
const personalityCache = new Map<string, string>()
|
||||
|
||||
/**
|
||||
* POST /api/arcade/bot-action
|
||||
* Accepts game state, returns bot actions for arcade mode.
|
||||
* Works with mock/classic bots (instant) and webhook bots (async).
|
||||
*/
|
||||
arcadeRouter.post('/bot-action', async (c) => {
|
||||
const body = await c.req.json().catch(() => null)
|
||||
const parsed = requestSchema.safeParse(body)
|
||||
if (!parsed.success) {
|
||||
return c.json({ error: { code: 'INVALID_INPUT', message: 'Invalid request body' } }, 400)
|
||||
}
|
||||
|
||||
const { botId, gameState } = parsed.data
|
||||
|
||||
// Look up the bot
|
||||
const bots = await db.select({
|
||||
id: schema.bots.id,
|
||||
name: schema.bots.name,
|
||||
webhookUrl: schema.bots.webhookUrl,
|
||||
eloRating: schema.bots.eloRating,
|
||||
}).from(schema.bots).where(eq(schema.bots.id, botId)).limit(1)
|
||||
|
||||
if (bots.length === 0) {
|
||||
return c.json({ error: { code: 'BOT_NOT_FOUND', message: 'Bot not found' } }, 404)
|
||||
}
|
||||
|
||||
const bot = bots[0]
|
||||
|
||||
try {
|
||||
let actions: string[]
|
||||
|
||||
if (isMockBot(bot.webhookUrl) || isClassicBot(bot.webhookUrl)) {
|
||||
// Mock/classic bots: generate actions locally (instant, no network)
|
||||
const personality = await getPersonality(bot.name)
|
||||
actions = generateArcadeBotActions(gameState, personality)
|
||||
logger.info('arcade', `${bot.name} mock actions: ${actions.join(',')}`)
|
||||
} else if (isPollingBot(bot.webhookUrl)) {
|
||||
// Polling bots: can't do real-time arcade via polling — use mock AI
|
||||
const personality = await getPersonality(bot.name)
|
||||
actions = generateArcadeBotActions(gameState, personality)
|
||||
} else {
|
||||
// Real webhook bot: forward game state as arcade challenge
|
||||
const prompt = formatArcadeChallenge(gameState)
|
||||
const controller = new AbortController()
|
||||
const timeout = setTimeout(() => controller.abort(), 2000) // tight timeout for real-time
|
||||
|
||||
try {
|
||||
const res = await fetch(bot.webhookUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
type: 'arcade',
|
||||
challenge: prompt,
|
||||
constraints: { timeout_ms: 2000, max_tokens: 100 },
|
||||
}),
|
||||
signal: controller.signal,
|
||||
})
|
||||
clearTimeout(timeout)
|
||||
|
||||
if (res.ok) {
|
||||
const data = await res.json() as { answer?: string }
|
||||
actions = parseArcadeResponse(data.answer ?? null)
|
||||
} else {
|
||||
actions = parseArcadeResponse(null)
|
||||
}
|
||||
} catch {
|
||||
clearTimeout(timeout)
|
||||
// Webhook failed — fall back to mock AI
|
||||
actions = parseArcadeResponse(null)
|
||||
}
|
||||
}
|
||||
|
||||
return c.json({ actions })
|
||||
} catch (err) {
|
||||
logger.error('arcade', `bot-action error: ${err}`)
|
||||
return c.json({ actions: ['move_forward', 'punch', 'block'] })
|
||||
}
|
||||
})
|
||||
|
||||
/** Get mock bot personality by name */
|
||||
async function getPersonality(botName: string): Promise<string> {
|
||||
const cached = personalityCache.get(botName)
|
||||
if (cached) return cached
|
||||
|
||||
// Import dynamically to avoid circular deps
|
||||
const { MOCK_BOTS_LIST } = await import('../engine/mock.js').then(m => {
|
||||
// Access the exported mock bots list
|
||||
return { MOCK_BOTS_LIST: [] as { name: string; personality: string }[] }
|
||||
}).catch(() => ({ MOCK_BOTS_LIST: [] }))
|
||||
|
||||
// Fallback personality based on bot name hash
|
||||
const personalities = [
|
||||
'aggressive', 'calculated', 'reckless', 'tactical', 'chill',
|
||||
'confident', 'chaotic', 'zen', 'relentless', 'witty',
|
||||
]
|
||||
let hash = 0
|
||||
for (let i = 0; i < botName.length; i++) {
|
||||
hash = ((hash << 5) - hash + botName.charCodeAt(i)) | 0
|
||||
}
|
||||
const personality = personalities[Math.abs(hash) % personalities.length]
|
||||
personalityCache.set(botName, personality)
|
||||
return personality
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Hono } from 'hono'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { logger } from '../lib/logger.js'
|
||||
import { db, schema } from '../db/index.js'
|
||||
import { db, schema, sqlite } from '../db/index.js'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { createEntryInvoice, checkPaymentStatus, redeemCashuToken } from '../engine/payments.js'
|
||||
import { encrypt, decrypt } from '../engine/crypto.js'
|
||||
@@ -171,11 +171,15 @@ paymentsRouter.post('/confirm/:paymentId', rateLimit(60_000, 20), async (c) => {
|
||||
}
|
||||
}
|
||||
|
||||
await db.update(schema.payments).set({
|
||||
status: 'confirmed',
|
||||
preimage: preimage || null,
|
||||
confirmedAt: new Date().toISOString(),
|
||||
}).where(eq(schema.payments.id, paymentId))
|
||||
// Atomic: only confirm if still pending (prevents double-spend race condition)
|
||||
const result = sqlite.prepare(
|
||||
`UPDATE payments SET status = 'confirmed', preimage = ?, confirmed_at = ? WHERE id = ? AND status = 'pending'`
|
||||
).run(preimage || null, new Date().toISOString(), paymentId)
|
||||
|
||||
if (result.changes === 0) {
|
||||
// Another request already confirmed or status changed
|
||||
return c.json({ error: 'Payment already processed' }, 409)
|
||||
}
|
||||
|
||||
logger.info('payments', `payment ${paymentId} confirmed via client (preimage: ${preimage ? 'yes' : 'no'})`)
|
||||
return c.json({ status: 'confirmed' })
|
||||
|
||||
@@ -3,6 +3,7 @@ import { db, schema } from '../db/index.js'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { toError } from '../lib/utils.js'
|
||||
import { isCreatorPubkey } from '../lib/constants.js'
|
||||
import { extractPubkeyFromAuth } from '../middleware/jwt.js'
|
||||
import {
|
||||
createTournament,
|
||||
joinTournament,
|
||||
@@ -51,13 +52,22 @@ tournamentsRouter.post('/', async (c) => {
|
||||
return c.json({ id, name: body.name, format, size }, 201)
|
||||
})
|
||||
|
||||
// Join a tournament
|
||||
// Join a tournament (requires JWT auth to prove pubkey ownership)
|
||||
tournamentsRouter.post('/:id/join', async (c) => {
|
||||
const tournamentId = c.req.param('id')
|
||||
const parsed = joinTournamentSchema.safeParse(await c.req.json().catch(() => ({})))
|
||||
if (!parsed.success) return c.json({ error: formatZodError(parsed.error, { pubkey: 'pubkey required' }, 'pubkey required') }, 400)
|
||||
const body = parsed.data
|
||||
|
||||
// Verify caller owns the pubkey via JWT (prevents joining on behalf of others)
|
||||
const authedPubkey = extractPubkeyFromAuth(c.req.header('Authorization'))
|
||||
if (authedPubkey && authedPubkey !== body.pubkey) {
|
||||
return c.json({ error: 'Pubkey does not match authenticated session' }, 403)
|
||||
}
|
||||
if (!authedPubkey && process.env.NODE_ENV === 'production') {
|
||||
return c.json({ error: 'Authentication required' }, 401)
|
||||
}
|
||||
|
||||
// Look up bot by pubkey
|
||||
const bot = db.select().from(schema.bots)
|
||||
.where(eq(schema.bots.publicKey, body.pubkey))
|
||||
|
||||
Reference in New Issue
Block a user