Uh oh!
There was an error while loading. Please reload this page.
fix(service-datasource): read a turso datasource's bound secret into authToken (#8152) - #8188
Merged
huangyiirene merged 1 commit intoAug 13, 2026
Merged
Conversation
…authToken (#8152) After #8078 a NEW turso datasource could not be authenticated by any route an author has. #7990/#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) - #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. #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. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lw4Dm3qYuWFNJFMwQDdkzv
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). ✅ |
This was referenced Aug 12, 2026
huangyiirene
marked this pull request as ready for review
August 13, 2026 00:34
Uh oh!
There was an error while loading. Please reload this page.
huangyiirene
deleted the
claude/issue-8152-turso-bound-secret-authtoken
branch
August 13, 2026 00:44
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#8152.
The defect
After #8078 a new turso datasource could not be authenticated by any route an author has.
#7990/#8078 made
config.authTokena 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 onlyexternal.credentialsRefon the record. The connect path resolves that ref and hands the cleartext to the driver factory asspec.secret. Nothing on the turso path read it.TURSO_CONFIG_READERS.authTokenconsultedconfig.authTokenalone, so the resolved secret was dropped and the connection was attempted unauthenticated:Inline refused at the door, bound dropped at the builder — no route left.
The fix — 5 lines, parity with the siblings
authTokennow readsspec.secretfirst and falls back toconfig:Exact parity with the postgres / mysql / mongodb arms in this same package (
spec.secret ? { password: spec.secret } : cfg.password ? { password: cfg.password } : {}). No new mechanism, no spec change, no second binder slot:specwas already onTursoConfigSourceforschemaMode, so the credential had been reaching this function all along.config.authTokenstays readable, and not only for legacy stored rows. The CLI and standalone hosts translateOS_DATABASE_AUTH_TOKEN/TURSO_AUTH_TOKENinto aconfig: { url, authToken }on a definition they construct themselves (packages/cli/src/utils/storage-driver.ts:451), which never meets the authoring schema that refuses the key. Dropping that arm would break thedefaultdatasource's env credential — a live route, refused only for authors. An emptyspec.secretis treated as unset and falls through toconfig, matching this builder's existing rule for string keys.Reverse verification — readings on
origin/main(a0fdc56)Measured by reverting the 5-line fix in place and re-running, not inferred.
RED on main (6) — carry the defect:
turso-driver-config: reads the bound secret intoauthToken{ url: 'libsql://my-db.turso.io' }— assertion onauthTokenfailedturso-driver-config: bound secret beats a stale inlineconfig.authToken'STALE-INLINE'turso-driver-config: binds toauthTokenONLY, neverencryptionKey['url'], expected['authToken','url']authoring(new): a datasource created through the real admin door with a bound secret reaches the driver with anauthTokenspec.secretarrived; the builder emitted noauthTokenauthoring(new): the bound route is the ONLY one a newly authored datasource hasauthoring(new): the credential lands in one slot, not twoauthTokenhalfGREEN on main (15) — guard behaviour that must not change:
createDatasourceAND atupdateDatasource, and its guidance still names bothexternal.credentialsRefand the secret binder. This card restores the alternative route; it must not reopen the inline one.config.authTokenis still read when no secret is bound — guards the host env-var route above.spec.secretas the password, and a bound secret still beats an inlineconfig.password— the sibling arms this change takes its shape from, and unpinned until now.The vacuity trap, and how the pins avoid it
Stored rows still work — stored config bypasses the parse — so any case built on an already-existing turso datasource is green on
maintoo. Every red pin therefore starts atcreateDatasource(): authored through the real admin door, credential bound through the real secret binder, resolved through the real connect path.The
encryptionKeyguard is asserted beside theauthTokenassertion rather than alone, for the same reason:expect(config).not.toHaveProperty('encryptionKey')on its own is green on main for the useless reason (main emitted neither key).One seam, stated in the test header rather than hidden: the factory's real
tursoarm cannot run in this package —@objectstack/driver-tursois deliberately not resolvable from it, and the missing-package arm's own pin depends on that. So the end-to-end pins capture the spec the connect path actually handsfactory.create()and run the realbuildTursoDriverConfigon exactly that spec, which is the line the factory arm itself executes. Both halves are production code; only thenew TursoDriver(...)call is absent.Scope — the four boundaries held
encryptionKeyuntouched. Different secret (AES-256 key for a local file vs. a bearer token for a remote), one binder slot, and whether it needs a second is a separate decision. A pin now fails if a later change resolves that by guessing one bound secret means both slots.external.credentials: {slot → ref}, JSON-bag ref).encryptionKeynot modified.packages/spec/src/**not touched. The contract needs no change:spec.secretis already onDatasourceConnectionSpecand the connect path already resolves it.Gates
check-test-source-alias@objectstack/specwas already this package's registry entry, so the new file's@objectstack/spec/dataimport adds nothing;KNOWN_UNALIASED_TEST_IMPORTSnot widened.check:type-check-coveragecheck:type-check-debt(--re-measure)lint.ymldoes. The 9 surplus notices are pre-existing entries in unrelated packages.tsc --noEmit(service-datasource).changeset/turso-bound-secret-authtoken.md(patch)Untouched:
content/docs/releases/**,docs/adr/**,.claude/skills/**,skills/**.Tests
@objectstack/service-datasource@objectstack/runtime@objectstack/cliRuntime and CLI are included because they are the other two readers of this builder (
turso-driver-factory.convergence.test.ts,storage-driver.test.ts).Generated by Claude Code