Repository navigation
fix(core): the fellow-org peer list keeps every member the first membership read returned - #29545
Conversation
…etion The completion read's rows replaced Leg 1's orgMembersLeg, so a completion that saw fewer members than the first read shortened org_user_ids below what the first read alone answered. Union them; both reads are filtered to the active organization, so no non-member can be added. Claude-Session: https://claude.ai/code/session_01JQwxbZDZLmVprAaL7DLh1K Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 39 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin b4feaf631e5f399ccc0011243c82bf3605e24078 && git checkout b4feaf631e5f399ccc0011243c82bf3605e24078
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 390bd2aedca2aa46776ae52380eb11577f2bea87 3c6ddc2d6426939732bf0afadcdc865313bb5b96 && git checkout -B drift-repro 390bd2aedca2aa46776ae52380eb11577f2bea87 && git merge --no-ff 3c6ddc2d6426939732bf0afadcdc865313bb5b96
node scripts/docs-audit/affected-docs.mjs --json 390bd2aedca2aa46776ae52380eb11577f2bea87
|
|
domain:server#4 · os-warren · session_01JQwxbacSXJQqAztgt4UMrS 席位核对ACCEPT ·
|
|
domain:server#4 · os-warren · session_01JQwxbacSXJQqAztgt4UMrS Queue ejection disposition · 2026-11-02T19:24Z · head
|
Requested by Warren · project thread
Refs #29398 (item 2)
Clause-②: no
Metadata-protocol: none
维护者速读
org_user_ids会比第一次读本身还短,这些成员在普通成员的sys_user读取里悄悄消失。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 通过。What changes
resolveUserAuthzGrantsstep 5 buildsorg_user_idsfrom Leg 1'ssys_memberread (orgMembersLeg, bounded at 1,000 rows). When that read comes back at its bound,readOrganizationMembersToCompletionreads the whole membership in id order. Its rows used to replaceorgMembersLeg. 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
patchchangeset 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
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 inorg_user_ids, and nobody from the other organization is.staleForOrderedReads, a set of user ids that its orderedsys_memberreads (the completion's) do not return.Ablation (head 3c6ddc2)
The union was reverted on disk with
scripts/ablation-replace.mjs: anchor[...orgMembersLeg, ...awaitchanged to[...await(anchor x1 to x0, replacement x0 to x1, blob c3eddd26 to 93bebcf0). The test imports the resolver fromsrc/, so no build is involved.Tests 1 failed | 8 passed (9). The new case is the only red:expected [ 'u000000', 'u000884', …(998) ] to include 'u000001'.git diff HEADis empty.Suites (head 3c6ddc2)
pnpm --filter @objectstack/core test: Test Files 131 passed (131), Tests 2927 passed (2927).pnpm --filter @objectstack/core typecheck: exit 0.Verification
Gates (head 3c6ddc2)
node scripts/pm/dispatch-gates.mjs --commandsderived 61 commands. All were run locally, exit codes were captured before any pipe, and--ranreconciled them: 61 derived, 60 run, 1 NOT-MEASURED, 0 UNRUN.pnpm check:dual-build-cjs-loadsexited 3 (PREREQUISITE NOT MET, it needs every package built). It is NOT MEASURED locally and left to CI.check-plugin-teardown-shape --self-testpassed aftergit fetch --unshallow(its fixture is pinned to an old commit).check:lean-entry-closurepassed after building@objectstack/objectql.pnpm lintexited 0.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-sqlemits a plainORDER BY idandid > ?, 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-8database:AC Ab9 B-2 a-c aB3 ab b_1, which matches JavaScript.en-USdatabase (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_cicollation 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
packages/specholds no runtime logic. (No spec change.)pnpm lintpasses.main, one pull request.packages/spectouched: n/a.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Generated by Claude Code