// keycloak's provisioner — the adapter that makes keycloak a provider of the mesh `oidc-client` // interface. 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 Keycloak creates, checks and removes a // consumer's client (novox/hq ADR 0039/0040/0048). What a client is, and which ones are the mesh's, // is in ../oidc.ts. // // The `oidc-client` interface: a consumer logs people in through the realm this module serves, as // the confidential client `as` with the secret the mesh minted, and is redirected back to the // callback it contributed under the names the mesh composed for its endpoint. What it is served — // the issuer and the endpoint paths under it — is in the manifest's `serves`, settled with the // assignment's settings. // // **The realm is read out of the issuer**, the one value an assignment sets (settings reach both the // served facts and this module's config.json): a realm set in one place and an issuer in another // would let the consumer be told one realm while its client is made in another. import { runProvisioner, type Provision } from "@novox/mesh-sdk/provisioner"; import { emit } from "@novox/mesh-sdk/events"; import { readFileSync } from "node:fs"; import { KeycloakClient } from "../client.js"; import { OidcClients, realmOf } from "../oidc.js"; /** The issuer this assignment serves, from the settings-merged config the mesh delivers. */ function issuer(): string { const file = process.env.MESH_KEYCLOAK_CONFIG_FILE; let cfg: Record = {}; if (file) { try { cfg = JSON.parse(readFileSync(file, "utf8")) as Record; } catch { // Absent or unreadable: fall through to the environment, and refuse below if that is empty too. } } const said = typeof cfg.issuer === "string" ? cfg.issuer : process.env.MESH_KEYCLOAK_ISSUER; if (!said) throw new Error("no issuer — the module's config.json carries none and MESH_KEYCLOAK_ISSUER is unset"); return said; } const clients = new OidcClients(KeycloakClient.fromEnv(), realmOf(issuer())); /** Emit a lifecycle event without letting a broker hiccup fail the provisioning itself. */ async function announce(type: string, body: Record): Promise { try { await emit(type, body); } catch (err) { console.error(`[provisioner:oidc-client] emit ${type} failed: ${err}`); } } runProvisioner("oidc-client", { async create(p: Provision): Promise { const done = await clients.ensure(p); if (done === "created") { console.log(`[provisioner:oidc-client] created client ${p.as} in realm ${clients.realm}`); await announce("client.created", { realm: clients.realm, clientId: p.as, consumer: p.consumer ?? "" }); } }, async remove(p: { as: string }): Promise { const done = await clients.remove(p.as); if (done === "not ours") { console.error(`[provisioner:oidc-client] ${p.as}: a client of that id exists that the mesh did not make — left alone`); } else if (done === "removed") { console.log(`[provisioner:oidc-client] removed client ${p.as} from realm ${clients.realm}`); } }, // Asked every minute by the harness: whether Keycloak still holds this consumer's client exactly as // the mesh gave it, so a client deleted or edited behind the mesh's back is made again (hq issue 120). async holds(p: Provision): Promise { return clients.holds(p); }, });