Files
hapi/server/src/web/routes/cli.ts
T
aababe6a57 fix: use timing-safe comparison for CLI API token validation (#9)
* fix: use timing-safe comparison for CLI API token validation

Replace direct string comparison (===) with constant-time comparison
using crypto.timingSafeEqual to prevent timing attacks that could
leak information about the token character by character.

Affected locations:
- server/src/web/routes/auth.ts (accessToken validation)
- server/src/web/routes/cli.ts (bearer token middleware)
- server/src/socket/server.ts (socket.io /cli namespace auth)

* Update server/src/utils/crypto.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-26 17:22:04 +08:00

101 lines
3.1 KiB
TypeScript

import { Hono } from 'hono'
import { z } from 'zod'
import { configuration } from '../../configuration'
import { safeCompareStrings } from '../../utils/crypto'
import type { SyncEngine } from '../../sync/syncEngine'
const bearerSchema = z.string().regex(/^Bearer\s+(.+)$/i)
const createOrLoadSessionSchema = z.object({
tag: z.string().min(1),
metadata: z.unknown(),
agentState: z.unknown().nullable().optional()
})
const createOrLoadMachineSchema = z.object({
id: z.string().min(1),
metadata: z.unknown(),
daemonState: z.unknown().nullable().optional()
})
export function createCliRoutes(getSyncEngine: () => SyncEngine | null): Hono {
const app = new Hono()
app.use('*', async (c, next) => {
const raw = c.req.header('authorization')
if (!raw) {
return c.json({ error: 'Missing Authorization header' }, 401)
}
const parsed = bearerSchema.safeParse(raw)
if (!parsed.success) {
return c.json({ error: 'Invalid Authorization header' }, 401)
}
const token = parsed.data.replace(/^Bearer\s+/i, '')
if (!safeCompareStrings(token, configuration.cliApiToken)) {
return c.json({ error: 'Invalid token' }, 401)
}
return await next()
})
app.post('/sessions', async (c) => {
const engine = getSyncEngine()
if (!engine) {
return c.json({ error: 'Not ready' }, 503)
}
const json = await c.req.json().catch(() => null)
const parsed = createOrLoadSessionSchema.safeParse(json)
if (!parsed.success) {
return c.json({ error: 'Invalid body' }, 400)
}
const session = engine.getOrCreateSession(parsed.data.tag, parsed.data.metadata, parsed.data.agentState ?? null)
return c.json({ session })
})
app.get('/sessions/:id', (c) => {
const engine = getSyncEngine()
if (!engine) {
return c.json({ error: 'Not ready' }, 503)
}
const sessionId = c.req.param('id')
const session = engine.getSession(sessionId)
if (!session) {
return c.json({ error: 'Session not found' }, 404)
}
return c.json({ session })
})
app.post('/machines', async (c) => {
const engine = getSyncEngine()
if (!engine) {
return c.json({ error: 'Not ready' }, 503)
}
const json = await c.req.json().catch(() => null)
const parsed = createOrLoadMachineSchema.safeParse(json)
if (!parsed.success) {
return c.json({ error: 'Invalid body' }, 400)
}
const machine = engine.getOrCreateMachine(parsed.data.id, parsed.data.metadata, parsed.data.daemonState ?? null)
return c.json({ machine })
})
app.get('/machines/:id', (c) => {
const engine = getSyncEngine()
if (!engine) {
return c.json({ error: 'Not ready' }, 503)
}
const machineId = c.req.param('id')
const machine = engine.getMachine(machineId)
if (!machine) {
return c.json({ error: 'Machine not found' }, 404)
}
return c.json({ machine })
})
return app
}