refactor: add centralized Zod validators for all API inputs

Create server/src/lib/validators.ts with reusable schemas for all API
inputs (auth, fights, bets, payments, tournaments, queue, docs).
Import and use in all route handlers, replacing inline validation.
Add formatZodError helper for user-friendly error messages.
77 test cases in validators.test.ts cover valid, invalid, boundary,
and attack inputs (SQL injection, XSS, prototype pollution).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-13 09:16:04 +00:00
co-authored by Claude Opus 4.6
parent 55d0f84251
commit 2051a95e13
11 changed files with 549 additions and 122 deletions
+22 -34
View File
@@ -8,6 +8,7 @@ import { validateCustomization } from '../engine/customization.js'
import { testWebhook } from '../engine/webhook-test.js'
import { rateLimit } from '../middleware/rate-limit.js'
import { isCreatorPubkey } from '../lib/constants.js'
import { loginSchema, registerSchema, registerHumanSchema, updateBotSchema, pubkeySchema, formatZodError } from '../lib/validators.js'
export const authRouter = new Hono()
@@ -26,12 +27,11 @@ authRouter.get("/check-name/:name", async (c) => {
// Login with Nostr pubkey (rate limited: 30 per minute per IP)
authRouter.post('/login', rateLimit(60_000, 30), async (c) => {
const body = await c.req.json()
const { pubkey } = body
if (!pubkey || typeof pubkey !== 'string' || pubkey.length !== 64) {
const parsed = loginSchema.safeParse(await c.req.json().catch(() => ({})))
if (!parsed.success) {
return c.json({ error: 'Invalid pubkey.' }, 400)
}
const { pubkey } = parsed.data
const rows = await db.select({
id: schema.bots.id,
@@ -130,20 +130,15 @@ authRouter.post('/login', rateLimit(60_000, 30), async (c) => {
// Register a new bot with Nostr pubkey
authRouter.post('/register', rateLimit(600_000, 10), async (c) => {
const body = await c.req.json()
const { pubkey, name, webhookUrl, archetype, profilePicUrl, customization: rawCustomization } = body
if (!pubkey || typeof pubkey !== 'string' || pubkey.length !== 64) {
return c.json({ error: 'Invalid pubkey.' }, 400)
}
if (!name || typeof name !== 'string' || name.length < 2 || name.length > 12) {
return c.json({ error: 'Name must be 2-12 characters.' }, 400)
}
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
return c.json({ error: 'Name must be alphanumeric, hyphens, or underscores.' }, 400)
const parsed = registerSchema.safeParse(await c.req.json().catch(() => ({})))
if (!parsed.success) {
return c.json({ error: formatZodError(parsed.error, {
pubkey: 'Invalid pubkey.',
name: 'Name must be 2-12 alphanumeric characters, hyphens, or underscores.',
webhookUrl: 'webhookUrl must be a valid URL.',
}, 'Invalid registration data.') }, 400)
}
const { pubkey, name, webhookUrl, archetype, profilePicUrl, customization: rawCustomization } = parsed.data
// Validate customization
const custResult = validateCustomization(rawCustomization)
@@ -242,20 +237,14 @@ authRouter.post('/register', rateLimit(600_000, 10), async (c) => {
// Register a human player (no webhook required)
authRouter.post('/register-human', rateLimit(600_000, 10), async (c) => {
const body = await c.req.json()
const { pubkey, name, profilePicUrl, avatarSeed } = body
if (!pubkey || typeof pubkey !== 'string' || pubkey.length !== 64) {
return c.json({ error: 'Invalid pubkey.' }, 400)
}
if (!name || typeof name !== 'string' || name.length < 2 || name.length > 12) {
return c.json({ error: 'Name must be 2-12 characters.' }, 400)
}
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
return c.json({ error: 'Name must be alphanumeric, hyphens, or underscores.' }, 400)
const parsed = registerHumanSchema.safeParse(await c.req.json().catch(() => ({})))
if (!parsed.success) {
return c.json({ error: formatZodError(parsed.error, {
pubkey: 'Invalid pubkey.',
name: 'Name must be 2-12 alphanumeric characters, hyphens, or underscores.',
}, 'Invalid registration data.') }, 400)
}
const { pubkey, name, profilePicUrl, avatarSeed } = parsed.data
const normalizedName = name.toLowerCase()
@@ -306,12 +295,11 @@ authRouter.post('/register-human', rateLimit(600_000, 10), async (c) => {
// Update bot webhook and/or customization (requires pubkey match)
authRouter.post('/update', rateLimit(60_000, 10), async (c) => {
const body = await c.req.json()
const { pubkey, webhookUrl, profilePicUrl, customization: rawCustomization } = body
if (!pubkey || typeof pubkey !== 'string' || pubkey.length !== 64) {
const parsed = updateBotSchema.safeParse(await c.req.json().catch(() => ({})))
if (!parsed.success) {
return c.json({ error: 'Invalid pubkey.' }, 400)
}
const { pubkey, webhookUrl, profilePicUrl, customization: rawCustomization } = parsed.data
const rows = await db.select({ id: schema.bots.id, customization: schema.bots.customization })
.from(schema.bots)