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%
3.8 KB · 83 lines typescript
Raw Blame History
1import { FetchaError, PLAN_LIMITS, newId, normalizeGeo, randomToken, sessionCreateSchema } from "@fetcha/core";2import { and, db, desc, eq, proxySessions } from "@fetcha/db";3import type { ApiPrincipal } from "../auth";4import { getEngine } from "../services/engine";5import { getKV } from "../redis";67export function serializeSession(s: typeof proxySessions.$inferSelect) {8  return {9    id: s.id,10    label: s.label,11    status: s.status === "active" && s.expiresAt.getTime() < Date.now() ? "expired" : s.status,12    network: s.network,13    country: s.country,14    region: s.region,15    city: s.city,16    request_count: s.requestCount,17    last_used_at: s.lastUsedAt?.toISOString() ?? null,18    expires_at: s.expiresAt.toISOString(),19    created_at: s.createdAt.toISOString(),20  };21}2223export async function createSession(principal: ApiPrincipal, body: unknown, idempotencyKey?: string) {24  const parsed = sessionCreateSchema.safeParse(body ?? {});25  if (!parsed.success) {26    throw new FetchaError("INVALID_REQUEST", "Invalid session options.", { details: { issues: parsed.error.issues.map((i) => ({ path: i.path.join("."), message: i.message })) } });27  }28  const input = parsed.data;29  const kv = getKV();30  if (idempotencyKey) {31    const existing = await kv.get(`fch:idem:${principal.projectId}:${idempotencyKey}`).catch(() => null);32    if (existing) {33      const [s] = await db.select().from(proxySessions).where(eq(proxySessions.id, existing)).limit(1);34      if (s) return serializeSession(s);35    }36  }37  const limits = PLAN_LIMITS[principal.plan];38  const geo = normalizeGeo({ country: input.country, region: input.region, city: input.city });39  const engine = await getEngine();40  const plan = engine.routing.plan({ domain: "", network: input.network, geo, plan: principal.plan, sessionRequired: true, browser: false });41  const cand = plan.candidates.find((c) => c.provider.id !== "direct");42  if (!cand) throw new FetchaError("NETWORK_UNAVAILABLE", "No sticky-capable network is available for these options.");43  if (!limits.networks.includes(cand.network)) throw new FetchaError("NETWORK_UNAVAILABLE");4445  const id = newId("sess");46  const row = {47    id,48    organizationId: principal.organizationId,49    projectId: principal.projectId,50    label: input.label ?? null,51    provider: cand.provider.id,52    network: cand.network,53    country: geo.country,54    region: geo.region,55    city: geo.city,56    stickyKey: randomToken(12).replace(/[^a-zA-Z0-9]/g, "").slice(0, 16) || id.slice(5),57    status: "active",58    expiresAt: new Date(Date.now() + input.ttl * 1000),59  };60  await db.insert(proxySessions).values(row);61  if (idempotencyKey) await kv.set(`fch:idem:${principal.projectId}:${idempotencyKey}`, id, 86_400).catch(() => {});62  const [s] = await db.select().from(proxySessions).where(eq(proxySessions.id, id)).limit(1);63  return serializeSession(s!);64}6566export async function getSession(principal: ApiPrincipal, id: string) {67  const [s] = await db.select().from(proxySessions).where(and(eq(proxySessions.id, id), eq(proxySessions.projectId, principal.projectId))).limit(1);68  if (!s) throw new FetchaError("SESSION_NOT_FOUND");69  return serializeSession(s);70}7172export async function closeSession(principal: ApiPrincipal, id: string) {73  const [s] = await db.select().from(proxySessions).where(and(eq(proxySessions.id, id), eq(proxySessions.projectId, principal.projectId))).limit(1);74  if (!s) throw new FetchaError("SESSION_NOT_FOUND");75  await db.update(proxySessions).set({ status: "closed" }).where(eq(proxySessions.id, id));76  return { id, status: "closed" };77}7879export async function listSessions(principal: ApiPrincipal) {80  const rows = await db.select().from(proxySessions).where(eq(proxySessions.projectId, principal.projectId)).orderBy(desc(proxySessions.createdAt)).limit(100);81  return { data: rows.map(serializeSession) };82}83