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
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 });
|
|
}
|
|
|