db_query: the DATABASE_URL password is silently ignored, so any connection fails with SASL: client password must be a string unless ~/.pgpass happens to have a matching entry #281
Labels
No labels
bug
chore
documentation
enhancement
feature
ready
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
david/pi-extensions-and-skills#281
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
The postgres extension never uses the password in
DATABASE_URL.parseConnUrl()deliberately returns only{ host, port, database, user }, and the pool is built with apasswordProvideroption thatpgdoes not support, so it is silently ignored. The only remaining source of a password ispg's built-in~/.pgpasslookup — which means a perfectly validDATABASE_URLfails to connect whenever~/.pgpasshas no entry for that host/user.The resulting error names SCRAM, not the missing credential, so the failure looks like a bad password rather than an unread password:
Impact
DATABASE_URLwhose host/user/database triple is absent from~/.pgpasscannot be used at all. Switching from a remote dev DB to a local one is enough to break the tool with no configuration change on the extension side.pg'spgpasssupport is deprecated and removed inpg@9(node_modules/pg/lib/client.js:23-27), so on apgmajor upgrade the extension loses password resolution entirely, including the.pgpasspath that currently works by accident.db_query,db_list_tables,db_table_schema. The extension loads, logs a success line, and only fails at first query, which makes it look like a server-side problem.Environment
pi-extensions-and-skills@1a73f36(2026-09-18), branchmainpg8.23.0,pgpass1.0.6 (package.jsondeclarespg: ^8.13.0,pgpass: ^1.0.6).envofprocess.cwd()(the project pi is launched in)DATABASE_URL=postgresql://postgres:***@localhost:5432/shoppy_devSteps to reproduce
.envcontaining a complete URL, e.g.:DATABASE_URL=postgresql://postgres:secret@localhost:5432/shoppy_dev~/.pgpasshas no line matchinglocalhost:5432:shoppy_dev:postgres. (Confirmed by an existing~/.pgpassthat only lists a different host, e.g.10.1.1.243:5432:shoppy_dev:shoppy_dev:….)db_querywithSELECT 1.SASL: SCRAM-SERVER-FIRST-MESSAGE: client password must be a string.Adding
localhost:5432:shoppy_dev:postgres:secretto~/.pgpassmakes the identical call succeed — which isolates the cause to password resolution, not to the URL, the server, or the credentials.Expected
The password from
DATABASE_URLis used for the connection.~/.pgpassremains a fallback for password-less URLs and for deployments that deliberately keep the secret out of.env.Actual
The URL password is discarded before the pool is constructed,
passwordProvideris ignored,pgfinds no.pgpassentry, and the password staysundefined.Root cause
The password is dropped during URL parsing.
extensions/postgres/env.ts:parseConnUrl()mapsURL→ exactly those four fields (parsed.hostname,parsed.port,parsed.pathname,parsed.username). There is nopasswordfield anywhere in the file (grep -n password env.ts→ no matches).The intended fallback is not a
pgoption.extensions/postgres/index.ts:131-143builds the pool with:passwordProviderdoes not exist inpg8.23.0 — nothing innode_modules/pg/lib/orpg-pool/references it — so it is dropped on the floor and the closure never runs.pgthen supplies its ownpgpasslookup, and gives up silently on a miss.node_modules/pg/lib/client.js:296-310:With no matching
.pgpassline,passisundefined, no password is set, and the SCRAM handshake throwsclient password must be a string. Note the file's own comment on the supported form:i.e. the supported option is
password, notpasswordProvider.Proposed fix
The two changes together make the URL authoritative and keep
.pgpassas a genuine fallback:env.ts— carry the password through. Addpassword?: stringtoConnInfo, returnpassword: parsed.password || undefinedfromparseConnUrl()(normalising""toundefinedso password-less URLs still fall back), and include it inextractConnInfo().index.ts— use the supported option. Either letconnInfo.passwordflow into the pool and delete the provider entirely (relying onpg's own.pgpassfallback when the URL has no password), or rename the option topasswordas the deprecation notice instructs. If the explicitpgpasscall is kept, a no-match must resolve tonull/undefinedrather than""— an empty string is a valid string to SCRAM and produces a different, equally confusingpassword authentication failederror.Worth deciding explicitly whether
ConnInfoomitting the password is a deliberate security boundary. As it stands it is not one: the extension already reads the same secret out of.envvialoadEnvFile(), so dropping it after parsing only breaks the connection without protecting anything. Fix 1 above assumes that is true; if the omission is intentional, then thepgpassfallback is the load-bearing path and must work forpg@9— which its deprecation rules out.Workaround (no code change)
Add a matching line to
~/.pgpass(mode600):This works today but leaves the extension dependent on a deprecated
pgfeature.Acceptance criteria
DATABASE_URLand no matching~/.pgpassentry, all three tools connect and return results.DATABASE_URLand a matching~/.pgpassentry, behaviour is unchanged.client password must be a string.pg's deprecated built-inpgpasspath.Test plan
extensions/postgres/env.test.ts—parseConnUrl()carries the password; an empty password normalises toundefined;extractConnInfo()preserves it.extensions/postgres/index.test.ts— the pool/client config passed topgcontains the resolved password (fakepgmodule), and a password-less URL still falls back rather than passing"".~/.pgpassstates above against a local database.