Startup database failure names the input and a code, not the URL's parts

The entrypoint harness showed pg printing a piece of the database URL file
on a failed startup: `getaddrinfo ENOTFOUND <host>` for an unresolvable host.
A password with an unescaped delimiter moves part of itself into the host or
the database name, and the resolver or server error then prints it. The only
thing keeping a malformed URL out of stderr was pg dropping Node's `input`
property (Soul S5).

createStore now turns a schema-preparation failure into one error naming
GC_ACCESS_DATABASE_URL_FILE and the error code, with no cause. The harness
cases expect that message, and a new case puts the canary in the host.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-30 14:46:22 +02:00
commit 376dfbd2c2
2 changed files with 23 additions and 5 deletions

View file

@ -14,7 +14,18 @@ export async function createStore(config: HeimdallConfig): Promise<HeimdallStore
: new InMemoryStore();
if (config.storage.applySchemaOnStartup) {
await store.ensureSchema();
try {
await store.ensureSchema();
} catch (error) {
// pg's and Node's errors name the host, the database or the URL itself,
// and any of those can hold part of a secret that was bound or escaped
// wrongly. Startup prints this error, so it names only the input and a
// code, and carries no cause.
const code = (error as { code?: unknown }).code;
throw new Error(
`Postgres storage could not be prepared (${typeof code === "string" ? code : "error"}); check GC_ACCESS_DATABASE_URL_FILE.`
);
}
}
return store;

View file

@ -112,22 +112,29 @@ const cases: Case[] = [
{
name: "a database URL with an unusable port",
env: () => ({ GC_ACCESS_DATABASE_URL_FILE: file("db-port", `postgres://heimdall:${CANARY}port@db:notaport/heimdall\n`) }),
expect: /./,
expect: /Postgres storage could not be prepared \(ERR_INVALID_URL\)/,
},
{
name: "a database URL that does not parse",
env: () => ({ GC_ACCESS_DATABASE_URL_FILE: file("db-parse", `postgres://heimdall:${CANARY} parse@[bad/heimdall\n`) }),
expect: /./,
expect: /Postgres storage could not be prepared \(E[A-Z_]+\)/,
},
{
name: "a database URL with a broken percent escape",
env: () => ({ GC_ACCESS_DATABASE_URL_FILE: file("db-percent", `postgres://heimdall:${CANARY}%zz@127.0.0.1:1/heimdall\n`) }),
expect: /./,
expect: /Postgres storage could not be prepared \(ECONNREFUSED\)/,
},
{
name: "a database that refuses the connection",
env: () => ({ GC_ACCESS_DATABASE_URL_FILE: file("db-refused", `postgres://heimdall:${CANARY}refused@127.0.0.1:1/heimdall\n`) }),
expect: /ECONNREFUSED/,
expect: /Postgres storage could not be prepared \(ECONNREFUSED\)/,
},
{
// A password holding an unescaped delimiter moves part of it into the
// host, and the resolver's error names the host.
name: "a database host that does not resolve",
env: () => ({ GC_ACCESS_DATABASE_URL_FILE: file("db-host", `postgres://heimdall:pw@${CANARY}host.invalid/heimdall\n`) }),
expect: /Postgres storage could not be prepared \(E[A-Z_]+\)/,
},
{
name: "private key text bound to the signing key path",