From 059051649a68d08d8a822a817ae4f7734eeec341 Mon Sep 17 00:00:00 2001 From: Dotta Date: Sun, 17 May 2026 18:11:53 -0500 Subject: [PATCH] Fail closed for public tenant DB startup Co-Authored-By: Paperclip --- .../server-startup-feedback-export.test.ts | 29 +++++++++++++++++++ server/src/index.ts | 26 +++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/server/src/__tests__/server-startup-feedback-export.test.ts b/server/src/__tests__/server-startup-feedback-export.test.ts index 0cf1e664eeb..c288b6e6322 100644 --- a/server/src/__tests__/server-startup-feedback-export.test.ts +++ b/server/src/__tests__/server-startup-feedback-export.test.ts @@ -216,6 +216,35 @@ describe("startServer feedback export wiring", () => { serverPort: 3210, }); }); + + it("refuses authenticated public startup without an external database URL", async () => { + loadConfigMock.mockReturnValue(buildTestConfig({ + deploymentExposure: "public", + authBaseUrlMode: "explicit", + authPublicBaseUrl: "https://tenant.example.com", + databaseMode: "embedded-postgres", + databaseUrl: undefined, + })); + + await expect(startServer()).rejects.toThrow( + "authenticated public deployments require DATABASE_URL or config.database.connectionString", + ); + expect(createDbMock).not.toHaveBeenCalled(); + }); + + it("refuses authenticated public startup when DATABASE_URL is not a postgres URL", async () => { + loadConfigMock.mockReturnValue(buildTestConfig({ + deploymentExposure: "public", + authBaseUrlMode: "explicit", + authPublicBaseUrl: "https://tenant.example.com", + databaseUrl: "secret://paperclip-cloud/stacks/alpha/database/runtime-url", + })); + + await expect(startServer()).rejects.toThrow( + "authenticated public deployments require DATABASE_URL to be a postgres/postgresql connection string", + ); + expect(createDbMock).not.toHaveBeenCalled(); + }); }); describe("startServer authenticated auth origin setup", () => { diff --git a/server/src/index.ts b/server/src/index.ts index 105f23ecaf7..dd831c3bc62 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -187,6 +187,31 @@ export async function startServer(): Promise { return normalized === "127.0.0.1" || normalized === "localhost" || normalized === "::1"; } + function isPostgresConnectionString(connectionString: string): boolean { + try { + const parsed = new URL(connectionString); + return parsed.protocol === "postgres:" || parsed.protocol === "postgresql:"; + } catch { + return false; + } + } + + function assertCloudDatabaseContract(): void { + if (config.deploymentMode !== "authenticated" || config.deploymentExposure !== "public") { + return; + } + if (!config.databaseUrl) { + throw new Error( + "authenticated public deployments require DATABASE_URL or config.database.connectionString; refusing embedded PostgreSQL fallback", + ); + } + if (!isPostgresConnectionString(config.databaseUrl)) { + throw new Error( + "authenticated public deployments require DATABASE_URL to be a postgres/postgresql connection string", + ); + } + } + function rewriteLocalUrlPort(rawUrl: string | undefined, port: number): string | undefined { if (!rawUrl) return undefined; try { @@ -270,6 +295,7 @@ export async function startServer(): Promise { let startupDbInfo: | { mode: "external-postgres"; connectionString: string } | { mode: "embedded-postgres"; dataDir: string; port: number }; + assertCloudDatabaseContract(); if (config.databaseUrl) { const migrationUrl = config.databaseMigrationUrl ?? config.databaseUrl; migrationSummary = await ensureMigrations(migrationUrl, "PostgreSQL");