fix(db): remove invalid postgres startup parameter

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
camilafagundesandCursor committed 2026-10-23 16:20:06 -03:00
1 parent b410fa4559
commit 60f15a3576
3 files changed
+65 -8

No files matched your search

+9 -6
View File
@@ -170,10 +170,16 @@ export type PostgresClientOptions = {
idle_timeout: number; idle_timeout: number;
max_lifetime: number; max_lifetime: number;
ssl?: { rejectUnauthorized: true }; ssl?: { rejectUnauthorized: true };
connection?: { family: 4 };
}; };
/** Opções do driver. Em ambiente implantado o certificado é verificado sempre. */ /**
* Opções do driver. Em ambiente implantado o certificado é verificado sempre.
*
* `connection` no postgres.js é um parâmetro de startup, não uma opção do
* socket. Enviar `family=4` faz o PostgreSQL recusar a sessão com 42704
* (`unrecognized configuration parameter "family"`) antes da primeira
* consulta. O IPv4 continua garantido pelo host do Session Pooler.
*/
export function buildPostgresOptions(connectionString: string, purpose: DbPurpose, env: EnvMap = process.env): PostgresClientOptions { export function buildPostgresOptions(connectionString: string, purpose: DbPurpose, env: EnvMap = process.env): PostgresClientOptions {
const deployed = isDeployedEnv(env); const deployed = isDeployedEnv(env);
if (deployed) assertDeployedUrl(connectionString, purpose, env); if (deployed) assertDeployedUrl(connectionString, purpose, env);
@@ -185,10 +191,7 @@ export function buildPostgresOptions(connectionString: string, purpose: DbPurpos
idle_timeout: 20, idle_timeout: 20,
max_lifetime: 60 * 30, max_lifetime: 60 * 30,
}; };
if (deployed) { if (deployed) options.ssl = { rejectUnauthorized: true };
options.ssl = { rejectUnauthorized: true };
options.connection = { family: 4 };
}
return options; return options;
} }
+8 -1
View File
@@ -113,6 +113,13 @@ function postgresErrorCode(error: unknown): string | null {
return null; return null;
} }
/** Código do driver, sem a mensagem. A mensagem pode conter usuário, host ou senha. */
function unavailableIdentityCode(error: unknown): string {
const code = postgresErrorCode(error);
if (code && /^[A-Z0-9_]{2,48}$/.test(code)) return `identidade_indisponivel:${code}`;
return "identidade_indisponivel";
}
async function readRole(db: Db): Promise<FactRow> { async function readRole(db: Db): Promise<FactRow> {
try { try {
const rows = await db.withService((query) => const rows = await db.withService((query) =>
@@ -132,7 +139,7 @@ async function readRole(db: Db): Promise<FactRow> {
return facts; return facts;
} catch (error) { } catch (error) {
if (error instanceof DatabaseGuardError) throw error; if (error instanceof DatabaseGuardError) throw error;
throw new DatabaseGuardError("identidade_indisponivel"); throw new DatabaseGuardError(unavailableIdentityCode(error));
} }
} }
+48 -1
View File
@@ -5,6 +5,7 @@ import {
DatabaseGuardError, DatabaseGuardError,
assertDeployedUrl, assertDeployedUrl,
assertLocalDatabaseUrl, assertLocalDatabaseUrl,
buildPostgresOptions,
expectedPoolerHost, expectedPoolerHost,
resolvePoolMax, resolvePoolMax,
scrubDbError, scrubDbError,
@@ -12,7 +13,8 @@ import {
import { readFileSync, readdirSync } from "node:fs"; import { readFileSync, readdirSync } from "node:fs";
import path from "node:path"; import path from "node:path";
import { assertInitAuthorized, projectIdentityDefinitionSql, projectIdentityRevertSql } from "@/infra/db/project-identity"; import { assertInitAuthorized, projectIdentityDefinitionSql, projectIdentityRevertSql } from "@/infra/db/project-identity";
import { evaluateAdminIdentity, evaluateProjectIdentity, evaluateRuntimeRole, type AdminAttestation } from "@/infra/db/target-guard"; import { assertAdminTarget, evaluateAdminIdentity, evaluateProjectIdentity, evaluateRuntimeRole, type AdminAttestation } from "@/infra/db/target-guard";
import type { Db } from "@/infra/db/types";
const REF = "abcdefghijabcdefghij"; const REF = "abcdefghijabcdefghij";
const REF_COM_DIGITO = "abcd1234abcd1234wxyz"; const REF_COM_DIGITO = "abcd1234abcd1234wxyz";
@@ -165,6 +167,16 @@ describe("barreira do banco de homologação", () => {
expect(resolvePoolMax("web", env)).toBe(2); expect(resolvePoolMax("web", env)).toBe(2);
expect(resolvePoolMax("worker", env)).toBe(2); expect(resolvePoolMax("worker", env)).toBe(2);
expect(resolvePoolMax("admin", env)).toBe(1); expect(resolvePoolMax("admin", env)).toBe(1);
const deployed = buildPostgresOptions(adminUrl(), "admin", env);
const production = buildPostgresOptions(adminUrl(), "admin", { ...env, ADAPTER_ENV: "production" });
for (const options of [deployed, production]) {
expect(options.ssl).toEqual({ rejectUnauthorized: true });
expect(options).not.toHaveProperty("connection");
expect(JSON.stringify(options)).not.toContain("family");
}
expect(buildPostgresOptions("postgres://adaptador:local@127.0.0.1:5432/adaptador", "local", { ADAPTER_ENV: "development" })).not.toHaveProperty(
"connection",
);
}); });
it("recusa a credencial administrativa na web e BYPASSRLS no worker", () => { it("recusa a credencial administrativa na web e BYPASSRLS no worker", () => {
@@ -207,6 +219,41 @@ describe("barreira do banco de homologação", () => {
); );
}); });
it("classifica a falha de conexão pelo código do driver e omite a mensagem", async () => {
const db = {
exec: async () => undefined,
close: async () => undefined,
withUser: async () => {
throw new Error("nao");
},
withService: async () => {
throw Object.assign(new Error("postgres://postgres.ref:segredo@host/db family"), { code: "42704" });
},
} as Db;
await expect(assertAdminTarget(db, env)).rejects.toMatchObject({
code: "identidade_indisponivel:42704",
});
await expect(assertAdminTarget(db, env)).rejects.not.toThrow(/segredo/);
const semCodigo = {
...db,
withService: async () => {
throw new Error("postgres://postgres.ref:segredo@host/db");
},
} as Db;
await expect(assertAdminTarget(semCodigo, env)).rejects.toMatchObject({ code: "identidade_indisponivel" });
await expect(assertAdminTarget(semCodigo, env)).rejects.not.toThrow(/segredo/);
const codigoInseguro = {
...db,
withService: async () => {
throw Object.assign(new Error("falha"), { code: "postgres://segredo" });
},
} as Db;
await expect(assertAdminTarget(codigoInseguro, env)).rejects.toMatchObject({ code: "identidade_indisponivel" });
await expect(assertAdminTarget(codigoInseguro, env)).rejects.not.toThrow(/segredo/);
});
it("não deixa senha nem URL no erro visível", () => { it("não deixa senha nem URL no erro visível", () => {
const scrubbed = scrubDbError(new Error("falha em postgres://postgres.abc:segredo@aws-0-us-west-2.pooler.supabase.com:5432/postgres")); const scrubbed = scrubDbError(new Error("falha em postgres://postgres.abc:segredo@aws-0-us-west-2.pooler.supabase.com:5432/postgres"));
expect(scrubbed).not.toContain("segredo"); expect(scrubbed).not.toContain("segredo");