65 lines
3.5 KiB
TypeScript
65 lines
3.5 KiB
TypeScript
// Which team of the forge's npm owner a consumer of `npm-package-registry` joins, and the per-consumer
|
|
// half of the provisioner around it — pure of the forge's address, so it is tested with a stand-in.
|
|
//
|
|
// **Two teams, by what the consumer said it needs** (novox/hq issue 624). A consumer that publishes —
|
|
// the build modules, whose builds publish the SDK and every TypeScript module's packages — joins `packages`:
|
|
// code read on the org's repositories and write on its packages. A consumer that contributes
|
|
// `{"access": "read"}` joins `readers`: code read and package read, and no write. The lab is the first: it
|
|
// clones branches that are not yet reviewed and runs their code beside its credential, and code that is
|
|
// not reviewed must not be able to publish the mesh's packages with it.
|
|
|
|
/** The npm registry's owner: the gitea org every consumer's packages live under (ADR 0076). */
|
|
export const ORG = "novox";
|
|
/** The team whose members read and write the org's packages. */
|
|
export const PACKAGE_TEAM = "packages";
|
|
/** The team whose members read the org's code and packages, and write nothing. */
|
|
export const READ_TEAM = "readers";
|
|
|
|
/** The team a consumer joins, from what it contributed; an access it may not ask for is refused. */
|
|
export function teamFor(values: Readonly<Record<string, unknown>> | undefined): { team: string; packageWrite: boolean } {
|
|
const access = values?.access;
|
|
if (access === undefined || access === "write") return { team: PACKAGE_TEAM, packageWrite: true };
|
|
if (access === "read") return { team: READ_TEAM, packageWrite: false };
|
|
throw new Error(`npm-package-registry: access ${JSON.stringify(access)} is not "read" or "write"`);
|
|
}
|
|
|
|
/** What the adapter asks of the forge's admin API (client.ts GiteaAdmin). */
|
|
export interface Forge {
|
|
ensureOrg(org: string): Promise<void>;
|
|
ensureTeam(org: string, team: string, packageWrite: boolean): Promise<number>;
|
|
ensureUser(username: string, password: string, email: string): Promise<void>;
|
|
addUserToTeam(teamId: number, username: string): Promise<void>;
|
|
removeUserFromTeam(teamId: number, username: string): Promise<void>;
|
|
holdsTeamMember(org: string, team: string, username: string, password: string): Promise<boolean>;
|
|
}
|
|
|
|
export interface Consumer {
|
|
readonly as: string;
|
|
readonly password: string;
|
|
readonly values: Readonly<Record<string, unknown>>;
|
|
}
|
|
|
|
/**
|
|
* grant makes the consumer's user, with exactly the mesh's password, a member of its one team. A consumer
|
|
* that asks for read only is also taken out of the package team, so one that asked for write before and
|
|
* read now loses the write at once.
|
|
*/
|
|
export async function grant(forge: Forge, p: Consumer): Promise<void> {
|
|
const { team, packageWrite } = teamFor(p.values);
|
|
await forge.ensureOrg(ORG);
|
|
const teamId = await forge.ensureTeam(ORG, team, packageWrite);
|
|
// 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, while `@noreply.localhost` is the shape gitea
|
|
// itself gives hidden addresses.
|
|
await forge.ensureUser(p.as, p.password, `${p.as}@noreply.localhost`);
|
|
await forge.addUserToTeam(teamId, p.as);
|
|
if (!packageWrite) {
|
|
await forge.removeUserFromTeam(await forge.ensureTeam(ORG, PACKAGE_TEAM, true), p.as);
|
|
}
|
|
}
|
|
|
|
/** holds is whether the forge still holds the consumer as the mesh gave it: its login, in its team. */
|
|
export async function holds(forge: Forge, p: Consumer): Promise<boolean> {
|
|
return forge.holdsTeamMember(ORG, teamFor(p.values).team, p.as, p.password);
|
|
}
|