Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
d34f40a
test(kv): a getter of a stored value is read once, and its first answ…
jddunn Oct 17, 2026
4f4ebf1
fix(kv): write a value's text from the copy structuredClone returns, …
jddunn Oct 17, 2026
0c3b544
test(indexeddb): PRAGMA assignments through get() and all() with a co…
jddunn Oct 17, 2026
750a607
fix(indexeddb): read a PRAGMA statement's head as SQLite's tokenizer …
jddunn Oct 17, 2026
a549091
test(resolver): a postgres candidate tried after an IndexedDB candida…
jddunn Oct 17, 2026
9a6769a
fix(resolver): a postgres candidate opens in its own or the top-level…
jddunn Oct 17, 2026
ecb8919
test(exporter): pg_dump reaches what the pool reaches: pg's reading o…
jddunn Oct 17, 2026
9b06562
fix(exporter): hand pg_dump a libpq URI of the connection pg makes, a…
jddunn Oct 17, 2026
436810c
docs: the exporter reads the pool as pg does, its TLS and password ru…
jddunn Oct 17, 2026
b2e841d
chore(release): 0.7.16
jddunn Oct 17, 2026
bd204ae
test(exporter): empty passwords keep the environment's off pg_dump, T…
jddunn Oct 17, 2026
0dbf7e6
fix(exporter): an empty password stays empty for pg_dump, TLS follows…
jddunn Oct 17, 2026
661e8c6
test(exporter): the no-password spec never opens a pool connection, w…
jddunn Oct 17, 2026
9daa91e
Merge remote-tracking branch 'origin/master' into fix/review-0715
jddunn Oct 17, 2026
234d89c
chore(release): 0.7.17
jddunn Oct 17, 2026
38bfc66
test(exporter): a pool's password function is asked as pg asks it, on…
jddunn Oct 18, 2026
b89a9a2
fix(exporter): ask a pool's password function as pg asks it: as a met…
jddunn Oct 18, 2026
fceaed5
docs(exporter): how a pool's password function is called where the ex…
jddunn Oct 18, 2026
9609a6b
test(exporter): pg_dump checks a pool's server as pg does, with verif…
jddunn Oct 18, 2026
681bdf4
fix(exporter): have pg_dump check the server as pg does: verify-full,…
jddunn Oct 18, 2026
7a70338
test(exporter): libpq's TLS checks follow pg's own reading of a conne…
jddunn Oct 18, 2026
9fa1d8b
fix(exporter): refuse verify-ca with uselibpqcompat=true and no root …
jddunn Oct 18, 2026
4053b3f
docs(exporter): pg_dump checks the server as pg does: verify-full, or…
jddunn Oct 18, 2026
37a7cd4
Merge remote-tracking branch 'origin/master' into fix/review-0715
jddunn Oct 18, 2026
33ba557
docs(changelog): 0.7.17 releases the mutation sync (#65), which merge…
jddunn Oct 18, 2026
a2e9119
test(indexeddb): a PRAGMA assignment whose schema or name is written …
jddunn Oct 18, 2026
6f45608
Merge remote-tracking branch 'origin/master' into fix/review-0715
jddunn Oct 18, 2026
3593412
chore(release): 0.7.18
jddunn Oct 18, 2026
75024e0
fix(indexeddb): read a PRAGMA's schema or name in single quotes as SQ…
jddunn Oct 18, 2026
0cbd32f
test(exporter): a pool's password function is asked through the pool'…
jddunn Oct 18, 2026
85fa0d8
Merge remote-tracking branch 'origin/master' into fix/review-0715
jddunn Oct 18, 2026
f77bc9d
fix(exporter): ask a pool's password function through the pool's own …
jddunn Oct 18, 2026
c8be144
docs: the exporter asks a password function through the pool's own Cl…
jddunn Oct 18, 2026
78f4a3a
test(exporter): pg_dump gets sslnegotiation only where the pool's own…
jddunn Oct 18, 2026
2696297
Merge origin/master into fix/review-0715
jddunn Oct 21, 2026
b488a85
fix(exporter): pg_dump gets sslnegotiation as the pool's own pg reads…
jddunn Oct 21, 2026
66aa170
fix(exporter): pg_dump reads no password file where the pool's passwo…
jddunn Oct 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,37 @@ jobs:
- name: Install dependencies
run: npm install

# A PostgreSQL that offers TLS with a certificate made for this run, for
# localhost, which no public certificate authority signed, so that
# tests/postgres.exporter.tls.spec.ts can show pg_dump checking a server
# as pg does. The spec reads POSTGRES_TLS_TEST_URL and
# POSTGRES_TLS_ROOT_CERT. The server reads its key only when the
# postgres user (uid 999 in the image) owns it with mode 0600.
- name: Start a PostgreSQL that offers TLS
run: |
dir="$RUNNER_TEMP/pg-tls"
mkdir -p "$dir"
openssl req -new -x509 -days 2 -nodes -subj "/CN=localhost" \
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1" \
-keyout "$dir/server.key" -out "$dir/server.crt"
chmod 644 "$dir/server.crt"
chmod 600 "$dir/server.key"
sudo chown 999:999 "$dir/server.key"
docker run -d --name pg-tls -p 5433:5432 \
-e POSTGRES_USER=adapter -e POSTGRES_PASSWORD=adapter -e POSTGRES_DB=adapter_test \
-v "$dir:/tls:ro" postgres:16 \
-c ssl=on -c ssl_cert_file=/tls/server.crt -c ssl_key_file=/tls/server.key
for i in $(seq 1 90); do
if docker exec pg-tls pg_isready -U adapter -d adapter_test -h localhost >/dev/null 2>&1; then
echo "POSTGRES_TLS_TEST_URL=postgresql://adapter:adapter@localhost:5433/adapter_test" >> "$GITHUB_ENV"
echo "POSTGRES_TLS_ROOT_CERT=$dir/server.crt" >> "$GITHUB_ENV"
exit 0
fi
sleep 1
done
docker logs pg-tls
exit 1

- name: Lint
run: npm run lint || true

Expand Down
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,33 @@
## [Unreleased]

### Added
- `PostgresExporterOptions.unsetEnvironment` names libpq variables left out of `pg_dump`'s environment. The exporter of a PostgreSQL adapter leaves out `PGSSLNEGOTIATION`.

### Security
- `PostgresExporter` refuses, before `pg_dump` starts, a connection string whose password it cannot move to `PGPASSWORD`: a URI password that is not valid percent-encoding (`%zz`, a `%` at its end, bytes that are not UTF-8) or that decodes to a NUL, and a connection string that holds a NUL. Versions through 0.8.2 passed such a URI whole, its password in `--dbname`, where the process list shows it. The refusal never holds the connection string.
- `PostgresExporter` moves the `password` of keyword=value settings (`host=db.example password='a b'`) to `PGPASSWORD` too, read as libpq reads them: a value runs to whitespace or sits in single quotes, a backslash takes the character after it, and the later of two passwords wins. An empty password goes to `PGPASSWORD` empty, as libpq reads an empty setting, so the environment's `PGPASSWORD` is never sent in its place. Settings libpq refuses (an unterminated quote, a setting with no `=`) are refused before `pg_dump` starts. Versions through 0.8.2 passed them whole on the command line.
- The exporter of a PostgreSQL adapter leaves `sslpassword`, `oauth_client_secret` and the SCRAM keys of a pool's connection string off `pg_dump`'s command line, where libpq takes them alone: pg reads none of them.
- The exporter of a PostgreSQL adapter has `pg_dump` check the server as the pool's pg does. pg's TLS is Node's, which checks the server's certificate and its host name, and versions through 0.8.2 wrote `sslmode=require` for it, with which libpq checks neither without a root certificate file: an export connected to a server whose certificate the pool refuses. `pg_dump` gets `sslmode=verify-full`, or `verify-ca` where pg checks no host name (`uselibpqcompat=true` with `verify-ca`, or `require` beside a root certificate), against the root certificate file the connection string names, else the file `PGSSLROOTCERT` names, else the system's certificate authorities (`sslrootcert=system`, which libpq reads from PostgreSQL 16 on; with an older libpq the export fails, saying what to do). `uselibpqcompat=true` with `sslmode=verify-ca` and no root certificate is refused before `pg_dump` starts, as pg refuses it.

### Changed
- `new PostgresExporter(connectionString)` takes a function that gives the connection string, or a promise of it, when an export runs.

### Fixed
- `createStorageFeatures(adapter).exporter` on PostgreSQL reads the connection of the adapter's pool as pg reads it, and hands `pg_dump` a libpq URI of it, so `pg_dump` reaches the server, the database and the user the pool reaches:
- A connection string pg and libpq read apart: `postgresql://db.example?application_name=x@y` names the host `db.example` to pg and `y` to libpq, and `pg_dump` connected to `y`. An `@` in a URL's password, a host left out, `ssl=1`, `sslmode=no-verify` and pg's own parameters are written as libpq reads them.
- A pool made without a host, a port, a user or a database: `PGHOST`, `PGPORT`, `PGUSER` and `PGDATABASE`, then localhost, 5432, the operating system's user and the user's name for the database, as pg fills them. Versions through 0.8.2 wrote localhost and 5432 for a pool that named no host or port, and refused one that named no database. The adapter's own settings keep the localhost and 5432 of the pool it makes.
- A pool whose password is a function: the function is asked when the export runs, as the pool asks it for each connection: through the `Client` the pool makes its connections with, which pg-pool keeps as the pool's `Client`, whose own routine calls it as a method of a client made of the pool's settings, with no arguments before pg 8.19.0 and from 8.19.0 with the client's `ConnectionParameters` (node-postgres #3602). So a pool made with another copy of pg than the one this library loads, or with a `Client` of its own, is asked as its own pg asks; a pool whose `Client` has no such routine (pg's native one) is asked as the pg this library loads asks. The client is never connected and is made without the settings' `stream`. Its answer goes to `PGPASSWORD`. A function that answers no password gives `pg_dump` an empty `PGPASSWORD` and a `passfile` that does not exist: pg then connects with no password and reads neither `PGPASSWORD` nor a password file, so neither the environment's password nor `~/.pgpass`, which libpq reads for an empty password, is sent. A function that fails, or answers anything but a string or `undefined`, refuses the export. Versions through 0.8.2 left the function's password out, so `pg_dump` logged in with `PGPASSWORD`, or not at all.
- A pool whose `ssl` holds `ca`, `cert`, `key`, `pfx` or another TLS option is refused, naming the options and what to do instead: `pg_dump` reads TLS material from files alone. Versions through 0.8.2 wrote `sslmode=require` in its place, so `pg_dump` connected without the client certificate, or checked the server against no certificate authority.
- TLS follows pg's. A pool that checks no certificate (`ssl: { rejectUnauthorized: false }`, pg's `no-verify`) gets `sslmode=require` over any `PGSSLMODE`: a `PGSSLMODE` of `verify-ca` or `verify-full` refused the certificate pg accepts. A pool that chose no TLS (`ssl: false`, `ssl=0`, `sslmode=disable`) gets `sslmode=disable`, where libpq's `prefer` or `allow` would use TLS. Where no TLS is pg's default, libpq's default stays, and an empty `PGSSLMODE` or one libpq does not know, which pg reads as no TLS and libpq refuses, gives way to `sslmode=disable`. A certificate file in the connection string turns TLS on over `ssl=0`, and an `sslmode` libpq does not know means TLS, as pg reads them.
- pg takes the database from the URL's path alone and drops a `database` parameter, and reads an empty `host` parameter as none; the URI holds that reading (pg-connection-string, `index.js`). libpq's own variables, which pg does not read (`PGHOSTADDR`, `PGSERVICE`, `PGREQUIRESSL`, `PGSSLROOTCERT`), configure `pg_dump` as they configure psql.
- The port is read as pg reads it, with `parseInt` after the setting, `PGPORT` and 5432: a port of `5432x` is 5432, which libpq refused as no number, and a port of 0 gives way to `PGPORT`, where versions through 0.8.2 wrote 0. A port pg reads no number in is refused before `pg_dump` starts, since the pool makes no connection with it.
- Of a connection string's parameters the URI holds the ones pg reads and libpq takes: `options`, `application_name`, `fallback_application_name`, `client_encoding`, `replication` and the certificate files.
- `sslnegotiation` reaches `pg_dump` as the pool's own pg reads it. pg reads it from 8.22.0 (the connection string's over the settings', an empty one as none, then `PGSSLNEGOTIATION`) and with `direct` starts TLS at once; pg 8.13.1 to 8.21.x read none and start TLS with an SSLRequest. The exporter reads it on a client of the pool's `Client`, never connected, so a pool whose pg reads none gives `pg_dump` none, and TLS for `direct` only where that pg's parse of the connection string turns TLS on for it (pg-connection-string 2.14.0 and later). `pg_dump` runs without `PGSSLNEGOTIATION`, which libpq 17 and later read: the URI holds what the pool's pg reads. A value pg refuses (one but `direct` or `postgres`, or `direct` without TLS) refuses the export, and where `pg_dump`'s libpq is older than 17, which does not know `sslnegotiation`, the error says to run a pg_dump of PostgreSQL 17 or later. Versions through 0.8.2 handed `pg_dump` the string's `sslnegotiation` and turned TLS on for `direct` whatever the pool's pg read, so an export failed against a server the pool reached: one that offers no TLS, or one older than PostgreSQL 17, which takes no direct TLS. libpq's others, which pg does not read (`passfile`, `target_session_attrs`, `require_auth`, `channel_binding`, `gssencmode`, `connect_timeout`, `sslcrl` and the rest), are left out: given them, `pg_dump` read another password file than the pool, or refused a server, an authentication or a session the pool takes.
- `resolveStorageAdapter()` and `createDatabase()` open a PostgreSQL candidate in the segment of its own options or the top-level one alone. Tried after an IndexedDB or Capacitor candidate whose options named a segment, it took that segment: on a pool of the caller's the adapter threw, and on a connection string the server got a schema only the browser's options asked for. A local candidate still opens in it.
- IndexedDB adapter: a PRAGMA assignment through `all()` or `get()` with a comment between its words (`PRAGMA user_version /* note */ = 7`), with a quoted name and no space before it (`PRAGMA"user_version"=9`), or with its schema or name in single quotes (`PRAGMA 'user_version' = 10`), which SQLite takes for a name there, is saved; the statement is read as SQLite reads it. Versions through 0.8.2 found no assignment in it, so it was stored neither with `durability: 'commit'` nor by `flush()` or `close()`.
- `createSqlKeyValueStore()` writes a value's text from the copy the runtime's `structuredClone` returns, so each property is read once, as IndexedDB's put reads it, and the value is copied once. Versions through 0.8.2 read each property a second time for the text: a getter's second answer was stored, and a value whose getter answered a function the second time was refused.
- `PostgresExporter` runs `pg_dump` with `--no-password`. Without a password, to a server that asks for one, `pg_dump` asked for it on the terminal, or with no terminal read its standard input, which the export leaves open, and the export never ended.

## [0.8.2] - 2026-10-21

### Fixed
Expand Down
Loading
Loading