// The umami API client — moved here from the shared sdk, because it is umami's own code and // changes when umami's API does (novox/hq ADR 0044). Both this module's tools and its provisioner // import it; nothing outside umami does. export interface Website { id: string; name: string; domain: string; } export class UmamiClient { readonly baseUrl: string; constructor( url: string, private readonly username: string, private readonly password: string, ) { this.baseUrl = url.replace(/\/$/, ""); } /** Build from the module's resolved environment. */ static fromEnv(env: NodeJS.ProcessEnv = process.env): UmamiClient { const url = env.MESH_PROVISION_UMAMI_URL ?? env.UMAMI_URL; const username = env.UMAMI_USERNAME ?? "admin"; const password = env.UMAMI_ADMIN_PASSWORD; if (!url || !password) { throw new Error("UMAMI url or admin password is not set — umami's own code cannot reach it"); } return new UmamiClient(url, username, password); } async getToken(): Promise { const res = await fetch(`${this.baseUrl}/api/auth/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: this.username, password: this.password }), }); if (!res.ok) throw new Error(`umami login failed: ${res.status} ${await res.text()}`); return ((await res.json()) as { token: string }).token; } async findWebsite(token: string, domain: string): Promise { const res = await fetch(`${this.baseUrl}/api/websites?limit=100`, { headers: { Authorization: `Bearer ${token}` }, }); if (!res.ok) throw new Error(`umami list websites failed: ${res.status} ${await res.text()}`); const data = (await res.json()) as { data: Website[] }; return data.data.find((w) => w.domain === domain) ?? null; } async createWebsite(token: string, domain: string, name: string): Promise { const res = await fetch(`${this.baseUrl}/api/websites`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, body: JSON.stringify({ domain, name }), }); if (!res.ok) throw new Error(`umami create website failed: ${res.status} ${await res.text()}`); return (await res.json()) as Website; } async deleteWebsite(token: string, id: string): Promise { const res = await fetch(`${this.baseUrl}/api/websites/${id}`, { method: "DELETE", headers: { Authorization: `Bearer ${token}` }, }); if (!res.ok) throw new Error(`umami delete website failed: ${res.status} ${await res.text()}`); } /** The embed snippet a tracked site includes — the heart of the analytics grant. */ snippet(websiteId: string): string { return ``; } dashboard(websiteId: string): string { return `${this.baseUrl}/websites/${websiteId}`; } }