Skip to content

Commit 01ff31c

Browse files
committed
wip(25050): re-home fixtures, loader tests and docs onto the retired key
Claude-Session: https://claude.ai/code/session_01B2EeooonSXrANpkpgUSfFZ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 94f006d commit 01ff31c

39 files changed

Lines changed: 907 additions & 645 deletions

File tree

‎content/docs/data-modeling/drivers.mdx‎

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -195,15 +195,14 @@ ObjectStack — is what makes them secrets:
195195
| `mongodb` | `options.key` | TLS private key material (PEM) |
196196
| `mongodb` | `options.passphrase` | TLS key passphrase |
197197
| `mongodb` | `options.authMechanismProperties.AWS_SESSION_TOKEN` | AWS STS session token (`MONGODB-AWS` auth) |
198-
| `turso` \| `libsql` | `encryptionKey` | A key the driver never applies (see below) |
199198

200199
Unlike `password` / `authToken` (typed `never` in their schemas — the parse
201-
refuses them outright), these six keys are **writable**: the parse accepts
200+
refuses them outright), these five keys are **writable**: the parse accepts
202201
them, and they are stored **at rest in `sys_metadata` as plain text**, right
203202
alongside the rest of the datasource row. The protection that exists today is
204203
on the READ side only — every one of them is stripped before a datasource
205204
record is ever served back over the admin API or shown in the Setup UI
206-
(`PASSTHROUGH_SECRET_PATHS` / `STILL_WRITABLE_CREDENTIAL_KEYS` in
205+
(`PASSTHROUGH_SECRET_PATHS` in
207206
`datasource-credential-redaction.ts`, shipped in
208207
[#9040](https://github.com/objectstack-ai/objectstack/issues/9040)). That stops the value
209208
round-tripping through a read; it is not encryption at rest, and an operator
@@ -231,19 +230,17 @@ that needs an authenticated proxy or a passphrase-protected key converts
231230
that group into named binder-slot support — one mechanism covering those
232231
four keys — rather than the current per-key accept-and-document posture.
233232

234-
Turso's `encryptionKey` is no longer part of that trade-off either. It was
235-
declared as an AES-256 key for the local database file, but the driver reads
236-
and writes that file through better-sqlite3, which has no encryption codec, so
237-
the file was written in plaintext whatever the key said. A local or
238-
embedded-replica turso datasource that declares it is now refused at write
239-
time and when the driver is built: drop the key, and put the file on an
240-
encrypted disk or volume if it must be encrypted at rest. A remote turso
241-
datasource (a `libsql://`, `https://` or `ws(s)://` url) still parses with the
242-
key, which the driver does not forward (a remote database's storage encryption
243-
is the server's), while a `file:` url or `:memory:` under a forced
244-
`mode: 'remote'` is refused like a local one, because the libSQL client the
245-
driver builds for it is handed no key; a stored value is redacted on read like
246-
the rest of this table.
233+
Turso's `encryptionKey` is not part of that trade-off: it was **removed** in
234+
`@objectstack/spec` 18 ([#25050](https://github.com/objectstack-ai/objectstack/issues/25050), ADR-0049).
235+
It was declared as an AES-256 key for the local database file, but the driver
236+
reads and writes that file through better-sqlite3, which has no encryption
237+
codec, and it never handed the key to a remote database, whose storage
238+
encryption the server configures. So it encrypted nothing, in any mode. The
239+
parse and the driver constructor now refuse it in every mode: delete the key,
240+
and put a local file on an encrypted disk or volume if it must be encrypted at
241+
rest. `os migrate meta --from 17` deletes it for you where it was unset or
242+
beside a remote url; a stored legacy row that still carries it is redacted on
243+
read like the keys above.
247244
</Callout>
248245

249246
## Startup: a driver that cannot connect aborts the boot

‎docs/design/driver-turso.md‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -516,14 +516,13 @@ package-published Spec / Studio mirror in `packages/drivers/driver-turso/src/spe
516516
Neither declares a key the driver does not read — the direction ADR-0049 (enforce-or-remove)
517517
governs. The converse does not hold, and the mirror is the shorter list: it does not declare
518518
`mode`, which the driver does read (`TursoDriverConfig.mode`). The mirror keeps its retired keys
519-
(`timeout` → `timeoutMs`; `localPath` and `wasm`, removed) as `z.never()` tombstones whose refusal
519+
(`timeout` → `timeoutMs`; `localPath`, `wasm` and — since #25050, because it encrypted nothing in any mode — `encryptionKey`, removed) as `z.never()` tombstones whose refusal
520520
carries the prescription. The live keys:
521521

522522
| Property | Type | Default | Description |
523523
|:---|:---|:---:|:---|
524524
| `url` | `string` | (required) | Database URL (`libsql://`, `https://`, `file:`, `:memory:`) — in replica mode, also the local file |
525525
| `authToken` | `string?` | — | JWT auth token for remote databases |
526-
| `encryptionKey` | `string?` | — | Encrypts nothing, in any mode: refused for a local or replica datasource (`VALIDATION_ERROR` / 400 at construction, a `custom` issue at authoring), because the local engine (better-sqlite3) has no encryption codec, and for a `file:` url or `:memory:` under a forced `mode: 'remote'`, whose libSQL client is handed no key; a remote database's storage encryption is the server's, not set by this key |
527526
| `concurrency` | `number` | `20` | Maximum concurrent requests |
528527
| `syncUrl` | `string?` | — | Remote sync URL for embedded replica mode |
529528
| `sync.intervalSeconds` | `number` | `60` | Periodic sync interval (0 = manual only) |

‎docs/qa/platform-checklist/areas/integration-system.json‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1716,10 +1716,10 @@
17161716
],
17171717
"knownGaps": [
17181718
"The legacy-alias clause tests a row shape the CURRENT parse refuses to create — a stored row from before the aliases moved to `guidance`. It is only reachable by writing the row directly (⚠️ NOT through the runtime wizard: POST /api/v1/datasources refuses an alias-spelled config key 400 DATASOURCE_ADMIN_ERROR — the aliases moved to `guidance`, which refuses them, #8078 — measured on a stock showcase boot with `passwd` and by QA run #21056. The run planted its rows with a direct sys_metadata write while the server was STOPPED, then booted and read through both doors). If the run cannot write such a row, that clause is blocked(fixture) and MUST be recorded — a redaction that has never been tested against the shape it exists for is an untested redaction.",
1719-
"`encryptionKey` (turso) is deliberately STILL WRITABLE on a REMOTE turso datasource while being redacted on read (#8081 scope item 4). Scoring it as a write refusal there is a false fail; the correct expectation is write-accepted + read-redacted. On a local or embedded-replica turso datasource (a file: url, with or without syncUrl) the write IS refused since #25020, on config.encryptionKey: the driver cannot encrypt that file, so that refusal is the correct expectation, not a false fail.",
1719+
"`encryptionKey` (turso) is RETIRED since #25050 (it encrypted nothing in any mode): the parse refuses it on EVERY turso datasource, remote or local, on config.encryptionKey with the removal prescription, so a write refusal is the correct expectation everywhere. A stored row from before the retirement may still carry it; such a row is reachable only by a direct sys_metadata write, and its value must still be redacted on read through both doors (packages/spec/src/data/datasource-credential-redaction.ts#RETIRED_CREDENTIAL_KEYS). Plant it beside a file: url: the load-path conversion (turso-config-encryption-key-removed) deletes it beside a remote url on the datasource loaders, and leaves a local-url value for its author.",
17201720
"acceptance[1] (the credential-bound datasource actually connects) and acceptance[6] (judged by a connect probe after the echoed-config round-trip) need a reachable, credential-protected database of a SHIPPED driver (postgres, mysql or mongodb — the driver id since 18; an authored `mongo` is refused by name, packages/spec/src/data/datasource.zod.ts). Stock showcase provides none, but the QA container has a PostgreSQL 16 install (/usr/lib/postgresql/16/bin, with a `postgres` system user), and QA run #24575 proved a recipe on it that scored both clauses pass (#24615 reused it): as `postgres`, `initdb -A scram-sha-256` into a scratch data directory, then `pg_ctl … -o '-p PORT -k SOCKETDIR -c listen_addresses=127.0.0.1' start`; create a login role WITH a password and a database it owns; bind it through POST /api/v1/datasources with the password in the top-level `secret`, never in `config`, so the door binds external.credentialsRef. Every parent directory of the data and socket directories must be traversable by `postgres`. Runs before #24575 recorded both clauses blocked(environment) (#21784, #21845). CI has postgres and mysql only inside the Temporal Conformance (live PG + MySQL) job (.github/workflows/ci.yml services postgres:16 and mysql:8.0), which runs no checklist item, and has no mongodb anywhere. ⛔ Never score either clause from a successful publish alone, which proves the paved path is accepted, not that it connects. A run that cannot start the database still reads the stored-credential half of acceptance[6] (after the PATCH, the admin door still reports hasSecret and the bound secret handle is unchanged); record that as a partial reading, separate from the connect half",
17211721
"the plugin-driver boundary is RULED (#21921, docs note in content/docs/data-modeling/drivers.mdx): for a driver the platform ships no contract for, the platform neither validates config nor guesses which of its keys are credentials. Keeping credentials out of a plugin driver's config, through external.credentialsRef, is the plugin author's responsibility. Only the platform's FIXED credential spellings are redacted there by name (acceptance[5]). A credential under any other key of a plugin driver's config being stored and served to administrators as written is that boundary, never a finding",
1722-
"no datasource read door serves the mask literal (packages/spec/src/data/secret-mask.ts#SECRET_MASK is read by the encrypted-field read path and the settings REST boundary, not here): the admin door OMITS each withheld credential key and names it in redactedConfigKeys beside hasSecret, and the metadata door serves the external.credentialsRef handle. So the round-trip acceptance[6] drives carries no mask, and what it exercises is the carry-forward of the withheld keys on PATCH (packages/services/service-datasource/src/datasource-config-redaction.ts#restoreRedactedConfig). QA run #24575 also measured a hand-written literal mask: PATCH /api/v1/datasources/NAME with `secret` set to it is accepted 200 and rewraps the stored secret to the mask (the reconnect then fails), and `config.encryptionKey` set to it overwrites the stored key. No read door serves that string, so it is not the echoed case acceptance[6] and negative[5] score; it is held for the maintainer on #24579 §7, where no contract declares it. ⛔ Score it under neither"
1722+
"no datasource read door serves the mask literal (packages/spec/src/data/secret-mask.ts#SECRET_MASK is read by the encrypted-field read path and the settings REST boundary, not here): the admin door OMITS each withheld credential key and names it in redactedConfigKeys beside hasSecret, and the metadata door serves the external.credentialsRef handle. So the round-trip acceptance[6] drives carries no mask, and what it exercises is the carry-forward of the withheld keys on PATCH (packages/services/service-datasource/src/datasource-config-redaction.ts#restoreRedactedConfig). QA run #24575 also measured a hand-written literal mask: PATCH /api/v1/datasources/NAME with `secret` set to it is accepted 200 and rewraps the stored secret to the mask (the reconnect then fails). No read door serves that string, so it is not the echoed case acceptance[6] and negative[5] score; it is held for the maintainer on #24579 §7, where no contract declares it. ⛔ Score it under neither"
17231723
]
17241724
},
17251725
"steps": [
@@ -1730,7 +1730,7 @@
17301730
"read the SAME datasource back through the metadata door (GET /api/v1/meta/datasource/<name>) — a different code path with its own redaction hook",
17311731
"plant a stored row carrying a legacy alias spelling (passwd / pwd / token / jwt / auth_token / authtoken) and read it back through both doors",
17321732
"author a datasource for a driver the platform ships no contract for, carrying a canonically-spelled credential key (password or authToken) AND one non-canonical credential-looking key (for example apiSecret), and read it back. The second key observes the ruled boundary; it is not a redaction target",
1733-
"for turso specifically, write `encryptionKey` on a remote (libsql:// or https://) turso datasource, then read it back through both doors",
1733+
"for turso specifically, confirm `encryptionKey` is refused at publish on a remote (libsql:// or https://) AND a local (file:) turso datasource, then plant a stored row carrying it beside a file: url and read it back through both doors",
17341734
"grep the full response bodies of every read above for the planted cleartext values, and of the third read door, GET /api/v1/data/sys_metadata?type=datasource (admin only), which serves the stored row and also redacts (QA run #24575)"
17351735
],
17361736
"acceptance": [
@@ -1797,7 +1797,7 @@
17971797
"ADR-0100 (credential read mask; §B3 echoed-mask write guard)",
17981798
"ADR-0015 / ADR-0062 (credentials route through datasource config)",
17991799
"packages/spec/src/data/secret-mask.ts#SECRET_MASK (the ADR-0100 mask literal; its two readers are the encrypted-field read path and the settings REST boundary, and neither datasource read door imports it: both withhold the key instead, knownGaps[4])",
1800-
"packages/spec/src/data/datasource-credential-redaction.ts#STILL_WRITABLE_CREDENTIAL_KEYS (the ONE definition of 'what is a credential key' — schema-derived z.never() refusals, FORMER_CREDENTIAL_ALIASES, STILL_WRITABLE_CREDENTIAL_KEYS)",
1800+
"packages/spec/src/data/datasource-credential-redaction.ts#RETIRED_CREDENTIAL_KEYS (the ONE definition of 'what is a credential key' — schema-derived z.never() refusals, FORMER_CREDENTIAL_ALIASES, RETIRED_CREDENTIAL_KEYS)",
18011801
"packages/spec/src/kernel/metadata-type-redaction.ts (the per-type redaction hook — the metadata read door)",
18021802
"packages/services/service-datasource/src/datasource-config-redaction.ts + datasource-credential-migration.ts (the datasource-admin read door)",
18031803
"packages/spec/src/migrations/entries/semantic/17.datasource-config-inline-credential-refused.ts · 17.datasource-config-placeholder-refused.ts · 17.datasource-config-url-userinfo-refused.ts · 17.connector-inline-authentication-publish-refused.ts",

‎packages/cli/src/commands/migrate/data-commands.absent-database.integration.test.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -429,7 +429,7 @@ await k('sys_audit_log').insert({
429429
id: 'aud_21552', object_name: 'sys_metadata', record_id: 'm_21552', action: 'create',
430430
new_value: JSON.stringify({
431431
id: 'm_21552', name: 'ds', type: 'datasource', scope: 'platform',
432-
metadata: JSON.stringify({ name: 'ds', driver: 'turso', config: { url: 'libsql://db.turso.io', encryptionKey: 'cleartext-21552' } }),
432+
metadata: JSON.stringify({ name: 'ds', driver: 'turso', config: { url: 'libsql://db.turso.io', authToken: 'cleartext-21552' } }),
433433
}),
434434
});
435435
const shard = (await k.raw("select name from sqlite_master where type = 'table' and name glob 'sys_activity__r*'"))[0];
@@ -440,7 +440,7 @@ await k(shard.name).insert({
440440
old: null,
441441
new: {
442442
id: 'm_21552', name: 'ds', type: 'datasource', scope: 'platform',
443-
metadata: JSON.stringify({ name: 'ds', driver: 'turso', config: { url: 'libsql://db.turso.io', encryptionKey: 'cleartext-21552-activity' } }),
443+
metadata: JSON.stringify({ name: 'ds', driver: 'turso', config: { url: 'libsql://db.turso.io', authToken: 'cleartext-21552-activity' } }),
444444
},
445445
}),
446446
});

‎packages/cli/src/commands/migrate/preview-read-only.integration.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ const AUDIT_COPY = JSON.stringify({
106106
name: 'ds',
107107
type: 'datasource',
108108
scope: 'platform',
109-
metadata: JSON.stringify({ name: 'ds', driver: 'turso', config: { url: 'libsql://db.turso.io', encryptionKey: CRED } }),
109+
metadata: JSON.stringify({ name: 'ds', driver: 'turso', config: { url: 'libsql://db.turso.io', authToken: CRED } }),
110110
});
111111

112112
/** Env that would point the command's boot somewhere other than the fixture. */

‎packages/cli/src/utils/storage-driver.test.ts‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -645,7 +645,6 @@ describe('#7314 — the shared libSQL config builder against the real TursoDrive
645645
config: {
646646
url: 'libsql://my-db.turso.io',
647647
authToken: 'jwt-token',
648-
encryptionKey: 'aes-256-key',
649648
concurrency: 7,
650649
syncUrl: 'libsql://replica.turso.io',
651650
sync: { intervalSeconds: 30, onConnect: false },
@@ -656,10 +655,14 @@ describe('#7314 — the shared libSQL config builder against the real TursoDrive
656655
const built = buildTursoDriverConfig(spec, resolveTursoUrl(spec));
657656
const pinned: TursoDriverConfig = built;
658657

659-
expect(pinned.encryptionKey).toBe('aes-256-key');
658+
// `encryptionKey` is retired (#25050): typed `never` on BOTH sides, so the
659+
// compile-time pin above holds it as read-and-forwarded (a legacy stored
660+
// row reaches the constructor's refusal), and no datasource can declare it.
661+
expect(pinned).not.toHaveProperty('encryptionKey');
660662
expect(pinned.syncUrl).toBe('libsql://replica.turso.io');
661663
expect(pinned.sync).toEqual({ intervalSeconds: 30, onConnect: false });
662-
expect(Object.keys(built).sort()).toEqual([...TURSO_DRIVER_CONFIG_KEYS].sort());
664+
expect(Object.keys(built).sort())
665+
.toEqual(TURSO_DRIVER_CONFIG_KEYS.filter((k) => k !== 'encryptionKey').sort());
663666
});
664667
});
665668

0 commit comments

Comments
 (0)