// The hub's API, called from server components and server actions. Calls go to the API's internal address // (API_URL) with the visitor's cookie, so the API sees the same session the browser has. import "server-only"; import { treaty } from "@elysiajs/eden"; import type { App } from "@t5de/api"; import { headers } from "next/headers"; import { cache } from "react"; const apiURL = process.env.API_URL ?? "http://localhost:4101"; /** The visitor's cookie and address, to pass on to the API. */ async function forwarded(): Promise> { const incoming = await headers(); const forward: Record = {}; for (const name of ["cookie", "x-forwarded-for", "user-agent"]) { const value = incoming.get(name); if (value) forward[name] = value; } return forward; } /** * The API's /api/v1 routes, as the person making this request. (Never await the client itself: it's a proxy, and * awaiting it would call a route named "then".) */ export const api = treaty(apiURL, { headers: forwarded }).api.v1; type V1 = typeof api; type Problem = { error: string; message: string }; /** What a route answers when it succeeds (the API's error shape is part of its declared answers). */ type Ok = Exclude; type Data Promise<{ data: unknown }>> = Ok< NonNullable>["data"]> >; export type Person = Data["person"]; export type Account = Data; export type Link = Account["links"][number]; export type Challenge = Account["challenges"][number]; export type OnlineEntry = Data["online"][number]; export type ConsoleData = Data; export type AuditEntry = ConsoleData["audit"][number]; export type ProductSummary = Data["products"][number]; export type ProductPage = Data["get"]>; export type Revision = ProductPage["revisions"][number]; export type Catalog = Data["catalogs"][number]; export type LibraryEntry = Data["library"][number]; /** An error the API answered: message is written to be shown as is. */ export class ApiError extends Error { constructor( readonly status: number, readonly slug: string, message: string, ) { super(message); } } type Answer = { data: T; error: null } | { data: null; error: { status: unknown; value: unknown } }; /** The answer's data, or throws ApiError with the API's message. */ export function unwrap(answer: Answer): Ok> { if (answer.error) { const status = typeof answer.error.status === "number" ? answer.error.status : 500; const value = answer.error.value as { error?: unknown; message?: unknown } | null; const slug = typeof value?.error === "string" ? value.error : "error"; const message = typeof value?.message === "string" && value.message ? value.message : "Something went wrong. Try again in a moment."; throw new ApiError(status, slug, message); } return answer.data as Ok>; } /** Who's signed in, or undefined. Once per request. */ export const getMe = cache(async (): Promise => { const { data } = await api.me.get(); return data && "person" in data ? data.person : undefined; }); export const isModerator = (p: Person | undefined) => p?.role === "moderator" || p?.role === "admin";