From f75e6b0877f770b7fe619cd98eaa200c020208b6 Mon Sep 17 00:00:00 2001 From: Theodore Song Date: Wed, 19 Aug 2026 08:29:48 -0400 Subject: [PATCH] Expose safe shared-state diagnostics --- api/_db.js | 24 ++++++++++++++++++++++++ api/state.js | 18 +++++++++++++++--- scripts/test-server-state.mjs | 8 +++++++- 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/api/_db.js b/api/_db.js index 9d51fea..eaba870 100644 --- a/api/_db.js +++ b/api/_db.js @@ -27,6 +27,30 @@ export function databaseUrl() { return databaseUrls()[0] || ""; } +export function databaseConnectionDiagnostics() { + const candidates = databaseUrls(); + return { + candidates: candidates.length, + urls: candidates.map(value => { + try { + const parsed = new URL(value); + return { + valid_postgres_url: /^postgres(?:ql)?:$/i.test(parsed.protocol), + neon_host: /(?:^|\.)neon\.tech$/i.test(parsed.hostname), + pooled_host: /-pooler(?:\.|$)/i.test(parsed.hostname), + has_username: Boolean(parsed.username), + has_password: Boolean(parsed.password), + has_database_name: parsed.pathname.length > 1, + sslmode: parsed.searchParams.get("sslmode") || "missing", + channel_binding: parsed.searchParams.get("channel_binding") || "missing", + }; + } catch { + return { valid_postgres_url: false }; + } + }), + }; +} + export function hasDatabase() { return Boolean(databaseUrl()); } diff --git a/api/state.js b/api/state.js index d3eb0ea..3c49157 100644 --- a/api/state.js +++ b/api/state.js @@ -1,5 +1,5 @@ import { get, list, put } from "@vercel/blob"; -import { hasDatabase, readSharedAppState, writeSharedAppState } from "./_db.js"; +import { databaseConnectionDiagnostics, hasDatabase, readSharedAppState, writeSharedAppState } from "./_db.js"; const STATE_PATH = process.env.PMA_STATE_PATH || "shared/state.json"; const STATE_VERSION_PREFIX = process.env.PMA_STATE_VERSION_PREFIX || "shared/state-versions/"; @@ -34,6 +34,16 @@ export function providerErrorCode(error) { return "unavailable"; } +function providerErrorMetadata(error) { + const name = String(error && error.name || "Error"); + const code = String(error && (error.code || error.cause && error.cause.code) || "").toUpperCase(); + return { + status: providerErrorCode(error), + error_class: /^[A-Za-z][A-Za-z0-9_]{0,39}$/.test(name) ? name : "Error", + error_code: /^[A-Z0-9_]{1,32}$/.test(code) ? code : "none", + }; +} + async function readJsonBlob() { let databaseAvailable = false; let databaseError = null; @@ -66,8 +76,10 @@ async function readJsonBlob() { if (primaryError && !databaseAvailable) { const error = new Error("Shared state providers are unavailable"); error.providers = { - database: hasDatabase() ? providerErrorCode(databaseError) : "not_configured", - blob: providerErrorCode(primaryError), + database: hasDatabase() + ? { ...providerErrorMetadata(databaseError), ...databaseConnectionDiagnostics() } + : { status: "not_configured", candidates: 0, urls: [] }, + blob: providerErrorMetadata(primaryError), }; throw error; } diff --git a/scripts/test-server-state.mjs b/scripts/test-server-state.mjs index c3b8c91..e578ffc 100644 --- a/scripts/test-server-state.mjs +++ b/scripts/test-server-state.mjs @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import { databaseUrl, databaseUrls, normalizeDatabaseUrl } from "../api/_db.js"; +import { databaseConnectionDiagnostics, databaseUrl, databaseUrls, normalizeDatabaseUrl } from "../api/_db.js"; import { compactAgentState, compactSuggestion, providerErrorCode } from "../api/state.js"; assert.equal(normalizeDatabaseUrl("psql 'postgresql://user:pass@example.test/db?sslmode=require'"), @@ -16,6 +16,12 @@ assert.equal(databaseUrl(), process.env.NEON_DATABASE_URL); assert.deepEqual(databaseUrls(), [process.env.NEON_DATABASE_URL]); process.env.DATABASE_URL = "postgresql://stale:pass@example.test/db"; assert.deepEqual(databaseUrls(), [process.env.DATABASE_URL, process.env.NEON_DATABASE_URL]); +const diagnostics = databaseConnectionDiagnostics(); +assert.equal(diagnostics.candidates, 2); +assert.equal(diagnostics.urls[0].valid_postgres_url, true); +assert.equal(diagnostics.urls[0].has_username, true); +assert.equal(diagnostics.urls[0].has_password, true); +assert.equal(diagnostics.urls[0].has_database_name, true); if (originalDatabaseUrl === undefined) delete process.env.DATABASE_URL; else process.env.DATABASE_URL = originalDatabaseUrl; if (originalNeonUrl === undefined) delete process.env.NEON_DATABASE_URL;