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
Datasource and approval refusals, warnings, field help and generated-draft comments no longer cite tracker numbers; each one states the decision behind it in words
7
+
8
+
Clause-②: no
9
+
10
+
Some strings these two packages show to operators, administrators and flow authors pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does.
11
+
12
+
-`@objectstack/service-datasource`: the credential-migration refusal says an unbindable key is either an alias spelling from before inline credentials were refused at publish, which no connection builder reads, or turso's `encryptionKey`, which has no secret slot of its own because the one slot carries the `authToken`; the remote-primary-key comment in a generated object draft says a driver's introspection can report only the first column of a composite key, so the list is a lower bound.
13
+
-`@objectstack/plugin-approvals`: the `queue` approver warning says the platform has no ownership queue to expand the type from, that the type is no longer offered for authoring, and to route the step to a team, department or position instead; the live-record warnings say approvers are being resolved against the trigger snapshot instead of the live record they are normally resolved from; the recall refusal's log line names the admin override; the `sys_approval_action``via_override` help (in every shipped locale) says a platform or organization admin may act on any pending request, so that one nobody in its slate can decide never stays stuck; the cross-organization team, team-member and manager warnings, the expanded-to-nobody warning, the revise-window refusal, the `attachments` help and the `sys_approval_delegation` description drop their citations.
14
+
15
+
Text only: no status, error code, field, route or control flow moves. A client or log filter that matches the old text (for example a tracker-number suffix) needs the new spelling.
fix(plugin-audit,platform-objects,plugin-auth,plugin-sharing,plugin-approvals)!: the audit ledger no longer records fields declared `internal`, and the platform's credential-class fields are declared `internal`
12
+
13
+
Clause-②: no (narrowing)
14
+
15
+
<!-- adr-0087: not-required (no-migration-prescription) No authorable key, export or config field is removed or renamed: the change narrows what the generic data path and the audit ledger return for platform-owned columns, and nothing an author wrote needs rewriting. The objectql half adds exports only. -->
16
+
17
+
**BREAKING for readers of credential-class columns on the generic data path and in the audit ledger.**
18
+
19
+
**What changed.**
20
+
21
+
- The audit plugin's CRUD mirror now omits every field declared `internal: true` from the
22
+
rows it writes to `sys_audit_log` and `sys_activity`: create `new_value`, both sides of an
23
+
update, delete `old_value`, and the activity row. It already masked `secret` and `password`
24
+
fields; `internal` is the same contract the generic data path already enforces ("never
25
+
returned on the generic data path"). An update that changes only an `internal` field still
26
+
writes its row, with neither value.
27
+
- These platform fields are now declared `internal: true`, so neither the generic data path
28
+
nor the ledger returns them: the JWT signing key's private key (`sys_jwks`), both credential
29
+
columns of the one-time verification object (`sys_verification`), the two-factor secret and
30
+
backup codes, the SSO provider's OIDC and SAML protocol blobs, the OAuth access and refresh
31
+
token columns, the OAuth client secret digest, the SCIM credential digest, the share link's
32
+
token and password hash, and the approval action-token digest. API key digests and email
33
+
headers were already `internal`; the ledger now honours that too.
34
+
- Every built-in consumer that needs one of these values reads it back through the engine's
35
+
privileged accessor rather than the generic path: JWT signing, password reset and the other
36
+
one-time verification flows, two-factor verification, SSO sign-in and the legacy SSO secret
37
+
migration, OAuth client authentication, share-link redemption (the password gate is held)
38
+
and the creator's share-link list, which keeps returning each link's token. The runtime's
39
+
share-link resolve route (the dispatcher twin of the plugin's) still answers "password
40
+
required" for a protected link rather than the unknown-link shape.
41
+
- The one-time verification object's record title is now the fixed label `Verification`; it no
42
+
longer shows the identifier column.
43
+
-`@objectstack/objectql` exports two helpers from its main and `/core` entries:
44
+
`collectInternalReadFields` (the names of an object's `internal` fields) and
45
+
`readInternalColumn` (recovers one `internal` column for rows already read, through the
46
+
engine's privileged accessor, and fails closed when the value cannot be recovered).
47
+
48
+
**What to do after upgrading.**
49
+
50
+
-**Rotate the JWT signing keys.** Ledger rows written before this release are not rewritten
51
+
(the ledger is append-only), so a signing key that existed before the upgrade may have a copy
52
+
in the ledger. Rotate the keys so that copy signs nothing.
53
+
-**Revoke and re-mint share links that must stay private.** A share link's token is a
54
+
capability that stays valid until the link expires or is revoked, and links minted before this
55
+
release may have a copy in the ledger.
56
+
- A copy of a one-time verification credential is usable only while that credential is still
57
+
outstanding: once it is consumed or expires, its copy names nothing that will be accepted.
58
+
- An integration that read any of these columns through `GET /api/v1/data/...` no longer
59
+
receives them. Read share links through `/api/v1/share-links`, and OAuth clients and SSO
fix(plugin-security,platform-objects): an org member reading a colleague's `sys_user` row is no longer served the identity object's `Admin` field group, directly or through the activity stream (#21237)
7
+
8
+
Clause-②: no
9
+
10
+
-**What a member was served.** The platform baseline `member_default` opens every org peer's `sys_user` row (the `sys_user_org_members` policy) and declared no field-level security on it. An org member reading a colleague's row was therefore served the whole `Admin` field group: the sign-in trail, the lockout state, the ban reason and expiry, the password and MFA stamps, the legacy platform role scalar and the AI-seat flag. With object-level read on `sys_activity`, the colleague's activity metadata carried the same fields, because the activity field redaction serves exactly what the data plane serves.
11
+
-**What changes.**`member_default` and `viewer_readonly` now declare the `Admin` group `readable: false` through the permission set's existing `fields` entries. The withheld set is built from the identity object's declaration, so a field the declaration adds to the group is withheld from the day it is declared. `admin_full_access` and `organization_admin` (and so `organization_admin_no_bypass`) declare the group readable and editable, the same state as a field no set names, so an administrator's reads and writes are unchanged. `member_default` is the additive baseline every authenticated user resolves, and field grants merge most-permissively, which is why the admin sets carry that keeping entry.
12
+
-**What a member sees now.** On the direct read, the list read and the activity metadata, a member is served no `Admin`-group field of a colleague's row. The directory fields (name, email, image) are still served. Field-level security does not distinguish rows, so the member's own row read through the generic data API is withheld the group too; every platform reader of those fields on a member's own row (the auth gates, the sign-in stamps, the session, the AI-seat resolution) reads under system or auth context and is unaffected. A member's query that filters or sorts on a withheld field is refused (`403 PERMISSION_DENIED`, the filter-oracle rule). A member's user-context write that names a withheld field is refused by the field-level write gate (`403 PERMISSION_DENIED`), and a payload mixing such a field with profile fields no longer lands partially.
13
+
-**The deactivation flag is directory data.**`sys_user.banned` moves from the `Admin` field group to the `Account` group in `@objectstack/platform-objects`, so members are still served it. Every user picker filters its candidates on it, and a filter on a withheld field would be refused. Its reason and expiry stay in the `Admin` group. In a record form the field now renders in the `Account` section.
14
+
15
+
**Migration.** None for shipped apps. A custom permission set that grants an org member read on `sys_user` and is meant to show them the `Admin` group must name those fields `readable: true` in its `fields` entries. A client that filtered members' `sys_user` queries on an `Admin`-group field must drop that predicate or run it with an administrator's grant.
The metric sub-caption is retired at both ends. A dashboard widget keeps one authored description, `widget.description`, which renders as the card-header subtitle and is translated by the widget's `description` translation key. The widget translation key `subCaption` is refused, and the server no longer writes a widget's `options.description`.
**What is retired.**`dashboards.DASHBOARD.widgets.WIDGET.subCaption` in a translation bundle (`defineTranslationBundle`, `stack.translations`, the platform bundle) and in a registered `translation` item. It overlaid a caption under a metric's value onto the widget's `options.description`. The dashboard schema never declared `options.description`, and no authored widget wrote it, so `translateDashboard`'s overlay was the key's only writer. That overlay is removed: `translateDashboard` now translates a widget's `title` and `description` and carries `options` through untouched.
14
+
15
+
**BREAKING** — an accept-set narrowing, shipped as `minor` under the launch-window convention.
16
+
17
+
### FROM → TO
18
+
19
+
| wrote | write instead |
20
+
| --- | --- |
21
+
|`dashboards.DASHBOARD.widgets.WIDGET.subCaption: 'TEXT'`| delete the entry. If the copy belongs on the card, put it in the widget's `description` and translate it under `dashboards.DASHBOARD.widgets.WIDGET.description`. |
22
+
|`dashboards.DASHBOARD.widgets.WIDGET.subtitle: 'TEXT'`|`subtitle` was only ever a rename suggestion for `subCaption`. Card-header copy goes under `description`; a caption under the value has nowhere to render, so delete it. |
23
+
24
+
**The one-line fix: delete every `subCaption:` entry under `dashboards.*.widgets.*` in your translation bundles.**`os migrate meta --from 17` lists the mechanical edits for existing sources; stored `translation` items are converted when they are read.
25
+
26
+
**What an author now sees.** Writing `subCaption` fails `tsc` (its input type is the retired-key mark) and fails the parse with a prescription naming the widget's `description`. Writing `subtitle` on a widget translation fails the parse with both readings named, instead of a rename suggestion onto a key that is refused next. `os validate`, `os build` and `os lint` now raise the `unconsumed-widget-option` warning on an authored widget `options.description`, like any other options key the dataset-bound render path does not read. It is a warning, so none of the three fails on it.
27
+
28
+
**Measured producers: none.** Zero `subCaption` entries and zero authored widget `options.description` in the four example apps (`app-crm`, `app-todo`, `app-showcase`, `app-multi-package`) and in the bundles `@objectstack/platform-objects` ships, so no shipped exit code changes.
29
+
30
+
### The retirement kit
31
+
32
+
-**Tombstone.**`subCaption` is a `retiredKey()` tombstone on the widget translation node, so the refusal carries the prescription on all three faces the node is spread into (per-app bundle entry, platform bundle entry, `translation` item). The node sits under two records (`dashboards`, `widgets`), below the authorable-surface walk, so it has no `RETIRED_KEYS_BY_MAJOR` row, the same as the `submitLabel` component-copy key before it.
33
+
-**The former alias.** The `subtitle` → `subCaption` rename suggestion moves to the node's `guidance` table. An alias whose target is a tombstone is the shape the alias-integrity audit refuses, and repointing it at `description` would silently change what the word is taken to mean.
34
+
-**Conversion.**`translation-widget-sub-caption-removed` (protocol 18) strips the key from bundle entries and bare translation items as a lossless delete. It is retired from the load path, so authors are refused at parse while stored rows and `os migrate meta` replay it. Its D3 record is the semantic entry `translation-widget-sub-caption-retired`.
35
+
-**`@objectstack/sdui-parser`.**`CONSUMED_WIDGET_OPTION_KEYS` drops `description`, its one undeclared member, which existed only because the overlay wrote it. `check:widget-option-census`'s `NON_DECLARED_MEMBERS` ledger is now empty, so the census asserts that nothing writes an undeclared key into `options`.
0 commit comments