registry, verdaccio, portainer: full nox modules (ADR 0044/0046)

registry (docker v2): catalog/tags/delete tools, emits image.pushed (a build's
image is now pullable). verdaccio (npm): list/info tools, emits
package.published. portainer: endpoints/stacks/containers tools — tools-only,
since its only events are the underlying containers' lifecycle, which the host
owns. Typecheck; manifests parse.
This commit is contained in:
2026-09-04 02:45:42 +02:00
parent aca237b2e0
commit 81dc74734b
16 changed files with 589 additions and 0 deletions
+81
View File
@@ -0,0 +1,81 @@
// The Verdaccio (npm registry) client — verdaccio's own code, living in the module (novox/hq
// ADR 0044). Both this module's tools and its events entrypoint import it, and nothing outside
// verdaccio does.
export interface VerdaccioPackage {
name: string;
version?: string;
description?: string;
time?: string;
}
export interface PackageInfo {
name: string;
latest?: string;
versions: string[];
description?: string;
modified?: string;
}
export class VerdaccioClient {
readonly baseUrl: string;
// A bearer token is optional: package listing and reading are public on most registries, so the
// token is sent only when configured, for a registry that gates reads behind auth.
constructor(
url: string,
private readonly token?: string,
) {
this.baseUrl = url.replace(/\/+$/, "");
}
/**
* Build from the module's resolved environment. The URL is MESH_VERDACCIO_URL (or the local
* port); an optional MESH_VERDACCIO_TOKEN authenticates. Throws when no URL is configured.
*/
static fromEnv(env: NodeJS.ProcessEnv = process.env): VerdaccioClient {
const url = env.MESH_VERDACCIO_URL ?? `http://127.0.0.1:${env.VERDACCIO_PORT ?? "4873"}`;
if (!url) throw new Error("no verdaccio URL — set MESH_VERDACCIO_URL");
return new VerdaccioClient(url, env.MESH_VERDACCIO_TOKEN);
}
private async getJson<T>(path: string): Promise<T> {
const res = await fetch(`${this.baseUrl}${path}`, {
headers: {
Accept: "application/json",
...(this.token ? { Authorization: `Bearer ${this.token}` } : {}),
},
});
if (!res.ok) throw new Error(`Verdaccio ${path}: ${res.status} ${await res.text()}`);
return res.json() as Promise<T>;
}
/**
* Every package the registry hosts, from Verdaccio's own web API — the same list its UI shows.
* Each entry carries the latest version and the time it was last published.
*/
async listPackages(): Promise<VerdaccioPackage[]> {
const raw = await this.getJson<any[]>("/-/verdaccio/data/packages");
return (raw ?? []).map((p) => ({
name: p.name,
version: p.version ?? p["dist-tags"]?.latest,
description: p.description,
time: p.time?.modified ?? p.time,
}));
}
/**
* The full detail of one package — its dist-tags, every published version, and timestamps —
* from the standard npm packument endpoint (`GET /<name>`).
*/
async getPackageInfo(name: string): Promise<PackageInfo> {
const doc = await this.getJson<any>(`/${encodeURIComponent(name).replace(/%2F/g, "/")}`);
return {
name: doc.name ?? name,
latest: doc["dist-tags"]?.latest,
versions: Object.keys(doc.versions ?? {}),
description: doc.description,
modified: doc.time?.modified,
};
}
}