import type { FastifyInstance } from "fastify"; import { z } from "zod"; import { FetchaError } from "@fetcha/core"; import { db, desc, providerHealth, sql } from "@fetcha/db"; import { assertInternalToken, principalForProject } from "../auth"; import { getEngine } from "../services/engine"; import { handleFetch } from "./fetch"; import { closeSession, createSession, listSessions } from "./sessions"; import { usageResponse } from "./misc"; /** * Internal routes used by the dashboard (Next.js server) with the shared service token. * They are never exposed publicly: the web app proxies only /v1/* to this service. */ export async function registerInternalRoutes(app: FastifyInstance) { app.addHook("onRequest", async (req) => { if (req.url.startsWith("/internal/")) assertInternalToken(req.headers.authorization); }); const playgroundSchema = z.object({ project_id: z.string(), user_id: z.string(), request: z.unknown() }); app.post("/internal/fetch", async (req, reply) => { const parsed = playgroundSchema.safeParse(req.body); if (!parsed.success) throw new FetchaError("INVALID_REQUEST", "project_id, user_id and request are required."); const principal = await principalForProject(parsed.data.project_id, parsed.data.user_id); const out = await handleFetch({ principal, body: parsed.data.request, source: "playground", clientIp: (req.headers["x-forwarded-for"] as string | undefined)?.split(",")[0]?.trim() ?? req.ip, userAgent: "fetcha-playground", }); reply.header("x-fetcha-request-id", out.requestId); return out.body; }); const sessionSchema = z.object({ project_id: z.string(), user_id: z.string(), options: z.unknown().optional() }); app.post("/internal/sessions", async (req) => { const parsed = sessionSchema.safeParse(req.body); if (!parsed.success) throw new FetchaError("INVALID_REQUEST"); const principal = await principalForProject(parsed.data.project_id, parsed.data.user_id); return createSession(principal, parsed.data.options ?? {}); }); app.get<{ Querystring: { project_id: string; user_id: string } }>("/internal/sessions", async (req) => { const principal = await principalForProject(req.query.project_id, req.query.user_id); return listSessions(principal); }); app.delete<{ Params: { id: string }; Querystring: { project_id: string; user_id: string } }>("/internal/sessions/:id", async (req) => { const principal = await principalForProject(req.query.project_id, req.query.user_id); return closeSession(principal, req.params.id); }); app.get<{ Querystring: { project_id: string; user_id: string } }>("/internal/usage", async (req) => { const principal = await principalForProject(req.query.project_id, req.query.user_id); return usageResponse(principal); }); /** Admin: provider status (names visible — admin only). */ app.get("/internal/providers", async () => { const engine = await getEngine(); const latest = await db .select() .from(providerHealth) .where(sql`${providerHealth.checkedAt} > now() - interval '1 day'`) .orderBy(desc(providerHealth.checkedAt)) .limit(200); const seen = new Set(); const health = latest.filter((h) => { const k = `${h.provider}:${h.network}`; if (seen.has(k)) return false; seen.add(k); return true; }); return { providers: engine.registry.all().map((p) => ({ id: p.id, label: p.label, configured: p.isConfigured(), networks: p.networks, prices: Object.fromEntries(p.networks.map((n) => [n, p.pricePerGb(n)])), health: health.filter((h) => h.provider === p.id).map((h) => ({ network: h.network, status: h.status, latency_ms: h.latencyMs, detail: h.detail, checked_at: h.checkedAt })), circuits: engine.circuit.snapshot().filter((c) => c.key.startsWith(`${p.id}:`)), })), available_networks: engine.registry.availableNetworks(), }; }); app.post("/internal/providers/probe", async () => { const engine = await getEngine(); await engine.reload(); await engine.probeAll(); return { ok: true }; }); app.post("/internal/providers/reload", async () => { const engine = await getEngine(); await engine.reload(); return { ok: true }; }); app.post<{ Body: { key?: string } }>("/internal/circuits/reset", async (req) => { const engine = await getEngine(); engine.circuit.reset(req.body?.key); return { ok: true }; }); app.post<{ Body: { key_hash: string } }>("/internal/keys/invalidate", async (req) => { const { invalidateKeyCache } = await import("../auth"); if (req.body?.key_hash) await invalidateKeyCache(req.body.key_hash); return { ok: true }; }); }