TypeScript 97.5%
SQL 1.4%
Python 0.8%
1import type { FastifyInstance } from "fastify";2import { z } from "zod";3import { FetchaError } from "@fetcha/core";4import { db, desc, providerHealth, sql } from "@fetcha/db";5import { assertInternalToken, principalForProject } from "../auth";6import { getEngine } from "../services/engine";7import { handleFetch } from "./fetch";8import { closeSession, createSession, listSessions } from "./sessions";9import { usageResponse } from "./misc";1011/**12 * Internal routes used by the dashboard (Next.js server) with the shared service token.13 * They are never exposed publicly: the web app proxies only /v1/* to this service.14 */15export async function registerInternalRoutes(app: FastifyInstance) {16 app.addHook("onRequest", async (req) => {17 if (req.url.startsWith("/internal/")) assertInternalToken(req.headers.authorization);18 });1920 const playgroundSchema = z.object({ project_id: z.string(), user_id: z.string(), request: z.unknown() });2122 app.post("/internal/fetch", async (req, reply) => {23 const parsed = playgroundSchema.safeParse(req.body);24 if (!parsed.success) throw new FetchaError("INVALID_REQUEST", "project_id, user_id and request are required.");25 const principal = await principalForProject(parsed.data.project_id, parsed.data.user_id);26 const out = await handleFetch({27 principal,28 body: parsed.data.request,29 source: "playground",30 clientIp: (req.headers["x-forwarded-for"] as string | undefined)?.split(",")[0]?.trim() ?? req.ip,31 userAgent: "fetcha-playground",32 });33 reply.header("x-fetcha-request-id", out.requestId);34 return out.body;35 });3637 const sessionSchema = z.object({ project_id: z.string(), user_id: z.string(), options: z.unknown().optional() });38 app.post("/internal/sessions", async (req) => {39 const parsed = sessionSchema.safeParse(req.body);40 if (!parsed.success) throw new FetchaError("INVALID_REQUEST");41 const principal = await principalForProject(parsed.data.project_id, parsed.data.user_id);42 return createSession(principal, parsed.data.options ?? {});43 });44 app.get<{ Querystring: { project_id: string; user_id: string } }>("/internal/sessions", async (req) => {45 const principal = await principalForProject(req.query.project_id, req.query.user_id);46 return listSessions(principal);47 });48 app.delete<{ Params: { id: string }; Querystring: { project_id: string; user_id: string } }>("/internal/sessions/:id", async (req) => {49 const principal = await principalForProject(req.query.project_id, req.query.user_id);50 return closeSession(principal, req.params.id);51 });52 app.get<{ Querystring: { project_id: string; user_id: string } }>("/internal/usage", async (req) => {53 const principal = await principalForProject(req.query.project_id, req.query.user_id);54 return usageResponse(principal);55 });5657 /** Admin: provider status (names visible — admin only). */58 app.get("/internal/providers", async () => {59 const engine = await getEngine();60 const latest = await db61 .select()62 .from(providerHealth)63 .where(sql`${providerHealth.checkedAt} > now() - interval '1 day'`)64 .orderBy(desc(providerHealth.checkedAt))65 .limit(200);66 const seen = new Set<string>();67 const health = latest.filter((h) => {68 const k = `${h.provider}:${h.network}`;69 if (seen.has(k)) return false;70 seen.add(k);71 return true;72 });73 return {74 providers: engine.registry.all().map((p) => ({75 id: p.id,76 label: p.label,77 configured: p.isConfigured(),78 networks: p.networks,79 prices: Object.fromEntries(p.networks.map((n) => [n, p.pricePerGb(n)])),80 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 })),81 circuits: engine.circuit.snapshot().filter((c) => c.key.startsWith(`${p.id}:`)),82 })),83 available_networks: engine.registry.availableNetworks(),84 };85 });8687 app.post("/internal/providers/probe", async () => {88 const engine = await getEngine();89 await engine.reload();90 await engine.probeAll();91 return { ok: true };92 });9394 app.post("/internal/providers/reload", async () => {95 const engine = await getEngine();96 await engine.reload();97 return { ok: true };98 });99100 app.post<{ Body: { key?: string } }>("/internal/circuits/reset", async (req) => {101 const engine = await getEngine();102 engine.circuit.reset(req.body?.key);103 return { ok: true };104 });105106 app.post<{ Body: { key_hash: string } }>("/internal/keys/invalidate", async (req) => {107 const { invalidateKeyCache } = await import("../auth");108 if (req.body?.key_hash) await invalidateKeyCache(req.body.key_hash);109 return { ok: true };110 });111}112