SPB Git forge

spb/fetcha

Public
11commits 1branches 0releases
1.5 MBsize
maindefault branch
16 days agolast push
TypeScript 97.5% SQL 1.4% Python 0.8%
4.7 KB · 112 lines typescript
Raw Blame History
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