Skip to content

fix(service-datasource): read a turso datasource's bound secret into authToken (#8152) - #8188

Merged
huangyiirene merged 1 commit into
mainfrom
claude/issue-8152-turso-bound-secret-authtoken
Aug 13, 2026
Merged

fix(service-datasource): read a turso datasource's bound secret into authToken (#8152)#8188
huangyiirene merged 1 commit into
mainfrom
claude/issue-8152-turso-bound-secret-authtoken

Conversation

@huangyiirene

Copy link
Copy Markdown
Collaborator

Closes#8152.

The defect

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. 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:

buildTursoDriverConfig({driver: 'turso', config: {url: 'libsql://my-db.turso.io'},
secret: 'THE-BOUND-JWT', external: {credentialsRef: 'sys_secret:abc'}})
→ { url: 'libsql://my-db.turso.io' } // no authToken

Inline refused at the door, bound dropped at the builder — no route left.

The fix — 5 lines, parity with the siblings

authToken now reads spec.secret first and falls back to config:

authToken: ({ spec, config })=>spec.secret
? spec.secret
: typeofconfig.authToken==='string'&&config.authToken
? config.authToken
: undefined,

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: 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: { 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 the default datasource's env credential — a live route, refused only for authors. An empty spec.secret is treated as unset and falls through to config, 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:

PinReading on main
turso-driver-config: reads the bound secret into authToken{ url: 'libsql://my-db.turso.io' } — assertion on authToken failed
turso-driver-config: bound secret beats a stale inline config.authTokengot 'STALE-INLINE'
turso-driver-config: binds to authToken ONLY, never encryptionKeykey set was ['url'], expected ['authToken','url']
authoring (new): a datasource created through the real admin door with a bound secret reaches the driver with an authTokenspec.secret arrived; the builder emitted no authToken
authoring (new): the bound route is the ONLY one a newly authored datasource hashalf one (door refuses inline) passed; half two failed — the dead end
authoring (new): the credential lands in one slot, not twored via its authToken half

GREEN on main (15) — guard behaviour that must not change:

  • feat(spec)!: refuse inline credentials at publish — driver config + connector authoring door (#7990, spec half) #8078's inline refusal still fires at createDatasource AND at updateDatasource, and its guidance still names both external.credentialsRef and the secret binder. This card restores the alternative route; it must not reopen the inline one.
  • config.authToken is still read when no secret is bound — guards the host env-var route above.
  • 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, and unpinned until now.
  • A turso config with no credential at all is still accepted (the shape a bound datasource stores).

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 main too. Every red pin therefore starts at createDatasource(): authored through the real admin door, credential bound through the real secret binder, resolved through the real connect path.

The encryptionKey guard is asserted beside the authToken assertion 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 turso arm cannot run in this package — @objectstack/driver-turso is 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 hands factory.create() and run the real buildTursoDriverConfig on exactly that spec, which is the line the factory arm itself executes. Both halves are production code; only the new TursoDriver(...) call is absent.

Scope — the four boundaries held

Gates

GateReading
check-test-source-aliasOK — 72 packages with tests scanned, 62 registered. @objectstack/spec was already this package's registry entry, so the new file's @objectstack/spec/data import adds nothing; KNOWN_UNALIASED_TEST_IMPORTSnot widened.
check:type-check-coverageOK — 64/77 packages type-checked, 13 in DEBT (442 frozen), 1 exempt.
check:type-check-debt (--re-measure)OK — 33 entries re-measured, 1966 raw errors, none above its ceiling. Required building the workspace closure first, as lint.yml does. The 9 surplus notices are pre-existing entries in unrelated packages.
tsc --noEmit (service-datasource)clean — this package does not hide its tests from tsc, so the new file is type-checked
eslintclean on all three changed files
ADR-0112 envelopenot applicable — no refusal surface changed and no error code added
Changeset.changeset/turso-bound-secret-authtoken.md (patch)

Untouched: content/docs/releases/**, docs/adr/**, .claude/skills/**, skills/**.

Tests

PackageResult
@objectstack/service-datasource16/16 files, 376/376 cases
@objectstack/runtime145/145 files
@objectstack/cli115/115 files

Runtime 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

…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
@vercel

vercelBot commented Aug 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 12, 2026 7:09pm

Request Review

@github-actionsgithub-actionsBot added size/l documentation Improvements or additions to documentation tests tooling labels Aug 12, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

No hand-written docs reference the 1 changed package(s). ✅

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

turso: the bound secret is never read, so post-#8078 a new turso datasource cannot be authenticated by any supported route

2 participants

@huangyiirene@claude