Skip to content

v1.57.0 - One connection's password, rotated on its own

Choose a tag to compare

@CarlLee1983 CarlLee1983 released this 14 Aug 04:38
· 458 commits to main since this release
78dcbce

Added

  • dbcli password [connection] — rotating one connection's password no longer means editing the rest of its config. A connection whose password is rotated on a schedule previously had two options: re-run init and re-enter every other field, or hand-edit the env file and hope the key name matched what the reader looks for. The new command changes the password and nothing else. Where the value lands is read from the config rather than derived from a naming rule: password: { "$env": "NAME" } rewrites NAME in that connection's envFile, and a connection still holding a literal password is converted to { "$env": "DBCLI_<CONN>_PASSWORD" } once, so every later rotation touches only the env file. v1 configs rewrite DBCLI_PASSWORD in .env.local, matching the v1 reader at src/core/config.ts:200; a v1 config whose password comes from any other environment variable is refused with the reason, because v1 has no per-connection env file and no file dbcli could write would make that variable resolve. The new password is verified by connecting with it before anything is written — a rejected credential exits 1 with the stored value untouched, rather than leaving a config that no longer opens the database — with --skip-test for when the database is unreachable from where the rotation runs. Three input paths, one of which must be chosen: a masked prompt (never a plain-text fallback, since that would print the secret into the scrollback), --stdin for rotation scripts, and --password for callers that accept shell-history exposure. The env file is written 0600 on POSIX (Windows has no equivalent mode bit, so the file inherits the directory's ACL), the value never reaches stdout, stderr, or the audit log, and DBCLI_AGENT_MODE=1 refuses at the first line of the action rather than after prompting and connecting.

Fixed

  • A password written to a connection that declared no envFile could not be read back. loadConnectionEnv (src/core/config-v2.ts:164) loads only the file a connection names, and the .env.local fallback in src/core/config.ts:274 recognizes exactly one key, v1's DBCLI_PASSWORD. So writing DBCLI_<CONN>_PASSWORD into .env.local for a v2 connection with no envFile produced a config whose $env reference resolved to nothing: Environment variable not defined, on every subsequent command. Rotation now records envFile on the connection as part of the write. tests/unit/core/connection-credential.test.ts asserts the round trip through configModule.read() for all four paths (v2 with and without envFile, v2 converted from a literal, v1) rather than asserting the file merely contains the expected line — the file being right while the reader cannot see it is precisely the failure that shipped otherwise.

  • A password containing $&, $1, or $` was silently stored as something else. The in-place rewrite passed the new line as String.replace's second argument, where $-patterns are substitution syntax: rotating to a$&b over an existing K=old wrote K=aK=oldb. Verification passed because it used the in-memory value, so the command reported success and the next connection attempt failed with a password nobody could reproduce. The replacement is a function now, which does no $ expansion. Values are also written quoted (NAME="…"), because both env parsers trim the whole line before splitting — an unquoted value silently lost leading and trailing whitespace. parseEnvPassword strips one layer of matching quotes to match parseEnvContent, so values written by either path read back identically.

Removed

  • writeConnectionSecret — the exported helper wrote to a file the reader never opens. Exported from @carllee1983/dbcli/core, it derived the env var name from the connection name and defaulted the file to .env.<connection> when a connection declared no envFile, while the reader falls back to .env.local and, for connections created with init --use-env-refs --env-password <VAR>, looks up <VAR> rather than the derived name. Both mismatches ended the same way: a write that succeeded and a password that could not be read. setConnectionPassword and resolvePasswordTarget replace it on the same barrel — they resolve the target from the config, convert a literal password once, and record envFile when it is missing. Callers of the old function should switch to setConnectionPassword(projectPath, connectionName, value); the field parameter is gone, since 'password' was its only accepted value.