umami is now a whole module, not a manifest: its own API client, its
tools, and its provisioner all live in the module and build on
@novox/mesh-sdk.
- client.ts — umami's API client, moved out of the shared sdk into the
module (ADR 0044); umami's tools and provisioner both import it.
- tools/ — umami_create_site / umami_delete_site on the sdk tool harness
(registerModuleTools); the tool logic and client are the module's.
- provisioner/ — the adapter making umami a provider of the mesh
'analytics' interface: a consumer contributes {domain}, receives
{siteId, snippet, dashboard}. ~20 lines, because the watch/seal/grant
loop is the sdk harness's.
Type-checks against the real mesh-sdk (tsc --noEmit clean); the manifest
parses against internal/catalogue. Remaining to actually run: build and
publish the module + its mesh-provision-umami-analytics image (the
placeholder digest), which is the pipeline's job.
Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
79 lines
2.9 KiB
TypeScript
79 lines
2.9 KiB
TypeScript
// 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<string> {
|
|
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<Website | null> {
|
|
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<Website> {
|
|
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<void> {
|
|
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 `<script defer src="${this.baseUrl}/script.js" data-website-id="${websiteId}"></script>`;
|
|
}
|
|
|
|
dashboard(websiteId: string): string {
|
|
return `${this.baseUrl}/websites/${websiteId}`;
|
|
}
|
|
}
|