Repository navigation
Commit 2354058
feat(spec,driver-turso)!: retire turso config.encryptionKey, which never encrypted anything (#25153)
<!-- claude-projects-attribution: {"github_login":"os-bill"} -->
_Requested by **Bill** · [project
thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wg6zwDuV2XvoDkqC7bXgqnpc)_
Fixes #25050
Clause-②: no (narrowing)
Metadata-protocol: narrowed
## What changes
**BREAKING.** Turso `config.encryptionKey` is removed (Route A, retire
the key, ADR-0049 enforce-or-remove). It never encrypted anything, in
any mode:
- a local or embedded-replica file is read and written through
better-sqlite3, which has no encryption codec. #25020 already refused
the key there;
- a remote database was never handed the key, because its storage
encryption is configured on the server.
After #25020 the key still parsed on a remote datasource, did nothing
there, and was the last credential-shaped key a datasource could write
in plain text.
| Surface | Before | After |
| --- | --- | --- |
| `@objectstack/spec` `TursoConfigSchema.encryptionKey` | accepted
beside a remote url; refused beside a local url (#25020) |
`retiredKey()` tombstone: a `tsc` error, and a parse error at
`config.encryptionKey` in every mode with any value (`''` included),
carrying the prescription. It is the only issue reported on that config.
`encryption_key` / `encryptionkey` get the same prescription |
| `@objectstack/driver-turso` mirror `TursoConfigSchema` | same split |
`z.never()` with the same prescription text, byte for byte |
| `new TursoDriver()` / `createTursoDriver()` | refused local / replica
/ forced-remote-on-a-file only | `TursoDriverConfig.encryptionKey?:
never`. A defined value is refused with `VALIDATION_ERROR` / 400 as the
constructor's first check, in every mode |
| `@objectstack/service-datasource`
`TursoDriverConfigInput.encryptionKey` | `string` | `never`. The shared
loader still forwards a declared value, so a legacy stored row reaches
the constructor's refusal and does not boot in silence |
| redaction (`datasource-credential-redaction.ts`) |
`STILL_WRITABLE_CREDENTIAL_KEYS = { turso: ['encryptionKey'] }` |
`RETIRED_CREDENTIAL_KEYS`, same content: a stored legacy value is still
withheld on read and named in `redactedConfigKeys` |
### FROM → TO
| You wrote | Write instead |
| --- | --- |
| `url: 'libsql://my-db.turso.io', encryptionKey: '…'` (also `https://`,
`ws(s)://`) | delete `encryptionKey`. A remote database's storage
encryption is configured on the Turso server |
| `encryptionKey: ''` (or any unset value) | delete `encryptionKey` |
| `url: 'file:./data/app.db', encryptionKey: '…'` (also `:memory:`, or a
replica beside `syncUrl`) | delete `encryptionKey`, and put the file on
an encrypted disk or volume if it must be encrypted at rest |
**The one-line fix:** delete `encryptionKey` from every turso
datasource.
### ADR-0087 disposition
- **D2 `turso-config-encryption-key-removed`** (`retiredFromLoadPath:
true`, `retiredAfter: '18.0.0'`) deletes the key when its value is unset
(any non-string, or `''`) or when it sits beside a remote url
(`libsql://`, `https://`, `http://`, `ws://`, `wss://`, in any case, url
trimmed). It emits one notice per key.
- A non-empty key beside a **local** url is left for the author. The row
keeps the #25020 posture: it fails at driver construction with the
prescription.
- **D3 `turso-config-encryption-key-retired`**.
`RETIRED_KEYS_BY_MAJOR[18]` gains `data/TursoConfig:encryptionKey`. The
`STEP18_RATIONALE` fragment has order 107.
- The changeset `.changeset/25050-retire-turso-encryption-key.md`
carries the one marker, both declaration lines, BREAKING, FROM → TO and
the one-line fix. Grades: `@objectstack/spec` minor,
`@objectstack/driver-turso` minor, `@objectstack/service-datasource`
minor.
## Decision: the partial conversion (four axes)
The question was whether the D2 conversion deletes every stored
`encryptionKey`, or only the ones it can prove were inert.
- **Real business need:** an operator who wrote a key beside a local
file believed the file was encrypted. A conversion that silently deletes
that key lets the datasource boot in plaintext with no signal, which is
the defect #25020 made loud. Beside a remote url, or when the value is
unset, nothing was ever believed and nothing changes.
- **Long-term soundness:** one rule ("a key the driver never read, with
nothing believed behind it") is mechanical and lossless. The local case
needs a human decision (an encrypted volume, or a remote database), so
it stays a refusal that names the fix.
- **Preventing AI metadata errors:** the tombstone, the guidance
spellings, and the constructor refusal all carry the same prescription.
An agent that writes the key in any spelling or mode is told to delete
it. There is no accepted-but-inert state left to learn from.
- **Startup no-sprawl:** one conversion, one D3 entry, one retired-key
entry, one renamed internal constant. There are no new options or modes,
and the loader keeps its single forwarding reader.
## Measured assumptions (one disproved)
- **Disproved: "an existing path still redacts a stored `encryptionKey`
once it is tombstoned."** It does not. The schema-derived credential
list excludes `retiredKey()` tombstones by their `[REMOVED] ` prefix
(#15680), and the derived turso list (`authToken`) is non-empty, so the
canonical fallback never applies.
- Without a fix, a legacy row's value would have been served on read.
- Fixed with the retired-key reader `RETIRED_CREDENTIAL_KEYS` (the
renamed `STILL_WRITABLE_CREDENTIAL_KEYS`).
- Proven by `datasource-credential-redaction.test.ts` ("a RETIRED
credential tombstone … is excluded from the derivation, yet still
redacted") and by the service-datasource admin-door test on a local-url
legacy row.
- **Held: zero in-repo producers.** No example, template, scaffold,
skill, host default or env var authors the key. A tree-scoped absence
pin (`turso-encryption-key-retirement.test.ts`, radius `packages
examples skills content scripts`) now holds it, with every exclusion
named.
- **Re-homed fixtures:** 15 stored-metadata redaction suites used
`encryptionKey` as their sentinel credential. 14 now use turso
`authToken`, which is refused inline and redacted. The runtime
reader-contexts pin seeds through the production writer, which refuses
both primary slots, so it now uses mongodb `options.proxyPassword`,
which is writable and redacted. The metadata-protocol legacy-row
redaction test keeps `encryptionKey` on purpose, as a real retired-key
row.
## Reverse verification (once, no permanent test)
Run with `scripts/ablation-replace.mjs` on the committed head. Each
mutation landed (anchor 1 → 0, blob moved) and was restored (blob equals
HEAD, `git diff HEAD` empty).
- **Constructor refusal removed**
(`refuseIgnoredSyncKey(ENCRYPTION_KEY_RETIRED_REFUSAL)`): driver-turso
`turso-driver-encryption-key-retired.test.ts` and
`turso-config-constructor-parity.test.ts` went red, 36 failed across
both files.
- **Retired-key reader emptied** (`turso: []`): spec
`datasource-credential-redaction.test.ts` and
`metadata-type-redaction.test.ts` went red, 7 failed.
## Verification
- `@objectstack/spec`: build 0, `pnpm test` (after fixing the ledger
total, which the first run caught) 0, typecheck 0. The repo-project
absence pin passes, 10 tests.
- `@objectstack/driver-turso`: build 0, `pnpm test` 0 (after measuring
the `SYNC_KEY_REFUSALS` floor at 20), typecheck 0.
- `@objectstack/service-datasource`: build 0, `pnpm test` 0, typecheck
0.
- `@objectstack/cli`: typecheck 0. The three touched test files pass.
One `storage-driver.test.ts` case timed out at 5 s under a shared-box
batch, then passed in isolation (225 passed).
- Touched suites in runtime, objectql, mcp, metadata-protocol,
plugin-audit and service-automation: all pass.
- dispatch-gates `--commands` on this diff (head `a15132cc4`): all 127
derived commands run, exit codes recorded before any pipe. `--ran`
reports 127 run, 0 NOT-MEASURED, 0 UNRUN.
- 5 of the 127 first refused on environment prerequisites, not findings:
a shallow clone (`check-engine-split-ratio` exit 2,
`check-plugin-teardown-shape --self-test` exit 3) and dist output that
was not built (`check:skill-examples`, `check:dual-build-cjs-loads`,
`check:i18n-coverage`, each exit 3).
- Each passed (exit 0) after `git fetch --shallow-since` and a full
`pnpm build`.
- Dogfood `pnpm --filter @objectstack/dogfood test` on `a15132cc4`,
lock-wrapped: lock VERDICT command-exit 0. Test Files 287 passed, 3
skipped (290). Tests 2395 passed, 18 skipped (2413).
The contract-review-tier review belongs to the seat. This PR stays a
draft.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01B2EeooonSXrANpkpgUSfFZ)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 0cf309f commit 2354058
55 files changed
Lines changed: 1385 additions & 997 deletions
File tree
- .changeset
- content/docs
- data-modeling
- references/data
- docs
- design
- qa/platform-checklist/areas
- packages
- cli/src
- commands/migrate
- utils
- drivers/driver-turso
- src
- spec
- mcp/src
- metadata-protocol/src
- objectql/src
- plugins/plugin-audit/src
- runtime/src
- services
- service-automation/src/builtin
- service-datasource/src
- __tests__
- spec
- authorable-surface
- src
- conversions
- data
- driver
- kernel
- migrations
- entries
- retired-keys
- semantic
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
195 | 195 | | |
196 | 196 | | |
197 | 197 | | |
198 | | - | |
199 | 198 | | |
200 | 199 | | |
201 | | - | |
| 200 | + | |
202 | 201 | | |
203 | 202 | | |
204 | 203 | | |
205 | 204 | | |
206 | | - | |
| 205 | + | |
207 | 206 | | |
208 | 207 | | |
209 | 208 | | |
| |||
231 | 230 | | |
232 | 231 | | |
233 | 232 | | |
234 | | - | |
235 | | - | |
236 | | - | |
237 | | - | |
238 | | - | |
239 | | - | |
240 | | - | |
241 | | - | |
242 | | - | |
243 | | - | |
244 | | - | |
245 | | - | |
246 | | - | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
247 | 244 | | |
248 | 245 | | |
249 | 246 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
68 | 68 | | |
69 | 69 | | |
70 | 70 | | |
71 | | - | |
| 71 | + | |
72 | 72 | | |
73 | 73 | | |
74 | 74 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
516 | 516 | | |
517 | 517 | | |
518 | 518 | | |
519 | | - | |
| 519 | + | |
520 | 520 | | |
521 | 521 | | |
522 | 522 | | |
523 | 523 | | |
524 | 524 | | |
525 | 525 | | |
526 | | - | |
527 | 526 | | |
528 | 527 | | |
529 | 528 | | |
| |||
0 commit comments