Skip to content

Commit dd41112

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-7536-like-wire-lowering
2 parents b157e8c + 90336e6 commit dd41112

40 files changed

Lines changed: 3110 additions & 124 deletions

.changeset/console-6314e87f2d49.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
---
2+
"@objectstack/console": minor
3+
---
4+
5+
Console (objectui) refreshed to `6314e87f2d49`. Frontend changes in this range:
6+
7+
Derived from the changesets objectui declared over the range — 27 releasing of 30 changesets added across 43 non-merge commits; omitted: 3 release-nothing changesets, 14 commits carrying no changeset (they ship no package code).
8+
9+
- **minor****BREAKING** — Retire the `global_nav` Studio designer surfaces, and track the `@objectstack` family at `17.0.0-rc.6` (objectstack#7100 / objectstack#6888). (objectui `38ab5054f`)
10+
- **minor** — One fullscreen long-text editor, hoisted to the package both render paths may import (objectui `cb1340058`)
11+
- **minor** — i18n: retire the orphaned `report.editor.*` namespace — 105 of its 106 keys, in all ten locale packs (~1050 translated strings) (objectui `fa511094a`)
12+
- **minor**`/accept-invitation/:invitationId` is one route, one component, one namespace — the console now renders the invitation page that actually shows you the invitation (objectui `0e67b53ff`)
13+
- **patch** — Inline-editing an `address` on the record detail page now edits it as real sub-fields, instead of collapsing it to one text box reading `[Object]` and saving a string over the str… (objectui `6314e87f2`)
14+
- **patch** — An image field's declared `maxSize` is enforced before the upload starts, not after it finishes (objectui `433ff9fd3`)
15+
- **patch** — A `Field.address` value now reads as a formatted postal address on the record detail page, instead of stringified JSON. (objectui `e2e6360c2`)
16+
- **patch** — Renaming a freshly-created view now persists — `updateView` reads and writes the same row, instead of reading the published overlay and losing the edit into a rejected partial wri… (objectui `b42558a4c`)
17+
- **patch** — Using a list's filter panel no longer overwrites the view's source-declared `filter` for everyone (objectui `f8595a054`)
18+
- **patch** — A list emptied by the view's own filter says "no records match", instead of inviting you to create your first record (objectui `f8595a054`)
19+
- **patch** — An illegal gantt dependency link now says why it was refused, instead of doing nothing (objectui `e1ade8f03`)
20+
- **patch** — The gantt's conflict dialog shows the number of affected tasks again, not a literal `{2}` (objectui `828549a9a`)
21+
- **patch** — An action rendered in the overflow menu, as an icon or inside a group now reaches the runner carrying the same authored keys as the same action rendered inline — `action:menu`, `a… (objectui `d6e5124a3`)
22+
- **patch** — A rejected Kanban drag rolls the card back on both data ownerships, not just when the board owns its own records (objectui `2c8ad7cdb`)
23+
- **patch** — ObjectGrid's bulk-bar **Clear** now unticks the row checkboxes, instead of only removing the toolbar (objectui `51ab34e34`)
24+
- **patch** — Conditional required (`requiredWhen`) now decides at SUBMIT time too — the star and the validator can no longer disagree (objectui `b1e42d09b`)
25+
- **patch** — fix(app-shell): the top-bar bell polls the inbox on every console surface, not only inside an app (#4110) (objectui `7b0783232`)
26+
- **patch** — An `autoTrigger` action that spills past `action:bar`'s `maxVisible` now still runs — `action:menu` consumes the flag instead of dropping it. (objectui `debad2796`)
27+
- **patch** — The first-run setup wizard no longer drops a brand-new owner outside the console (objectui `1f34b3825`)
28+
- **patch** — The AI build conversation no longer blanks itself the moment the preview opens (objectui `e16fd9597`)
29+
- **patch**`features.passkeys` and `features.magicLink` are documented as reserved, so enabling them no longer implies a login-page entry point that does not exist (objectui `564252cd8`)
30+
- **patch**`/setup` is a real address again — the console gets a stable deep link into platform administration instead of bouncing you back to home (objectui `b3f665b49`)
31+
- **patch**`?runAction=create_environment` is no longer consumed when the environments toolbar has no create action to run it on. (objectui `bf2fd3d1f`)
32+
- **patch** — The build-history panel tells an operator a 503 means "the commit store could not be reached — retry", instead of `commits HTTP 503` (objectui `f7c6430ec`)
33+
- **patch** — Stop the report config panel being titled "Title", and the view-settings colour section "Color" (objectui `ff84b0523`)
34+
- **patch** — Fail when a `t()` call site's arguments are not the holes its `en` value has, and delete the three that were inert (objectui `5f40de7d4`)
35+
- **patch**`DatasetWidget`'s option-color / dimension-label probe now rides the host's authenticated fetch (`SchemaRendererContext.apiFetch`) instead of the bare global `fetch`. (objectui `ee7a68d2d`)
36+
37+
⚠️ 1 of these carries a breaking change: 1 by the author's own breaking annotation in the changeset body — objectui declares no `major` inside a launch window (`scripts/check-changeset-no-major.mjs`). Each is marked **BREAKING** in the list above — read them before compiling the release record.
38+
39+
**In this console build, declared nowhere** — objectui merged 14 commits in this range with no `.changeset/*.md`. The code is inside the pin above and ships here, but nothing upstream declared them, so they appear in no objectui CHANGELOG and in no entry above. Listed by subject rather than counted, because a count cannot tell a dependency bump from a form-behaviour change (objectstack#6174); the upstream gate that would prevent this is objectui#3387.
40+
41+
- _(no changeset)_ fix(editor,markdown): complete the vite alias tables so the per-package test task resolves (#4218) (objectui `a9a67ec5b`)
42+
- _(no changeset)_ ci(turbo): derive the `lint` and `build` inputs guards from each package's real program (#4184, #4185) (#4200) (objectui `a5b1b8917`)
43+
- _(no changeset)_ ci(turbo): derive the test inputs guard from each package's Vitest config program (#4178) (#4188) (objectui `2b9428338`)
44+
- _(no changeset)_ ci(turbo): derive the type-check inputs guard from each package's tsc program (#3514) (#4176) (objectui `eb5f8cea0`)
45+
- _(no changeset)_ docs(ci): pin the 'can never be required, structurally' bullet to the YAML it quotes (#4170) (#4175) (objectui `c27c8981f`)
46+
- _(no changeset)_ ci(shadcn): close the three declared alarm-channel gaps (#3586) (#4174) (objectui `e63853173`)
47+
- _(no changeset)_ docs(links): scan the app READMEs and the rest of the repo root (#4148) (#4173) (objectui `da8109300`)
48+
- _(no changeset)_ chore(site): ignore the AGENTS.md/CLAUDE.md that `next dev` mints, and turn the minting off (#4172) (objectui `0ead48368`)
49+
- _(no changeset)_ docs(ci): stop the Merge Queue section keeping its own copy of the subscriber list (#4154) (#4171) (objectui `6eb40b8d7`)
50+
- _(no changeset)_ docs: repair QUICK_REFERENCE's dead commands and layout claims, and pin them (#4149) (#4159) (objectui `521a37bd0`)
51+
- _(no changeset)_ docs(ci): drop the fourth hand-copy of the object-ui ratchet list, and gate the page (#3782) (#4153) (objectui `492223d9a`)
52+
- _(no changeset)_ docs: repair and pin QUICK_REFERENCE's Current Release block, drop the console README's hand-written versions (#4143) (#4150) (objectui `43b2e4565`)
53+
- _(no changeset)_ docs: drop the hardcoded package versions from the utilities pages and correct the data-objectstack README install line (#4125, #4130) (#4144) (objectui `d86d372ad`)
54+
- _(no changeset)_ docs(console-starter): correct the 'Without a backend' root-route paragraph (#4102) (#4142) (objectui `148ade326`)
55+
56+
<!-- adr-0087: not-required (already-registered action-global-nav-location-removed) The one breaking entry in this range is objectui#4169, which drops the Studio designer surfaces for the `global_nav` action location. The spec-side retirement of that enum value landed in this repo with objectstack#7100 / #6888 and is already on the ledger as the conversion `action-global-nav-location-removed` (packages/spec/src/conversions/registry.ts) plus its protocol-17 semantic entry; this pin bump ships the console catching up to that decision and changes no spec surface of its own, so it registers nothing new. -->
57+
58+
objectui range: `92c0b1f403f7...6314e87f2d49`
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
---
2+
"@objectstack/runtime": patch
3+
---
4+
5+
fix(runtime): stop the dispatcher answering a permission denial's internal authorization payload (#7450)
6+
7+
`plugin-security`'s object gate attaches
8+
`{ operation, object, positions, permissionSets }` to every
9+
`PermissionDeniedError`. The two HTTP transports disagreed about what to do with
10+
it. `@objectstack/rest`'s `mapDataError` never reads `error.details` — its 403
11+
body is `{ error, code, object? }`, and the `object` on it is the one the ROUTE
12+
named. The dispatcher's `dispatch()` catch spread the whole payload
13+
(`{ code: 'PERMISSION_DENIED', ...(e.details ?? {}) }`), and `buildApiError`
14+
puts everything that is not the `code` on the wire as `error.details`.
15+
16+
Per the maintainer's 2026-08-11 ruling the two transports now agree on REST's
17+
shape: **message + code + the route-derived object**. `positions` and
18+
`permissionSets` are server-side diagnostics and are no longer serialized; the
19+
full withheld payload is written to a server log line instead, so a false
20+
denial is still diagnosable.
21+
22+
**The object is derived from the request path, not from `error.details`.** This
23+
is the part an "allowlist `operation` + `object`" reading gets wrong.
24+
`ObjectQL.cascadeDeleteRelations` re-enters `delete()` for every child of the
25+
row being deleted, so a child's own trip through the security middleware throws
26+
with `details.object` set to the CHILD. Forwarding that field would have reached
27+
the ruled field set and still answered the API name of an object the caller
28+
never addressed. The dispatcher now reads no field of `error.details` at all:
29+
`object` comes from `cleanPath`, exactly as REST takes `req.params.object`, and
30+
a denial on a route whose path names no object carries no `object` — REST's
31+
`...(object ? { object } : {})` behaviour.
32+
33+
**Also fixed, and required for the above to have any effect on the wire.** The
34+
domain-registry branch of `dispatch()` returned its handler's promise without
35+
awaiting it. In an async function a bare `return <promise>` settles outside the
36+
enclosing `try`, so a domain handler's rejection never reached that method's
37+
`catch` — and every domain that can raise an object-gate denial (`/data` among
38+
them) resolves through that branch, which made the `PERMISSION_DENIED` branch
39+
unreachable in practice. Denials escaped to the Hono catch-all instead, which
40+
answered `{ error: { message, code: 403 } }`: a numeric `code`, the shape
41+
`error-envelope.ts` exists to prevent, with no `PERMISSION_DENIED` string for a
42+
client to branch on. The branch now awaits, so a `/data` denial answers the
43+
ruled envelope. Non-denial errors are unaffected — the catch rethrows them and
44+
they reach the adapter exactly as before.
45+
46+
**Wire-visible.** A consumer reading `error.details.positions`,
47+
`error.details.permissionSets` or `error.details.operation` off a dispatcher 403
48+
no longer receives them, and `error.details.object` is now the object the
49+
request addressed rather than whichever object the gate refused. A `/data`
50+
denial's `error.code` is now the string `PERMISSION_DENIED` rather than the
51+
number `403`.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
"@objectstack/lint": minor
3+
---
4+
5+
feat(lint): report `sharingModel: controlled_by_parent` with nothing to derive access from (#7503)
6+
7+
An object that declares `controlled_by_parent` and gives the platform no
8+
relation to derive access from is a metadata defect, and until now nothing said
9+
so before the app was in someone's hands. Both runtime halves of ADR-0055
10+
already refuse the shape — reads resolve to a deny-all filter, and writes are
11+
refused with `422 INVALID_METADATA` (`MasterDetailRelationMissingError`, added
12+
in #7474) — but only when a caller happens to touch the object. The defect
13+
exists from the moment the metadata is authored, which is where it is now
14+
reported.
15+
16+
**New rule — `security-controlled-by-parent-no-relation` (`error`).** It fires
17+
on an object whose `sharingModel` is `controlled_by_parent` and which matches
18+
none of the three shapes the runtime's `resolveCbpRelation` resolves: a
19+
`required` `master_detail`, else any `master_detail`, else a `required`
20+
`lookup` — each of which must also name a `reference` target. Anything the
21+
runtime resolves stays silent, including the `master_detail` an author left
22+
un-`required`.
23+
24+
`error`, not advisory, by the criterion the security linter states for itself
25+
(ADR-0090 D7 / ADR-0049): every `error` rule mirrors a runtime enforcement
26+
point, and this one mirrors a hard refusal exactly. The defect is also a
27+
self-contained property of the object document — unlike the advisory rules in
28+
this family, there is no per-permission-set nuance the linter cannot adjudicate
29+
and no reading under which the object works.
30+
31+
It matters most for AI-authored metadata: `controlled_by_parent` next to a
32+
`lookup` nobody marked `required` is a plausible thing for an agent to write,
33+
and nothing in the authoring loop used to say so.
34+
35+
Runs on the `os compile` / `os lint` / `os validate` CLI surface, alongside the
36+
rest of `validateSecurityPosture`.
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
---
2+
"@objectstack/driver-mongodb": patch
3+
---
4+
5+
fix(driver-mongodb): refuse malformed `$between`, undeclared node-level `$`-keys and `{ field: {} }` (#5346, #5376)
6+
7+
`driver-mongodb` was the last backend still ANSWERING three filter shapes every
8+
other backend refuses. All three failed the same way — the query ran, reported
9+
nothing, and returned a row set nobody asked for. Measured through
10+
`translateFilter` (a pure function whose output *is* the document MongoDB
11+
receives):
12+
13+
```
14+
{ score: { $between: 5 } } => {"score":{}}
15+
{ $where: 'return true' } => {"$where":"return true"}
16+
{ stage: {} } => {"stage":{}}
17+
```
18+
19+
All three now refuse with `INVALID_FILTER` / 400 (ADR-0112), naming the position
20+
(`filter.$or[1].score.$between`), through the same `unsupportedFilterError`
21+
constructor this package's other filter refusals already used — no new envelope.
22+
23+
- **Malformed `$between`** — the emitter arm wrote both bounds inside
24+
`if (Array.isArray(value) && value.length === 2)` and had no `else`, so a
25+
malformed comparand dropped the whole range and normalised the field to `{}`.
26+
The twin, down to the missing `else`, of the arm #5328 fixed on
27+
`driver-memory`. The leading sentence is `driver-sql`'s verbatim — one
28+
condition, one wording (#5240).
29+
30+
- **An undeclared `$`-key in a NODE position** — the severe one. The translator's
31+
switch knows three combinators (`$and` / `$or` / `$not`); every other key took
32+
the FIELD path, and a key carrying no `$`-prefixed sub-keys fell to implicit
33+
equality and was written into the outgoing document verbatim, where **MongoDB
34+
executed it**. `$where` is server-side JavaScript; `$nor` is a real combinator
35+
the Filter Protocol never declared. The emitter's field-level `default:` arm
36+
has named exactly these spellings as its P0 reason for refusing them one level
37+
down for two releases — that gate was only ever installed at the field
38+
position. On the other backends the same input compiled to a column name and
39+
returned zero rows (#5348 / cloud#1077, since refused) or was already refused
40+
(#5324); only here was it evaluated.
41+
42+
- **`{ field: {} }`** — a field constrained by zero operators, ruled REFUSE on
43+
#5240 and gated on `driver-sql` / `driver-sqlite-wasm` / `driver-memory` /
44+
`formula` by #5327. This driver translated it to `{ field: {} }`, which MongoDB
45+
reads as "the field is deep-equal to the empty document" — not the FALSE the
46+
ruling declined to take, but a DIFFERENT filter that merely looks like FALSE
47+
until a document actually stores `{}` there.
48+
49+
Each gate sits on the validating walk (`classifyFilterKey`), beside the existing
50+
`$null` (#5347) and `$icontains` (#6520) gates, rather than in the emitter — the
51+
emitter is skipped wholesale when a boolean identity settles the enclosing node,
52+
so a gate there would fire or not depending on a shape's SIBLINGS. Measured
53+
before the fix: `{ $or: [ {}, { $where: 'x' } ] }`,
54+
`{ $or: [ {}, { score: { $between: 5 } } ] }` and `{ $or: [ { a: {} }, {} ] }`
55+
all translated to `{}` — match-all. The `$between` emitter arm additionally
56+
keeps a local check as defense for its own invariant, the dual-gate pattern the
57+
`$null` arm documents; both sites call one constructor with one path spelling.
58+
59+
Every filter that translated before still translates byte-identically: this adds
60+
refusals in front of the verdict, it does not reclassify any surviving shape.
61+
Authored filters using these shapes were already not doing what they appeared to
62+
do, and now say so instead of answering silently.

0 commit comments

Comments
 (0)