Skip to content

Commit 6dd8c07

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-20790-flow-hook-secret-seam
2 parents 9cc9fdf + 16eefc6 commit 6dd8c07

95 files changed

Lines changed: 4379 additions & 409 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/service-datasource': patch
3+
'@objectstack/plugin-approvals': patch
4+
---
5+
6+
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.
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
---
2+
'@objectstack/plugin-audit': minor
3+
'@objectstack/platform-objects': minor
4+
'@objectstack/plugin-auth': minor
5+
'@objectstack/plugin-sharing': minor
6+
'@objectstack/plugin-approvals': minor
7+
'@objectstack/objectql': minor
8+
'@objectstack/runtime': patch
9+
---
10+
11+
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
60+
providers through their auth routes.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/plugin-security': patch
3+
'@objectstack/platform-objects': patch
4+
---
5+
6+
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.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/sdui-parser': minor
4+
'@objectstack/lint': minor
5+
---
6+
7+
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`.
8+
9+
Clause-②: no (narrowing)
10+
11+
<!-- adr-0087: registered translation-widget-sub-caption-removed, translation-widget-sub-caption-retired -->
12+
13+
**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`.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec)!: `action.ai.outputSchema` and `agent.structuredOutput.schema` refuse an untyped subschema that carries a type-scoped keyword, at its path, as the AI runtime does (#21289)
6+
7+
Clause-②: yes (narrowing)
8+
9+
<!-- adr-0087: registered ai-json-schema-untyped-subschema-refused -->
10+
11+
**BREAKING** — an accept-set narrowing on two published authoring slots, shipped as `minor` under the repo's launch-window convention for accept-set narrowings. Every schema it refuses was already refused by the AI runtime before the action or agent ran, so nothing that worked stops working; what moves is where the refusal is reported — at authoring, at the subschema's path, instead of at the first invocation.
12+
13+
**`@objectstack/spec`**
14+
15+
- **`action.ai.outputSchema`** (stack actions and object-nested actions) and **`agent.structuredOutput.schema`** were open records. The cloud AI runtime compiles both through one guard whose schema reader does not check a type-scoped keyword on a subschema with no `type`, and refuses the whole schema. Both slots are now declared by one factory that mirrors that guard exactly:
16+
- **refused:** an object node whose `type` is absent and which carries any of the 22 type-scoped keywords (`properties`, `required`, `additionalProperties`, `patternProperties`, `propertyNames`, `minProperties`, `maxProperties`, `items`, `prefixItems`, `contains`, `minItems`, `maxItems`, `uniqueItems`, `minLength`, `maxLength`, `pattern`, `format`, `minimum`, `maximum`, `exclusiveMinimum`, `exclusiveMaximum`, `multipleOf`), with any value;
17+
- **where:** the schema root, every value of `properties`, `patternProperties`, `$defs`, `definitions` and `dependentSchemas`, and the subschema (or each array entry) of `items`, `additionalProperties`, `contains`, `propertyNames`, `not`, `if`, `then`, `else`, `unevaluatedProperties`, `unevaluatedItems`, `anyOf`, `oneOf`, `allOf` and `prefixItems` — under typed parents too; `$ref` is not followed;
18+
- **accepted:** boolean subschemas, `{}`, a node with any `type` value, and an untyped node carrying only keywords outside the list (`enum`, `const`, `$ref`, `anyOf`, `title`, `description`, `default`, …);
19+
- each offending subschema is its own issue, located at the slot path plus the subschema path (`ai.outputSchema.properties.customer`), and the message names the keyword and the `type` to declare.
20+
- The TypeScript types of both slots are unchanged (`Record<string, unknown>`). The published JSON Schema does not state the rule: it is a refinement, which the JSON Schema projection does not carry, so a JSON Schema validator still accepts such a schema in either slot. The affected published schemas name the slot in their `x-dropped-refinements` list.
21+
22+
## FROM → TO
23+
24+
| you wrote | write instead |
25+
|:--|:--|
26+
| `outputSchema: { properties: { id: { type: 'string' } }, required: ['id'] }` | `outputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] }` |
27+
| `schema: { type: 'object', properties: { tags: { items: { type: 'string' } } } }` | `schema: { type: 'object', properties: { tags: { type: 'array', items: { type: 'string' } } } }` |
28+
| `{ properties: { code: { pattern: '^[A-Z]+$' } } }` anywhere in either slot | `{ type: 'object', properties: { code: { type: 'string', pattern: '^[A-Z]+$' } } }` |
29+
30+
The one-line fix: declare its `type` on every subschema that carries a type-scoped keyword — `"object"`, `"array"`, `"string"`, or `"number"` / `"integer"`, as the refusal names.
31+
32+
## Who is affected, measured
33+
34+
On `origin/main` `135daaa06b`: the package fixtures author either slot three times (one action `ai.outputSchema`, two `structuredOutput.schema`), every subschema typed; the examples, the documentation and the published skills author neither slot. No fixture needed a change. Deployed metadata was not measured. A stored action or agent carrying such a schema still loads; its next save is refused until the `type` is declared.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
docs(spec): an `object-master-detail-form` detail entry's `sortField` and `amountField` describes say what happens when the key is omitted on each of the renderer's paths (#21315)
6+
7+
Clause-②: no
8+
9+
- **Entry the renderer resolves** (any entry that does not name `relationshipField` together with at least one column whose every column has a `type`): an omitted `sortField` is the child object's first field named `position`, `sort_order`, `sequence`, `line_no`, `line_number` or `sort`, and an omitted `amountField` is picked from the grid's number and currency columns. This is unchanged. It includes an entry that names `relationshipField` and columns of which some have no `type`: the renderer keeps that entry's `formFields` and `inlineMode` as authored, but it still derives these two.
10+
- **Entry kept exactly as authored** (one that names `relationshipField` and at least one column, and gives every column a `type`): the renderer derives neither. An omitted `sortField` means the grid stamps no line position, so a drag-reorder is not saved. An omitted `amountField` means the sums read a child column named `amount`, and the grid shows a running total only when `totalField` is set.
11+
- The `sortField` describe used to say only "derived from a `position` / `sort_order` / … field when omitted", which does not hold for an entry kept exactly as authored. The `amountField` describe said nothing about omission.
12+
- No schema accepts or refuses anything new. Only the two describes and the reference page that lifts them change.

0 commit comments

Comments
 (0)