Repository navigation
Commit 05be352
Fixes #20869
Clause-②: no
## What this changes
The refusal of a field-to-field comparison across comparison classes
(`INVALID_FILTER` / 400) now opens with its remedy, on both of its
producers. The REST door bounds a 4xx message by cutting its tail
(`CLIENT_MESSAGE_MAX = 500`: 499 characters plus an ellipsis). Both
producers wrote the remedy last, so no caller ever read it. The bound is
not touched; that is #5423's decision. The producers change.
- **`packages/formula/src/matches-filter.ts`, `crossFieldClassError`.**
The remedy sentence comes first, byte-identical to the one the message
ended with. The diagnosis is shortened so the whole message is 494
characters and reaches the wire whole. Order: the remedy; what is
refused (two columns with no shared class, and the classes); why it is
refused; why the columns are withheld. It still names no column,
operator or policy; the columns travel on the error's symbol key for the
server log only.
- **`packages/plugins/plugin-security/src/explain-engine.ts`,
`crossFieldRefusalForExplain`.** The remedy sentence, also
byte-identical, moves before the subject and the diagnostic. Those have
no length bound: object, field and policy names declare no maximum
(`SnakeCaseIdentifierSchema`, the object `name` and the RLS policy
`name` are regex-only), and the subject lists every refused policy. So
no subject-first order keeps a trailing remedy for every policy; at
index 0 it survives any length. The reason drops one redundant clause
("instead of judging a record": the same sentence already says explain
"reports no verdict").
Code, status and trigger are unchanged. Only the text moves and
shortens.
## Measured through the real handlers
ObjectQL on driver-sql (better-sqlite3), `SecurityPlugin`, `RestServer`
route handlers, and a policy `record.status != record.amount`. "Remedy
at" is the index where the remedy sentence starts.
| door | producer | before (`013f97df93`) | after |
|---|---|---|---|
| `POST /data/:object` (insert: the RLS write check) | record matcher |
thrown 972, remedy at 825; wire 500 with ellipsis, **remedy absent** |
thrown 494 = wire 494, no ellipsis, remedy at 0 |
| `GET /data/:object` (find) | driver-sql's read refusal | 383, whole,
no ellipsis | unchanged |
| `POST /security/explain`, short names | explain engine | thrown 601,
remedy at 477; wire 500, **remedy absent** | thrown 573, remedy at 0;
wire 500, cut in the reason |
| `POST /security/explain`, 60-character names | explain engine | thrown
920, remedy at 796; wire 500, **remedy absent** | thrown 892, remedy at
0; wire 500 |
| control: `GET /data/:object` with `{ title: { $bogus: 1 } }` |
driver-sql, another class | 228, wire equals thrown | unchanged |
**A find never carries the matcher's message.** Only the RLS write check
(`security-plugin.ts`, `satisfiesCheck`) and explain
(`matchUnderDeclaredColumns`) hand `matchesFilterCondition` the declared
columns its class rule reads; the other runtime caller (`objectql`
`having-filter.ts`) passes none. On `/data` a find answers driver-sql's
own read refusal of the same comparison, 383 characters, which already
reached the wire whole and states the rule ("compared as the same type
class"). The matcher's text reaches `/data` on an insert or an update.
So the `/data` pin covers both: the insert carries the matcher's remedy,
and the find carries its read refusal whole.
## Pins
- `packages/rest/src/cross-class-refusal-remedy-on-the-wire.test.ts`
(new, the real stack, both envelopes this family speaks):
- `/data` insert: the wire message starts with the matcher's remedy, is
under the bound, equals the thrown message, and names neither column;
- `/data` find: the read refusal reaches the wire whole and states the
same-class rule;
- `POST /security/explain`: the wire message starts with the explain
remedy, is within the bound, and names the policy;
- long names (object, policy and fields about 120 characters each):
explain is cut at exactly 500 with the remedy intact, and the matcher's
message is identical to the short fixture's;
- control: a short refusal of another class (unsupported operator, 228
characters) reaches the wire equal to the thrown message.
- `packages/formula/src/matches-filter-cross-field-class.test.ts`: the
message starts with the remedy, is under 500, is the same for long
column names, and keeps the clause order.
-
`packages/plugins/plugin-security/src/explain-cross-class-refusal.test.ts`:
every refused cell (5 predicates, read / update / delete, SQLite and
sqlite-wasm) asserts the message starts with the remedy.
-
`packages/plugins/plugin-security/src/rls-check-cross-class-field-refused.test.ts`:
the pinned opening moves with the text.
**Red, then green.** The new REST pin reads the producers through their
`dist/` (the `rest` to `plugin-security` pair is unaliased and
registered in `check:test-source-alias`). At `2cfaa6522f`, against
producer builds from BASE `013f97df93` (new-text markers: 0 in both
dists): 3 failed (insert, explain, long names) and 2 passed (find,
control). After rebuilding both producers from `2cfaa6522f` (markers: 1
in each): 5 passed.
## Verification (head `dc440bff6b`, after merging `origin/main` at
`f6ccca4a44`)
- **Package suites at `5fde18e296`** (before the merge):
- `@objectstack/formula`: `test` 42 files, 1241 passed; `typecheck` OK,
test-layer debt held;
- `@objectstack/plugin-security`: 149 files, 3227 passed, 23 skipped
(the PostgreSQL legs, no server); `typecheck` OK;
- `@objectstack/rest`: `--project local` 245 files, 4875 passed, 114
skipped; `typecheck` OK, 0 test-layer errors.
- **At `dc440bff6b`** (the merge brought commits into `rest`): the
formula pin file 23 passed; the three plugin-security cross-class files
150 passed, 23 skipped; `@objectstack/rest` `--project local` 246 files,
4890 passed, 114 skipped.
- **Gates.** `node scripts/pm/dispatch-gates.mjs --commands` at
`dc440bff6b` derives 65 commands; all 65 exit 0. `--ran` reconciliation:
"65 derived, 65 run, 0 NOT-MEASURED, 0 UNRUN". Three of them
(`check:dual-build-cjs-loads`, `check:i18n`, `check:type-check-debt`)
first exited 3 (PREREQUISITE NOT MET: nothing measured). After the full
`turbo run build --filter='./packages/*' --filter='./packages/*/*'` (71
tasks), all three exit 0.
- **Lint, narrowed, at `dc440bff6b`.** ESLint's own `isPathIgnored` and
`calculateConfigForFile` put 6 of the 7 changed paths in its population;
its config ignores the changeset `.md`. `lintFiles` over those 6 with
`allowInlineConfig: false` (the `lint` script's `--no-inline-config`),
JSON formatter: 6 files, 0 errors, 0 warnings. The config enables no
type-aware linting (no `parserOptions.project`, no typed rules), so this
diff cannot move a verdict on an untouched file. The repository-wide
`pnpm lint` belongs to CI.
## Acceptance notes
- **driver-mongodb, a sibling refusal of another class**
(`fieldReferenceUnsupportedError`: the driver has no field-to-field
lowering at all). It is 538 characters from source, so the bound cuts
it. Its remedy ("Compare against a literal value instead.") ends at 410
and survives; the cut drops the end of its withholding sentence. Not
edited here. Source reading plus the bound's arithmetic; not measured
through a REST door, because no MongoDB server was available.
- `packages/rest/src/security-explain-envelope.test.ts` still builds its
matcher and explain refusals by hand, with the old opening, and says
neither `@objectstack/formula` nor `@objectstack/plugin-security` is a
dependency of `rest`. `plugin-security` is a devDependency now. The
hand-built text is a fixture, not a pin of either producer, and the
route reads only its code and status. Left as is.
- The explain copy never carried the class list or a withholding
sentence (it names the policy and both columns by design, since the
explain report publishes the same predicate), so it keeps remedy,
subject and diagnostic, and reason.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 525b813 commit 05be352
7 files changed
Lines changed: 349 additions & 16 deletions
File tree
- .changeset
- packages
- formula/src
- plugins/plugin-security/src
- rest/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
Lines changed: 25 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
161 | 161 | | |
162 | 162 | | |
163 | 163 | | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
164 | 189 | | |
165 | 190 | | |
166 | 191 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
507 | 507 | | |
508 | 508 | | |
509 | 509 | | |
| 510 | + | |
| 511 | + | |
| 512 | + | |
| 513 | + | |
| 514 | + | |
| 515 | + | |
| 516 | + | |
| 517 | + | |
510 | 518 | | |
511 | 519 | | |
512 | 520 | | |
513 | | - | |
514 | | - | |
515 | | - | |
516 | | - | |
517 | | - | |
518 | | - | |
519 | | - | |
520 | | - | |
521 | | - | |
522 | | - | |
| 521 | + | |
| 522 | + | |
| 523 | + | |
| 524 | + | |
| 525 | + | |
| 526 | + | |
523 | 527 | | |
524 | 528 | | |
525 | 529 | | |
| |||
Lines changed: 12 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
180 | 180 | | |
181 | 181 | | |
182 | 182 | | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
183 | 192 | | |
184 | 193 | | |
185 | | - | |
| 194 | + | |
| 195 | + | |
186 | 196 | | |
187 | 197 | | |
188 | 198 | | |
189 | 199 | | |
190 | 200 | | |
191 | 201 | | |
| 202 | + | |
192 | 203 | | |
193 | 204 | | |
194 | 205 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
923 | 923 | | |
924 | 924 | | |
925 | 925 | | |
| 926 | + | |
| 927 | + | |
| 928 | + | |
| 929 | + | |
| 930 | + | |
| 931 | + | |
| 932 | + | |
| 933 | + | |
926 | 934 | | |
927 | 935 | | |
928 | 936 | | |
| |||
939 | 947 | | |
940 | 948 | | |
941 | 949 | | |
942 | | - | |
943 | | - | |
944 | | - | |
945 | | - | |
| 950 | + | |
| 951 | + | |
| 952 | + | |
| 953 | + | |
946 | 954 | | |
947 | 955 | | |
948 | 956 | | |
| |||
Lines changed: 2 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
204 | 204 | | |
205 | 205 | | |
206 | 206 | | |
207 | | - | |
| 207 | + | |
| 208 | + | |
208 | 209 | | |
209 | 210 | | |
210 | 211 | | |
| |||
0 commit comments