// gitea's provisioner — the adapter that makes gitea a provider of the mesh // `npm-package-registry` interface. The reconcile loop, the contributions file, and reading the // mesh's minted password are the sdk harness's; this writes only the per-service half: how gitea // creates and removes a consumer's npm credential (novox/hq ADR 0048/0076). // // **A package registry seat is one per ecosystem (novox/hq ADR 0109).** gitea holds the npm seat // (ADR 0110). Adding cargo or PyPI is adding a provision — another `provides` entry, another // `receives` path and another registration below — not widening this one. `git`, which gitea also // provides, mints nothing and so registers nothing here: the mesh's own repositories are public, // and a clone credential is not yet decided (ADR 0111). // // The `npm-package-registry` interface: a consumer authenticates to the npm registry at // `/api/packages/novox/npm/` with basic auth, as `as` with the password the mesh minted, and can // read packages under the `@novox` scope, and write them unless it asked for read only (teams.ts). The // registry's npm owner is the gitea org `novox`; a consumer is a gitea *user* on one of that org's teams. // // **The user name and password are the mesh's, not the provisioner's (ADR 0048).** The mesh derives // the login and hands it to both ends, and mints the password. gitea creates a user under exactly // that login and sets exactly that password every run — so a rotation takes — and seals nothing: the // consumer already has its copy through the mesh's own channel. // // The admin calls run through GiteaAdmin (basic auth as the mesh's gitea admin), which is the // module's one boundary to the forge's admin API (see client.ts). import { runProvisioner, type Provision } from "@novox/mesh-sdk/provisioner"; import { GiteaAdmin } from "../client.js"; import { grant, holds } from "./teams.js"; // Which team a consumer joins — `packages` to publish, `readers` to read only — is teams.ts's. const gitea = GiteaAdmin.fromEnv(); // Where this registration's contributions land comes from $MESH_RECEIVES, never a path written // here: the mesh writes the file where the manifest's `receives` says, and a second copy of that // path in code would drift from it. One variable carries one path, so a second registration in this // module needs the mesh to say where each provision's file is — not yet possible, and not faked. runProvisioner("npm-package-registry", { async create(p: Provision): Promise { // The org and its teams are ensured per create: idempotent, and no separate bootstrap step. The // user carries the consumer's login and the mesh's minted password, set every run so a rotation // takes; membership of its team is what grants it what it may do. await grant(gitea, p); }, async remove(p: { as: string }): Promise { // Login prohibited, never deleted (novox/hq issue 241: a withdrawal never destroys a consumer's data — on 2026-10-04 a misread grants file withdrew every consumer at once): deleting purges every repository the user owns. await gitea.prohibitLogin(p.as); }, // Asked every minute by the harness: whether the backend still holds this consumer exactly as // the mesh gave it, so a login lost behind the provisioner's back is made again (novox/hq issue 120). async holds(p: Provision): Promise { return holds(gitea, p); }, });