// mongodb's admin client — mongodb's own code, living in the module (novox/hq ADR 0039). Both this // module's tools and its provisioner import it, and nothing outside mongodb does. // // **The backend's own driver, inside the bundle** (novox/hq ADR 0198 §4). This used to shell out to // `mongosh`, which the module's container installed from MongoDB's apt repository; the module's code // now runs in the node's runtime, on machines whose system carries no mongosh, so it speaks to the // server through the official `mongodb` driver its package.json names — installed and inlined into // the bundle by the builder. One connection per call, as one mongosh invocation was: the module is // called rarely, and a pool held open across calls would hold a credential the mesh may rotate. import { randomBytes } from "node:crypto"; import { readFileSync } from "node:fs"; import { MongoClient as Driver, MongoServerError, BSON, type Document } from "mongodb"; export interface DatabaseInfo { readonly name: string; readonly sizeBytes: number; } export interface MongoConn { readonly host: string; readonly port: number; readonly user: string; readonly password: string; /** The database the admin user authenticates against — `admin` for the root user. */ readonly authSource: string; } export class MongoClient { constructor(private readonly conn: MongoConn) {} /** * Build from the module's resolved environment. Reads MESH_MONGODB_* first (the documented names), * falling back to the MESH_PROVISION_* keys the manifest already sets on the provisioner container. * Throws if it cannot find a host and an admin password. */ static fromEnv(env: NodeJS.ProcessEnv = process.env): MongoClient { const url = env.MESH_PROVISION_MONGODB ? safeUrl(env.MESH_PROVISION_MONGODB) : undefined; const host = env.MESH_MONGODB_HOST ?? url?.hostname; const port = Number(env.MESH_MONGODB_PORT ?? url?.port ?? "27017") || 27017; const user = env.MESH_MONGODB_USER ?? (url?.username ? decodeURIComponent(url.username) : "root"); const authSource = env.MESH_MONGODB_AUTHSOURCE ?? url?.searchParams.get("authSource") ?? "admin"; const password = env.MESH_MONGODB_PASSWORD ?? readSecretFile(env.MESH_PROVISION_PASSWORD_FILE); if (!host || !password) { throw new Error("mongodb host or admin password is not set — mongodb's own code cannot reach the server"); } return new MongoClient({ host, port, user, password, authSource }); } get host(): string { return this.conn.host; } get port(): number { return this.conn.port; } /** The admin connection URI, credentials percent-encoded. */ private uri(): string { const u = encodeURIComponent(this.conn.user); const p = encodeURIComponent(this.conn.password); const a = encodeURIComponent(this.conn.authSource); return `mongodb://${u}:${p}@${this.conn.host}:${this.conn.port}/?authSource=${a}&directConnection=true`; } /** * The one execution boundary: connect as the administrator, do `work`, and close — a failure to * connect or to authenticate rejects here rather than returning a partial success. */ private async admin(work: (client: Driver) => Promise): Promise { const client = new Driver(this.uri(), { serverSelectionTimeoutMS: 10_000 }); try { await client.connect(); return await work(client); } finally { await client.close(); } } /** * Create a login user and the database it owns, idempotently. The user is created inside the * target database with the `dbOwner` role scoped to that database, so the consumer owns exactly * its own and authenticates with the target database as its authSource. Re-running updates the * password and roles, so a rotated credential converges. */ async createDatabaseAndUser(database: string, user: string, password: string): Promise { await this.admin(async (client) => { const target = client.db(database); const roles = [{ role: "dbOwner", db: database }]; const found = await target.command({ usersInfo: user }); if (Array.isArray(found.users) && found.users.length > 0) { await target.command({ updateUser: user, pwd: password, roles }); } else { await target.command({ createUser: user, pwd: password, roles }); } }); } /** * Whether `user` authenticates against `database` with exactly `password` and holds `dbOwner` * there: checked by connecting as the consumer, the way it connects. Read-only. `false` only on an * authentication failure or a missing role; an unreachable server rejects (novox/hq issue 120). */ async canAuthenticateAs(database: string, user: string, password: string): Promise { // Credentials as options, never in a URI, so the consumer's password is in no message a failed // connection prints. const client = new Driver(`mongodb://${this.conn.host}:${this.conn.port}/?directConnection=true`, { auth: { username: user, password }, authSource: database, serverSelectionTimeoutMS: 10_000, }); try { await client.connect(); const status = await client.db(database).command({ connectionStatus: 1 }); const roles = (status.authInfo?.authenticatedUserRoles ?? []) as { role: string; db: string }[]; return roles.some((r) => r.role === "dbOwner" && r.db === database); } catch (err) { if (isAuthFailure(err)) return false; throw new Error(`mongodb could not check ${user}: ${String((err as Error).message).split("\n")[0]}`); } finally { await client.close(); } } /** Drop a database and its owning user, idempotently. Dropping the database evicts its data; the * user is removed first so a re-grant of the same login starts clean. */ async dropDatabaseAndUser(database: string, user: string): Promise { await this.admin(async (client) => { const target = client.db(database); try { await target.command({ dropUser: user }); } catch (err) { if (!(err instanceof MongoServerError && err.code === 11)) throw err; // 11: UserNotFound } await target.dropDatabase(); }); } /** List the databases on the server, with on-disk size, for the mongodb_list_databases tool. */ async listDatabases(): Promise { const res = await this.admin((client) => client.db("admin").admin().listDatabases()); return (res.databases ?? []) .map((d) => ({ name: String(d.name), sizeBytes: Number(d.sizeOnDisk ?? 0) })) .sort((a, b) => a.name.localeCompare(b.name)); } /** * Run a read-only `find` against a collection in a named database, for the mongodb_query tool. * `find` mutates nothing; the limit is capped so a tool call cannot stream an unbounded result. * Documents come back as relaxed Extended JSON — an ObjectId as `{"$oid": …}` — exactly as the * shell's `EJSON.stringify` rendered them before. */ async find( database: string, collection: string, filter: Readonly>, limit: number, ): Promise[]> { const capped = Math.max(1, Math.min(limit, 1000)); const docs = await this.admin((client) => client .db(database) .collection(collection) .find(BSON.EJSON.deserialize(filter as Document, { relaxed: true }) as Document) .limit(capped) .toArray(), ); return BSON.EJSON.serialize(docs, { relaxed: true }) as Record[]; } } /** An authentication failure, as the server or the driver reports it. */ function isAuthFailure(err: unknown): boolean { if (err instanceof MongoServerError && err.code === 18) return true; // 18: AuthenticationFailed return /Authentication failed|AuthenticationFailed/i.test(String((err as Error)?.message ?? "")); } /** Generate a URL-safe password. */ export function generatePassword(): string { return randomBytes(24).toString("base64url"); } function readSecretFile(path: string | undefined): string | undefined { if (!path) return undefined; try { return readFileSync(path, "utf8").trim(); } catch { return undefined; } } function safeUrl(raw: string): URL | undefined { try { return new URL(raw); } catch { return undefined; } }