// The reconcile harness every provider shares. The loop below is identical for postgres, redis, // minio and umami: read the contributions the mesh delivered, bring each consumer's resource into // being through the provider's adapter under the login and password the mesh minted, and withdraw // what the mesh no longer asks for. A module writes ONLY the adapter — the per-service half — which // is why this lives in the sdk and the adapter lives in the module (novox/hq ADR 0044). // // **A provider creates the credential the mesh minted, and seals nothing (novox/hq ADR 0053).** The // control plane mints one password per consumer and seals it to this node; the host unseals it into // the file the contribution names. The provider does not generate a password, does not seal one, and // does not hand one back — the consumer already receives its copy through the mesh's own channel. // This is why the old $MESH_SEAL_KEY, the symmetric seal, and the *.grant.json / *.credential files // are gone: they described a second, contradictory credential path the mesh does not have. import { readFile } from "node:fs/promises"; /** One consumer's resource to bring into being — everything the mesh derived and delivered. */ export interface Provision { /** The login the mesh derived and gave the consumer to present. The provider creates exactly * this name — a name the consumer cannot learn is a name it cannot authenticate with. */ readonly as: string; /** The password the mesh minted for this consumer, read from the file the mesh sealed to this * node and the host unsealed. The provider sets it; it never invents one. */ readonly password: string; /** What the consumer contributed, per the interface's spec keys (e.g. `{ name: "umami" }`). */ readonly values: Readonly>; /** Where the consumer is, for a provider that must reach back to it. Usually unused. */ readonly at?: string; /** The consumer node, for logging and lifecycle events. */ readonly consumer?: string; } /** What a provider implements — the only per-service code. It is handed the login and password the * mesh made and brings the resource into being under them, or withdraws it. It returns nothing: * the credential is the mesh's, already delivered to the consumer. */ export interface Adapter { create(p: Provision): Promise; remove(p: { readonly as: string }): Promise; } export interface ProvisionerOptions { /** The contributions file the mesh writes for this resource (the module's `receives` path). * Defaults to $MESH_RECEIVES. */ receives?: string; /** Reconcile interval in ms. Defaults to 5000. */ everyMs?: number; } /** One entry in the mesh's contributions file: a consumer the provider must serve. */ interface Contribution { readonly as: string; readonly secret: string; readonly node?: string; readonly at?: string; readonly values?: Readonly>; } /** * Run the reconcile loop for one provided resource. Returns a stop function. Never throws for a * single bad contribution — it logs and keeps converging, because one consumer's failure must not * stop the others' provisioning. */ export function runProvisioner(resource: string, adapter: Adapter, opts: ProvisionerOptions = {}): () => void { const receives = opts.receives ?? envOrThrow("MESH_RECEIVES"); const everyMs = opts.everyMs ?? 5000; const applied = new Map(); // login (`as`) -> hash of what was last applied let stopped = false; async function reconcile(): Promise { const given = await readContributions(receives, resource); const wantByAs = new Map(given.map((g) => [g.as, g])); // Create or update every consumer whose login, password or values changed. for (const g of given) { let password: string; try { // The file the mesh sealed to this node, unsealed by the host into plaintext. Trailing // newline trimmed: a sealed value is exactly the secret, and a host writing a file may add // one. password = (await readFile(g.secret, "utf8")).replace(/\n$/, ""); } catch (err) { // The contribution names a secret the host has not written yet — a normal race on the // first pass. Skipped, retried on the next tick, never fatal. console.error(`[provisioner:${resource}] ${g.as}: secret not readable yet (${g.secret}): ${err}`); continue; } const h = hash(g.as, password, g.values ?? {}); if (applied.get(g.as) === h) continue; try { await adapter.create({ as: g.as, password, values: g.values ?? {}, at: g.at, consumer: g.node }); applied.set(g.as, h); } catch (err) { console.error(`[provisioner:${resource}] ${g.as}: create failed, will retry: ${err}`); } } // Withdraw every login the provider made that the mesh no longer asks for. The mesh drops a // consumer from the file when it goes away; that is how a login is withdrawn rather than kept // working for ever. Only logins this harness created are touched. for (const as of [...applied.keys()]) { if (wantByAs.has(as)) continue; try { await adapter.remove({ as }); applied.delete(as); } catch (err) { console.error(`[provisioner:${resource}] ${as}: remove failed, will retry: ${err}`); } } } const tick = async (): Promise => { if (stopped) return; try { await reconcile(); } catch (err) { console.error(`[provisioner:${resource}] reconcile error: ${err}`); } if (!stopped) setTimeout(() => void tick(), everyMs); }; void tick(); return () => { stopped = true; }; } /** * Read the mesh's contributions file for one resource. Absent or empty means "no consumer asks for * this" — the file is always written, so a provider can tell that from "the mesh never wrote it". */ async function readContributions(path: string, resource: string): Promise { let raw: string; try { raw = await readFile(path, "utf8"); } catch { return []; // not written yet, or nobody serves anything here — nothing to converge to } let doc: { requirement?: string; given?: Contribution[] }; try { doc = JSON.parse(raw) as { requirement?: string; given?: Contribution[] }; } catch (err) { console.error(`[provisioner:${resource}] contributions file is not JSON (${path}): ${err}`); return []; } if (doc.requirement && doc.requirement !== resource) { console.error(`[provisioner:${resource}] ${path} is for ${doc.requirement}, not ${resource}`); return []; } // A contribution with no `as` is not a credential grant (a module offering something on its own // machine) — the provisioner has nothing to create for it. return (doc.given ?? []).filter((g) => g.as && g.secret); } function hash(as: string, password: string, values: Readonly>): string { return JSON.stringify([as, password, values]); } function envOrThrow(name: string): string { const v = process.env[name]; if (!v) throw new Error(`${name} is not set — the provisioner cannot run without it`); return v; }