Skip to content

Commit 939d798

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-6569-identical-variant-dedupe
2 parents 3201973 + 61282f9 commit 939d798

71 files changed

Lines changed: 5785 additions & 377 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: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
---
2+
'@objectstack/formula': patch
3+
---
4+
5+
fix(formula): `classifyError` grades a CEL fault by error class + code, never by the message (#6223)
6+
7+
`EvalResult.error.kind` is author-facing — `@objectstack/objectql`'s `cel-fault`
8+
puts it in front of the author as `` `${kind}: ${first line}` `` and
9+
`packages/rest` re-emits it as the HTTP body's `reason`. cel-js embeds the
10+
author's own **source line** in `message` (`formatErrorWithHighlight`), so a
11+
classifier that regex-matches that text is matching text the author writes.
12+
PR #6202 closed the `ParseError` arm this way and left `type` / `runtime` on the
13+
keyword table pending a per-code audit. This is that audit, and its verdict is
14+
that the table goes entirely.
15+
16+
Measured on cel-js 8.0.0 — one `no such overload` **evaluation** fault, four
17+
field names, three wrong answers:
18+
19+
```text
20+
record.status > 1 -> runtime (right)
21+
record.parse_status > 1 -> parse (wrong)
22+
record.syntax_mode > 1 -> parse (wrong)
23+
record.type_code > 1 -> type (wrong)
24+
```
25+
26+
`parse` is the inverse of the #6133 misdirection: the expression is
27+
syntactically perfect and failed on the data, and the author was told to go fix
28+
an expression that has nothing wrong with it.
29+
30+
`classifyError` now reads only structured contract:
31+
32+
- `ParseError` -> `bounds` when `code === 'limit_exceeded'`, else `parse`
33+
(unchanged, from #6202);
34+
- `EvaluationError` -> `type` for the one declaration-class code
35+
(`unknown_variable`, the root identifier is not bound in this scope at all),
36+
else `runtime`;
37+
- anything that is not a cel-js error -> `runtime`.
38+
39+
Two findings from the audit worth recording. First, the residual keyword arm was
40+
**not** dormant: `matches()` is an ObjectStack stdlib binding over `new
41+
RegExp(...)`, so an uncompilable pattern escapes as a native `SyntaxError` whose
42+
message echoes the pattern — and the pattern can come off the row, not just out
43+
of the source. `matches(record.name, record.re)` with `re = "type("` was
44+
graded `type`; with `"Exceeded maxAstNodes("` it was graded `bounds`. A data
45+
value was picking the error kind. Second, there is deliberately no `TypeError`
46+
arm: cel-js raises that class only from its non-evaluating `TypeChecker`, which
47+
runs only inside `Environment#check`, and that method catches it and *returns*
48+
`{ valid: false, error }`. The check-time `TypeError -> type` mapping already
49+
lives in `celEngine.compile`, which reads that object.
50+
51+
Six evaluate-time codes change verdict from `type` to `runtime`
52+
(`int_conversion_error`, `uint_conversion_error`, `double_conversion_error`,
53+
`invalid_index_type`, `heterogeneous_list_element`,
54+
`invalid_comprehension_range`). Each is a fault decided against the row; every
55+
one of them was graded `type` only because cel-js happens to use the word "type"
56+
in its prose (`int() type error: cannot convert to int`). Every evaluate-time
57+
code the engine can reach now has a fixture pinning its `kind`.

.changeset/console-b1204af0a1f7.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
"@objectstack/console": patch
3+
---
4+
5+
Console (objectui) refreshed to `b1204af0a1f7`. Frontend changes in this range:
6+
7+
Derived from the changesets objectui declared over the range — 8 releasing of 8 changesets added across 22 non-merge commits; omitted: 14 commits carrying no changeset (they ship no package code).
8+
9+
- **patch** — Anchor the scaffold's build-side `devDependencies` to this repo's real toolchain, and pin the whole generated manifest against drift (objectui `e473b6c29`)
10+
- **patch** — fix(data-table): don't render a row overflow ("⋮") trigger that opens an empty menu (objectui `7ed3360dc`)
11+
- **patch** — Show the `compareTo` comparison in a dataset pivot cross-tab instead of dropping it (objectui `02eb44490`)
12+
- **patch** — data-objectstack: type `queryDataset(selection)` as the spec's `DatasetSelection` instead of a hand-written copy (objectui `5f08c052d`)
13+
- **patch** — Point the `sys-objects` navigation entries at the canonical metadata-admin route instead of the `system/metadata/object` alias, removing a redirect hop from each click (objectui#3… (objectui `b7b05da7f`)
14+
- **patch** — create-plugin: make the scaffolded plugin's own test suite runnable (objectui `f4f42b4ae`)
15+
- **patch** — Accept React 19 in `@object-ui/plugin-report`'s peer range, the last UI package still declaring React 18 alone (objectui#3690). (objectui `3b1f888e5`)
16+
- **patch** — Point System Hub's Permissions card — both its link and its count — at `sys_permission_set`, closing the last of the five `system/*` navigation targets (objectui#3655). (objectui `cc95c2c31`)
17+
18+
**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.
19+
20+
- _(no changeset)_ docs(create-plugin): 按 route B 收缩到不漂移的部分,产物一律指向 buildPluginFiles() (#3715) (#3760) (objectui `b1204af0a`)
21+
- _(no changeset)_ test(scripts): assert every peer-line restatement against its own manifest (#3717) (#3751) (objectui `0c28a0720`)
22+
- _(no changeset)_ fix(runner): 删掉 vite 别名表里指向不存在包目录的 data-objectql 条目 (#3747) (objectui `5a297c3cd`)
23+
- _(no changeset)_ docs(ci): delete the unpinned .github/WORKFLOWS.md duplicate inventory, move what was true into the pinned guide page (#3724) (#3745) (objectui `28364bdd9`)
24+
- _(no changeset)_ ci(docs): fail OPEN when the Build Docs path gate cannot compute its diff (#3723) (#3744) (objectui `b1a67e0f6`)
25+
- _(no changeset)_ docs(skills): point console-development.md's 13 relocated symbols at their real paths (#3730) (#3734) (objectui `8ad6070fb`)
26+
- _(no changeset)_ docs(ROADMAP): P1.12/P1.16 的 MetadataManagerPage 铺与 Total 计数按现实改写 (#3712) (#3732) (objectui `3835d121c`)
27+
- _(no changeset)_ docs(skills): rewrite console-development.md to the post-cccdf84d reality and sync its eval (#3713) (#3729) (objectui `4e93e40d7`)
28+
- _(no changeset)_ docs: 清偿 #3711 门禁记账里属这三单的 8 处版本失真,并同步收缩 ledger (#3726) (objectui `fbf7d6d01`)
29+
- _(no changeset)_ docs(runner): Error Boundaries 一条改为可达的 SchemaErrorBoundary (#3635) (#3725) (objectui `b887282cb`)
30+
- _(no changeset)_ fix(fields): echo stored date values in the sub-grid's native date cells (#3718) (objectui `918888a30`)
31+
- _(no changeset)_ docs(console): drop the objectstack.config.ts ghost, the dead app-creation entries, and the routing-table overreach (#3580) (objectui `15a0d366b`)
32+
- _(no changeset)_ test(app-shell): 同步 pseudoRouteSegments 里的第二份 MetadataRedirectStub,并用整链断言钉住 (#3669) (#3691) (objectui `ae3bd96a1`)
33+
- _(no changeset)_ ci: subscribe the four gate workflows to merge_group, and move ci/lint path filtering into the jobs (#3523 steps 1-2) (#3722) (objectui `f710fc4e3`)
34+
35+
objectui range: `0cf8f0f70d10...b1204af0a1f7`
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
---
2+
"@objectstack/spec": patch
3+
"@objectstack/core": patch
4+
"@objectstack/lint": patch
5+
---
6+
7+
fix(spec,core): a filter placeholder is recognised by INTENT — `{TODAY()}` refuses loudly instead of comparing as a literal (#5586)
8+
9+
`UnknownFilterTokenError` had a hole exactly where authors fall in. Recognition
10+
used the token-NAME grammar `/^\$?\{([a-zA-Z0-9_]+)\}$/`, so any placeholder
11+
carrying a **non-word character** classified as "not a placeholder at all" and
12+
was handed to the driver verbatim, to be compared as a literal string — the
13+
silent-wrong-result failure the diagnostic exists to abolish.
14+
15+
The failure was inverted against the author. Measured on 17.0.0-rc.2 against a
16+
four-row fixture:
17+
18+
| filter value | before | |
19+
|---|---|---|
20+
| `due_date < '{today}'` | 2 rows | correct — the two overdue rows |
21+
| `due_date < '{TODAY}'` | throws `UnknownFilterTokenError` | diagnostic working |
22+
| `due_date < '{TODAY()}'` | **4 rows** | diagnostic bypassed — literal string compare, and `'2026-…' < '{'` in lexicographic order swallowed a row due a week later |
23+
24+
So misspelling `{today}` as `{TODAY}` was reported by name, while misspelling it
25+
as `{TODAY()}` returned the wrong rows in silence — and the parenthesised,
26+
kebab-case, natural-language and dotted spellings (`{TODAY()}`,
27+
`{current-user-id}`, `{30 days ago}`, `{user.id}`) are precisely what an author
28+
migrating from another system's macro syntax writes first.
29+
30+
**Both directions of the behaviour change:**
31+
32+
- **Previously silent, now refuses loudly** — a filter value that is entirely
33+
brace-wrapped and outside the vocabulary now throws `UnknownFilterTokenError`
34+
(`code: FILTER_TOKEN_UNKNOWN`, `status: 400`) on the ObjectQL read and write
35+
paths and the analytics dataset executor, and is reported as
36+
`filter-token-unknown` by `objectstack build` / `validate` / `lint`. Before,
37+
it reached the data engine and compared as text.
38+
- **Unchanged**`{today}` / `{current_user_id}` still resolve; `{TODAY}` still
39+
refuses with the same identity; a value that merely *contains* braces
40+
(`'acme {x} deal'`), or is not ONE pair around the whole value (`{a}{b}`,
41+
`{{x}}`, `{}`), is still an ordinary literal and still reaches the driver
42+
untouched.
43+
44+
Recognition and vocabulary are now two named grammars rather than one:
45+
`FILTER_TOKEN_WRAPPED_RE` (`/^\$?\{([^{}]+)\}$/`) answers "did the author mean a
46+
placeholder", and `isContextToken` / `isDateMacroToken` answer "is it in the
47+
vocabulary". Wide in, strict out. No escape hatch for a literal `{…}` comparand
48+
ships with this: a repo-wide measurement across structured metadata, examples,
49+
seed data and fixtures found zero legitimate consumers comparing a
50+
brace-wrapped literal, and an escape syntax is a public micro-contract that can
51+
be added the day one shows up.
52+
53+
Flow templates are unaffected. `interpolateFilter` in
54+
`@objectstack/service-automation` already recognised the same wide shape and
55+
resolves `{record.id}` / `{TODAY() + 30}` from flow variables **before** the
56+
filter reaches ObjectQL; its hand-off to the engine is keyed on the token
57+
vocabulary (`isKnownFilterToken`), which this change does not touch.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
---
2+
"@objectstack/lint": minor
3+
---
4+
5+
feat(lint): literal empty combinators are refused at authoring time, with a per-shape prescription (#5330)
6+
7+
#5322 settled what an empty combinator MEANS at run time — the boolean identity
8+
reduction — and #5659/PR #6528 made that reduction one implementation
9+
(`reduceFilterVerdict` in `@objectstack/spec/data`, proven against
10+
`FILTER_LOGIC_CASES`, consumed by every backend). This change adds the other half
11+
the ruling deliberately left open: the literal SPELLINGS are now refused where an
12+
author writes them, which is Prime Directive #12's standard shape (reject at the
13+
producer, do not tolerate at the consumer) and #5240's same-direction precedent
14+
one shape over.
15+
16+
`validateEmptyCombinators` is a new gating rule in `AUTHORING_RULES`, so it runs
17+
on `os validate` / `os build` / `os lint` at once, and on the runtime publish
18+
gate for `flow` writes — the door a Studio tenant, a REST `/meta` client and an
19+
MCP/AI author all use. Two rule ids:
20+
21+
- `filter-empty-combinator` — a literal `$and: []`, `$or: []` or `$not: {}`.
22+
- `filter-empty-node` — a literal `{}` standing as the whole filter, or as a
23+
branch of `$and` / `$or`.
24+
25+
**The prescription is per shape, because the identities disagree.** `{$and: []}`
26+
and `{}` reduce to TRUE (match EVERY row); `{$or: []}` and `{$not: {}}` reduce to
27+
FALSE (match NO row). A generic "empty combinator, fix it" message teaches the
28+
wrong fix half the time, so each shape names its own: delete the key to mean "no
29+
filter"; fill the array to mean a constraint; put the negated condition inside
30+
`$not`; and, when zero rows really is the intent, `{ <field>: { $in: [] } }` is
31+
the declared spelling that says so instead of implying it. The row-set wording in
32+
every message is DERIVED from `reduceFilterVerdict` rather than retyped, and a
33+
test drives the four #5322 identity cases straight out of `FILTER_LOGIC_CASES` and
34+
asserts the message agrees with the rows the table says the filter selects.
35+
36+
**Nothing at run time changed.** No translate or evaluation path is touched, the
37+
conformance matrix is untouched, and a stack that ignores the finding runs exactly
38+
as before. The literal-vs-programmatic boundary the ruling requires is structural,
39+
not heuristic: this rule sees only values that reached the metadata graph, so a
40+
producer that assembles zero disjuncts while serving a request — an RLS lowering,
41+
a CEL `!expr`, a client-built query — never reaches it and keeps the runtime
42+
identity, which is what makes `{$or: []}` = zero rows fail-closed (#5134).
43+
44+
Also internal: the filter-subtree traversal `validate-filter-tokens.ts` grew for
45+
#3574 moved to a shared `filter-walk.ts` now that it has a second consumer — the
46+
same argument `page-walk.ts` (#3583) and `view-walk.ts` (#6381) make. Each rule
47+
still declares its OWN surface list, so one rule's widening cannot land silently
48+
in the other; `validate-filter-tokens`'s behaviour is unchanged.
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
---
2+
"@objectstack/plugin-security": major
3+
---
4+
5+
<!-- adr-0087: not-required (no-migration-prescription) what this change removes is a VALUE in one platform-seeded sys_permission_set row, not an authorable key. No spec schema key is retired: object_permissions['*'] stays fully authorable, and admin_full_access / organization_admin / viewer_readonly still ship one. Nothing an app authored becomes invalid, nothing stored fails to parse, and the seeded row itself is rewritten by the boot seeder, so there is no stored shape for `objectstack migrate meta` to rewrite and nothing for the ledger to carry. The Migration section below prescribes a DEPLOYMENT action -- declare the object access you were relying on -- not a consumer code or metadata rewrite. -->
6+
7+
fix(plugin-security)!: `member_default` no longer grants a `*` wildcard — the platform baseline is explicit-allow (#5491)
8+
9+
**This is a deliberate, breaking narrowing of the default security posture.
10+
Deployments that relied on the implicit wildcard lose that access. That is the
11+
intended behaviour change, not a side effect — read the migration below before
12+
upgrading.**
13+
14+
`member_default` is the additive `everyone` baseline: it resolves for **every**
15+
authenticated member, in addition to whatever else they hold. It carried
16+
`object_permissions["*"] = {allowCreate: true, allowRead: true, allowEdit: true,
17+
allowDelete: false}`, and object permissions merge most-permissively — so that
18+
entry was not a default, it was a **floor no application could get under**. An
19+
app's explicit-allow object gate was erased on three of the four axes; only
20+
delete stayed profile-driven, because the baseline never granted it.
21+
22+
HotCRM's 17.0 GA sweep measured the consequence across 5 profiles × 17 objects
23+
(188 probes, each user with their own bearer token):
24+
25+
- **21 of 21 create-DENIAL probes returned `201`** — every profile created on
26+
every object once validation passed, including objects the profile explicitly
27+
denied;
28+
- a `service_agent` profile that declares no edit anywhere edited its own
29+
`crm_account`;
30+
- on `public_read` objects the wildcard yielded **`200` with ALL rows** for
31+
non-holders — real unauthorized reads, not the documented "200 with 0 rows"
32+
empty-set pattern;
33+
- `security/explain` stated it outright for a profile carrying an all-false
34+
deny: *"create on 'crm_opportunity' is granted by [member_default]"*.
35+
36+
Because app-side authorization suites validate the app's *declarations*, CI
37+
stayed green while the runtime posture was default-open — `declared ≠ enforced`
38+
inside the security layer itself.
39+
40+
**The change.** The wildcard is removed on all three live axes. The platform
41+
baseline narrows to explicit-allow: object access now comes from OWDs plus
42+
profile / permission-set **declarations** only. Deny-precedence merge semantics
43+
were considered and rejected — permission sets remain additive capability
44+
containers (ADR-0090); the fix is to stop the platform shipping a grant nobody
45+
asked for, not to invent a veto.
46+
47+
What `member_default` still declares, it still enforces, and nothing here is
48+
newly granted: read on the better-auth identity tables (their writes stay
49+
denied — that door is better-auth), self-service on `sys_user_preference` (now
50+
an explicit entry rather than an implicit one; the effective access for a member
51+
is byte-identical, and its `sys_user_preference_self` RLS carve-out already
52+
declared exactly that intent), and every row-level policy it shipped before —
53+
`owner_only_writes`, `owner_only_deletes` and the identity `_self` carve-outs
54+
are untouched. The set stays anchor-safe, so its `everyone` binding is
55+
unaffected. `admin_full_access`, `organization_admin` and `viewer_readonly` keep
56+
their wildcards: those are granted deliberately to a principal, which is exactly
57+
what the baseline was not.
58+
59+
## Migration
60+
61+
After upgrading, a member holding **no** application profile has no access to
62+
application objects. Restore access by declaring it, in one of two places:
63+
64+
1. **Ship an app default profile.** Mark a permission set `isDefault: true` and
65+
the CLI wires it as the additive per-request baseline (ADR-0056 D7 /
66+
ADR-0090 D5). This is the recommended route and what the bundled showcase app
67+
already does — list the objects members legitimately touch, with the axes
68+
they need.
69+
2. **Grant per position / per user.** Bind an ordinary permission set through
70+
`sys_position_permission_set` or `sys_user_permission_set`.
71+
72+
To find what a deployment was silently relying on, ask
73+
`GET /api/v1/security/explain?object=<name>&operation=<op>` for a
74+
representative member before upgrading: any answer attributing the grant to
75+
`[member_default]` on an application object is access that will stop. An app
76+
whose own profiles already declare everything its users do is unaffected.

0 commit comments

Comments
 (0)