// Recap Relay — operator-side credit-metered proxy in front of Gemini // (and optionally a co-located Parakeet+Gemma setup). // // Two public endpoints: // POST /relay/transcribe — audio → text (Gemini File API) // POST /relay/analyze — text → topic sections JSON (Gemini Pro) // Plus admin endpoints under /admin/* gated by an HTTP session cookie. import express from "express"; import cors from "cors"; import cookieParser from "cookie-parser"; import path from "path"; import { fileURLToPath } from "url"; import { initConfig } from "./config.js"; import { initCredits } from "./credits.js"; import { setupAdminAuthMiddleware, setupAdminAuthRoutes, } from "./admin-auth.js"; import { transcribeRouter } from "./routes/transcribe.js"; import { analyzeRouter } from "./routes/analyze.js"; import { healthRouter } from "./routes/health.js"; import { balanceRouter } from "./routes/balance.js"; import { policyRouter } from "./routes/policy.js"; import { adminRouter } from "./routes/admin.js"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); // DATA_DIR is /data on StartOS, project root in dev. const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, ".."); const PORT = parseInt(process.env.PORT || "3002", 10); await initConfig({ dataDir: DATA_DIR }); await initCredits({ dataDir: DATA_DIR }); const app = express(); app.use(cors()); app.use(cookieParser()); // Admin auth must run BEFORE the admin routes register so the cookie // check applies to /admin/usage, /admin/config, etc. /admin/login and // /admin/status are explicitly exempted inside the middleware. setupAdminAuthMiddleware(app); setupAdminAuthRoutes(app); // Public relay endpoints. No app-level auth — each route handler // authenticates per-call via headers (X-Recap-Install-Id required, // Authorization optional). app.use("/relay", healthRouter()); app.use("/relay", policyRouter()); app.use("/relay", balanceRouter()); app.use("/relay", transcribeRouter()); app.use("/relay", analyzeRouter()); // Admin dashboard endpoints (cookie-gated). app.use("/admin", adminRouter({ dataDir: DATA_DIR })); // Static admin UI (v0.2 will flesh out public/admin.html). For v0.1 // the dashboard is JSON-only; serve any static assets dropped into // public/ but don't error if the directory is empty. app.use(express.static(path.join(__dirname, "..", "public"))); // Root: redirect to /admin/ for operator convenience, or show a tiny // placeholder for Recap clients that hit the root by mistake. app.get("/", (_req, res) => { res.type("text/plain").send( "Recap Relay\n" + "===========\n" + "Public endpoints: POST /relay/transcribe, POST /relay/analyze, GET /relay/health\n" + "Operator dashboard: /admin/\n" ); }); const HOSTNAME = process.env.HOSTNAME || "0.0.0.0"; app.listen(PORT, HOSTNAME, () => { console.log(`[relay] listening on http://${HOSTNAME}:${PORT}`); console.log(`[relay] data directory: ${DATA_DIR}`); });