fix: validate Cashu token format before placing bets (BUG-S4)
Added getDecodedToken validation that rejects malformed tokens with 400 before any DB lookups. Tests cover empty, non-base64, truncated, and random base64 tokens. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
c6a54d63c4
commit
e4a7f47e0f
@@ -0,0 +1,50 @@
|
|||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { Hono } from 'hono'
|
||||||
|
import { betsRouter } from './bets.js'
|
||||||
|
|
||||||
|
const app = new Hono()
|
||||||
|
app.route('/api/bets', betsRouter)
|
||||||
|
|
||||||
|
async function placeBet(cashuToken: string) {
|
||||||
|
return app.request('/api/bets/place', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
fightId: 'test-fight',
|
||||||
|
pubkey: 'deadbeef',
|
||||||
|
botId: 'test-bot',
|
||||||
|
amountSats: 100,
|
||||||
|
cashuToken,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('bets cashu token validation', () => {
|
||||||
|
it('rejects empty string token', async () => {
|
||||||
|
const res = await placeBet('')
|
||||||
|
// Empty string fails the required fields check first
|
||||||
|
expect(res.status).toBe(400)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects non-base64 garbage token', async () => {
|
||||||
|
const res = await placeBet('not-a-valid-cashu-token!!!')
|
||||||
|
expect(res.status).toBe(400)
|
||||||
|
const json = await res.json() as { error: string }
|
||||||
|
expect(json.error).toBe('Invalid Cashu token format.')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects truncated token', async () => {
|
||||||
|
// A truncated token that starts like cashu but is incomplete
|
||||||
|
const res = await placeBet('cashuAey')
|
||||||
|
expect(res.status).toBe(400)
|
||||||
|
const json = await res.json() as { error: string }
|
||||||
|
expect(json.error).toBe('Invalid Cashu token format.')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects random base64 that is not cashu format', async () => {
|
||||||
|
const res = await placeBet('eyJhbGciOiJIUzI1NiJ9')
|
||||||
|
expect(res.status).toBe(400)
|
||||||
|
const json = await res.json() as { error: string }
|
||||||
|
expect(json.error).toBe('Invalid Cashu token format.')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Hono } from 'hono'
|
import { Hono } from 'hono'
|
||||||
import { toError } from '../lib/utils.js'
|
import { toError } from '../lib/utils.js'
|
||||||
|
import { getDecodedToken } from '@cashu/cashu-ts'
|
||||||
import { db, schema } from '../db/index.js'
|
import { db, schema } from '../db/index.js'
|
||||||
import { eq, desc } from 'drizzle-orm'
|
import { eq, desc } from 'drizzle-orm'
|
||||||
import { calculateOdds } from '../engine/odds.js'
|
import { calculateOdds } from '../engine/odds.js'
|
||||||
@@ -59,6 +60,13 @@ betsRouter.post('/place', rateLimit(60_000, 10), async (c) => {
|
|||||||
return c.json({ error: 'amountSats must be an integer between 1 and 1,000,000' }, 400)
|
return c.json({ error: 'amountSats must be an integer between 1 and 1,000,000' }, 400)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate Cashu token format before any DB lookups
|
||||||
|
try {
|
||||||
|
getDecodedToken(cashuToken)
|
||||||
|
} catch {
|
||||||
|
return c.json({ error: 'Invalid Cashu token format.' }, 400)
|
||||||
|
}
|
||||||
|
|
||||||
// Verify fight is still open
|
// Verify fight is still open
|
||||||
const fight = await db.select().from(schema.fights)
|
const fight = await db.select().from(schema.fights)
|
||||||
.where(eq(schema.fights.id, fightId)).limit(1)
|
.where(eq(schema.fights.id, fightId)).limit(1)
|
||||||
|
|||||||
Reference in New Issue
Block a user