import { Hono } from 'hono'; import { ValidationError } from '@shade/core'; import type { KeyTransparencyService } from './kt-integration.js'; import { encodeSthForWire } from './kt-integration.js'; /** * Mountable routes that expose the KT log to clients and witnesses. * * GET /v1/kt/log_id — public-key + log_id (operator pinning) * GET /v1/kt/sth — latest signed tree head * GET /v1/kt/sth/:treeSize — historical STH at a specific tree_size * GET /v1/kt/consistency — consistency proof between two tree_sizes * * These are intentionally **anonymous & read-only** so witnesses can * poll without sharing identity. Rate-limiting is the prekey-server's * existing fetch RL bucket. */ export function createKTRoutes(svc: KeyTransparencyService): Hono { const app = new Hono(); app.get('/log_id', (c) => { const logId = svc.getLogId(); const pub = svc.getSigningPublicKey(); return c.json({ logId: Buffer.from(logId).toString('base64'), publicKey: Buffer.from(pub).toString('base64'), }); }); app.get('/sth', async (c) => { const sth = await svc.getLatestSTH(); return c.json(encodeSthForWire(sth)); }); app.get('/sth/:treeSize', async (c) => { const sizeRaw = c.req.param('treeSize'); const size = Number(sizeRaw); if (!Number.isFinite(size) || size < 0 || size !== Math.floor(size)) { throw new ValidationError('treeSize must be a non-negative integer'); } const sth = await svc.getSTHByTreeSize(size); if (!sth) { return c.json({ error: 'STH not found at that tree_size', code: 'SHADE_NOT_FOUND' }, 404); } return c.json(encodeSthForWire(sth)); }); app.get('/consistency', async (c) => { const fromRaw = c.req.query('from'); const toRaw = c.req.query('to'); if (fromRaw === undefined) { throw new ValidationError('from query param required'); } const from = Number(fromRaw); if (!Number.isFinite(from) || from < 0 || from !== Math.floor(from)) { throw new ValidationError('from must be a non-negative integer'); } let to: number | undefined; if (toRaw !== undefined) { to = Number(toRaw); if (!Number.isFinite(to) || to < from || to !== Math.floor(to)) { throw new ValidationError('to must be an integer >= from'); } } const result = await svc.buildConsistencyProof(from, to); return c.json({ fromTreeSize: result.fromTreeSize, toTreeSize: result.toTreeSize, proof: result.proof.map((p) => Buffer.from(p).toString('base64')), }); }); return app; }