// 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, sealing and grant-file handling 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 0044/0045). // // The `s3-bucket` interface: a consumer receives `{ endpoint, bucket, accessKey, secretKey, region }` // — an S3 endpoint and a credential confined to its own bucket. It depends on `s3-bucket`, not on // minio, so any S3-compatible provider could serve it. // // The bucket and access-key id are derived deterministically from the consumer's identity, because // the harness hands `remove` only that identity (no stored values) — so teardown recomputes exactly // what creation minted, with nothing to persist. The emits fire here, at the real provisioning // points (novox/hq ADR 0046/0047); the module's events entrypoint (../index.ts) consumes them. import { runProvisioner, type Grant, type Credential } from "@novox/mesh-sdk/provisioner"; import { emit } from "@novox/mesh-sdk/events"; import { MinioClient, accessKeyFor, bucketFor } from "../client.js"; const minio = MinioClient.fromEnv(); runProvisioner("s3-bucket", { async create(grant: Grant): Promise { const bucket = bucketFor(grant.consumer); const accessKeyId = accessKeyFor(grant.consumer); if (!(await minio.bucketExists(bucket))) await minio.createBucket(bucket); // Re-mint the scoped key idempotently: drop any prior one under this id, then add fresh. try { await minio.removeAccessKey(accessKeyId); } catch { /* none yet — first provision */ } const key = await minio.createAccessKey(bucket, accessKeyId); await announce("module.minio.bucket.created", { bucket, consumer: grant.consumer, node: grant.node, accessKey: key.accessKey, // the secret is never put on the bus — only the credential file carries it endpoint: minio.baseUrl, }); return { fields: { endpoint: minio.baseUrl, bucket, accessKey: key.accessKey, secretKey: key.secretKey, region: minio.region, }, }; }, async remove(grant: Grant): Promise { const bucket = bucketFor(grant.consumer); const accessKeyId = accessKeyFor(grant.consumer); // 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(accessKeyId); } 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, consumer: grant.consumer, node: grant.node }); }, }); /** Emit best-effort: with no broker bound (a provisioner is not yet a runtime — novox/hq ADR 0052) * the event 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}`); } }