popiart-server
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

223 lines
4.7 KiB

import { cookies } from "next/headers";
export const SESSION_COOKIE_NAME = "popiart_session";
type ApiEnvelope<T> = {
ok: boolean;
data?: T;
error?: {
code?: string;
message?: string;
details?: unknown;
};
};
export type PopiartUser = {
id: string;
email: string;
name: string;
scopes?: string[];
};
export type LoginResponse = {
key: string;
token: string;
user: PopiartUser;
};
export type AuthSessionView = {
user: PopiartUser;
session_key?: string;
upstream_key_masked?: string;
};
export type Skill = {
id: string;
name: string;
description: string;
tags: string[];
version: string;
model_type: string;
route_key?: string;
estimated_duration_s: number;
};
export type SkillListResponse = {
items: Skill[];
total: number;
limit: number;
offset: number;
};
export type BudgetSummary = {
period: {
start: string;
end: string;
};
used: {
tokens: number;
cost_usd: number;
};
limit: {
monthly_tokens: number;
monthly_cost_usd: number;
};
remaining: {
tokens: number;
cost_usd: number;
};
};
export type BudgetUsageRow = {
dimension: string;
tokens_used: number;
cost_usd: number;
job_count: number;
};
export type BudgetUsage = {
rows: BudgetUsageRow[];
total: {
tokens_used: number;
cost_usd: number;
job_count: number;
};
};
export class PopiartApiError extends Error {
status: number;
code?: string;
details?: unknown;
constructor(status: number, message: string, code?: string, details?: unknown) {
super(message);
this.name = "PopiartApiError";
this.status = status;
this.code = code;
this.details = details;
}
}
function stripTrailingSlash(value: string) {
return value.replace(/\/+$/, "");
}
export function getPopiartEndpoint() {
const configured =
process.env.POPIART_ENDPOINT ??
process.env.POPIART_SERVER_URL ??
"http://127.0.0.1:8080/v1";
return stripTrailingSlash(configured);
}
export function buildPopiartUrl(pathname: string) {
const path = pathname.startsWith("/") ? pathname : `/${pathname}`;
return `${getPopiartEndpoint()}${path}`;
}
async function readEnvelope<T>(response: Response) {
try {
return (await response.json()) as ApiEnvelope<T>;
} catch {
return null;
}
}
export async function popiartFetchEnvelope<T>(pathname: string, init: RequestInit = {}) {
const response = await fetch(buildPopiartUrl(pathname), {
...init,
cache: "no-store",
});
const payload = await readEnvelope<T>(response);
return { response, payload };
}
export async function popiartRequest<T>(
pathname: string,
options: {
method?: string;
body?: BodyInit | object;
headers?: HeadersInit;
token?: string;
} = {},
) {
const headers = new Headers(options.headers);
let body: BodyInit | undefined;
if (options.token) {
headers.set("Authorization", `Bearer ${options.token}`);
}
if (options.body instanceof FormData || typeof options.body === "string") {
body = options.body;
} else if (options.body !== undefined) {
headers.set("Content-Type", "application/json");
body = JSON.stringify(options.body);
}
const { response, payload } = await popiartFetchEnvelope<T>(pathname, {
method: options.method ?? (body ? "POST" : "GET"),
headers,
body,
});
if (!response.ok || !payload?.ok) {
throw new PopiartApiError(
response.status,
payload?.error?.message ?? `Request failed with status ${response.status}`,
payload?.error?.code,
payload?.error?.details,
);
}
return payload.data as T;
}
export async function getSessionToken() {
const cookieStore = await cookies();
return cookieStore.get(SESSION_COOKIE_NAME)?.value ?? null;
}
export async function getViewerSession() {
const token = await getSessionToken();
if (!token) {
return null;
}
try {
return await popiartRequest<AuthSessionView>("/auth/me", { token });
} catch (error) {
if (error instanceof PopiartApiError && error.status === 401) {
return null;
}
throw error;
}
}
export async function getSkillsCatalog(search?: string) {
const token = await getSessionToken();
if (!token) {
return null;
}
const query = new URLSearchParams();
query.set("limit", "100");
if (search) {
query.set("search", search);
}
return popiartRequest<SkillListResponse>(`/skills?${query.toString()}`, { token });
}
export async function getBudgetSummary() {
const token = await getSessionToken();
if (!token) {
return null;
}
return popiartRequest<BudgetSummary>("/budget", { token });
}
export async function getBudgetUsage() {
const token = await getSessionToken();
if (!token) {
return null;
}
return popiartRequest<BudgetUsage>("/budget/usage", { token });
}