Skip to content

Commit 40618fa

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-21880-search-companion-scope
2 parents eaddaca + faf8dce commit 40618fa

52 files changed

Lines changed: 4042 additions & 208 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: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/plugin-sharing': patch
3+
'@objectstack/plugin-hono-server': patch
4+
'@objectstack/hono': patch
5+
'@objectstack/runtime': patch
6+
---
7+
8+
Share-link passwords follow the platform's credential rules (#21839).
9+
10+
- **The stored hash never leaves the server.** The share-link mint response (`POST /api/v1/share-links`, and `ShareLinkService.createLink`'s return value) no longer carries `password_hash`. The list and the redemption result are projected the same way. A client that reads a link's password state keeps reading it from the redemption route's `NEEDS_PASSWORD` answer, as before.
11+
- **The stored form is the platform's slow password hash.** New passwords are hashed with scrypt at the parameters account passwords use, instead of one salted SHA-256. Links minted before this release keep working: a stored password in a legacy form still verifies, and it is re-hashed into the new form on its first successful redemption. Every comparison is constant-time. A deployment that injects its own `hashPassword` / `verifyPassword` pair is unaffected, and its stored forms are left alone.
12+
- **The password travels in a header.** Both public share-link routes (`GET /api/v1/share-links/:token/resolve` and `/:token/messages`) accept the `x-share-password` request header, the preferred form, because a header is not part of the request URL. The `?password=` query parameter is still accepted for compatibility, so current consoles keep working until they move to the header. `/messages` accepted only the query parameter on this mount before.
13+
- **Cross-origin clients can send the header.** `X-Share-Password` is in the default CORS preflight allow-list (`DEFAULT_CORS_ALLOW_HEADERS` in `@objectstack/plugin-hono-server`, which the `@objectstack/hono` adapter also applies). A deployment that passes its own `allowHeaders` is unchanged; add the header to that list to let a cross-origin client use it.
14+
- **Public share-link answers are not cached.** Both public routes answer with `Cache-Control: no-store` and `Vary: X-Share-Password` on every outcome, on both mounts (the sharing plugin's routes and the runtime dispatcher's `/share-links` domain). The authenticated create, list and revoke routes are unchanged.
15+
- **Hashing works in WebContainer.** On StackBlitz WebContainer, where `node:crypto.scrypt` is incomplete, the password is hashed with the pure-JS scrypt from `@noble/hashes` (now a dependency of `@objectstack/plugin-sharing`, as it already is of `@objectstack/plugin-auth`), at the same parameters and in the same stored form. A hash made on either runtime verifies on the other.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
'@objectstack/plugin-auth': minor
3+
---
4+
5+
fix(plugin-auth)!: implicit account linking on external sign-in requires the library's standard local-ownership condition; the platform identity provider keeps its documented exception; an unlink is honoured
6+
7+
Clause-②: no (narrowing)
8+
9+
<!-- adr-0087: not-required (no-migration-prescription) No authorable key, export or config field is removed or renamed: the change narrows when an external sign-in links implicitly to an existing local user, and nothing an author wrote needs rewriting. The one config key it reads, account.accountLinking.requireLocalEmailVerified, keeps its name and gains an explicit opt-out meaning. -->
10+
11+
**BREAKING for deployments that relied on external sign-in (OAuth, OIDC, SSO) linking implicitly to a local user whose email is not verified.**
12+
13+
**What changed.**
14+
15+
- An external sign-in links implicitly to an existing local user only when that local user's email is verified. Otherwise the sign-in is refused with `error=account_not_linked`, the same code better-auth's own refusal produces. No link is written and the local user stays unverified. A verified local user links as before.
16+
- The platform's own identity provider (`objectstack-cloud`) keeps its documented exception and still links to an unverified local user, because it seeds the environment owner's row without a mailbox round-trip.
17+
- After a user unlinks a provider, an implicit sign-in through it no longer links the identity again, for any provider. An explicit, signed-in link from account settings (`/link-social`) is still allowed and ends the refusal. If the unlink cannot be recorded, the unlink itself is refused and the provider stays linked. Deleting a user removes the user's unlink records.
18+
- A deployment that passes `secondaryStorage` to the auth plugin now also keeps verification values in the database (`verification.storeInDatabase: true`). The cache still fronts them. This keeps the unlink records durable when the cache evicts entries. Deployments without `secondaryStorage` are unchanged.
19+
- `account.accountLinking.requireLocalEmailVerified` now reads as follows. Unset (the default) means the rules above. `true` applies the strict check to every provider, including `objectstack-cloud`. `false` turns off only the local-verification check and keeps the unlink rule.
20+
21+
**What to do after upgrading.**
22+
23+
- A user refused this way signs in with their existing method, then links the provider from account settings, or verifies their email first.
24+
- To let unverified local users link implicitly again, set `account.accountLinking.requireLocalEmailVerified: false`. Before you do, read the library's warning about account takeover.
25+
- If you pass `secondaryStorage`: verification values written to the cache alone before the upgrade (password-reset links, one-time codes, magic links and email-verification links that were in flight at deploy time) can no longer be consumed afterwards. Users who hit this request a fresh link or code once.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@objectstack/spec': patch
3+
'@objectstack/platform-objects': patch
4+
---
5+
6+
Studio's action form now offers `onSuccess` (the route an `api` or `script` action opens once it succeeds, and whether it opens in place or in a new tab) and `outcomeMessages` (a JSON map from each `outcome` the handler returns to the success message shown for it), with their labels and help text translated for `zh-CN`, `ja-JP` and `es-ES`.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/runtime': patch
3+
'@objectstack/service-datasource': patch
4+
---
5+
6+
An import over a code-defined datasource is held to the namespace of the package that declares it (ADR-0028), and the draft door answers the prefixed name (#21889).
7+
8+
Clause-②: no
9+
10+
- **Before.** `POST /api/v1/datasources/:name/external/tables/:remote/import` with an explicit `name` that carried no namespace prefix answered `201` and saved an unprefixed federated object, and `POST …/external/tables/:remote/draft` answered the bare remote table name with a `TODO(namespace)` note. Measured on the showcase's `showcase_external` under `objectstack dev` and `objectstack start`.
11+
- **`@objectstack/runtime`.** `AppPlugin` registers each code-defined datasource through `applyProtection` with the id and version of the package body that declares it, so the item carries `_packageId`, `_packageVersion` and `_provenance: 'package'`. On an ADR-0130 `packages[]` artifact each datasource takes its own body's id, never the artifact's top-level manifest id. A top-level datasource that no body declares keeps its registration under the artifact's own id, and a warning names it.
12+
- **`@objectstack/service-datasource`.** The federation service reads the datasource's package record from the engine registry (`registry.getPackage` on the `objectql` service), the store the runtime publish gate reads for the same check. It used to ask the `metadata` service, which holds no package records in any composition, so no datasource resolved a namespace. The package id still comes only from the stamped `_packageId`.
13+
- **What a caller sees now.** On a datasource whose package declares `manifest.namespace`, an unprefixed import `name` answers `400 EXTERNAL_IMPORT_ERROR` with ADR-0028's message, which names the prefixed name to use. An import with no `name` override saves the prefixed name the draft derives (for example `showcase_customers` instead of `customers`). `GET /api/v1/meta/datasource` lists the three provenance keys on a code-defined datasource; all three are declared on `DatasourceSchema`. The datasource admin list (`GET /api/v1/datasources`) is unchanged, and the admin door still refuses to edit or remove a code-defined datasource.
14+
- **Unchanged.** A datasource that carries no `_packageId` (the host `default` is one) and a package that declares no namespace resolve no namespace, so their imports and drafts answer as before.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@objectstack/objectql': patch
3+
---
4+
5+
Deleting an organization no longer fails with a 500 on a deployment that has a federated (ADR-0015 `external`) object bound. The engine's referential cascade no longer treats the `organization_id` the platform injects into a federated object as a reference to `sys_organization`.
6+
7+
Clause-②: no
8+
9+
- **What was wrong.** The registry injects `organization_id` into every object, federated ones included, and the platform provisions no storage for a federated object. The cascade's dependents probe filtered the remote table on that column, the SQL driver refused the unknown column (`INVALID_FILTER`), and the probe's failure propagated, so the delete failed. The showcase, with its federated fixture provisioned, answered every organization delete with 500.
10+
- **What changed.** The cascade scan skips a federated object's injected tenant anchor. It asks the same `isFederatedObject` predicate as the driver-option builder and the related-record read, plus the injected-column provenance marker, so an `organization_id` the author declared on a federated object is still probed. The cascade's atomicity plan asks the same question, so it keeps counting exactly the relations the scan probes.
11+
- **What did not change.** Any lookup an author declares on a federated object is still probed, and a probe that cannot run still fails the delete. Only a missing child table is passed over as having no dependents.

‎content/docs/permissions/authentication.mdx‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,12 @@ window.location.href = data.url ?? data.data.url;
527527

528528
Better-Auth automatically handles the OAuth callback at `/api/v1/auth/callback/google` and redirects the user back to your application with a session.
529529

530+
When the provider's email matches an existing local account that is not linked to this
531+
provider yet, the sign-in links to that account only if its email is verified. Otherwise
532+
the callback redirects with `error=account_not_linked`. The platform identity provider
533+
(`objectstack-cloud`) is the one exception, and an unlinked provider never re-links
534+
itself. See [Linking to an existing account](/docs/permissions/sso#linking-to-an-existing-account).
535+
530536
---
531537

532538
## Enterprise Authentication Hardening (ADR-0069)

‎content/docs/permissions/sso.mdx‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,35 @@ The Console `/login` and `/register` pages will now show a button for each enabl
385385
3. Browser redirects to the provider's authorization endpoint.
386386
4. Provider redirects back; better-auth validates the OIDC token and creates a session.
387387

388+
## Linking to an existing account
389+
390+
A social, OIDC or enterprise SSO sign-in may arrive with an email that already belongs to a
391+
local account, one that is not linked to this provider yet. The platform then links the
392+
identity to that account automatically (an *implicit* link) only under these rules:
393+
394+
- **The local account's email must be verified.** If it is not, no link is written and the
395+
callback redirects with `error=account_not_linked`. The user signs in with their existing
396+
method and links the provider from account settings, or verifies their email first.
397+
- **The platform identity provider is the exception.** `objectstack-cloud` links to an
398+
unverified local account, because it creates the environment owner's account itself,
399+
without a mailbox round-trip.
400+
- **An unlink is honoured.** After a user unlinks a provider, signing in with that provider
401+
no longer re-links it, for any provider, `objectstack-cloud` included. The user links it
402+
again explicitly from account settings (`POST /api/v1/auth/link-social` while signed in),
403+
which also ends the refusal. If the unlink cannot be recorded, the unlink itself fails and
404+
the provider stays linked.
405+
406+
An explicit link from account settings does not depend on whether the signed-in user's
407+
email is verified. It still requires the provider to vouch for the email (a trusted
408+
provider or an `email_verified` claim) and the email to match the account's.
409+
410+
`account.accountLinking.requireLocalEmailVerified` in the auth plugin config changes the first
411+
rule. Unset (the default) gives the rules above. `true` applies the verified-email
412+
requirement to every provider, `objectstack-cloud` included. `false` turns the requirement
413+
off, which the library warns is an account-takeover risk; the unlink rule still applies.
414+
`account.accountLinking.trustedProviders` relaxes only the provider-side `email_verified`
415+
claim and never the local requirement.
416+
388417
---
389418

390419
## Notes

‎content/docs/permissions/tenant-audit-census.mdx‎

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ are reported as `undecidable` rather than assumed either way.
122122

123123
The same holds twice over for the context. An options argument spelled as a
124124
literal can be read; one spelled `options`, `{ ...opts }`, or handed through a
125-
forwarding shim cannot, and **67 of the 232 sites are spelled that way**. A
125+
forwarding shim cannot, and **67 of the 233 sites are spelled that way**. A
126126
context resolved from an inline literal or a local `const` can be tested for
127127
`isSystem`; one arriving from a helper call cannot.
128128

@@ -187,10 +187,10 @@ reproduce them. Where it disagrees, it disagrees on the page:
187187

188188
| carried figure | where it survives | this census |
189189
| :--- | :--- | ---: |
190-
| 175 write call sites | quoted in the merged changeset | **232** |
190+
| 175 write call sites | quoted in the merged changeset | **233** |
191191
| 24 carrying no tenant context | quoted in the merged changeset | **9** provable and tenancy-enabled; **34** more whose options argument is unreadable |
192-
| 127 of 175 statically decidable, 48 runtime-parameter-name sites | restated on the `isSystem`-scoping card | **154 of 232** decidable, **78** undecidable |
193-
| 135 (77%) silenced by the `isSystem` guard before the posture gate | the lost issue body — **no surviving corroboration** | **not reproduced**: 113 decidably elevated, 0 decidably not, 102 undecidable |
192+
| 127 of 175 statically decidable, 48 runtime-parameter-name sites | restated on the `isSystem`-scoping card | **155 of 233** decidable, **78** undecidable |
193+
| 135 (77%) silenced by the `isSystem` guard before the posture gate | the lost issue body — **no surviving corroboration** | **not reproduced**: 114 decidably elevated, 0 decidably not, 102 undecidable |
194194
| 141 and 132, two independent re-derivations | the card that filed this work | — |
195195

196196
**The differences are not reconciled, and deliberately so.** The old census's
@@ -207,11 +207,11 @@ would report a smaller number and would not say so.
207207

208208
The fourth row is the one worth flagging to anyone citing it. **The 135 / 77%
209209
figure has no surviving corroboration anywhere in the tree.** This census reads
210-
113 of 232 (49%) as decidably elevated, with 102 more whose elevation is a
210+
114 of 233 (49%) as decidably elevated, with 102 more whose elevation is a
211211
run-time fact — so the claim is neither confirmed nor refuted, and the honest
212212
answer is that a static reading cannot settle it.
213213

214-
⇒ **Cite `9 / 232`, and say what it is**: the sites whose options argument was
214+
⇒ **Cite `9 / 233`, and say what it is**: the sites whose options argument was
215215
READ and holds no tenant context, against a decidably tenancy-enabled object.
216216
That is the control's provable yield surface. ⛔ Do not cite it as "the sites
217217
without tenant context" — **34 further sites** have an options argument this
@@ -223,28 +223,28 @@ cannot read, and they are neither in nor out.
223223

224224
| what | count |
225225
| :--- | ---: |
226-
| write call sites on the application surface | **232** |
227-
| …whose object name is statically decidable | 154 |
226+
| write call sites on the application surface | **233** |
227+
| …whose object name is statically decidable | 155 |
228228
| …whose object name is chosen at run time | 78 |
229-
| …against an object with tenancy ENABLED | 153 |
229+
| …against an object with tenancy ENABLED | 154 |
230230
| …against an object that declares tenancy off | 1 |
231-
| threading a tenant context | 148 |
231+
| threading a tenant context | 149 |
232232
| PROVABLY carrying none (options read, no context key) | **17** |
233233
| …of those, against a decidably tenancy-enabled object | **9** |
234234
| options argument UNREADABLE — may or may not carry one | 67 |
235235
| …of those, against a decidably tenancy-enabled object | 34 |
236-
| threading a decidably ELEVATED (`isSystem`) context | 113 |
236+
| threading a decidably ELEVATED (`isSystem`) context | 114 |
237237
| threading a context that is decidably NOT elevated | 0 |
238238
| threading a context whose elevation is a run-time fact | 102 |
239239

240240
| how the instrument reached the site | count |
241241
| :--- | ---: |
242-
| receiver carried a readable engine type | 184 |
242+
| receiver carried a readable engine type | 185 |
243243
| receiver erased, placed by the object NAME | 28 |
244244
| receiver erased, placed by an `object: string` PARAMETER | 15 |
245245
| receiver erased, placed by an `UNTYPED_RECEIVERS` row | 5 |
246246

247-
| object name spelled inline | 103 |
247+
| object name spelled inline | 104 |
248248
| object name spelled through a `const` | 51 |
249249
| object name is an `object: string` parameter | 17 |
250250
| object name is some other run-time expression | 61 |
@@ -297,13 +297,13 @@ holds still. They are required to be HERE and to say WHEN they were true;
297297
their values are not compared. The reasoning, and the measurement behind it,
298298
are in `scripts/check-tenant-audit-census.mjs`.
299299

300-
Measured on 2026-10-05 at `34782539c`.
300+
Measured on 2026-10-05 at `3d34c6efd`.
301301

302302
| corpus scale (not enforced) | count |
303303
| :--- | ---: |
304-
| tracked non-test sources scanned | 605 |
305-
| engine-shaped types recognised | 68 |
304+
| tracked non-test sources scanned | 607 |
305+
| engine-shaped types recognised | 69 |
306306
| declared objects in the registry | 116 |
307-
| same-named calls subtracted as non-engine | 155 |
307+
| same-named calls subtracted as non-engine | 158 |
308308

309309
{/* END GENERATED: tenant-audit-census */}

‎content/docs/protocol/kernel/http-protocol.mdx‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -999,18 +999,19 @@ ObjectStack sends CORS headers automatically:
999999
```http
10001000
Access-Control-Allow-Origin: https://app.acme.com
10011001
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS
1002-
Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With, X-Tenant-ID, X-Environment-Id, If-Match
1002+
Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With, X-Tenant-ID, X-Environment-Id, If-Match, X-Share-Password
10031003
Access-Control-Expose-Headers: set-auth-token, x-objectstack-dropped-fields
10041004
Access-Control-Max-Age: 86400
10051005
```
10061006

1007-
Three of the allowed request headers are easy to overlook, and each one disables
1007+
Four of the allowed request headers are easy to overlook, and each one disables
10081008
a feature if an intermediate proxy strips it:
10091009

10101010
| Header | Why it is allowed |
10111011
| --- | --- |
10121012
| `X-Tenant-ID` / `X-Environment-Id` | Route the request to its environment on a multi-tenant host. |
10131013
| `If-Match` | Carries the OCC token on record `PATCH`es. Without it, a cross-origin save fails in the browser with "Failed to fetch". |
1014+
| `X-Share-Password` | Carries a share-link password to the public `/share-links/:token/resolve` and `/messages` routes. It is the preferred form because a header stays out of URLs; without it, a cross-origin client can only send the password as a query parameter. |
10141015

10151016
The two **exposed** response headers matter to browser clients specifically:
10161017
`set-auth-token` delivers a rotated session token (without it a cross-origin

0 commit comments

Comments
 (0)