Skip to content

A bound external.credentialsRef is silently dropped on the DSN branches of the mysql and mongodb driver arms #8696

Description

@qq9340100

Blocked-by: #8876

Status (PM, domain:services, 2026-08-15): the mysql arm is LANDED; this card is open for the mongodb arm only.
PR #8875 (Part of, merged) closed the mysql half — buildMysqlConnection's DSN branch now returns { uri, password: spec.secret }. The mongodb half is blocked on #8876 (a userinfo-username accessor in packages/spec, beside the existing urlUserinfoPassword); hand-rolling one here is the shape #8082's ruling rejects by name. The reasoning is also recorded in-code, in the ⚠️ KNOWN OPEN HALF (#8696) docblock above buildMongoUrl.
⚠️Two premises in the text below are now measured FALSE — read this before acting on them:

  1. "The postgres arm does honour it on the DSN branch (:389)"false. pg merges parse(connectionString)over the explicit config, so the injected password resolves to the DSN's own: effective password: null on a credential-free DSN (pg 8.22.0). Filed as A bound external.credentialsRef reaches the knex config but NOT the server on the postgres DSN branch — pg overrides it with the connection string #8873. The two clients merge in opposite directions, so ⛔ the remaining arms must not be fixed by symmetry with mysql.
  2. "What a fix has to decide — merge or refuse?"not an open fork. packages/spec already declares injection on all three URL-bearing SQL arms ("bind the secret … and it is injected at connect time"), so refusing would contradict a published contract, require a domain:spec change, and reject the exact configuration Migration story for stored cleartext datasource credentials: operator-initiated re-homing into sys_secret (blocked by the managed-schemaMode refusal) #8155's landed migration instructs operators to write. Also measured: honouring the secret on the mongo arm needs no URL rewrite — MongoClient's auth injects beside an unmodified url and wins over an embedded userinfo password.

Found while implementing #8155 (credential re-homing), and filed rather than fixed there — it is a defect in the producer (the driver factory), independent of that card.

What happens

DatasourceConnectionService resolves external.credentialsRef to a cleartext secret and hands it to the factory as spec.secret. Two arms of createDefaultDatasourceDriverFactory then throw it away when the datasource's config carries a connection URL:

  • buildMysqlConnectionpackages/services/service-datasource/src/default-datasource-driver-factory.ts:442if (url) return url;. The DSN string is returned as the whole knex connection; spec.secret is never applied. (FIXED by PR fix(service-datasource): a bound credentialsRef reaches the mysql client on the DSN branch (#8696) #8875.)
  • buildMongoUrl — same file, :542if (explicit) return explicit;. The authored config.url is returned verbatim; spec.secret is never applied. (STILL OPEN — this card's remaining scope.)

The postgres arm does honour it on the DSN branch (:389, { connectionString: url, ...(spec.secret ? { password: spec.secret } : {}) }), and turso honours it too (buildTursoDriverConfig's authToken reader prefers spec.secret, #8152). So the behaviour is decided by which driver the operator picked, which is exactly the asymmetry #7314/#7385/#8152 have each closed one arm at a time. ⚠️ See the status block above: the postgres claim in this paragraph is measured false — #8873.

Why it is not merely cosmetic

The connect path is fail-closed on a ref it cannot resolve, so an operator reasonably reads "the datasource connected" as "the bound credential was used". On these two arms with a URL present, it was not: the connection is opened with whatever the URL itself carries, i.e. no credential at all once the URL is credential-free — which is the shape #8082 now requires authored URLs to be. A mysql or mongodb datasource that binds its secret and writes a clean user@host URL therefore connects unauthenticated, or fails with a driver-level auth error that names nothing about the binding.

This is the declared-≠-enforced shape of Prime Directive #10 one layer down: external.credentialsRef is declared, resolved, injected — and then dropped at the last call site, with no diagnostic.

Why #8155 could not fix it there

#8155's migration refuses any row whose credential is embedded in a URL, and this asymmetry is the measured reason for that refusal: extracting such a password and binding it would leave these two arms connecting with nothing. Making URL-bearing rows migratable is gated on this being fixed first — a consumer-side accommodation in the migration would have been the tolerant fallback Prime Directive #12 rejects.

What a fix has to decide

⚠️ Superseded — see the status block above. This section is kept for the record, not as guidance.

  1. Merge or refuse? For mysql, knex's mysql2 dialect accepts either a DSN string or an object — supplying both a DSN and a discrete password means parsing the URL, which is a behaviour change for existing rows. For mongo, the credential belongs inside the URL's userinfo, so honouring spec.secret means rewriting the URL (and URL-encoding the secret).
  2. Or refuse loudly instead: a datasource that declares credentialsRef AND a URL the arm cannot inject into could be rejected at connect with a named error, rather than silently connecting. That is the "absence must be loud" reading (Route & surface ownership §3) and is cheaper to get right.

Either way the fix belongs beside the pins that already exist for these arms (datasource-pool-support.test.ts reads arm source; turso-bound-secret-authoring.test.ts pins the #8152 equivalent for turso), and it wants a per-arm pin asserting that a bound secret reaches the client on both the discrete and DSN branches. (PR #8875 added that pin for mysql: __tests__/bound-secret-dsn-branches.test.ts.)

Repro sketch

A runtime datasource, driver mysql, config: { url: 'mysql://app@db.internal:3306/app' }, created through the wizard with a secret in the connection form's secret field. The sys_secret row is written, external.credentialsRef is stored, DatasourceConnectionService resolves it — and the connection is opened with the passwordless DSN. (This is the fixed arm; substitute driver mongodb for the open one.)

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions