diff --git a/markets-migrate.mjs b/markets-migrate.mjs index 9d04dff..22f1db3 100644 --- a/markets-migrate.mjs +++ b/markets-migrate.mjs @@ -3,6 +3,7 @@ import process from "node:process" import { Client } from "pg" const databaseUrl = process.env.DATABASE_URL?.trim() +const LEGACY_SSL_MODES = new Set(["prefer", "require", "verify-ca"]) const MARKET_STORAGE_SCHEMA_SQL = ` CREATE TABLE IF NOT EXISTS symbol_directory ( symbol text PRIMARY KEY, @@ -151,13 +152,41 @@ ALTER TABLE IF EXISTS portfolio_holdings DROP CONSTRAINT IF EXISTS "portfolio_holdings_userId_fkey"; ` +function normalizeDatabaseConnectionString(connectionString) { + const normalizedConnectionString = connectionString.trim() + if (!normalizedConnectionString) { + return normalizedConnectionString + } + + let connectionUrl + + try { + connectionUrl = new URL(normalizedConnectionString) + } catch { + return normalizedConnectionString + } + + const useLibpqCompat = connectionUrl.searchParams.get("uselibpqcompat") + if (useLibpqCompat?.toLowerCase() === "true") { + return normalizedConnectionString + } + + const sslMode = connectionUrl.searchParams.get("sslmode")?.toLowerCase() + if (!sslMode || !LEGACY_SSL_MODES.has(sslMode)) { + return normalizedConnectionString + } + + connectionUrl.searchParams.set("sslmode", "verify-full") + return connectionUrl.toString() +} + if (!databaseUrl) { console.error("Missing DATABASE_URL.") process.exit(1) } const client = new Client({ - connectionString: databaseUrl, + connectionString: normalizeDatabaseConnectionString(databaseUrl), }) await client.connect() diff --git a/src/lib/server/__tests__/postgres.test.ts b/src/lib/server/__tests__/postgres.test.ts new file mode 100644 index 0000000..9c2006e --- /dev/null +++ b/src/lib/server/__tests__/postgres.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from "vitest" + +import { normalizeDatabaseConnectionString } from "../postgres" + +describe("normalizeDatabaseConnectionString", () => { + it("upgrades legacy sslmode=require to verify-full", () => { + expect( + normalizeDatabaseConnectionString( + "postgresql://user:pass@example.com/db?sslmode=require" + ) + ).toBe( + "postgresql://user:pass@example.com/db?sslmode=verify-full" + ) + }) + + it("keeps connection strings with explicit libpq compatibility", () => { + expect( + normalizeDatabaseConnectionString( + "postgresql://user:pass@example.com/db?uselibpqcompat=true&sslmode=require" + ) + ).toBe( + "postgresql://user:pass@example.com/db?uselibpqcompat=true&sslmode=require" + ) + }) + + it("leaves non-legacy sslmode values unchanged", () => { + expect( + normalizeDatabaseConnectionString( + "postgresql://user:pass@example.com/db?sslmode=verify-full" + ) + ).toBe( + "postgresql://user:pass@example.com/db?sslmode=verify-full" + ) + }) + + it("leaves connection strings without sslmode unchanged", () => { + expect( + normalizeDatabaseConnectionString( + "postgresql://user:pass@example.com/db" + ) + ).toBe("postgresql://user:pass@example.com/db") + }) +}) diff --git a/src/lib/server/postgres.ts b/src/lib/server/postgres.ts index 819f046..3d060ba 100644 --- a/src/lib/server/postgres.ts +++ b/src/lib/server/postgres.ts @@ -15,6 +15,8 @@ type DatabaseUrlEnvName = | typeof DATABASE_URL_ENV_NAME | typeof AUTH_DATABASE_URL_ENV_NAME +const LEGACY_SSL_MODES = new Set(["prefer", "require", "verify-ca"]) + function getConfiguredDatabaseUrl(name: DatabaseUrlEnvName): string | null { const databaseUrl = process.env[name]?.trim() if (!databaseUrl) { @@ -38,9 +40,41 @@ function getRequiredDatabaseUrl(name: DatabaseUrlEnvName): string { return databaseUrl } +export function normalizeDatabaseConnectionString( + connectionString: string +): string { + const normalizedConnectionString = connectionString.trim() + if (!normalizedConnectionString) { + return normalizedConnectionString + } + + let connectionUrl: URL + + try { + connectionUrl = new URL(normalizedConnectionString) + } catch { + return normalizedConnectionString + } + + const useLibpqCompat = connectionUrl.searchParams.get("uselibpqcompat") + if (useLibpqCompat?.toLowerCase() === "true") { + return normalizedConnectionString + } + + const sslMode = connectionUrl.searchParams.get("sslmode")?.toLowerCase() + if (!sslMode || !LEGACY_SSL_MODES.has(sslMode)) { + return normalizedConnectionString + } + + // pg currently treats these legacy modes as verify-full. Make that explicit + // so builds and migrations keep the same behavior without the warning. + connectionUrl.searchParams.set("sslmode", "verify-full") + return connectionUrl.toString() +} + function createPool(connectionString: string) { return new Pool({ - connectionString, + connectionString: normalizeDatabaseConnectionString(connectionString), }) } diff --git a/threads-migrate.mjs b/threads-migrate.mjs index fad556a..0974c77 100644 --- a/threads-migrate.mjs +++ b/threads-migrate.mjs @@ -3,6 +3,7 @@ import process from "node:process" import { Client } from "pg" const databaseUrl = process.env.DATABASE_URL?.trim() +const LEGACY_SSL_MODES = new Set(["prefer", "require", "verify-ca"]) const THREAD_STORAGE_SCHEMA_SQL = ` CREATE TABLE IF NOT EXISTS thread ( "userId" text NOT NULL, @@ -32,13 +33,41 @@ ALTER TABLE IF EXISTS thread DROP CONSTRAINT IF EXISTS "thread_userId_fkey"; ` +function normalizeDatabaseConnectionString(connectionString) { + const normalizedConnectionString = connectionString.trim() + if (!normalizedConnectionString) { + return normalizedConnectionString + } + + let connectionUrl + + try { + connectionUrl = new URL(normalizedConnectionString) + } catch { + return normalizedConnectionString + } + + const useLibpqCompat = connectionUrl.searchParams.get("uselibpqcompat") + if (useLibpqCompat?.toLowerCase() === "true") { + return normalizedConnectionString + } + + const sslMode = connectionUrl.searchParams.get("sslmode")?.toLowerCase() + if (!sslMode || !LEGACY_SSL_MODES.has(sslMode)) { + return normalizedConnectionString + } + + connectionUrl.searchParams.set("sslmode", "verify-full") + return connectionUrl.toString() +} + if (!databaseUrl) { console.error("Missing DATABASE_URL.") process.exit(1) } const client = new Client({ - connectionString: databaseUrl, + connectionString: normalizeDatabaseConnectionString(databaseUrl), }) await client.connect()