Skip to content

Commit 172b4cf

Browse files
fix(spec)!: TursoConfigSchema refuses the turso configs the driver refuses or ignores (#19977) (#20199)
Fixes #19977 Clause-②: no The `Clause-②: no` line above is the claim's line (comment 5852270976), copied as it stands. The changeset carries `Clause-②: no (narrowing)`, because AGENTS.md makes a narrowing BREAKING and `check:adr-0087-registration` reads the arm there. Both lines give the same value. Nothing is widened: no key is added, removed or renamed, and no exported symbol moves. Session `session_01Rjy9MeetSfq34PKn81CRiN` (PM dispatch, `domain:spec` seat 1), branch `claude/issue-19977-turso-config-authoring-refusals`. Base `49144fcc`, then merged with `origin/main` at `369bcbed`, which includes #20104 as `84880f92`. Head `59c2391e`. Every reading below was taken on that merged tree unless it says otherwise. ## 1. The premise, measured - **Spec copy** (`packages/spec/src/data/driver/turso.zod.ts`): the `superRefine` checked only `sync` without `syncUrl`. **Driver-local mirror** (`packages/drivers/driver-turso/src/spec/turso.zod.ts`): no refinement at all, and no `mode` key. It is a plain `z.object`, so an authored `mode` is stripped. - **What the constructor refuses on `main`.** Read from the source, not from notes: `localEngineDefect`, `refuseWebSocketTimeout`, `refuseSuppliedClientTimeout` and `detectMode` in `turso-driver.ts`. Probed on the built dist at `49144fcc`, the constructor refuses every row below and **both schemas accepted every one of them**: | authored config | `new TursoDriver` | spec / mirror before | | --- | --- | --- | | `libsql://`, `https://`, `wss://`, `LIBSQL://` url + `syncUrl` | VALIDATION_ERROR / 400 | accept / accept | | `libsql://` url + `mode: 'replica'` or `'local'` | VALIDATION_ERROR / 400 | accept / accept | | bare path, `sqlite:`, `:MEMORY:`, `libsql:` with no `//` (no mode, or `syncUrl`, or `mode: 'local'`) | VALIDATION_ERROR / 400 | accept / accept | | `:memory:`, `file::memory:`, `FILE::memory:`, `file::memory:?cache=shared` + `syncUrl`; `:memory:` + `mode: 'replica'` | VALIDATION_ERROR / 400 | accept / accept | | `wss://` or `WS://` + `timeoutMs` (no mode, or `mode: 'remote'`) | VALIDATION_ERROR / 400 | accept / accept | - **Arm 2, re-probed on the built dist at `49144fcc`.** `new TursoDriver({ url, mode: 'remote', syncUrl, sync: { intervalSeconds: 60 } })` constructs and connects. After that, `isSyncEnabled` answers `true`, no interval is started, and the sync call rejects with `SYNC_NOT_SUPPORTED` (on a `libsql://` url) or `SyncNotSupported("File")` (on a `file:` url). The built `createRemoteClient` forwards `url`, `authToken`, `concurrency` and `fetch`, and no `syncUrl`. - **#20104 (merged as `84880f92`).** Its `turso-driver.ts` hunks sit at `:26`, `:588` and `:2720` only (imports, and the remote refusals of inherited members). `constructor`, `detectMode`, `localEngineDefect`, the `refuse*` helpers, `isSyncEnabled`, the sync method and `createRemoteClient` are untouched, and its only `syncUrl` hit is a message string. So the refused set this PR mirrors is unchanged by it, and it covers nothing here. ## 2. The change - **`@objectstack/spec` `TursoConfigSchema`.** A new module-local `tursoTransportIssues` runs inside the existing `superRefine`, after the `sync`-without-`syncUrl` refusal. That refusal is byte-identical and pinned verbatim. The predicates mirror the constructor's: a scheme matches in any letter case, `:memory:` matches exactly, and a `file:` url whose path is `:memory:` (or starts `:memory:?`) is in-memory. The url is read **trimmed**, because both loaders trim it before construction (`resolveTursoUrl`), and `syncUrl` counts when it is a non-empty string, as `buildTursoDriverConfig` forwards it. Checks run in the constructor's order, so each config gets exactly one refusal on `url`. - **The same refinement in the driver-local mirror, byte for byte, plus the spec's `sync`-without-`syncUrl` refusal in the same words.** The mirror had no refinement at all. The driver ignores `sync` without `syncUrl`, and the one-table parity pin below needs the mirror to answer every row the spec answers. The helper is not shared, deliberately. It is internal to `@objectstack/spec` and not on a published entry point, and publishing a turso-specific function only to feed one mirror would grow the spec's public surface. One case table holds the two copies together instead. - **`url` describe** (spec), before → after: - `libSQL endpoint or local file: a remote libsql/https Turso URL, a file path, or :memory:` - `libSQL endpoint or local file: a remote libsql/https Turso URL, a local file written as a file: URL (never a bare path), or :memory:` `content/docs/references/data/driver-turso.mdx` was regenerated from it (`check:generated --fix`, the only stale artifact). - **ADR-0087:** a new semantic entry, `turso-config-transport-mismatch-refused` (`packages/spec/src/migrations/entries/semantic/18.turso-config-transport-mismatch-refused.ts`), with `registry.ts` regenerated. The changeset `.changeset/19977-turso-config-transport-refusals.md` is `minor` for `@objectstack/spec` and `@objectstack/driver-turso`, with the FROM → TO table and the `registered` marker. ## 3. Every refusal, verbatim (rendered from the built spec dist) Each is one `custom` issue on the key named in the heading. **on `url` — a remote url beside `syncUrl`** (`{ url: 'libsql://db.turso.io', syncUrl: 'libsql://db.turso.io' }`): > `url` is a remote `libsql://` url, but `syncUrl` makes this datasource an embedded replica, which runs every read and write through a local SQLite engine that cannot open a remote url — the turso driver refuses this configuration when it starts. (An embedded replica is a local file kept in sync with the remote: @libsql/client builds no replica for a remote url and ignores `syncUrl` beside one.) For a remote database, drop `syncUrl` (and `sync`) and keep the remote url alone. For an embedded replica, point `url` at a local file and keep the remote in `syncUrl`: `url: 'file:./data/replica.db'`. **on `url` — a remote url under a forced `mode: 'replica'`** (`{ url: 'libsql://db.turso.io', mode: 'replica' }`): > `url` is a remote `libsql://` url, but `mode: 'replica'` makes this datasource an embedded replica, which runs every read and write through a local SQLite engine that cannot open a remote url — the turso driver refuses this configuration when it starts. (An embedded replica is a local file kept in sync with the remote: @libsql/client builds no replica for a remote url and ignores `syncUrl` beside one.) For a remote database, drop `mode` (a `libsql://` url is detected as remote) or set `mode: 'remote'`. For an embedded replica, point `url` at a local file and name the remote in `syncUrl`: `url: 'file:./data/replica.db'`. **on `url` — a remote url under a forced `mode: 'local'`** (`{ url: 'https://db.turso.io', mode: 'local' }`): > `url` is a remote `https://` url, but `mode: 'local'` makes this datasource a local database, which runs every read and write through a local SQLite engine that cannot open a remote url — the turso driver refuses this configuration when it starts. For a remote database, drop `mode` (a `https://` url is detected as remote) or set `mode: 'remote'`. For a local database, point `url` at a file: `url: 'file:./data/app.db'`. **on `url` — a url that is none of `file:`, `:memory:` or remote** (`{ url: './data/app.db' }`): > `url` is not a url the turso driver can open: it is not `:memory:`, not a `file:` url, and not a remote `libsql://`, `https://`, `http://`, `wss://` or `ws://` url (a scheme matches in any letter case). With no `mode` and no remote scheme, this datasource is a local database, which runs every read and write through a local SQLite engine that opens only a `file:` url or `:memory:` — the turso driver refuses this configuration when it starts. For a local database file, spell the path as a `file:` url: `url: 'file:./data/app.db'`. For a throwaway in-memory database, `url: ':memory:'`. For a remote database, use one of the remote schemes above. The same refusal beside `syncUrl` (`{ url: './data/replica.db', syncUrl: … }`) swaps in the replica wording: > … `syncUrl` makes this datasource an embedded replica, which runs every read and write through a local SQLite engine that opens only a `file:` url or `:memory:` — the turso driver refuses this configuration when it starts. For an embedded replica, spell the local path as a `file:` url and keep the remote in `syncUrl`: `url: 'file:./data/replica.db'`. For a remote database, drop `syncUrl` (and `sync`) and use one of the remote schemes above. **on `url` — a replica on an in-memory url** (`{ url: ':memory:', syncUrl: … }`): > `url` names an in-memory database, so it cannot hold the embedded replica `syncUrl` asks for: a replica is a local file kept in sync with the remote named in `syncUrl` — the turso driver refuses this configuration when it starts. Point `url` at a local file (`url: 'file:./data/replica.db'` beside `syncUrl`), or drop `syncUrl` (and `sync`) for a plain in-memory local database. With `mode: 'replica'` instead of `syncUrl`, the way out reads `or drop \`mode: 'replica'\` for a plain in-memory local database.` **on `timeoutMs` — a window beside a WebSocket url in remote mode** (`{ url: 'wss://db.turso.io', timeoutMs: 5000 }`): > `timeoutMs` is set beside a `wss://` url, which rides @libsql/client's WebSocket transport, and that transport takes no timeout: the window would bound nothing, so the turso driver refuses this configuration when it starts. Either drop `timeoutMs` and run this remote database unbounded, or keep it and spell the url `libsql://` or `https://`, where every request is bounded. **on `syncUrl` — `syncUrl` under a forced `mode: 'remote'`** (constructed and ignored by the driver; `{ url: 'libsql://db.turso.io', mode: 'remote', syncUrl: … }`): > `syncUrl` configures an embedded replica, but `mode: 'remote'` sends every read and write straight to `url` and builds no replica: the turso driver never hands `syncUrl` to the remote client and runs no sync, so the setting changes nothing. For a remote database, drop `syncUrl` (and `sync`). For an embedded replica, drop `mode` and point `url` at a local file beside `syncUrl`: `url: 'file:./data/replica.db'`. **on `sync`, in the mirror only** (`{ url: 'file:…', sync: {…} }`), the spec's existing words, unchanged: > `sync` configures embedded-replica syncing, which only runs when `syncUrl` names the remote to replicate from. Set `syncUrl`, or remove `sync` — on its own it configures nothing. No message echoes a url, only a remote url's scheme, because a url may carry a token. No message carries an issue number. **Deliberately NOT refused**, because the constructor accepts them: an uppercase `LIBSQL://` or `FILE:` url; `file:` + `mode: 'replica'` with no `syncUrl`; `file:` + `syncUrl` under `mode: 'local'`; any url under a forced `mode: 'remote'`, including `file:` and a bare path (the constructor does not judge it, and `@libsql/client` refuses a bare path at connect); `timeoutMs` beside `libsql://` / `https://`; `timeoutMs` on a replica syncing from `wss://`; an empty `syncUrl`; and a whitespace-padded url, which is read trimmed as the loaders read it. Each is a preservation row below. ## 4. Tests - **`packages/drivers/driver-turso/src/spec/turso-config-constructor-parity.test.ts`** (new): one table of **54 rows**. For each row it asserts (a) the constructor's verdict, with a refusal asserted as `code: 'VALIDATION_ERROR'`, `status: 400`; (b) the spec contract's verdict, which must be refused exactly when the constructor refuses or the row is marked inert, as ONE `custom` issue on the named key; and (c) the mirror's verdict, whose `{ path, code, message, count }` must **equal** the spec's (messages byte-identical). Rows that force a `mode` skip (c), because the mirror strips `mode` (16 skips). Row floors per verdict class make a shrunken table fail. The spec half resolves `@objectstack/spec/data` through its built `dist`, per the `KNOWN_UNALIASED_TEST_IMPORTS` pair `driver-turso` → `spec`. - **`packages/spec/src/data/driver/turso.test.ts`:** 14 new refusal cases. Each asserts the issue code, the path, the prescription substrings and that there is exactly one issue. It also asserts the `DatasourceSchema` door (re-pathed to `config.url`), the `validateDriverConfig` door, the unchanged `sync` message verbatim, and the describe text. The preservation list gained 11 accepted configs. - **Fixtures rewritten by the pin sweep** (a repo-wide grep for turso configs spelling a remote url beside `syncUrl`, a forced mode or a bare url, parsed by either schema): 1. `packages/spec/src/data/driver/turso.test.ts`: the `sync.intervalSeconds` fixture put `syncUrl` beside a remote url. It now uses `url: 'file:./data/replica.db'`. The spelling changed and the assertion is unchanged. 2. `packages/drivers/driver-turso/src/spec/turso.test.ts` "all fields": same, `url: 'file:./local-replica.db'`. 3. `packages/drivers/driver-turso/src/spec/turso.test.ts` "environment variable patterns" **pinned the opposite**: `url: '${TURSO_DATABASE_URL}'` was asserted as accepted. The constructor refuses that literal (it is none of `file:`, `:memory:` or remote, in local mode), and nothing resolves a placeholder. The case is replaced by a refusal assertion: `code: 'custom'`, `path: ['url']`, and the prescription. 4. `packages/services/service-datasource/src/__tests__/datasource-config-redaction.test.ts`, the #8337 legacy row. It put both token-bearing urls on a remote `url` beside `syncUrl`, which `updateDatasource` now refuses at the door: 2 of 693 were red on the first run. Both url-bearing keys still carry `?authToken=` and are still redacted and restored independently, but the row is now an embedded replica on `file:./data/replica.db?authToken=…`, and the author's hand edit is `file:./data/elsewhere.db`. This file is outside the claim's first file surface. The seat amended the surface for it. Loader fixtures in `@objectstack/runtime` (`turso-driver-factory.convergence.test.ts`), `@objectstack/cli` (`storage-driver.test.ts`) and `@objectstack/service-datasource` (`turso-driver-config.test.ts`) also spell a remote url beside `syncUrl` + `mode: 'replica'`. They exercise only `buildTursoDriverConfig` or a capturing constructor, never this schema or the real driver, so they are left unchanged (see Acceptance notes). Readings on the merged tree: | suite | result | | --- | --- | | `@objectstack/driver-turso` vitest (whole package) | 72 files · 1909 passed · 16 skipped · exit 0 | | `@objectstack/driver-turso` typecheck (`tsc --noEmit`, program includes both touched test files) | exit 0 | | `@objectstack/service-datasource` vitest (consumer) | 34 files · 693 passed · exit 0 | | `@objectstack/service-datasource` typecheck | exit 0 | | `@objectstack/spec` typecheck (`tsc` + scripts + `check:test-typecheck`) | exit 0 | | `@objectstack/spec` vitest `--project local`, 3 shards | 540 files · 15835 passed · 2 todo (5637 + 4846 + 5352), exit 0 on each shard | Before the merge, at `dd67e8ed`, the spec suite read 540 files · 15812 passed · 2 todo in 3 shards, and the targeted `src/data/driver`, `src/data/datasource`, `src/migrations`, `src/conversions` and `src/shared` run read 55 files · 1937 passed. ## 5. Reverse verification (ablations, via `scripts/ablation-replace.mjs`, restore proven) Both ablations ran from the committed state. Each replaced the refinement's loop source `tursoTransportIssues(cfg)` with `tursoTransportIssues(cfg).slice(0, 0)`, which empties the refusals and leaves everything else in place. - **Spec copy**, running `src/data/driver/turso.test.ts` (spec tests import from `src/`, so no build is involved): mutation landed (anchor 1 → 0, blob `5e1932a6` → `3725d3fa`). **14 failed · 15 passed**: every new refusal case went red, and the preservation, describe and pre-existing cases stayed green. Restored: blob == HEAD (`5e1932a6`), `git diff HEAD` empty. - **Mirror**, running `src/spec/` in driver-turso (the mirror is imported from source): mutation landed (blob `7b32d246` → `1bcc7514`). **22 failed · 157 passed · 16 skipped**: exactly the 21 mirror-equality rows for `url` / `timeoutMs` refusals without a forced mode, plus the flipped placeholder case. The mirror's `sync` row stayed green, because its refusal is a separate block. Restored: blob == HEAD (`7b32d246`), `git diff HEAD` empty. The direction observed was the expected one (red). ## 6. Gates `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` was re-derived on the actual change set at `b7c250ca` (10 paths vs merge base `369bcbed`): **114 commands**, 36 more than the dispatch-time lead, because of `content/docs/references/**`, the changeset and the service-datasource test. All 114 were run with exit codes written to disk first, and re-run at head `59c2391e` after the last commit. **Result at `59c2391e`: 111 exit 0, and 3 exit 3 (`PREREQUISITE NOT MET`, NOT MEASURED): `check:skill-examples` (no `packages/client-react/dist`), `check:dual-build-cjs-loads` (59 packages have no `dist/`) and `check:type-check-debt` (12 ledgered dependencies have no built types). All three need a whole-workspace build, which is CI's run. `dispatch-gates --ran` reconciliation: `114 derived famil(ies) accounted for — 111 run, 3 NOT-MEASURED (3 DERIVED from a recorded exit 3)`, 0 UNRUN.** Notable readings: `check-adr-0087-registration` → `registered turso-config-transport-mismatch-refused (new here: …)`; `check:generated` → `All 15 generated artifacts are up to date`; `check:changeset-no-major` → no `major` bump; `check:authorable-surface`, `check:api-surface`, `check:docs`, `check:migration-registry`, `check:test-source-alias`, `check:cross-package-test-inputs`, `check:nul-bytes`, `check:doc-authoring` → exit 0. The six artifact-roster gates whose roster sits under a touched directory were also run: `check:meta-url-spelling`, `check:authz-resolver`, `check:error-code-casing`, `check:filter-alias-parity`, `check:object-def-param-keys` and `check:tenant-chokepoint`, all exit 0. Not run locally, and left to CI: `pnpm lint` and the whole-workspace type-check lanes. ## Arm 2's constructor half: answered, not written `turso-driver.ts` is fenced for this card. Should `new TursoDriver` also refuse `syncUrl` / `sync` under `mode: 'remote'`? **Yes, and the evidence is above.** The constructor accepts the pair, `isSyncEnabled` answers `true` for a sync that can never run, and the sync call rejects as not supported. That is a declared setting the runtime does not honour. The refusal would sit beside `refuseWebSocketTimeout` (remote mode, before `super()`), in the same `VALIDATION_ERROR` / 400 envelope, with this PR's `syncUrl` message as its wording. This PR refuses the pair at every authoring door. A stored row or a host-built config that bypasses the schema still constructs. Routed to the engine lane (carrier #20104's lane, `domain:engine`) in the report. ## Acceptance notes - **The mirror declares no `mode`.** zod strips an authored `mode`, so the mirror judges every config in the mode its url and `syncUrl` select. For example, it accepts `{ url: 'libsql://…', mode: 'replica' }` and returns it without `mode`. That shortness is documented in `docs/design/driver-turso.md` §10 and is not changed here. No in-repo producer parses this mirror (a census of `TursoConfigSchema` importers in this repo, objectui and cloud found its own test only). - **`mode: 'replica'` on a `file:` url with no `syncUrl`** constructs and runs as a plain local database: the replica sync arm needs `syncUrl`. It is the same family as arm 2 (a declared mode the driver does not honour), but no constructor refusal fixes its shape, so it is not refused here. - **A bare path under a forced `mode: 'remote'`** is accepted by the constructor and refused by `@libsql/client` at connect (`URL_INVALID`). That is loud, not silent, and the constructor does not judge it, so authoring does not either. - The loader fixtures listed in §4 spell a configuration both the constructor and the schema now refuse, while testing key forwarding only. They were left as they are. - No example, template, published skill or hand-written doc in this repo authors a refused combination. Stored datasource rows are not re-parsed on load (`assertValidConfig` runs on create, test connection, and an update that touches `config` / `driver`), so a stored row keeps loading. --- _Generated by [Claude Code](https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 4db1bf1 commit 172b4cf

10 files changed

Lines changed: 1018 additions & 14 deletions

File tree

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/driver-turso': minor
4+
---
5+
6+
fix(spec)!: a turso datasource config the driver refuses, or ignores a key of, is refused where it is written
7+
8+
Clause-②: no (narrowing) — nothing is widened. No key is added, removed or renamed and no exported symbol moves. Combinations of `url`, `syncUrl`, `mode` and `timeoutMs` that the turso driver refuses when it is built, and one it builds and then ignores, are now refused at parse.
9+
10+
`TursoConfigSchema` (the `turso` / `libsql` `datasource.config` contract in `@objectstack/spec`, and the published mirror in `@objectstack/driver-turso`) parsed each key on its own. So it accepted configurations that `new TursoDriver()` refuses with `VALIDATION_ERROR` / 400: a datasource published clean and then failed at boot, or at a test connection. Measured on `main` before the change, both schemas accepting every row:
11+
12+
```
13+
libsql:// (any scheme, any case) + syncUrl -> constructor refuses
14+
libsql:// + mode: 'replica' or mode: 'local' -> constructor refuses
15+
./data/app.db (a bare path), sqlite:, :MEMORY: -> constructor refuses
16+
:memory: or file::memory: + syncUrl -> constructor refuses
17+
wss:// or ws:// + timeoutMs -> constructor refuses
18+
libsql:// + mode: 'remote' + syncUrl (+ sync) -> constructs; syncUrl ignored
19+
```
20+
21+
On that last row the remote client is built without `syncUrl`, no sync interval starts, the driver's sync call rejects `SYNC_NOT_SUPPORTED`, and the driver still reports sync as enabled.
22+
23+
**BREAKING** accept-set narrowing on a published schema, shipped as `minor` under the repo's launch-window convention for breaking changes (`scripts/check-changeset-no-major.mjs`). Refused now, each as one `custom` issue on the key it names:
24+
25+
- **on `url`**, in a local or replica mode (a forced `mode: 'local'` / `'replica'`, or `syncUrl`, or a url that is not remote): a remote url (`libsql://`, `https://`, `http://`, `wss://`, `ws://`, in any letter case); a url that is none of a `file:` url, `:memory:` or a remote url, such as a bare path, another scheme, `:MEMORY:`, a remote scheme with no `//` or a blank url; and a replica on an in-memory url (`:memory:`, `file::memory:` in any case, with or without a query string);
26+
- **on `timeoutMs`**: a window beside a `wss://` / `ws://` url in remote mode;
27+
- **on `syncUrl`**: `syncUrl` under a forced `mode: 'remote'`. The constructor accepts this one, so it is refused at authoring only;
28+
- **on `sync`**, in the `@objectstack/driver-turso` mirror only: `sync` with no `syncUrl`, in the words the spec contract has always used for it.
29+
30+
The rules mirror the constructor's own: a scheme matches in any letter case, `:memory:` matches exactly, and the url is read trimmed, as both datasource loaders hand it to the driver. A forced `mode: 'remote'` keeps its url unjudged, as the constructor does. Nothing the constructor accepts is refused, the `syncUrl`-under-`mode: 'remote'` row aside. The mirror declares no `mode` key and strips an authored one, so it judges every config in the mode its url and `syncUrl` select. A test in `@objectstack/driver-turso` holds the constructor and both schemas to one case table of 54 rows, with messages compared byte for byte.
31+
32+
The spec `url` describe named "a file path" among the accepted spellings, which is the one spelling the driver refuses. It now reads "a local file written as a file: URL (never a bare path)".
33+
34+
### Migration: FROM → TO
35+
36+
| You wrote | Write instead |
37+
| --- | --- |
38+
| `url: 'libsql://my-db.turso.io', syncUrl: 'libsql://my-db.turso.io'` | a remote database: `url: 'libsql://my-db.turso.io'` alone. An embedded replica: `url: 'file:./data/replica.db', syncUrl: 'libsql://my-db.turso.io'` |
39+
| `url: 'libsql://my-db.turso.io', mode: 'replica'` (or `'local'`) | drop `mode`, or set `mode: 'remote'` |
40+
| `url: './data/app.db'` | `url: 'file:./data/app.db'` |
41+
| `url: ':memory:', syncUrl: …` | a replica on a file: `url: 'file:./data/replica.db'` beside `syncUrl`. An in-memory database: drop `syncUrl` and `sync` |
42+
| `url: 'wss://my-db.turso.io', timeoutMs: 30000` | `url: 'libsql://my-db.turso.io', timeoutMs: 30000`, or drop `timeoutMs` |
43+
| `url: 'libsql://my-db.turso.io', mode: 'remote', syncUrl: …` | drop `syncUrl` and `sync` |
44+
45+
Each refusal prints these ways out and names only a remote url's scheme, never the url, which may carry a token. Stored datasource rows are not re-parsed when they load, so a stored row keeps loading as before; creating, testing or editing its `config` through the datasource admin service, `defineStack` or `os validate` is refused at the key until it is rewritten. The constructor already refuses the first five rows at boot.
46+
47+
Blast radius, measured on this tree: no example, template, published skill or hand-written doc authors a refused combination. Four test fixtures spelled one and are rewritten in this change, each named in the PR: one in `@objectstack/spec`, two in `@objectstack/driver-turso` (one of them pinned a placeholder url as accepted), and the stored-row redaction fixture in `@objectstack/service-datasource`, now an embedded replica on a `file:` url. Loader fixtures in `@objectstack/runtime`, `@objectstack/cli` and `@objectstack/service-datasource` that spell a remote url beside `syncUrl` exercise only the config builder or a capturing constructor. They never parse this schema or build the real driver, and are unchanged. Whether any out-of-repo deployment declares such a config is NOT measured and is not claimed to be zero.
48+
49+
<!-- adr-0087: registered turso-config-transport-mismatch-refused -->

‎content/docs/references/data/driver-turso.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ Turso / libSQL Connection Configuration
6565

6666
| Property | Type | Required | Description |
6767
| :--- | :--- | :--- | :--- |
68-
| **url** | `string` | ✅ | libSQL endpoint or local file: a remote libsql/https Turso URL, a file path, or :memory: |
68+
| **url** | `string` | ✅ | libSQL endpoint or local file: a remote libsql/https Turso URL, a local file written as a file: URL (never a bare path), or :memory: |
6969
| **authToken** | `never` | optional | Set through the connection form's secret field or `external.credentialsRef` — encrypted into `sys_secret`, never stored in `config` |
7070
| **encryptionKey** | `string` | optional | AES-256 encryption key for the local database file (local/replica modes) |
7171
| **concurrency** | `integer` | optional | Maximum concurrent requests to the remote database |

0 commit comments

Comments
 (0)