Confine the Postgres driver to bootstrap, bound plugin connections, and gate the storage DDL

This commit is contained in:
2026-08-19 00:08:35 +02:00
parent d2211cf75a
commit 060535c8ab
11 changed files with 313 additions and 129 deletions
+45
View File
@@ -0,0 +1,45 @@
// The connecting half of plugin storage: runs the DDL storage.ts plans. Imported by bootstrap
// alone — the only process holding superuser credentials, which is why the driver stops here.
import postgres from "postgres";
import { derivePassword, NAME_PREFIX, provisionSql, storageName } from "./storage.ts";
export interface ProvisionOptions {
adminUrl: string; // needs CREATEDB + CREATEROLE, not superuser
connectionLimit: number;
pluginIds: string[];
secret: string;
}
export interface ProvisionResult {
orphans: string[]; // provisioned once, but no installed plugin claims them any more
provisioned: string[];
}
// Idempotent, and it drops nothing: an uninstalled plugin keeps its data until an operator removes
// it deliberately. Orphans are reported rather than removed, so nobody has to guess they exist.
export async function provisionStorage(options: ProvisionOptions): Promise<ProvisionResult> {
const sql = postgres(options.adminUrl, { connect_timeout: 10, max: 1, onnotice: () => {} });
try {
const provisioned: string[] = [];
for (const pluginId of options.pluginIds) {
const name = storageName(pluginId);
const [role] = await sql`SELECT 1 FROM pg_roles WHERE rolname = ${name}`;
const [database] = await sql`SELECT 1 FROM pg_database WHERE datname = ${name}`;
const plan = provisionSql({
connectionLimit: options.connectionLimit,
databaseExists: database !== undefined,
name,
password: derivePassword(options.secret, pluginId),
roleExists: role !== undefined,
});
for (const statement of plan) await sql.unsafe(statement); // provisionSql quotes what it interpolates
provisioned.push(name);
}
const existing = await sql<{ datname: string }[]>`SELECT datname FROM pg_database WHERE starts_with(datname, ${NAME_PREFIX})`;
const orphans = existing.map((row) => row.datname).filter((name) => !provisioned.includes(name)).sort();
return { orphans, provisioned };
} finally {
await sql.end();
}
}