// The s3-bucket provisioner's create, remove and holds, with what they touch handed in, so a unit test drives // them with a fake admin client (the provisioner's index.ts runs them against minio). import type { Provision } from "@novox/mesh-sdk/provisioner"; import { ensureLinkKey, revokeLinkKey, type LinkAdmin } from "../links.js"; export interface Admin extends LinkAdmin { bucketExists(bucket: string): Promise; createBucket(bucket: string): Promise; canReachAs(bucket: string, accessKey: string, secretKey: string): Promise; readonly baseUrl: string; } export interface Deps { minio: Admin; linksDir: string; announce(type: string, body: unknown): Promise; } export function handlers({ minio, linksDir, announce }: Deps) { return { async create(p: Provision): Promise { const bucket = bucketNamed(p.derived); const accessKeyId = p.as; if (!(await minio.bucketExists(bucket))) await minio.createBucket(bucket); // Re-mint the scoped key idempotently: drop any prior one under this id, then add it back with // the mesh's secret. try { await minio.removeAccessKey(accessKeyId); } catch { /* none yet — first provision */ } await minio.createAccessKey(bucket, accessKeyId, p.password); await ensureLinkKey(minio, linksDir, bucket); await announce("bucket.created", { bucket, consumer: p.consumer ?? "", accessKey: accessKeyId, endpoint: minio.baseUrl, }); }, async remove(p: { as: string; derived: Readonly> }): Promise { const bucket = bucketNamed(p.derived); // Revoking the key is what cuts the consumer's access, and the bucket is kept, empty or not // (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). A bucket is removed by a person, never by this loop. try { await minio.removeAccessKey(p.as); } catch { /* already gone */ } await revokeLinkKey(minio, linksDir, bucket); console.error(`[minio] ${p.as} withdrawn: access key and link key revoked, bucket ${bucket} kept`); await announce("bucket.removed", { bucket, accessKey: 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 minio.canReachAs(bucketNamed(p.derived), p.as, p.password); }, }; } /** The bucket the mesh derived for this consumer. * * Absent means this module is running against a control plane that does not fill `${consumer:…}` * yet, or a manifest whose `serves` block lost the line. Both are the same mistake from here — * nobody said which bucket — and both are said rather than guessed: a provisioner that fell back * to deriving one would restore the second rule and hide the fault behind a bucket that happens * to be right. */ function bucketNamed(derived: Readonly>): string { const bucket = derived.bucket; if (typeof bucket !== "string" || bucket === "") { throw new Error( "the mesh did not say which bucket this consumer gets: minio's manifest must serve " + "`bucket` under s3-bucket (novox/hq ADR 0202)", ); } return bucket; }