65 lines
3.8 KiB
TypeScript
65 lines
3.8 KiB
TypeScript
// 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 and write packages under the `@novox` scope. The registry's npm owner is the gitea org
|
|
// `novox`; a consumer is a gitea *user* placed on that org's package team.
|
|
//
|
|
// **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";
|
|
|
|
// The npm registry owner: a gitea org named `novox`, whose package team every consumer joins so it
|
|
// can read and write packages under the `@novox` scope (ADR 0076).
|
|
const ORG = "novox";
|
|
const PACKAGE_TEAM = "packages";
|
|
|
|
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<void> {
|
|
// The org and its package team are the same for every consumer; ensuring them per-create is
|
|
// idempotent and needs no separate bootstrap step.
|
|
await gitea.ensureOrg(ORG);
|
|
const teamId = await gitea.ensureTeam(ORG, PACKAGE_TEAM, true);
|
|
// The user carries the consumer's login and the mesh's minted password, set every run so a
|
|
// rotation takes. Membership of the package team is what grants read+write on packages.
|
|
//
|
|
// The address is gitea's own convention for one that is not real: its email validation
|
|
// requires a dotted domain, so `@localhost` was refused at create — the fault that had this
|
|
// grant retrying for a day — while `@noreply.localhost` is the shape gitea itself gives
|
|
// hidden addresses.
|
|
await gitea.ensureUser(p.as, p.password, `${p.as}@noreply.localhost`);
|
|
await gitea.addUserToTeam(teamId, p.as);
|
|
},
|
|
|
|
async remove(p: { as: string }): Promise<void> {
|
|
await gitea.deleteUser(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<boolean> {
|
|
return gitea.holdsTeamMember(ORG, PACKAGE_TEAM, p.as, p.password);
|
|
},
|
|
});
|