Repository navigation
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
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
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Aug 17, 2026
…authToken (objectstack-ai#8152) (objectstack-ai#8188) After objectstack-ai#8078 a NEW turso datasource could not be authenticated by any route an author has. objectstack-ai#7990/objectstack-ai#8078 made `config.authToken` a refused inline credential (`z.never()`) at every authoring door, exactly like the SQL drivers' `config.password`, and diverted the author to the secret binder: bind the credential, keep only `external.credentialsRef` on the record. The connect path resolves that ref and hands the cleartext to the driver factory as `spec.secret` — and nothing on the turso path read it. `TURSO_CONFIG_READERS.authToken` consulted `config.authToken` alone, so the resolved secret was dropped and the connection was attempted unauthenticated. `authToken` now reads `spec.secret` first and falls back to `config`. That is exact parity with the postgres / mysql / mongodb arms in this same package (`spec.secret ? { password: spec.secret } : cfg.password ? … : {}`) — no new mechanism, no spec change, no second binder slot. `spec` was already on `TursoConfigSource` for `schemaMode`, so the credential had been reaching this function all along. `config.authToken` stays readable, and not only for legacy stored rows: the CLI and standalone hosts translate `OS_DATABASE_AUTH_TOKEN` / `TURSO_AUTH_TOKEN` into a `config` they construct themselves, which never meets the authoring schema that refuses the key. An empty `spec.secret` is unset and falls through to `config`, matching this builder's existing rule for string keys. Pins, with their readings on origin/main (a0fdc56): RED on main (6) - turso-driver-config: bound secret reaches `authToken`; bound secret beats a stale inline `config.authToken`; binds to `authToken` only, never `encryptionKey`. - turso-bound-secret-authoring (new): a datasource created through the real admin door with a bound secret arrives at the driver with an `authToken`; the bound route is the ONLY one a newly authored datasource has (both halves in one case); the credential lands in one slot, not two. GREEN on main (15) - objectstack-ai#8078's inline refusal still fires at create AND at update, and its guidance still names both `external.credentialsRef` and the secret binder. - `config.authToken` is still read when no secret is bound (the host env route). - postgres / mysql / mongodb still read `spec.secret` as the password, and a bound secret still beats an inline `config.password` — the sibling arms this change takes its shape from, unpinned until now. The gap was invisible because a stored row bypasses the parse and still connects (only new authoring was dead) and because `turso-driver-config.test.ts` had no `secret` case at all. Every red pin therefore starts at `createDatasource()`, not at an existing record: the vacuous version of this test is green on main. `encryptionKey` deliberately untouched — a different secret, one binder slot, and whether it needs a second is a separate decision. objectstack-ai#8126's read-time redaction of it is untouched too. Gates: check-test-source-alias OK (72 packages scanned, 62 registered; `@objectstack/spec` was already the registry entry for this package, so no widening); check:type-check-coverage OK (64/77 type-checked); the new test file is type-checked (package `tsc --noEmit` clean). No refusal surface changed, so no ADR-0112 envelope. Tests: service-datasource 16/16 files 376/376 cases, runtime 145/145, cli 115/115. Claude-Session: https://claude.ai/code/session_01Lw4Dm3qYuWFNJFMwQDdkzv Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Aug 17, 2026
…driver config.url at publish (objectstack-ai#8082) (objectstack-ai#8341) * feat(spec)!: refuse URL-embedded credentials in driver config.url at publish (objectstack-ai#8082) The objectstack-ai#7990 closure refused the inline credential keys; objectstack-ai#8078 measured and pinned that config.url still accepted the identical secret one syntax over. Maintainer-ruled Option A (2026-08-12): one value-level parse (urlUserinfoPassword / credentialFreeUrl, data/driver/common.zod.ts) shared by the four URL-bearing driver schemas — postgres/mysql/mongo url, turso url + syncUrl — refuses a URL whose userinfo carries a non-empty password. - Bare-user userinfo (user@host) stays accepted, matching objectstack-ai#7990's posture (username is a writable key; only the secret is refused) — and it is the exact shape the objectstack-ai#8126 read path serves for legacy rows, so an untouched Save keeps working. - The refusal message names the working mechanisms (secret binder / external.credentialsRef), states the runtime-DSN carve-out explicitly (OS_DATABASE_URL never passes the publish door), and warns that ${...} placeholders resolve to nothing (objectstack-ai#8078, measured) instead of steering authors into that broken escape. - The objectstack-ai#8078 acceptance pin (driver-credential-refusal.test.ts) is INVERTED to a rejection pin, not deleted; the objectstack-ai#8126 write-door acceptance pin in service-datasource flips the same way (its own comment said it waited on exactly this ruling). - ADR-0087: D3 semantic entry datasource-config-url-userinfo-refused (no D2 conversion — a credential cannot be mechanically encrypted into sys_secret); registry, spec-changes, upgrade guide, api-surface, export-origins, reference docs regenerated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Euoy6wyfzgiWtgCg4s6JK2 * test(spec): re-spell two driver URI fixtures credential-free (objectstack-ai#8082 fixture triage) mongo.test.ts / postgres.test.ts 'accept config with connection URI' merely used the userinfo spelling to demonstrate URI acceptance — re-spelled to the bare-user form the rule still accepts (disposition: re-spell; the family rejection pins live in driver-credential-refusal.test.ts). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Euoy6wyfzgiWtgCg4s6JK2 * chore(spec): regenerate artifact projections on the merged tree (os-regen step 4) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Euoy6wyfzgiWtgCg4s6JK2 --------- Co-authored-by: Claude <noreply@anthropic.com>
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 free
to 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