Uh oh!
There was an error while loading. Please reload this page.
fix(service-datasource): stop serving stored credentials on the datasource read path, and fix the false "credential-stripped" claim (#8081) - #8126
Merged
huangyiirene merged 1 commit intoAug 12, 2026
Conversation
…ource read path, and fix the false "credential-stripped" claim (#8081) `getDatasource()` returned the driver `config` verbatim while its own doc comment promised "with the credential stripped" and described `config` as "non-sensitive — credentials live in `sys_secret`, never in config". Nothing stripped anything. The comment was load-bearing: it is why the gap survived a 26-surface credential survey. #8078 closed the WRITE door (`config.password` / `config.authToken` are `z.never()` on every driver that has them) and deliberately left stored rows alone. Those rows still hold cleartext, and `GET /api/v1/datasources/:name` handed it to every caller. The refused-key set is DERIVED from each driver's contract — #8078 spells a refused inline credential as `z.never()`, so the schema is the list — plus the pre-#8078 alias spellings (a stored row never met the parse that renamed them) and turso's still-writable `encryptionKey`. A driver with no shipped contract keeps the canonical spellings hidden by name. A credential embedded in `config.url` is redacted too, at the password component of the userinfo only. Refusing such a URL at the write door stays UNRULED (#7990) and untouched: redacting on the way out is not refusing on the way in, and a scrub that dropped `config.password` while serving the same secret one key over would be a scrub in name only. `updateDatasource` carries the hidden material forward across a same-driver round-trip, after the validation gate rather than before it — the gate judges what the author wrote, and this is material the author never saw. Without it the scrub would have turned every untouched "Save" into silent credential deletion. It also repairs a regression #8078 introduced and this card measured: the form was served `config.password` verbatim, posted it back, and the gate refused it — so editing any legacy datasource answered 400 for a value the server itself had just supplied. The stored record is never mutated; the connect path is unaffected. Reverse verification: 9 of 21 pins fail on `origin/main` (each naming the defect it carries), 21/21 pass here. The two guard pins — #8078's refusal with its guidance intact, and URL credentials still ACCEPTED at the write door — pass on both sides by design. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WCoM9vVXw6yAVgEc4oio4m
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckNo hand-written docs reference the 1 changed package(s). ✅ |
huangyiirene
marked this pull request as ready for review
August 12, 2026 17:21
This was referenced Aug 12, 2026
Uh oh!
There was an error while loading. Please reload this page.
huangyiirene
deleted the
claude/issue-8081-datasource-credential-read-path
branch
August 12, 2026 17:32
This was referenced Aug 12, 2026
This was referenced Aug 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes nothing on its own — #8081 stays open until scope items 3/4/5 are dispositioned, and #7990 stays open behind it. Per the
domain:servicesdispatch order (#8081 comment 5269562252), this PR delivers scope items 1 and 2 only; 3/4/5 are reported on the issue, not implemented.Item 1 — read-path scrub (delivered)
DatasourceAdminService.getDatasource()returned the driverconfigverbatim (datasource-admin-service.ts:199) while:179carried a doc comment promising "with the credential stripped" and describingconfigas "non-sensitive — credentials live insys_secret, never in config". Nothing stripped anything.The comment was not merely stale, it was load-bearing — it is why the gap survived a 26-surface credential survey. It is rewritten to say what the code does, and to record what it used to claim.
What is redacted, and where the list comes from. The refused-key set is derived from each driver's own contract rather than retyped: #8078 spells a refused inline credential as
z.never(), so the schema is the list, and a credential key refused tomorrow is covered the day it lands. Three sources feed the scrub:z.never())password(postgres/mysql/mongodb),authToken(turso)passwdpwdtokenjwtauth_tokenauthtokenmetadata.registerencryptionKeyA driver the platform ships no contract for still has the canonical spellings hidden by name. Declining to refuse an unrecognised key is a boundary choice about authoring; serving a key literally named
passwordback in cleartext is a leak under any boundary.URL-embedded credentials — the explicit reading the dispatch asked for. A
postgresql://user:pass@host/dbinconfig.urlcarries the same secret asconfig.password. Measured onorigin/main,getDatasource()served both for the same row. A scrub stopping atconfig.passwordwould have been half a scrub — the same "claims a protection it does not perform" shape this card exists to end. So the read path redacts the password component of the userinfo and leaves scheme, username and everything from the host onward byte-for-byte.⛔ Nothing here refuses a URL, at any door. #7990 left that unruled and #8078 pinned the acceptance as a FACT; a pin guards that boundary and passes on both sides. Redacting a value on the way out is a different act from refusing it on the way in, and only the second one is unruled.
The response names what it withheld —
redactedConfigKeys, the same courtesyhasSecretalready pays for the bound handle.Why the scrub needed an inverse. The edit form reads this config and patches it straight back, so a scrub with no inverse turns every untouched "Save" into silent credential deletion — trading a disclosure bug for a data-loss bug.
updateDatasourcetherefore carries the hidden material forward on a same-driver round-trip, after the validation gate: the gate judges what the author wrote, and this is material the author never saw and is not asking to change. Same rule thecredentialsRefbeside it has always followed.That also repairs a regression #8078 introduced, measured here for the first time: on
mainthe form was servedconfig.passwordverbatim, posted it back unchanged, and the write gate refused it — so editing any legacy datasource through the wizard answered 400 for a value the server itself had just supplied, including theactive: falsethat takes a misconfigured datasource out of service.Not changed: the stored record is never mutated. Redaction is a read-path act; the connect path reads the raw record and a legacy datasource keeps authenticating exactly as before. Getting cleartext out of the store is item 3's migration and is deliberately not attempted.
Item 2 — write-door measurement (measured; nothing to fix)
Measured end-to-end against the real
HonoHttpServer, not reasoned from source:POST /api/v1/datasources(wizard)config.passwordDATASOURCE_ADMIN_ERROR, full guidance inerror.messagePOST /api/v1/datasourcespasswdDATASOURCE_ADMIN_ERROR, guidance + unrecognised-key prosePATCH /api/v1/datasources/:nameconfig.passwordDATASOURCE_ADMIN_ERROR, full guidancePUT /metaconfig.passwordINVALID_METADATA+ structuredissues[]carrying the same messageconfig.passwordNo door collapses into a generic 500. The refusal guidance — naming both the secret binder and
external.credentialsRef— reaches the caller intact at every door. Both codes are already ADR-0112-registered (DATASOURCE_ADMIN_ERROR,INVALID_METADATA), so no envelope change was needed and none was made. The card's worry did not materialise; that is the finding.Reverse verification
9 of 21 pins fail on
origin/main, each naming the defect it carries; 21/21 pass here. Method: the service file was reverted toorigin/main's version in-tree and the suite re-run, so the readings are measured, not asserted.Red on main: cleartext password served · URL credential served ·
redactedConfigKeysabsent · tursoauthToken/encryptionKeyserved · alias spellings served · served-vs-stored split · round-trip 400 · non-credential edit blocked · driver change.Green on both sides, by design: #8078's parse refusal still fires with its guidance intact (guards the spec half) · a URL-embedded credential is still ACCEPTED at the write door (guards the unruled boundary).
One pin was rewritten mid-task because it passed vacuously on both sides — "the stored record is untouched" is green on
mainfor the worst possible reason (nothing redacts there, so served and stored are identical). It now asserts the served and stored halves together, which makes it red on main. That is the #7801 failure mode, caught here.Gates
check:test-source-alias— OK, 72 packages scanned. New test file; no new workspace dependency, noKNOWN_UNALIASED_TEST_IMPORTSedit.check:type-check-coverage— OK, 64/77 packages type-checked.service-datasourceis type-checked, not ledgered;tsc --noEmitclean.check:type-check-debt --re-measure— OK on the built closure, 1966 raw errors, none above its recorded number. (The surplus notices are pre-existing entries below their ceiling, untouched by this PR.)service-datasourcesuite: 361/361.content/docs/releases/**,docs/adr/**,.claude/skills/**,skills/**,sys-session.object.ts,packages/spec/src/**.Reported on the issue, NOT implemented here
Items 3 (migration story), 4 (turso
encryptionKey) and 5 (managed credential slot) are decisions belonging to other seats. Two findings from measuring them are load-bearing and are written up on #8081:GET /api/v1/meta/datasourcesstill serves the same stored cleartext. Fixing it leaves the services lane ⇒ STOP and report, per the dispatch order.authToken— not justencryptionKey— has no working supported path post-feat(spec)!: refuse inline credentials at publish — driver config + connector authoring door (#7990, spec half) #8078.Generated by Claude Code