Skip to content

Commit 90a6068

Browse files
committed
Merge origin/main (c16040d) into claude/harden-occ-stale-token-order
Claude-Session: https://claude.ai/code/session_017xRBcVy4VXmkQjn2MGeaWk Co-authored-by: Claude <noreply@anthropic.com>
2 parents ae97f57 + c16040d commit 90a6068

177 files changed

Lines changed: 12257 additions & 506 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+
'@object-ui/app-shell': patch
3+
'@object-ui/i18n': patch
4+
'@objectstack/service-automation': patch
5+
'@objectstack/platform-objects': patch
6+
---
7+
8+
feat(console): Connector connections, a Setup page to connect, reconnect or disconnect each OAuth connector instance and see its status
9+
10+
Clause-②: yes (widening: new Setup entry nav_connector_connections resolving automation:connector-connections; no existing key, route or shape changes)
11+
Metadata-protocol: none
12+
13+
A connector instance declared with `auth: { type: 'oauth2', grant: 'authorization_code' }` needs an administrator to sign in to the vendor once for their organization. The Console now has a page for that (`automation:connector-connections`). `@objectstack/service-automation` contributes its Setup → Integrations entry, **Connector Connections**, gated on `manage_connector_connections`, the organization-scoped capability the connection door asks, so an organization administrator sees it; `@objectstack/platform-objects` carries the entry's label in its four Setup translation packs. The page lists every such instance with the organization's connection status as the server answers it (Not connected, Connected, or Reconnect required with the reason the server recorded), the granted scopes, who connected it and when, and when the access token renews. A `degraded` instance shows the server's reason in the same row as the connection that caused it.
14+
15+
**Connect** and **Reconnect** open the vendor's sign-in in its own window, detached from the page, and send it to the authorization URL the server answered. The page re-reads the status until the connect lands, then closes the window and shows the new status. **Disconnect** removes the connection after a confirmation. No token, code or authorization URL is ever rendered.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/formula': patch
3+
---
4+
5+
feat(formula): a CEL syntax refusal keeps the engine's own fault code and the source range it points at, so an editor can say what is wrong and where in its own words (#27904)
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
`validateExpression`'s catch-all CEL refusal, `invalid-cel`, now carries two optional params beside `role` and `detail`:
11+
12+
- `celCode` — cel-js's own code for the fault, read off its error object: the parser's for a syntax fault (`unexpected_token` when an operand or the end is missing, `expected_token` when a closing `)` / `]` / `}` is missing, `unexpected_character`, `unterminated_string`, …) or the checker's for a type fault (`no_such_overload`, …).
13+
- `pos` — `{ start, end }`, the UTF-16 offsets into `source` that the engine points at (`end` exclusive). A missing `)` reads `start === end === source.length`.
14+
15+
Both are absent when the engine reported none, and `pos` is absent when the engine read a source the platform rewrote first (the null-guard ternary lowering), since a range into that text would point at characters the author never wrote. A guessed `0` is never written.
16+
17+
`EvalError.pos`, declared before and never filled, is now filled by `celEngine.compile` and `celEngine.evaluate` for a cel-js parse, check or evaluation fault that carries a range, on the same terms.
18+
19+
The refusal's `message` is unchanged, byte for byte, and so are the codes and the params of every other refusal.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@object-ui/core': patch
3+
'@object-ui/app-shell': patch
4+
---
5+
6+
fix(console): Create User left without an email marks the Email input as required, in the user's language
7+
8+
Clause-②: yes (widening)
9+
Metadata-protocol: none
10+
11+
Create User with neither an email nor a phone number answers `400 VALIDATION_FAILED` with one `required` entry per input that would satisfy it on `error.fields`. The Console showed only the server's English sentence above the dialog's buttons and marked no input.
12+
13+
- **`@object-ui/core`**: `refusalFieldErrors`, the reader behind `ActionResult.fieldErrors`, now also reads a refusal's `error.fields` (the spec's `EnhancedApiErrorSchema.fields`), beside `error.details.fields` and the legacy inner `fields`. It is exported.
14+
- **`@object-ui/app-shell`**: a refused `type: 'api'` action carries the refusal's field entries as `fieldErrors`, on the list and record pages alike. The action parameter dialog marks each `required` entry on the param it names, with its own translated "required" sentence and the param's label. When every entry is marked, the refusal box is not drawn. A refusal that names no field, or names one the dialog does not draw, keeps the box with the server's text.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/plugin-sharing': patch
3+
'@objectstack/plugin-hono-server': patch
4+
---
5+
6+
feat(plugin-hono-server,plugin-sharing): `GET /auth/me/permissions` answers `hasAnyQueue`, whether the caller's organization has at least one ownership queue
7+
8+
Clause-②: yes
9+
Metadata-protocol: none
10+
11+
The console's list toolbar offers an "in a queue" toggle, and it needs to know, once per session, whether that toggle can ever return a row. A plain member cannot find out: `member_default` names no `sys_queue` entry, so `GET /data/sys_queue` answers 403. The server now answers the one question for them.
12+
13+
- **The field.** Every authenticated `/auth/me/permissions` body carries `hasAnyQueue: boolean`. It is `true` when the caller's organization has at least one ownership queue: a declared queue the activation ledger has not switched off, or a `sys_queue` row of the organization. A row under a declared queue's name does not count, since that name resolves nowhere. Only the boolean is disclosed: no names and no counts. An anonymous caller still gets `{ "authenticated": false }` and nothing else.
14+
- **Where it is resolved.** `OwnershipQueueService.hasAnyQueue(organizationId)` in `@objectstack/plugin-sharing`, under system context, with the same registry-first read and activation-ledger filter `queuesOf` uses.
15+
- **When there is no answer.** No ownership-queue service registered, a service without the method, or a service that throws answers `false`. A throw is logged at `warn` and does not cost the caller the rest of the permissions body. The closed body the handler answers on a fault carries `false` too.
16+
- **Unchanged.** A member's read of `sys_queue` and `sys_queue_member` stays refused. No route is added.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/service-analytics': patch
3+
---
4+
5+
fix(service-analytics)!: members read through a relationship path answer one refusal on the native, ObjectQL and draft-preview faces
6+
7+
Clause-②: no (narrowing) — the draft preview (`queryDataset` with `previewDrafts: true`) now refuses `INVALID_FIELD` / 400 any member it would read through a relationship path (a dimension, time dimension, measure or filter member), which it served as one `null` group, `[]` or a total of `0`, because it joins nothing; a `timeDimensions` entry and a `runtimeFilter` member spelled through a relationship path the dataset's `include` does not declare are refused `INVALID_FIELD` / 400 on every face, where the native strategy answered `DATASET_INVALID` / 400 and the preview served; and a declared path to a column the related object lacks is refused `INVALID_FIELD` / 400 before SQL, where the native strategy answered `DATABASE_ERROR` / 500. Nothing is widened: no export, answer member, error code or status is added; every code was already on the wire.
8+
Metadata-protocol: none
9+
10+
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed, renamed or re-shaped, and no export or type changes: a selection that reads through a relationship path is refused by the preview that cannot join it, a selection naming an undeclared path or a missing related column is refused on the faces that served it or failed on it, and the native face's refusal takes the member-level code the other member gates answer. There is nothing for a migration to rewrite and no tombstone. -->
11+
12+
**BREAKING** accept-set narrowing on the `queryDataset` draft preview (`@objectstack/service-analytics`), and a code change on two native refusals.
13+
14+
- **Before.** Measured on real SQLite over a dataset `include: ['account']`: the preview answered a backed `account.name` (or a declared dimension over it, or a declared measure over a related field) with one `null` group holding the whole total, a backed `runtimeFilter` member with `[]`, and a windowed backed time dimension with a total of `0`, while the live strategies served the real answer. `OBJECT.region` and `CUBE.region`, which both live strategies resolve to the declared `region`, also came back as one `null` group. A time dimension through an undeclared path was dropped by the preview and refused `DATASET_INVALID` by the native strategy when only windowed; a `runtimeFilter` member through an undeclared path got `DATASET_INVALID` (native), the cross-object filter refusal (ObjectQL) and `[]` or, under an `$or`, the other arm's rows (preview). `account.bogus` reached SQL on the native strategy as a 500.
15+
- **Now.** The service's join gate (the one `dimensions` already asked since the previous fix) also judges `timeDimensions` entries and `where` leaves, so an undeclared path is refused on every face with one message naming the member and the path. A second gate refuses a declared path to a column the related object does not have, naming the member, the column (`field`) and the object (`object`), on every face with one message. The preview then refuses any remaining relationship-path member with a message saying it joins no related object, and answers `OBJECT.region` / `CUBE.region` with the declared dimension's groups.
16+
- **Unchanged.** The live strategies serve a backed path as before (the ObjectQL strategy keeps its own cross-object refusals for a filter, a bucketed time dimension or a measure it cannot join). A path through an object the caller may not read keeps its 403. A hand-authored cube's configured allowlist still answers `DATASET_INVALID`, and so does every other reason that code is raised.
17+
- **Who sees it.** An author previewing a draft dataset that groups, filters or measures through a related object: the preview now says it cannot join instead of showing a number that changes after publish. A caller naming a related field the dataset does not include, or a related column that does not exist: one `INVALID_FIELD` / 400 on every face.
18+
19+
**The remedy.** Publish the draft to query a related field through the live strategies, or preview without it; add the relationship to the dataset's `include`; check the related column's spelling.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec): an object extension can carry a per-field display patch (`fieldPatches`), its keys derived from `BASE_FIELD_DISPLAY_WHITELIST`; not applied yet
6+
7+
Clause-②: yes (widening)
8+
Metadata-protocol: added
9+
10+
ADR-0146 D10's second rule: a customer changes a vendor field's display properties without taking the field over, so the field's type, values and rules stay the vendor's and the vendor's next version still applies. `ObjectExtensionSchema` gains one optional key for it:
11+
12+
```ts
13+
objectExtensions: [{
14+
extend: 'crm_account',
15+
fieldPatches: {
16+
industry: {
17+
label: 'Sector',
18+
inlineHelpText: 'The market the account sells into',
19+
options: { tech: { label: 'Technology' } },
20+
},
21+
},
22+
}]
23+
```
24+
25+
- **Keys.** A patch takes the field keys of `BASE_FIELD_DISPLAY_WHITELIST.field` (`label`, `description`, `inlineHelpText`, `placeholder`) plus `options`, a map from an option's stored `value` to that option's patch, which takes the keys of `BASE_FIELD_DISPLAY_WHITELIST.option` (`label`). Both sets are read from the constant: widening the whitelist widens the slot.
26+
- **Refused at parse, each with a prescription.** Every other field key (`type`, `required`, `unique`, `reference`, `defaultValue`, `expression`, …) and every other option key (`color`, `default`, …) is refused by name, naming the whitelist and stating that it stays the vendor's (ADR-0146 D6). An option's `value` is refused: the option is addressed by it, and it cannot be renamed. A field's option list under `options` is refused as membership. A key spelled as an extension field (`region__c`) is refused with "declare it in `fields`; it is your own field".
27+
- **Pure shape.** Whether the target object has the field or the option, and who may patch it, are the server's to judge.
28+
- **Not enforced yet.** The slot's description reads `[EXPERIMENTAL — not enforced]`. The engine's extension registration does not carry the key, so a patch is parsed and then dropped until the registry merge applies it (ADR-0146 rollout card N-23d). Nothing reads it before then.
29+
30+
An extension without `fieldPatches` parses exactly as before.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
'@object-ui/i18n': patch
4+
---
5+
6+
feat(studio): customize a read-only package's view, dashboard or report into its `<base>.custom` pair, with an override badge and "Restore package original"
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
An installed package is read-only in Studio, so until now its views, dashboards and reports could only be looked at there. Studio now customizes them into the base's paired customization package (`<base>.custom`, ADR-0146 D2 and D3), as a declared override of the base's item.
12+
13+
- **Customize.** The item header of a view, dashboard or report on a read-only base, and of an object leaf's list view, offers Customize. Pressing it only unlocks the item's editors. Nothing is written until an edit is saved.
14+
- **The first saved edit.** It calls `POST /api/v1/packages/:id/ensure-customization` once for that item, with no prompt and no body. It then saves the draft into the package the answer names (`201` when the door created the pair, `200` when it was there). The draft is the item as Studio holds it, without its `_` keys, with `overrides: { package, basedOn }`. `basedOn` is the base's `manifest.version` as the package list serves it. ⛔ The pair's id for a write is the door's answer, never a `.custom` spelled in the browser. The first save is pinned to "no draft there" (`If-None-Match: *`), so a draft someone else left in the pair is asked about, never overwritten. A base whose listed manifest states no version gets a disabled Customize that says so. Nothing is guessed.
15+
- **Walled callers.** Under a `group` or `isolated` posture, an admin who is not the platform operator sees Customize disabled, with the reason. This is read from the two facts the server publishes (`features.tenancyPosture`, `user.isPlatformAdmin`). A door that refuses anyway is shown as the server sent it.
16+
- **Pending and Publish.** On a read-only base with a pair, the pending count, the changes panel and Publish act on the pair's drafts. A refusal at the publish door is shown as it is sent. A read-only base with no pair keeps the read-only Publish, as before.
17+
- **Badge.** The header says the item overrides the base (for example "Overrides HotCRM", with the version in its title). It is read from the declaration the server serves: the pair's draft, else the layered read's overlay. It is never computed.
18+
- **Restore package original.** The item's ⋯ menu offers it on an item customized into the pair. After a confirmation, it deletes the pair's draft of the item, then its row (`DELETE /meta/:type/:name?package=<pair>`). The base's item reads back.
19+
- **The read-only mark** no longer links a view, dashboard or report to Setup's overlay page.
20+
- **Unchanged.** A writable package is edited in place, as before. A new view ("Add view") on a read-only base stays unavailable. The item tools (history, source) stay read-only on the base.
21+
- **New strings.** `@object-ui/i18n`'s studio namespace gains twelve `engine.studio.customize.*` keys in `en` and `zh`. The other languages fall back to `en`, as the whole namespace does.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
'@object-ui/i18n': patch
4+
---
5+
6+
feat(studio): "New similar" on a flow in the Automations pillar, opening the copy where the clone door landed it
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
An admin who wants a second flow like one a package ships, for another rule, gets it without changing the package's flow (ADR-0146 D4). The flow clone door lands the copy switched off in a writable package, by default the source's paired `<base>.custom`, so Studio can offer the gesture where the flow is designed.
12+
13+
- **New similar in the flow's menu.** The open flow's ⋯ menu offers "New similar" on a read-only package's flow and on a writable package's flow. It asks for a new machine name and a label, both mandatory, and posts them to `POST /api/v1/automation/:name/clone`. On a read-only base the request names no package, so the door picks the pair; on a writable package it names the open package.
14+
- **The copy opens where the answer says.** The door's answer names the package the copy landed in. A copy in another package opens on that package's Automations pillar (`/studio/<package>/automations?surface=flow:<copy>`); a copy in the open package opens in place after the rail re-reads. Either way the server's sentence that the copy is switched off is shown as sent. An answer that names no package says so and opens nothing. Nothing records or draws what the copy was made from.
15+
- **Refusals and gates.** A refusal, a `409` for a taken name included, is the server's sentence, shown in the dialog, which stays open. A caller without `manage_metadata` sees the item disabled with the console's sentence for why. A read-only package's flow still offers no Delete, and the package-less scope draws no menu.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
'@object-ui/i18n': patch
4+
---
5+
6+
feat(studio): the All metadata area marks a base's customizations, filters to them and counts them
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
In Studio's All metadata area (ADR-0146 D3, "On top, one surface"), an installed base's lists now show its own items and the items of its paired `BASE.custom` package together. Every mark and number comes from the server's customization read, `GET /api/v1/packages/:id/customizations`. Nothing is computed from the documents: there is no diff and no `basedOn` verdict.
12+
13+
- **Marks.** An item the read lists as an override is marked "Customized (BASE)". For a type identified by name and member, such as an email template's locale, only that member is marked. Any other item of the pair is marked "Added". The base's other items carry no mark.
14+
- **Filter.** Each list offers "Only customizations (n)" once the read has answered and the base has a customization.
15+
- **Counts.** Each directory tile shows how many items of its type the read lists as overrides. The object tile also counts the extension members (fields, validation rules, indexes). The directory header shows the base's total, the served overrides plus the served extensions, which is the landing's "customized" figure.
16+
- **Pending or failed read.** While the read is pending, or after it fails, the area draws no mark, no filter and no number, never a "0". A failure shows the server's sentence. A base with no pair is unchanged, and so is Setup, which makes no customization read.
17+
- **Across packages.** In `/studio/~all/metadata`, an item of a `BASE.custom` package is labelled "Customization of BASE".
18+
- **One mark component.** `CustomizationMark` (`views/studio-design/CustomizationMark.tsx`) draws these marks for the whole console, and the pillar rails will reuse it. `fetchPackageCustomizations` returns the read in full, and `fetchPackageCustomizationCounts` (the landing's reader) now uses it.

0 commit comments

Comments
 (0)