Repository navigation
Commit 4b8cbe2
fix(core): the fellow-org peer list keeps every member the first membership read returned (#29545)
<!-- claude-projects-attribution: {"github_login":"os-warren"} -->
_Requested by **Warren** · [project
thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ8UqrAZsJzxmjyAzE8Z3mZR1)_
Refs #29398 (item 2)
Clause-②: no
Metadata-protocol: none
## 维护者速读
- 改了什么:组织成员数达到 1,000 时,解析器会先读一次 1,000 行、再按 id
分页读完整成员;现在两次读的结果取并集,而不是用第二次的结果替换第一次。
- 为什么改:若补全读看到的成员比第一次少(另一连接上的旧快照),同组织成员列表 `org_user_ids`
会比第一次读本身还短,这些成员在普通成员的 `sys_user` 读取里悄悄消失。
- 风险与回滚:两次读都按当前组织过滤,并集不会加入组织外的人;不增加也不改变任何读;回滚即 revert 本 PR。
- 先看:`packages/core/src/security/resolve-authz-context.ts` 第 5
步(`orgMembers` 的并集一行),再看同目录
`resolve-authz-context.org-members-past-first-read.test.ts` 的新用例。
- 两分钟验证:`pnpm --filter @objectstack/core exec vitest run
src/security/resolve-authz-context.org-members-past-first-read.test.ts`,预期
9/9 通过。
- 需要你做的:无,席位落地。卡的第 1 项(按 id 顺序的逐页校验)不在本 PR,卡保持打开,见 Acceptance notes。
## What changes
`resolveUserAuthzGrants` step 5 builds `org_user_ids` from Leg 1's
`sys_member` read (`orgMembersLeg`, bounded at 1,000 rows). When that
read comes back at its bound, `readOrganizationMembersToCompletion`
reads the whole membership in id order. Its rows used to replace
`orgMembersLeg`. They are now unioned with it, so a completion that sees
fewer members than Leg 1 already held cannot shorten the list. Both
reads filter on the active organization, so the union adds nobody
outside it. No read is added, removed or changed, and the
batch-equivalence goldens do not move.
One `patch` changeset for `@objectstack/core`.
## Why
This is item 2 of the card: a completion that answers from an older
snapshot on another connection returned a shorter list than the first
read alone. Item 1 (the per-page order check) is held back. The
Acceptance notes say why.
## Tests
- New case in
`resolve-authz-context.org-members-past-first-read.test.ts`: 1,005
members, and the completion's ordered reads cannot see five members the
unordered first read returned. Every one of the five, and the whole
membership, is still in `org_user_ids`, and nobody from the other
organization is.
- The fake driver gains `staleForOrderedReads`, a set of user ids that
its ordered `sys_member` reads (the completion's) do not return.
<details><summary>Ablation (head 3c6ddc2)</summary>
The union was reverted on disk with `scripts/ablation-replace.mjs`:
anchor `[...orgMembersLeg, ...await ` changed to `[...await ` (anchor x1
to x0, replacement x0 to x1, blob c3eddd2 to 93bebcf0). The test
imports the resolver from `src/`, so no build is involved.
- Mutated: `Tests 1 failed | 8 passed (9)`. The new case is the only
red: `expected [ 'u000000', 'u000884', …(998) ] to include 'u000001'`.
- Restored: blob back to c3eddd2, equal to HEAD, and `git diff HEAD` is
empty.
</details>
<details><summary>Suites (head 3c6ddc2)</summary>
- `pnpm --filter @objectstack/core test`: Test Files 131 passed (131),
Tests 2927 passed (2927).
- `pnpm --filter @objectstack/core typecheck`: exit 0.
- The past-first-read suite plus the batch-equivalence suite: Test Files
2 passed (2), Tests 45 passed (45).
</details>
## Verification
<details><summary>Gates (head 3c6ddc2)</summary>
- `node scripts/pm/dispatch-gates.mjs --commands` derived 61 commands.
All were run locally, exit codes were captured before any pipe, and
`--ran` reconciled them: 61 derived, 60 run, 1 NOT-MEASURED, 0 UNRUN.
- `pnpm check:dual-build-cjs-loads` exited 3 (PREREQUISITE NOT MET, it
needs every package built). It is NOT MEASURED locally and left to CI.
- `check-plugin-teardown-shape --self-test` passed after `git fetch
--unshallow` (its fixture is pinned to an old commit).
`check:lean-entry-closure` passed after building
`@objectstack/objectql`.
- `pnpm lint` exited 0.
</details>
## Acceptance notes
**Item 1 of the card is held back, as the seat ruled.** The card's
suggested per-page check ("each id strictly greater than the row before
it") would compare ids in JavaScript code-point order. `driver-sql`
emits a plain `ORDER BY id` and `id > ?`, so the database collation
decides the order. The keyset itself is sound under any collation,
because the cursor and the order use the same comparison. A JavaScript
check would not be. Measured on a local PostgreSQL 16, sorting seven
nanoid-shaped values:
- `C.UTF-8` database: `AC Ab9 B-2 a-c aB3 ab b_1`, which matches
JavaScript.
- ICU `en-US` database (`CREATE DATABASE ... LOCALE_PROVIDER icu
ICU_LOCALE 'en-US'`): `a-c ab aB3 Ab9 AC b_1 B-2`.
Member ids are mixed-case (nanoid in the drivers, better-auth's
generator through plugin-auth). On such a database, every correctly
ordered completion page would fail the check, and every request in an
organization past the first read's bound would get a 503. MySQL's
default `utf8mb4_0900_ai_ci` collation is case-insensitive and differs
from code-point order in the same way (not measured here).
Collation-safe designs are left on the card for a decision.
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(No spec change.)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes.
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: n/a.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01JQwxbZDZLmVprAaL7DLh1K)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent 85f77e9 commit 4b8cbe2
3 files changed
Lines changed: 43 additions & 3 deletions
File tree
- .changeset
- packages/core/src/security
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
35 | 35 | | |
36 | 36 | | |
37 | 37 | | |
38 | | - | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
39 | 42 | | |
40 | 43 | | |
41 | 44 | | |
| |||
53 | 56 | | |
54 | 57 | | |
55 | 58 | | |
56 | | - | |
57 | 59 | | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
58 | 64 | | |
59 | 65 | | |
60 | 66 | | |
| |||
163 | 169 | | |
164 | 170 | | |
165 | 171 | | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
166 | 189 | | |
167 | 190 | | |
168 | 191 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1922 | 1922 | | |
1923 | 1923 | | |
1924 | 1924 | | |
| 1925 | + | |
| 1926 | + | |
| 1927 | + | |
| 1928 | + | |
| 1929 | + | |
| 1930 | + | |
| 1931 | + | |
1925 | 1932 | | |
1926 | 1933 | | |
1927 | | - | |
| 1934 | + | |
1928 | 1935 | | |
1929 | 1936 | | |
1930 | 1937 | | |
| |||
0 commit comments