// The package seats write as an account of their own, never the administrator (design 51: each holder keeps its write // side as an own secret beside a read-only tool token; the forge module's is `write:package`, its tool token // `read:package`). // // The Go bundle (cmd/package-registry) serves both package seats' verbs: it reads the registry with the tools' token // (token.ts, which carries `read:package`) and writes (a publish, a remove, retention's deletion) with this one: // // - **the account**: `mesh-packages`, plain, made by the module with the administrator's login as tools-account.ts // makes `mesh-tools`; its password random, sent once and kept nowhere. // - **the team**: `mesh-packages` in the organisation that owns the registry, writing packages and nothing else: // no code, no issues, no pull requests, no administration. // - **the token**: minted for the account by the administrator's login with the one scope `write:package`, kept at // 0600 in the module's runtime state as `packages-token`, its scopes line beside it saying whose it is // (MintedToken: one mint at a time across the bundle's processes, re-minted when the kept one is gone or other). // // The watcher mints it at its start (keepPackagesToken). No timer looks at it after that: the Go bundle mints nothing, // and when the forge refuses its write it raises the finding PACKAGES_TOKEN_REFUSED in the module's findings state; the // watcher, watching that key, looks again (a refused token is minted afresh) and clears the finding (renewOnRefusal). import { randomBytes } from "node:crypto"; import { join } from "node:path"; import { MintedToken } from "./token.js"; import { TOOLS_ORG, type ToolsForge } from "./tools-account.js"; /** The account the package seats write as (internal/forgehttp PackagesAccount). */ export const PACKAGES_ACCOUNT = "mesh-packages"; /** Its team in the registry's organisation, and the token's name in the account's list. */ export const PACKAGES_TEAM = "mesh-packages"; export const PACKAGES_TOKEN_NAME = "mesh-packages"; /** The one scope the write side needs; reads go with the tools' token. */ export const PACKAGES_TOKEN_SCOPES: readonly string[] = ["write:package"]; /** Where the token is kept in the module's runtime state (the manifest's MESH_GITEA_PACKAGES_TOKEN_FILE). */ export const PACKAGES_TOKEN_FILE = "packages-token"; /** What the team grants: write on the organisation's packages, and no more. */ export const PACKAGES_TEAM_UNITS = { units_map: { "repo.packages": "write" }, includes_all_repositories: false, can_create_org_repo: false, } as const; /** Make the packages account, plain, a member of its team. Its password is set afresh each time and kept nowhere. */ export async function setUpPackagesAccount(forge: ToolsForge, account = PACKAGES_ACCOUNT, password: () => string): Promise { await forge.ensureUser(account, password(), `${account}@noreply.localhost`, true); const team = await forge.ensureTeamWith(TOOLS_ORG, PACKAGES_TEAM, { name: PACKAGES_TEAM, ...PACKAGES_TEAM_UNITS }); await forge.addUserToTeam(team, account); } /** The packages account's token, minted by the administrator's login from what the manifest hands the watcher. */ export function packagesToken(url: string, env: NodeJS.ProcessEnv = process.env, log?: (line: string) => void): MintedToken { const admin = env.MESH_GITEA_ADMIN_USER; const passwordFile = env.MESH_GITEA_ADMIN_PASSWORD_FILE; const stateDir = env.MESH_GITEA_STATE_DIR; const missing = [ admin ? null : "MESH_GITEA_ADMIN_USER", passwordFile ? null : "MESH_GITEA_ADMIN_PASSWORD_FILE", stateDir ? null : "MESH_GITEA_STATE_DIR", ].filter((v): v is string => v !== null); if (missing.length) throw new Error(`the package seats' write token cannot be minted: ${missing.join(", ")} not set by the manifest`); const account = env.MESH_GITEA_PACKAGES_USER || PACKAGES_ACCOUNT; if (account === admin) throw new Error(`MESH_GITEA_PACKAGES_USER names the admin account "${admin}"; the package seats write as an account of their own`); return new MintedToken({ url, admin: admin!, passwordFile: passwordFile!, file: join(stateDir!, PACKAGES_TOKEN_FILE), account, name: PACKAGES_TOKEN_NAME, scopes: PACKAGES_TOKEN_SCOPES, log, setUp: async (password) => { const { GiteaAdmin } = await import("./client.js"); await setUpPackagesAccount(new GiteaAdmin(url, admin!, password), account, () => randomBytes(24).toString("base64url")); }, }); } /** * One look at the packages token: mint it when none is kept, then ask the forge one read the scope covers (the * organisation's package list); a 401 renews it. Answers what was done, for the watcher's line. */ export async function keepPackagesToken(source: MintedToken, url: string, owner = TOOLS_ORG, fetchImpl: typeof fetch = fetch): Promise { const token = await source.current(); const look = (t: string) => fetchImpl(`${url.replace(/\/+$/, "")}/api/v1/packages/${encodeURIComponent(owner)}?limit=1`, { headers: { Authorization: `token ${t}`, Accept: "application/json" } }); const res = await look(token); if (res.status === 401 || MintedToken.lacksScope(res.status, await res.text())) { const fresh = await source.renew(token); const again = await look(fresh); if (!again.ok) throw new Error(`the forge answered ${again.status} to the renewed package token's read`); return "the forge rejected the package seats' write token; minted afresh"; } if (!res.ok) throw new Error(`the forge answered ${res.status} to the package token's read`); return "the package seats' write token holds"; } /** The finding the Go bundle raises in the module's findings state when the forge refuses the write token (cmd/package-registry PackagesTokenRefusedKey). */ export const PACKAGES_TOKEN_REFUSED = "seat.gitea.packages-token-refused_write"; /** One change of the findings state, as its watch hands it. */ export interface FindingChange { readonly key: string; readonly op: "put" | "delete" } /** * The watcher's answer to the Go bundle's refusal: on the finding's put, look at the token (a refused one is minted * afresh) and clear the finding; a look that fails leaves the finding standing, said. Other keys are not its own. */ export function renewOnRefusal(source: MintedToken, url: string, findings: { delete(key: string): Promise }, log: (line: string) => void, fetchImpl: typeof fetch = fetch): (change: FindingChange) => Promise { return async (change) => { if (change.op !== "put" || change.key !== PACKAGES_TOKEN_REFUSED) return; try { log(await keepPackagesToken(source, url, TOOLS_ORG, fetchImpl)); } catch (err) { log(`the package seats' write token was refused and could not be minted afresh; the finding stands — ${err instanceof Error ? err.message : err}`); return; } await findings.delete(change.key); }; }