// minio's provisioner — the adapter that makes minio a provider of the mesh `s3-bucket` interface // (the name in module.json's `provides`). The reconcile loop, the contributions file, and reading // the mesh's minted secret are the sdk harness's; this writes only the per-service half: how minio // creates and removes a consumer's bucket and its scoped access key (novox/hq ADR 0039/0040/0048). // // The `s3-bucket` interface: a consumer connects to an S3 endpoint with an access key confined to // its own bucket. It depends on `s3-bucket`, not on minio, so any S3-compatible provider could serve // it. // // **The access key and its secret are the mesh's, not the provisioner's (ADR 0048).** The mesh // derives the login (the access-key id) and hands it to both ends, and mints the secret key. minio // creates the service account under exactly that access key with exactly that secret — a credential // the provisioner invented is one the consumer could never present. The bucket is derived from the // login, so teardown recomputes it with nothing to persist. import { runProvisioner, type Provision } from "@novox/mesh-sdk/provisioner"; import { emit } from "@novox/mesh-sdk/events"; import { MinioClient, bucketFor } from "../client.js"; const minio = MinioClient.fromEnv(); runProvisioner("s3-bucket", { async create(p: Provision): Promise { const bucket = bucketFor(p.as); 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 announce("module.minio.bucket.created", { bucket, consumer: p.consumer ?? "", accessKey: accessKeyId, endpoint: minio.baseUrl, }); }, async remove(p: { as: string }): Promise { const bucket = bucketFor(p.as); // Revoking the key is what cuts the consumer's access. The bucket is emptied-then-dropped only if // empty; a bucket that still holds objects is left for an operator rather than erroring on every // reconcile tick — access is already gone, and silently deleting a consumer's data would be worse. try { await minio.removeAccessKey(p.as); } catch { /* already gone */ } try { await minio.removeBucket(bucket); } catch (err) { console.error(`[minio] bucket ${bucket} not removed (likely non-empty), access revoked: ${err}`); } await announce("module.minio.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(bucketFor(p.as), p.as, p.password); }, }); /** Emit best-effort: a broker hiccup is logged and dropped, never allowed to throw back and fail a * bucket that was made. */ async function announce(type: string, body: unknown): Promise { try { await emit(type, body); } catch (err) { console.error(`[minio] could not emit ${type}: ${err}`); } }