You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 40618fa
Browse filesBrowse the repository at this point in the historyBrowse files
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.
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.
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`.
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.
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.
Copy file name to clipboardExpand all lines: content/docs/permissions/tenant-audit-census.mdx
+17-17Lines changed: 17 additions & 17 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -122,7 +122,7 @@ are reported as `undecidable` rather than assumed either way.
122
122
123
123
The same holds twice over for the context. An options argument spelled as a
124
124
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
126
126
context resolved from an inline literal or a local `const` can be tested for
127
127
`isSystem`; one arriving from a helper call cannot.
128
128
@@ -187,10 +187,10 @@ reproduce them. Where it disagrees, it disagrees on the page:
187
187
188
188
| carried figure | where it survives | this census |
189
189
| :--- | :--- | ---: |
190
-
| 175 write call sites | quoted in the merged changeset |**232**|
190
+
| 175 write call sites | quoted in the merged changeset |**233**|
191
191
| 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 |
194
194
| 141 and 132, two independent re-derivations | the card that filed this work | — |
195
195
196
196
**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.
207
207
208
208
The fourth row is the one worth flagging to anyone citing it. **The 135 / 77%
209
209
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
211
211
run-time fact — so the claim is neither confirmed nor refuted, and the honest
212
212
answer is that a static reading cannot settle it.
213
213
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
215
215
READ and holds no tenant context, against a decidably tenancy-enabled object.
216
216
That is the control's provable yield surface. ⛔ Do not cite it as "the sites
217
217
without tenant context" — **34 further sites** have an options argument this
@@ -223,28 +223,28 @@ cannot read, and they are neither in nor out.
223
223
224
224
| what | count |
225
225
| :--- | ---: |
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|
228
228
| …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|
230
230
| …against an object that declares tenancy off | 1 |
231
-
| threading a tenant context |148|
231
+
| threading a tenant context |149|
232
232
| PROVABLY carrying none (options read, no context key) |**17**|
233
233
| …of those, against a decidably tenancy-enabled object |**9**|
234
234
| options argument UNREADABLE — may or may not carry one | 67 |
235
235
| …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|
237
237
| threading a context that is decidably NOT elevated | 0 |
238
238
| threading a context whose elevation is a run-time fact | 102 |
239
239
240
240
| how the instrument reached the site | count |
241
241
| :--- | ---: |
242
-
| receiver carried a readable engine type |184|
242
+
| receiver carried a readable engine type |185|
243
243
| receiver erased, placed by the object NAME | 28 |
244
244
| receiver erased, placed by an `object: string` PARAMETER | 15 |
245
245
| receiver erased, placed by an `UNTYPED_RECEIVERS` row | 5 |
246
246
247
-
| object name spelled inline |103|
247
+
| object name spelled inline |104|
248
248
| object name spelled through a `const`| 51 |
249
249
| object name is an `object: string` parameter | 17 |
250
250
| 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;
297
297
their values are not compared. The reasoning, and the measurement behind it,
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
1008
1008
a feature if an intermediate proxy strips it:
1009
1009
1010
1010
| Header | Why it is allowed |
1011
1011
| --- | --- |
1012
1012
|`X-Tenant-ID` / `X-Environment-Id`| Route the request to its environment on a multi-tenant host. |
1013
1013
|`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. |
1014
1015
1015
1016
The two **exposed** response headers matter to browser clients specifically:
1016
1017
`set-auth-token` delivers a rotated session token (without it a cross-origin
0 commit comments