Files
CashuMints.space/api/src/server.ts
T
michilis 6f17b572b1 Expand ecash explorer capabilities
Add Fedimint discovery, dual SQLite/Postgres storage, richer review handling, and generated social imagery.
2026-08-21 02:10:48 +02:00

65 lines
2.4 KiB
TypeScript

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;
}