Phase 0: monorepo skeleton (hub, live-map, api, packages, infra, CI)

This commit is contained in:
Mavis
2026-06-02 15:47:28 +00:00
commit 7b19c54943
59 changed files with 2391 additions and 0 deletions
+79
View File
@@ -0,0 +1,79 @@
/**
* Build the Hono app. Exported separately from index.ts so tests can
* import it without binding a port.
*/
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { logger } from 'hono/logger';
import { UniverseSnapshotSchema, type AppType as _ } from '@kerbal-rt/shared-types';
import { z } from 'zod';
import { state } from './state.js';
export function buildApp() {
const app = new Hono();
app.use('*', logger());
app.use('*', cors({ origin: process.env.CORS_ORIGIN?.split(',') ?? '*' }));
// ─── health ─────────────────────────────────────────────────────────────
app.get('/health', (c) =>
c.json({
ok: true,
service: 'kerbal-rt-api',
version: '0.1.0',
ts: new Date().toISOString(),
}),
);
// ─── ingest (called by the KSP telemetry bridge) ───────────────────────
const IngestAuth = z.object({ 'x-api-key': z.string() });
app.post('/api/v1/ingest', async (c) => {
const headerParse = IngestAuth.safeParse(
Object.fromEntries(Array.from(c.req.raw.headers.entries())),
);
if (!headerParse.success) {
return c.json({ error: true, code: 'UNAUTHORIZED', message: 'Missing API key' }, 401);
}
if (headerParse.data['x-api-key'] !== process.env.INGEST_API_KEY) {
return c.json({ error: true, code: 'FORBIDDEN', message: 'Bad API key' }, 403);
}
const body = await c.req.json();
const parsed = UniverseSnapshotSchema.safeParse(body);
if (!parsed.success) {
return c.json(
{
error: true,
code: 'BAD_PAYLOAD',
message: 'Invalid snapshot',
details: parsed.error.format(),
},
400,
);
}
state.applySnapshot(parsed.data);
return c.json({ error: false, data: { ok: true, ts: new Date().toISOString() } });
});
// ─── read-only endpoints ───────────────────────────────────────────────
app.get('/api/v1/state', (c) => {
const snap = state.latestSnapshot();
if (!snap)
return c.json({ error: true, code: 'NO_DATA', message: 'No snapshot yet' }, 503);
return c.json({ error: false, data: snap });
});
app.get('/api/v1/bodies', (c) => c.json({ error: false, data: state.latestBodies() }));
app.get('/api/v1/vessels', (c) => c.json({ error: false, data: state.latestVessels() }));
app.get('/api/v1/vessels/:id', (c) => {
const v = state.getVessel(c.req.param('id'));
if (!v) return c.json({ error: true, code: 'NOT_FOUND', message: 'Vessel not found' }, 404);
return c.json({ error: false, data: v });
});
return app;
}
export type AppType = ReturnType<typeof buildApp>;