Commit 215840f
fix(hono-server): seed /me/permissions for a viewAll-only wildcard principal too (#19242)
Fixes #18990
Clause-②: no
`/auth/me/permissions` stayed completely silent about every object a
principal could reach when that principal's only grant was a wildcard
carrying `viewAllRecords`. `seedSuperUserRestrictedObjects` returned
early unless the wildcard carried `modifyAllRecords`, so the object got
no entry, the client fell back to its default-allow path, and it
rendered write and Export affordances the server answers `403
EXPORT_NOT_PERMITTED`. Same silence and same consequence as the class
#18931 closed, for a different principal class.
Implements the ruling on this card — batch #159 item 1, **letter A**,
maintainer 「同意」 2026-09-18T11:42Z, recorded at comment `5729478681`.
## What changed
**One predicate admits both principal classes.** The seed now asks the
wildcard READ bypass rather than the modify bit alone. That question
already existed twice in this file's neighbourhood —
`foldWildcardSuperUser` asks it to decide whose `allowRead` it pulls
true, and `PermissionEvaluator.checkObjectPermission` applies the same
bypass server-side (`viewAllRecords || modifyAllRecords` short-circuits
read; only the modify bit reaches the write axis). It is now a single
module-local reading that the fold and the seed share, so the seed
structurally cannot materialise an entry for a principal the fold leaves
entirely false.
```
seed -> fold -> clamp -> annotate (unchanged order, unchanged call site)
{allow*: false} allowRead: true writes stay false apiOperations attached
```
**The export axis needed no new branch.** This was the premise worth
falsifying, since adding a second predicate for one question is the
exact defect #18931 was about. It holds: the seed resolves
`resolveEffectiveApiMethods` with `userExportAllowed` read off the
wildcard's `allowExport`, annotate resolves it with `acc.allowExport ??
wildExport`, and a seeded entry carries no `allowExport` of its own
while the fold adds none — so the two resolve to the same bit, and both
skip on the same `unrestricted && userExportAllowed`. The ruling's
carve-out ("skip only an unrestricted object whose export stays
allowed") is therefore reached through the predicate that was already
there, not a copy of it.
**A plain wildcard grant carrying neither bypass bit is still not
seeded**, and that is what makes the admission the read bypass rather
than "any wildcard": the fold pulls nothing true for such a principal,
so a seeded entry would be an all-false claim with no server behaviour
behind it. Pinned as its own case.
## ⭐ Two `toBeUndefined` pins are INVERTED on purpose
A reviewer should not have to guess whether a flipped pin was
intentional. Both flips are the ruling being carried out, not
collateral:
1. `does not reach a viewAll-only principal (the seed guard is
unchanged)` — the pin PR #18984 added, and the one the ruling names by
its text. That PR pinned this boundary deliberately while saying in the
same breath that the pin was **not** a ruling that the silence was
correct. The ruling on this card is what now says otherwise.
2. `does not seed for a viewAll-only wildcard (avoids flipping check()
to explicit deny)` — the older #3391-era pin on the same principal
class. The ruling names only the first; this one asserts the identical
thing about the identical class, so it moves with it. Flagging it
explicitly because the dispatch order named one pin and the diff flips
two.
The behaviour change they pinned is real and deliberate: for a
viewAll-only principal, a client that read "no entry" as default-allow
now reads an explicit `allowEdit: false`. That is the truth about it —
the seed only ever touches objects with **no explicit entry** (`if
(objects[name]) continue`), and on those a viewAll-only principal
genuinely can only read, while the silence it replaces was not true
about anything.
## Verification
Reverse verification, run from the committed fix and restored byte-exact
(`git hash-object` equal to the HEAD blob, `git diff HEAD` empty).
**Expected direction chosen before the run: the two new POSITIVE pins go
red; the two negative/control pins cannot distinguish and stay green.**
That is what happened — replacing the shared predicate with the old
`modifyAllRecords`-only guard: `Tests 2 failed | 22 passed`, failing
exactly `seeds for a viewAll-only wildcard too` and `reaches a
viewAll-only principal: read folded true, writes explicitly false`. The
two absence-asserting cases stayed green under the old guard, correctly:
absence held there too, so they are controls on the predicate's other
half and not evidence for the fix.
- `pnpm --filter @objectstack/plugin-hono-server test` :: exit 0 — 26
files, 311 passed, 1 todo
- `pnpm --filter @objectstack/plugin-hono-server typecheck` :: exit 0 —
0 errors, 0 pinned signatures
- `pnpm lint` (the full repo union, `eslint . --no-inline-config`) ::
exit 0, at `0625da76a`
- 59 derived gate families via `dispatch-gates --commands --repo
objectstack-ai/objectstack`, reconciled with `--ran` carrying every exit
code: **56 run green, 3 NOT MEASURED, 0 UNRUN**
The three NOT MEASURED are `check:dual-build-cjs-loads`,
`check:lean-entry-closure` and `check:type-check-debt`. Each exits **3**
(PREREQUISITE NOT MET, which is neither a pass nor a failure) because it
reads built output across the whole workspace and this tree has only the
`plugin-hono-server` dependency closure built. CI builds the workspace
before them. A declared narrowing, not a skipped gate.
`check-widening-tells --declaration no` exits 0 but reports all 3 files
NOT MEASURED — no declared surface covers `packages/plugins/**` — so the
`Clause-②: no` above is a structural measurement, not that gate's
clearance: the diff adds no exported symbol (`wildcardGrantsSuperRead`
is module-local), no key on the published payload (the `objects` map
gains **entries**, not keys), no registration and no error code.
`EXPORT_NOT_PERMITTED` appears in this diff only as prose citing the
existing code.
## Acceptance notes
- **Changeset: `patch` on `@objectstack/plugin-hono-server`.** A
behaviour fix in a released package. The body carries the
deliberate-behaviour-change warning for whoever compiles the release
notes, since a consumer reading "no entry" as default-allow sees a
different answer after this.
- Nothing here reaches `packages/spec` — `resolveEffectiveApiMethods` is
imported, not edited. The declared file surface
`packages/plugins/plugin-hono-server/src/` was not breached; the only
file outside it is the changeset this package owes.
- The re-lane the ruling flagged as possible does not materialise: all
three symbols resolve out of `plugin-hono-server`, confirmed again at
this branch's base.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01QCdUBjM47SxioST9z5Zwdf)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 7ec8534 commit 215840f
3 files changed
Lines changed: 110 additions & 18 deletions
File tree
- .changeset
- packages/plugins/plugin-hono-server/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 | + | |
Lines changed: 39 additions & 12 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
237 | 237 | | |
238 | 238 | | |
239 | 239 | | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
240 | 257 | | |
241 | 258 | | |
242 | 259 | | |
| |||
263 | 280 | | |
264 | 281 | | |
265 | 282 | | |
266 | | - | |
| 283 | + | |
267 | 284 | | |
268 | 285 | | |
269 | 286 | | |
| |||
443 | 460 | | |
444 | 461 | | |
445 | 462 | | |
446 | | - | |
| 463 | + | |
447 | 464 | | |
448 | 465 | | |
449 | 466 | | |
| |||
453 | 470 | | |
454 | 471 | | |
455 | 472 | | |
456 | | - | |
457 | | - | |
458 | | - | |
459 | | - | |
460 | | - | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
461 | 487 | | |
462 | 488 | | |
463 | 489 | | |
| |||
473 | 499 | | |
474 | 500 | | |
475 | 501 | | |
476 | | - | |
| 502 | + | |
477 | 503 | | |
478 | 504 | | |
479 | 505 | | |
| |||
1075 | 1101 | | |
1076 | 1102 | | |
1077 | 1103 | | |
1078 | | - | |
1079 | | - | |
1080 | | - | |
1081 | | - | |
| 1104 | + | |
| 1105 | + | |
| 1106 | + | |
| 1107 | + | |
| 1108 | + | |
1082 | 1109 | | |
1083 | 1110 | | |
1084 | 1111 | | |
| |||
Lines changed: 57 additions & 6 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3 | 3 | | |
4 | 4 | | |
5 | 5 | | |
| 6 | + | |
6 | 7 | | |
7 | 8 | | |
8 | 9 | | |
| |||
189 | 190 | | |
190 | 191 | | |
191 | 192 | | |
192 | | - | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
193 | 199 | | |
194 | 200 | | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
195 | 215 | | |
| 216 | + | |
196 | 217 | | |
197 | 218 | | |
198 | 219 | | |
| |||
277 | 298 | | |
278 | 299 | | |
279 | 300 | | |
280 | | - | |
281 | | - | |
282 | | - | |
283 | | - | |
284 | | - | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
285 | 334 | | |
| 335 | + | |
| 336 | + | |
286 | 337 | | |
287 | 338 | | |
288 | 339 | | |
| |||
0 commit comments