import { spawnSync } from "node:child_process"; import { chmodSync, existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { createRequire } from "node:module"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; const chartDir = join(dirname(fileURLToPath(import.meta.url)), ".."); const repoRoot = join(chartDir, "..", "..", ".."); const denDbDir = join(repoRoot, "ee", "packages", "den-db"); const denDbPackage = join(denDbDir, "package.json"); const denDbRequire = createRequire(denDbPackage); const mysql = denDbRequire("mysql2/promise"); const bootstrapPath = join(denDbDir, "dist", "scripts", "bootstrap.js"); const currentSchemaPath = join(denDbDir, "dist", "current-schema.sql"); const denDbIndexPath = join(denDbDir, "dist", "index.js"); const image = process.env.OPENWORK_EVAL_MYSQL_IMAGE?.trim() || "mysql:8.4"; const containerName = `openwork-custom-ca-mysql-${process.pid}-${Date.now()}`; const tmp = mkdtempSync(join(tmpdir(), "openwork-custom-ca-mysql-")); const certsDir = join(tmp, "certs"); const caKey = join(certsDir, "ca.key"); const caCert = join(certsDir, "ca.crt"); const serverKey = join(certsDir, "server.key"); const serverCsr = join(certsDir, "server.csr"); const serverCert = join(certsDir, "server.crt"); const opensslConfig = join(tmp, "openssl.cnf"); const mysqlConfig = join(tmp, "tls.cnf"); const databaseName = "openwork_den"; const databaseUser = "openwork"; const databasePassword = "openwork_tls_test_password"; let containerStarted = false; function run(command, args, options = {}) { return spawnSync(command, args, { cwd: repoRoot, encoding: "utf8", maxBuffer: 20 * 1024 * 1024, timeout: 180_000, ...options, }); } function ensureSuccess(result, description) { if (result.status !== 0) { throw new Error(`${description} failed\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`); } } function runOpenSsl(args) { ensureSuccess(run("openssl", args), `openssl ${args.join(" ")}`); } function ensureBuildArtifacts() { if (existsSync(bootstrapPath) && existsSync(currentSchemaPath) && existsSync(denDbIndexPath)) { return; } const result = run("pnpm", ["--dir", denDbDir, "run", "build"], { timeout: 240_000 }); ensureSuccess(result, "pnpm --dir ee/packages/den-db run build"); console.log("Built @openwork-ee/den-db migration artifacts"); } function generateCertificates() { mkdirSync(certsDir, { recursive: true }); writeFileSync( opensslConfig, `[req] distinguished_name = dn prompt = no req_extensions = v3_req [dn] CN = localhost [v3_req] basicConstraints = CA:FALSE keyUsage = digitalSignature,keyEncipherment extendedKeyUsage = serverAuth subjectAltName = @alt_names [alt_names] DNS.1 = localhost DNS.2 = mysql.private.test IP.1 = 127.0.0.1 `, ); runOpenSsl(["req", "-x509", "-newkey", "rsa:2048", "-nodes", "-days", "1", "-subj", "/CN=OpenWork Test MySQL CA", "-keyout", caKey, "-out", caCert]); runOpenSsl(["req", "-newkey", "rsa:2048", "-nodes", "-keyout", serverKey, "-out", serverCsr, "-config", opensslConfig]); runOpenSsl(["x509", "-req", "-in", serverCsr, "-CA", caCert, "-CAkey", caKey, "-CAcreateserial", "-out", serverCert, "-days", "1", "-extensions", "v3_req", "-extfile", opensslConfig]); chmodSync(serverKey, 0o644); chmodSync(serverCert, 0o644); chmodSync(caCert, 0o644); writeFileSync( mysqlConfig, `[mysqld] ssl-ca=/etc/mysql/certs/ca.crt ssl-cert=/etc/mysql/certs/server.crt ssl-key=/etc/mysql/certs/server.key require_secure_transport=ON tls_version=TLSv1.2,TLSv1.3 `, ); chmodSync(mysqlConfig, 0o644); console.log("Generated private CA and MySQL server certificate with SAN DNS:localhost, DNS:mysql.private.test, IP:127.0.0.1"); } function docker(args, options = {}) { return run("docker", args, { timeout: 240_000, ...options }); } function startMysqlContainer() { const result = docker([ "run", "--rm", "--detach", "--name", containerName, "--publish", "127.0.0.1::3306", "--env", "MYSQL_ROOT_PASSWORD=openwork_root_password", "--env", "MYSQL_ROOT_HOST=%", "--env", `MYSQL_DATABASE=${databaseName}`, "--env", `MYSQL_USER=${databaseUser}`, "--env", `MYSQL_PASSWORD=${databasePassword}`, "--volume", `${certsDir}:/etc/mysql/certs:ro`, "--volume", `${mysqlConfig}:/etc/mysql/conf.d/tls.cnf:ro`, image, ]); ensureSuccess(result, `docker run ${image}`); containerStarted = true; console.log(`Started TLS-enabled MySQL container ${containerName} from ${image}`); } function mappedPort() { const result = docker(["port", containerName, "3306/tcp"]); ensureSuccess(result, "docker port"); const endpoint = result.stdout.trim().split("\n").find(Boolean); const port = endpoint?.split(":").pop(); if (!port) { throw new Error(`Unable to resolve mapped MySQL port from: ${result.stdout}`); } return Number(port); } async function waitForMysql(port) { const started = Date.now(); let lastError = ""; while (Date.now() - started < 90_000) { try { const connection = await mysql.createConnection({ host: "127.0.0.1", port, user: databaseUser, password: databasePassword, database: databaseName, ssl: { rejectUnauthorized: false }, }); await connection.query("select 1"); await connection.end(); return; } catch (error) { lastError = error instanceof Error ? error.message : String(error); await new Promise((resolve) => setTimeout(resolve, 750)); } } const logs = docker(["logs", containerName], { timeout: 30_000 }); throw new Error(`MySQL container did not become ready: ${lastError}\n${logs.stdout}\n${logs.stderr}`); } function strictDatabaseUrl(port) { return `mysql://${databaseUser}:${databasePassword}@127.0.0.1:${port}/${databaseName}?sslmode=verify-full`; } function redactedDatabaseUrl(port) { return `mysql://${databaseUser}:REDACTED@127.0.0.1:${port}/${databaseName}?sslmode=verify-full`; } function childEnv(extra = {}) { const env = { ...process.env, ...extra }; delete env.NODE_TLS_REJECT_UNAUTHORIZED; return env; } function runOpenWorkMysqlQuery(databaseUrl, query, extraEnv = {}) { const source = ` import { createRequire } from "node:module"; const mysql = createRequire(process.argv[1])("mysql2/promise"); const { parseMySqlConnectionConfig } = await import(process.argv[2]); const connection = await mysql.createConnection(parseMySqlConnectionConfig(process.argv[3])); try { const [rows] = await connection.query(process.argv[4]); console.log(JSON.stringify(rows)); } finally { await connection.end(); } `; return run(process.execPath, ["--input-type=module", "-e", source, denDbPackage, pathToFileURL(denDbIndexPath).href, databaseUrl, query], { env: childEnv(extraEnv), timeout: 90_000, }); } function runBootstrap(databaseUrl) { return run(process.execPath, [bootstrapPath], { cwd: denDbDir, env: childEnv({ DATABASE_URL: databaseUrl, DEN_DB_ENCRYPTION_KEY: "openwork-custom-ca-mysql-tls-test-key-1234567890", NODE_EXTRA_CA_CERTS: caCert, OPENWORK_DEN_DB_ENV_PATH: join(tmp, "does-not-exist.env"), }), timeout: 180_000, }); } function cleanup() { if (containerStarted) { docker(["rm", "-f", containerName], { timeout: 30_000 }); } rmSync(tmp, { recursive: true, force: true }); } try { ensureBuildArtifacts(); generateCertificates(); startMysqlContainer(); const port = mappedPort(); await waitForMysql(port); const databaseUrl = strictDatabaseUrl(port); console.log(`TLS MySQL endpoint ready at ${redactedDatabaseUrl(port)}`); const withoutCa = runOpenWorkMysqlQuery(databaseUrl, "select 1 as ok"); if (withoutCa.status === 0) { throw new Error("Strict OpenWork/mysql2 unexpectedly connected without NODE_EXTRA_CA_CERTS"); } const withoutCaError = `${withoutCa.stdout}\n${withoutCa.stderr}`.trim(); if (!/certificate|verify|self-signed|unable/i.test(withoutCaError)) { throw new Error(`Strict OpenWork/mysql2 failed for a non-certificate reason:\n${withoutCaError}`); } const certificateError = withoutCaError.split("\n").find((line) => /certificate|verify|self-signed|unable/i.test(line)) ?? withoutCaError.split("\n").find(Boolean); console.log(`Strict OpenWork/mysql2 without custom CA: rejected (${certificateError})`); const bootstrap = runBootstrap(databaseUrl); ensureSuccess(bootstrap, "OpenWork migration bootstrap with NODE_EXTRA_CA_CERTS"); console.log("OpenWork migration bootstrap completed with NODE_EXTRA_CA_CERTS and strict DATABASE_URL"); console.log(bootstrap.stdout.trim()); const query = runOpenWorkMysqlQuery( databaseUrl, "select (select count(*) from information_schema.tables where table_schema = database()) as table_count, (select count(*) from `__drizzle_migrations`) as migration_count", { NODE_EXTRA_CA_CERTS: caCert }, ); ensureSuccess(query, "OpenWork/mysql2 strict post-migration query with NODE_EXTRA_CA_CERTS"); const rows = JSON.parse(query.stdout.trim()); const tableCount = Number(rows[0]?.table_count ?? 0); const migrationCount = Number(rows[0]?.migration_count ?? 0); if (tableCount <= 0 || migrationCount <= 0) { throw new Error(`Expected migrated OpenWork tables and migration ledger rows, got ${query.stdout}`); } console.log(`OpenWork/mysql2 strict query succeeded after migration: tables=${tableCount}, migrations=${migrationCount}`); } finally { cleanup(); }