issue-281: use the DATABASE_URL password for postgres connections #284

Merged
david merged 1 commit from bug/issue-281/database-url-password-silently-ignored into main 2026-09-28 07:07:24 +00:00
Owner

Summary

Fixes the postgres extension silently ignoring the password in DATABASE_URL, which made every connection fail with the misleading SASL: SCRAM-SERVER-FIRST-MESSAGE: client password must be a string whenever ~/.pgpass had no matching entry.

Root cause

  1. parseConnUrl() returned only {host, port, database, user} — the URL's password was dropped.
  2. The pool was built with a passwordProvider option, which pg does not support, so it was silently dropped.
  3. The only remaining password source was pg's own deprecated ~/.pgpass lookup (removed in pg@9), which yields undefined on a miss → SCRAM throws client password must be a string.

A second latent bug: the pgpass call used a 2-arg (err, password) callback, but pgpass calls its callback with a single argument — so a found password would be treated as an error and a miss would resolve to "".

The fix

  • env.ts — ConnInfo gains password?: string; parseConnUrl() returns password: parsed.password || undefined (a password-less URL normalises to undefined so it still triggers the .pgpass fallback).
  • index.ts — two small, injectable, exported helpers:
    • resolvePassword(conn, pgpassFn?) — the URL's password is authoritative; a password-less URL does an explicit ~/.pgpass lookup; a miss resolves to undefined (never "").
    • buildPoolConfig(conn, pgpassFn?) — builds the pg.Pool config with password as a function (the form pg's deprecation notice recommends). Because it's a function, pg never touches its deprecated built-in pgpass path.
    • The factory is now just new pg.Pool(buildPoolConfig(connInfo)).

Acceptance criteria (issue-281)

  • ✅ Complete DATABASE_URL, no matching ~/.pgpass → all tools connect (URL password is a real string reaching SCRAM).
  • ✅ Password-less DATABASE_URL + matching ~/.pgpass → unchanged (explicit lookup).
  • ✅ A genuinely wrong password now yields a clear password authentication failed, not client password must be a string.
  • ✅ No behaviour depends on pg's deprecated built-in pgpass path (the password is always a function, so pg uses the typeof this.password === 'function' branch and never its internal require('pgpass') fallback).

Verification

  • bun test → 53 pass, 0 fail (21 new assertions across env.test.ts and index.test.ts, with a fake injectable pgpass).
  • tsc --noEmit --strict introduces no new type errors vs. the original (only the 3 pre-existing ones: untyped pg/pgpass imports and the pre-existing db_query_one signature).
  • Runtime sanity: a real pg.Pool constructed from buildPoolConfig() output accepts the function-valued password without throwing (connects lazily, so pgpass is only invoked at connect time).

Files

File Change
extensions/postgres/env.ts ConnInfo.password?: string; parseConnUrl() carries parsed.password || undefined
extensions/postgres/index.ts Added PgpassLookup, resolvePassword(), buildPoolConfig(); factory now new pg.Pool(buildPoolConfig(connInfo)); removed the invalid passwordProvider
extensions/postgres/env.test.ts +4 tests: password carried/normalised in parseConnUrl and preserved in extractConnInfo
extensions/postgres/index.test.ts +7 tests: resolvePassword (URL authoritative, pgpass fallback, miss→undefined, no user/db skip) and buildPoolConfig (config handed to pg)
extensions/postgres/README.md Documented password-resolution order + manual integration test

Closes #281

## Summary Fixes the postgres extension silently ignoring the password in `DATABASE_URL`, which made **every** connection fail with the misleading `SASL: SCRAM-SERVER-FIRST-MESSAGE: client password must be a string` whenever `~/.pgpass` had no matching entry. ### Root cause 1. `parseConnUrl()` returned only `{host, port, database, user}` — the URL's password was dropped. 2. The pool was built with a `passwordProvider` option, which `pg` does not support, so it was silently dropped. 3. The only remaining password source was `pg`'s own **deprecated** `~/.pgpass` lookup (removed in `pg@9`), which yields `undefined` on a miss → SCRAM throws `client password must be a string`. A second latent bug: the `pgpass` call used a 2-arg `(err, password)` callback, but `pgpass` calls its callback with a single argument — so a *found* password would be treated as an error and a *miss* would resolve to `""`. ### The fix - **`env.ts`** — `ConnInfo` gains `password?: string`; `parseConnUrl()` returns `password: parsed.password || undefined` (a password-less URL normalises to `undefined` so it still triggers the `.pgpass` fallback). - **`index.ts`** — two small, injectable, exported helpers: - `resolvePassword(conn, pgpassFn?)` — the URL's password is **authoritative**; a password-less URL does an **explicit** `~/.pgpass` lookup; a miss resolves to `undefined` (never `""`). - `buildPoolConfig(conn, pgpassFn?)` — builds the `pg.Pool` config with `password` as a **function** (the form `pg`'s deprecation notice recommends). Because it's a function, `pg` never touches its deprecated built-in `pgpass` path. - The factory is now just `new pg.Pool(buildPoolConfig(connInfo))`. ### Acceptance criteria (issue-281) - ✅ Complete `DATABASE_URL`, no matching `~/.pgpass` → all tools connect (URL password is a real string reaching SCRAM). - ✅ Password-less `DATABASE_URL` + matching `~/.pgpass` → unchanged (explicit lookup). - ✅ A genuinely wrong password now yields a clear `password authentication failed`, not `client password must be a string`. - ✅ No behaviour depends on `pg`'s deprecated built-in `pgpass` path (the `password` is always a function, so `pg` uses the `typeof this.password === 'function'` branch and never its internal `require('pgpass')` fallback). ### Verification - `bun test` → **53 pass, 0 fail** (21 new assertions across `env.test.ts` and `index.test.ts`, with a fake injectable `pgpass`). - `tsc --noEmit --strict` introduces **no new** type errors vs. the original (only the 3 pre-existing ones: untyped `pg`/`pgpass` imports and the pre-existing `db_query_one` signature). - Runtime sanity: a real `pg.Pool` constructed from `buildPoolConfig()` output accepts the function-valued `password` without throwing (connects lazily, so `pgpass` is only invoked at connect time). ## Files | File | Change | |------|--------| | `extensions/postgres/env.ts` | `ConnInfo.password?: string`; `parseConnUrl()` carries `parsed.password \|\| undefined` | | `extensions/postgres/index.ts` | Added `PgpassLookup`, `resolvePassword()`, `buildPoolConfig()`; factory now `new pg.Pool(buildPoolConfig(connInfo))`; removed the invalid `passwordProvider` | | `extensions/postgres/env.test.ts` | +4 tests: password carried/normalised in `parseConnUrl` and preserved in `extractConnInfo` | | `extensions/postgres/index.test.ts` | +7 tests: `resolvePassword` (URL authoritative, pgpass fallback, miss→`undefined`, no user/db skip) and `buildPoolConfig` (config handed to `pg`) | | `extensions/postgres/README.md` | Documented password-resolution order + manual integration test | Closes #281
david merged commit c5c45dfa79 into main 2026-09-28 07:07:24 +00:00
david deleted branch bug/issue-281/database-url-password-silently-ignored 2026-09-28 07:07:24 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
david/pi-extensions-and-skills!284
No description provided.