Repository navigation
v1.57.0 - One connection's password, rotated on its own
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-runinitand 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" }rewritesNAMEin that connection'senvFile, 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 rewriteDBCLI_PASSWORDin.env.local, matching the v1 reader atsrc/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-testfor 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),--stdinfor rotation scripts, and--passwordfor callers that accept shell-history exposure. The env file is written0600on 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, andDBCLI_AGENT_MODE=1refuses at the first line of the action rather than after prompting and connecting.
Fixed
-
A password written to a connection that declared no
envFilecould not be read back.loadConnectionEnv(src/core/config-v2.ts:164) loads only the file a connection names, and the.env.localfallback insrc/core/config.ts:274recognizes exactly one key, v1'sDBCLI_PASSWORD. So writingDBCLI_<CONN>_PASSWORDinto.env.localfor a v2 connection with noenvFileproduced a config whose$envreference resolved to nothing:Environment variable not defined, on every subsequent command. Rotation now recordsenvFileon the connection as part of the write.tests/unit/core/connection-credential.test.tsasserts the round trip throughconfigModule.read()for all four paths (v2 with and withoutenvFile, 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 asString.replace's second argument, where$-patterns are substitution syntax: rotating toa$&bover an existingK=oldwroteK=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.parseEnvPasswordstrips one layer of matching quotes to matchparseEnvContent, 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 noenvFile, while the reader falls back to.env.localand, for connections created withinit --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.setConnectionPasswordandresolvePasswordTargetreplace it on the same barrel — they resolve the target from the config, convert a literal password once, and recordenvFilewhen it is missing. Callers of the old function should switch tosetConnectionPassword(projectPath, connectionName, value); thefieldparameter is gone, since'password'was its only accepted value.