import { Hono } from 'hono'; import { cors } from 'hono/cors'; import { serveStatic } from '@hono/node-server/serve-static'; import path from 'node:path'; import { config } from './config.ts'; import { getHealth, getMintDetail, getStats, listMints } from './queries.ts'; export function createApp(): Hono { const app = new Hono(); // Public read-only API. There is nothing to protect and every client is a browser. app.use('/api/*', cors()); app.use('/icons/*', cors()); app.get('/api/health', async (c) => { const health = await getHealth(); c.header('Cache-Control', 'no-store'); return c.json(health, health.status === 'ok' ? 200 : 503); }); app.get('/api/stats', async (c) => c.json(await getStats())); /* * The whole index, every ecosystem, each item carrying its `type`. * * `?type=cashu` / `?type=fedimint` narrows it, which is what the two list pages ask * for at build time. Unknown values are passed through rather than rejected: they * return an empty list, which is the honest answer to "show me the ecosystem this * build does not have". */ app.get('/api/mints', async (c) => { const raw = c.req.query('limit'); const limit = raw ? Number.parseInt(raw, 10) : undefined; const type = c.req.query('type')?.trim() || undefined; return c.json(await listMints(Number.isFinite(limit) ? limit : undefined, type)); }); app.get('/api/mints/:host', async (c) => { const detail = await getMintDetail(c.req.param('host')); if (!detail) return c.json({ error: 'not_found', message: 'No mint with that host' }, 404); return c.json(detail); }); // Cached mint icons, so an offline mint keeps its icon. app.use( '/icons/*', serveStatic({ root: path.relative(process.cwd(), config.iconDir) || '.', rewriteRequestPath: (p) => p.replace(/^\/icons/, ''), onFound: (_p, c) => { c.header('Cache-Control', 'public, max-age=86400'); // These files came from mint operators. nosniff pins the served type, and the // sandbox neutralizes anything script-capable (an SVG cached before icons.ts // stopped accepting them) when the file is opened directly on this origin. c.header('X-Content-Type-Options', 'nosniff'); c.header('Content-Security-Policy', 'sandbox'); }, }), ); app.notFound((c) => c.json({ error: 'not_found' }, 404)); return app; }