mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
* 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>
101 lines
3.1 KiB
TypeScript
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
|
|
}
|