Skip to content

Commit 7316682

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-25111-sso-dns-record
2 parents 7f0292d + 2354058 commit 7316682

69 files changed

Lines changed: 2566 additions & 1061 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/driver-turso': minor
4+
'@objectstack/service-datasource': minor
5+
---
6+
7+
feat(spec,driver-turso)!: turso `config.encryptionKey` is removed — it never encrypted anything, in any mode, and is now refused everywhere it is written instead of accepted on a remote datasource
8+
9+
Clause-②: no (narrowing)
10+
11+
Metadata-protocol: narrowed
12+
13+
<!-- adr-0087: registered turso-config-encryption-key-removed, turso-config-encryption-key-retired -->
14+
15+
`encryptionKey` was declared as an AES-256 key for the database file. Nothing ever applied it. A local or embedded-replica file is read and written through better-sqlite3, which has no encryption codec, so #25020 already refused the key there. A remote database was never handed the key either: its storage encryption is configured on the server. That left a key that parsed on a remote datasource and did nothing. It was also the one credential-shaped key a datasource could still write in plain text (`STILL_WRITABLE_CREDENTIAL_KEYS`). Under ADR-0049 (enforce-or-remove) it is removed.
16+
17+
**BREAKING** accept-set narrowing on a published schema, a published constructor and an exported type. It ships as `minor` under the repo's launch-window convention for breaking changes (`scripts/check-changeset-no-major.mjs`).
18+
19+
- **`@objectstack/spec`**: `TursoConfigSchema.encryptionKey` is a `retiredKey()` tombstone. Authoring it is a `tsc` error and a parse error at `config.encryptionKey`, in every mode and with any value, the empty string included. The error carries the removal prescription. It is the only issue reported on that config, whatever else is wrong. The misspellings `encryption_key` and `encryptionkey` get the same prescription. The #25020 local and replica refusal arms are gone, since the tombstone subsumes them.
20+
- **`@objectstack/driver-turso`**: the published mirror `TursoConfigSchema` refuses the key the same way, as `z.never()`. `TursoDriverConfig.encryptionKey` is typed `never`. `new TursoDriver()` and `createTursoDriver()` refuse a defined `encryptionKey` with `VALIDATION_ERROR` / 400 in every mode, before anything else is checked, in the schema's words.
21+
- **`@objectstack/service-datasource`**: the exported `TursoDriverConfigInput.encryptionKey` is typed `never`. The shared loader still forwards a declared value, so a legacy stored row reaches the constructor's refusal instead of being dropped in silence.
22+
23+
### Migration: FROM → TO
24+
25+
| You wrote | Write instead |
26+
| --- | --- |
27+
| `url: 'libsql://my-db.turso.io', encryptionKey: '…'` (also `https://`, `ws(s)://`) | delete `encryptionKey`. Storage encryption for a remote database is configured on the Turso server |
28+
| `encryptionKey: ''` (or any unset value) | delete `encryptionKey` |
29+
| `url: 'file:./data/app.db', encryptionKey: '…'` (also `:memory:`, or a replica beside `syncUrl`) | delete `encryptionKey` (already refused since #25020). Put the file on an encrypted disk or volume if it must be encrypted at rest |
30+
31+
The fix in one line: delete `encryptionKey` from every turso datasource.
32+
33+
### The ADR-0087 disposition
34+
35+
- **D2 `turso-config-encryption-key-removed`** deletes the key from a turso datasource's `config` 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). It emits one notice per key. Nothing is lost: the driver never read the value.
36+
- A non-empty key beside a local url is **not** deleted. Removing it would let a datasource whose operator believed the file was encrypted boot in silence. It keeps the #25020 posture instead: the row fails at driver construction with the prescription, and the author deletes the key by hand.
37+
- The conversion is retired from the load path. A live author is refused at parse and taught. Stored rows (the datasource loaders), built artifacts inside their declared-floor window and `os migrate meta --from 17` replay it.
38+
- `RETIRED_KEYS_BY_MAJOR[18]` gains `data/TursoConfig:encryptionKey`. The D3 entry is `turso-config-encryption-key-retired`.
39+
40+
### Stored rows stay redacted
41+
42+
A tombstone is excluded from the schema-derived credential list, by its `[REMOVED]` prefix. Measured: after the retirement, no existing path would have withheld a stored `encryptionKey` on read. The derived turso list (`authToken`) is non-empty, so the canonical fallback never applies. `STILL_WRITABLE_CREDENTIAL_KEYS` is therefore renamed `RETIRED_CREDENTIAL_KEYS` and keeps `turso: ['encryptionKey']`. A legacy row's value is still withheld and still named in `redactedConfigKeys`, on the admin door and the metadata door alike.
43+
44+
### Blast radius
45+
46+
Measured on this tree: no example, template, CLI scaffold, published skill, host default or environment variable authors the key. A tree-scoped absence pin now holds that. Whether any out-of-repo deployment declares it is not measured, and is not claimed to be zero.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
'@objectstack/plugin-auth': patch
3+
'@objectstack/plugin-email': patch
4+
---
5+
6+
feat(plugin-auth, plugin-email): under the `open` and `email_domain` postures, a sign-up for an address that already has an account answers like a new sign-up and mails that address, while mail is really delivered
7+
8+
Clause-②: yes
9+
Metadata-protocol: none
10+
11+
The public email-and-password sign-up changes its answer in one case. Under the `open` and `email_domain` audience postures, a sign-up for an address that already has an account was refused explicitly. Now, when the deployment's mail transport really delivers, it answers exactly as a successful sign-up does on that deployment, and the address's holder receives a mail saying the address already has an account, with a link to sign in and a link to reset the password. Nothing is written: no new user, credential or session. The caller can no longer tell from the answer whether the address was registered.
12+
13+
- **When the explicit refusal stays, unchanged.**
14+
- The mail would not be delivered: the email service says its transport is not live (`IEmailService.isConfigured()` answers `false`, which plugin-email answers for its `log` transport in every environment), the email service does not implement `isConfigured()`, or no email service is wired. A success with no mail behind it is what the refusal replaced, so it is never answered.
15+
- The posture is `invite_only`. A stranger still learns nothing there, and an invited person whose address already has an account is still refused.
16+
- The posture is `open` with email verification declared off (and auto sign-in left on). A new sign-up there is signed in at once, and an existing address cannot get that answer without a session for someone else's account.
17+
- **Read at sign-up time.** The transport is asked on every sign-up, so a transport saved from the Mail settings applies to the next sign-up without a restart.
18+
- **The mail.** plugin-email ships a new built-in template, `auth.existing_account_sign_up`, in `en-US`, `zh-CN`, `ja-JP` and `es-ES`. It is registered like the other built-in auth templates, so an app declaration or a Studio save overrides it. Its variables are `user.name` (optional), `user.email`, `signInUrl`, `resetPasswordUrl` and `appName` (optional). The two links are the Console's sign-in and forgot-password pages and carry no token. `BUILTIN_AUTH_TEMPLATES` grows from 28 rows to 32.
19+
- **The mail's language.** The recipient's stored language (`sys_user.locale`), then the deployment default. The language of the browser that asked is never used, because the person signing up may not be the address's holder.
20+
- **For a client of the sign-up door.** In the configurations above, a screen that showed an "already registered" message on the refusal now receives the success answer instead, and should show its usual "check your email" step.
21+
- **A send that fails after the answer.** The answer is still the success, and the server logs one error-level line saying the mail was not sent. A verification mail that fails for a new address leaves the same gap.
22+
- **Unchanged.** A sign-up for a new address, every refusal the audience posture raises, and every other auth mail.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@objectstack/plugin-security': patch
3+
---
4+
5+
fix(plugin-security)!: the raw-SQL read scope (`getReadFilter`) answers the deny sentinel for a delegated context the operation prologue refuses — the delegator's membership must back the organization the read runs in, on a walled posture
6+
7+
Clause-②: no (narrowing)
8+
Metadata-protocol: none
9+
10+
<!-- adr-0087: not-required (no-migration-prescription) No metadata moves: no spec key, authorable spelling, object definition or stored shape is added, removed, renamed or re-shaped, and no exported declaration is removed or narrowed, so there is nothing for `objectstack migrate meta` to rewrite. What changes is the read scope the security service answers an in-process caller that builds an on-behalf-of execution context in an organization the delegator does not belong to; the remedy is to build that context in an organization the delegator is a member of, never a rewrite of a package source or a stored row. -->
11+
12+
**BREAKING** (the security service's read scope narrows for one context shape). ADR-0021 D-C makes the raw-SQL read scope the find path's scope, and the provider's own contract is agreement with the engine middleware. Under `isolated` or `group`, the delegated-principal prologue already refuses every operation of an on-behalf-of context whose active organization the delegator's membership does not back (ADR-0090 D10, `vetOrganizationClaim`). `getReadFilter` did not ask that question, so for the same context it answered a composed read scope where the find path refuses. It now asks it, once, beside the dangling-delegator refusal it already mirrored.
13+
14+
- **FROM:** under `isolated` or `group`, `getReadFilter` for an execution context carrying `onBehalfOf` and an active organization the delegator is not a member of answered a read scope composed from the two principals' layers, where the engine's find refuses the same context.
15+
- **TO:** it answers `RLS_DENY_FILTER` itself (the spec's sentinel, by reference), so a door that bypasses the engine reads zero rows for that context, as the engine's find refuses it.
16+
- **Unchanged:** the `single` posture, an on-behalf-of context with no active organization, an on-behalf-of context in an organization the delegator is a member of, a context without `onBehalfOf`, a dangling delegator (still the deny sentinel) and system contexts. The `/mcp` OAuth door, the one producer of an on-behalf-of context in the platform, vets the request's organization against the same membership, so the contexts it builds keep their scope.
17+
- **The fix for an in-process producer:** code that builds an `ExecutionContext` with `onBehalfOf` by hand and hands it to the security service or to the analytics service must set its `tenantId` to an organization the delegating user is a member of, or leave it unset.

‎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

0 commit comments

Comments
 (0)