import { fetchIt } from "@putkoff/abstract-utilities"; export interface AppConfig { API_BASE_URL?: string; BASE_API_URL?: string; } export interface EndpointInfo { endpoint: string; methods: string[]; url: string; } const DEFAULT_BASE_URL = "https://api.abstractendeavors.com"; let _baseUrl: string | null = null; let _endpointMap: Record | null = null; let _configPromise: Promise | null = null; function cleanBaseUrl(value: unknown): string { if (typeof value !== "string" || !value.trim()) { return DEFAULT_BASE_URL; } return value.trim().replace(/\/+$/, ""); } function joinApi(base: string, pathOrSlug: string): string { const cleanBase = base.replace(/\/+$/, ""); const cleanPath = String(pathOrSlug || "").replace(/^\/+/, ""); return `${cleanBase}/${cleanPath}`.replace(/\/api\/api\//g, "/api/"); } async function loadRuntimeConfig(): Promise { if (_configPromise) return _configPromise; _configPromise = fetch("/assets/config.json", { cache: "no-store", headers: { Accept: "application/json", }, }) .then(async (res) => { if (!res.ok) { console.warn(`Config load failed: HTTP ${res.status}`); return {}; } return (await res.json()) as AppConfig; }) .catch((err) => { console.warn("Config load failed; using defaults", err); return {}; }); return _configPromise; } /** * Get API base URL from /assets/config.json, or fall back. */ export async function getBaseUrl(): Promise { if (_baseUrl) return _baseUrl; const config = await loadRuntimeConfig(); _baseUrl = cleanBaseUrl( config.API_BASE_URL || config.BASE_API_URL || DEFAULT_BASE_URL, ); return _baseUrl; } /** * Always resolves a usable base URL. */ export async function getUrl(baseUrl?: string | null): Promise { if (baseUrl && typeof baseUrl === "string") { return cleanBaseUrl(baseUrl); } return getBaseUrl(); } function unwrapFetchData(result: unknown): T { // Supports plain data returns: // [...] if (Array.isArray(result)) { return result as T; } // Supports object style: // { data: [...] } if ( result && typeof result === "object" && "data" in result ) { return (result as { data: T }).data; } return result as T; } /** * Fetch the endpoint list from /endpoints. */ export async function getEndpoints(baseUrl?: string | null): Promise { const base = await getUrl(baseUrl); const endpointsUrl = joinApi(base, "/endpoints"); const result = await fetchIt(endpointsUrl, {}, "GET"); return unwrapFetchData(result); } /** * Fetch and cache endpoint slug/url map. */ export async function getUrls(): Promise> { if (_endpointMap) return _endpointMap; const base = await getBaseUrl(); const endpoints = await getEndpoints(base); _endpointMap = endpoints.reduce>((map, item) => { if (!item || typeof item.url !== "string") { return map; } let slug = item.url.replace(/^\/+/, ""); if (slug.startsWith("api/")) { slug = slug.slice(4); } map[slug] = joinApi(base, slug); // Optional aliases: // endpoint name -> full URL if (item.endpoint) { map[item.endpoint] = joinApi(base, slug); } return map; }, {}); return _endpointMap; } /** * Find best-matching endpoint by slug, endpoint name, or URL. */ export async function getEndpoint(keyword: string): Promise { const lower = keyword.toLowerCase(); const urls = await getUrls(); let best: [string, string] | null = null; for (const [slug, fullUrl] of Object.entries(urls)) { if (slug.toLowerCase().includes(lower)) { if (!best || slug.length > best[0].length) { best = [slug, fullUrl]; } } } if (!best) { const base = await getBaseUrl(); throw new Error(`No endpoint matching "${keyword}" under ${base}`); } return best[1]; }