Repository navigation
Commit ac9c2f0
fix(platform-objects): withhold the sign-in phone fields from org peers in the user directory (#27431)
domain:devx#1 · sunhaolin · session_017xRBcVy4VXmkQjn2MGeaWk
Clause-②: no (narrowing)
Metadata-protocol: none
Skills-docs: none — no file under `skills/**` teaches the `sys_user`
phone fields or the user directory's field security (`git grep -i -c
'phone_number\|phoneNumber' -- 'skills/**'`: zero hits, exit 1; control
`git grep -c sys_user -- 'skills/**'`: hits), so no skill text describes
the served set this narrows.
## 维护者速读
- 改了什么:`sys_user.phone_number` / `phone_number_verified` 从 `Account`
字段组移入 `Admin` 字段组;普通成员读用户目录时不再拿到其他用户的登录手机号。
- 为什么改:手机号是 better-auth 的登录标识,不是通讯录数据;`Admin`
组已由内置权限集做字段级只读屏蔽(成员屏蔽、管理员保留),沿用同一机制,不新增规则。
- 风险与回滚:这是一次接受集收窄(BREAKING,按 `patch` 发布):成员按这两个字段过滤、排序、分组、聚合会被 403
拒绝,`$search` 静默不再扫手机号列,成员读自己那行也看不到手机号(本人手机号从登录会话读取);回滚 = 把两个字段的 group 改回
`Account`。
- 先看:`packages/platform-objects/src/identity/sys-user.object.ts`(两个字段的
`group`);`.changeset/user-directory-sign-in-phone-fields.md`(BREAKING
横幅与
FROM/TO);`packages/qa/dogfood/test/identity-admin-fields-org-peer.dogfood.test.ts`(成员按姓名搜同事命中、按手机号搜无结果)。
- 两分钟验证:`pnpm --filter @objectstack/plugin-security exec vitest run
src/identity-admin-field-group.test.ts`,预期 19 passed。
- 需要你做的:无,席位落地(本人行问题已在 6497499302 裁决「接受」)。
## What changes
The user directory no longer returns another user's sign-in phone number
to non-admin readers. Security hardening, declared as an accept-set
narrowing.
- `sys_user.phone_number` and `sys_user.phone_number_verified` (the
better-auth phone sign-in identifier and its verified flag) move from
the `Account` field group to the `Admin` field group.
- The `Admin` group is already the declaration the shipped permission
sets read (`identityAdminFieldSecurity` in `plugin-security`'s
`default-permission-sets.ts`): `member_default`, `viewer_readonly` and
`external_default` mark every group field `readable: false, editable:
false`; `admin_full_access`, `organization_admin` and its no-bypass
variant keep it. No new rule, list or field attribute: the server's
existing field-level security (field masker, predicate guard, activity
redaction, metadata-plane masking) does the work, so the Console needs
no change.
- **Positions that narrow for a non-admin member** (the changeset
carries the full FROM / TO under its `**BREAKING** (an accept-set
narrowing)` banner):
- row position: both fields are withheld from every `sys_user` row
served, the member's own row included (field-level read is row-blind,
ADR-0066);
- filter, sort, group-by and aggregate positions naming either field are
refused `403 PERMISSION_DENIED`;
- `$search`: the default search set is narrowed silently, so a user
picker keeps answering on `name` and `email` and a search for a
colleague's number returns no row.
- `banned` / `ban_reason` / `ban_expires`: reviewed, no change. `banned`
stays served to org peers by an earlier ruling (directory status every
user picker filters on); `ban_reason` and `ban_expires` are already in
the `Admin` group and already withheld.
- Changeset: `@objectstack/platform-objects` `patch` (a non-protocol
narrowing ships `patch` and keeps its BREAKING banner); exactly one
ADR-0087 disposition, `not-required (no-migration-prescription)`: no
metadata key, spelling, export or stored shape moves.
## Acceptance notes
- **Own row (ruled).** Field-level read security is row-blind (ADR-0066,
2026-10-26 note: `readable` stays row-blind; `when` beside `readable:
false` is refused at parse). So a member's OWN row on the generic data
API is withheld the phone fields too, exactly as for every other
`Admin`-group field. The maintainer ruled this accepted in comment
6497499302 (「接受」): ship as is, the own number stays reachable through
the auth session, no row-aware read vote. better-auth's phone-number
plugin declares `phoneNumber` / `phoneNumberVerified` with `returned:
true`, so the session user carries them when the plugin is enabled.
- System-context readers (sign-in, SMS delivery, admin import, the auth
adapter) are unaffected.
- A deployment that wants members to read colleagues' phone numbers
grants a set naming the two fields `readable: true` (field grants merge
most-permissively).
- Rework round (contract review 6501154665, FAIL on the declaration
alone): this head adds the `(narrowing)` arm, the BREAKING banner with
FROM / TO and the ADR-0087 marker to the changeset, the `Skills-docs:`
line here, and the should-fix `$search` cases at the HTTP door. No
package source changed in this round. Rework run dispatched from PM
session session_017xRBcVy4VXmkQjn2MGeaWk.
## Tests
-
`packages/plugins/plugin-security/src/identity-admin-field-group.test.ts`:
the two phone fields are declared inside the group; a member
(`member_default`) is withheld both and the field masker strips them
from a colleague row; each admin set keeps both and the masker serves
them.
-
`packages/qa/dogfood/test/identity-admin-fields-org-peer.dogfood.test.ts`
(real showcase boot, HTTP door): the colleague fixture carries a
synthetic phone; a member's directory read projecting the phone fields
is served the colleague's id, name and email without them; the org owner
and the platform admin read the stored values. New this round: a member
`$search` on `sys_user` by the colleague's name is served `200` with the
colleague listed; a member `$search` by the colleague's synthetic number
is served `200` with no row; the platform admin's `$search` by the same
number lists the colleague (the control that the term matches when the
column is scanned). The existing generic group pins (member reads
colleague, list door, activity door, member own row, filter refusal)
cover the phone fields too.
## Verification
All readings below were taken on head `a77f981d` (the final head; `git
rev-parse --short HEAD` = `a77f981d`). No package source changed in this
round; the package-suite readings of the previous round on `a195fe983`
(unchanged sources) stand for `platform-objects` and `plugin-security`.
<details><summary>Declaration gates (this round's must-fix)</summary>
- `pnpm check:adr-0087-registration`: exit 0. `✓
check-adr-0087-registration: 1 declared-breaking changeset(s), each
carrying an ADR-0087 disposition.` —
`.changeset/user-directory-sign-in-phone-fields.md
[BREAKING+clause-②-narrowing] not-required (no-migration-prescription)`.
Self-test 705 assertions.
- `node scripts/check-changeset-no-major.mjs` (local, no event): exit 0,
no `major`; level axis NOT APPLICABLE (no payload, by construction).
- `node scripts/check-changeset-no-major.mjs --event EVENT` with a
`pull_request` payload built from this staged body: exit 0. `✓ LEVEL
AXIS: this PR declares clause-② no (narrowing) and metadata-protocol
none ... a non-protocol change at patch`. Control: the same run over a
payload built from the previous body reads `direction arm: none
declared` — the arm is read from the body, not assumed.
</details>
<details><summary>Dogfood (the narrowing owes the battery)</summary>
- Closure build `pnpm --workspace-concurrency=2 --filter
'@objectstack/dogfood^...' build`: lock VERDICT command-exit 0.
- Touched file `identity-admin-fields-org-peer.dogfood.test.ts`: Test
Files 1 passed, Tests 18 passed (15 before this round, plus the three
`$search` cases). Lock VERDICT command-exit 0.
- `pnpm --filter @objectstack/dogfood typecheck`: exit 0.
- Full battery `pnpm --filter @objectstack/dogfood test` (no shard, no
path) on `a77f981d`: Test Files 348 passed, 3 skipped (351); Tests 2812
passed, 20 skipped (2832). Lock VERDICT command-exit 0. The CI `Dogfood
Regression Gate` shards were skipped by their selector on this head, so
this local run is the battery's evidence.
</details>
<details><summary>Ablation of the new search pins (one-shot, not
kept)</summary>
Mutation via `scripts/ablation-replace.mjs`: `phone_number`'s `group:
'Admin'` replaced with a marker group (anchor 1 → 0, blob `67491e481b64`
→ `c9c848e94377`), `platform-objects` rebuilt, marker present in 4 built
files per `ablation-dist-preflight.mjs`.
- Dogfood file: 2 failed / 16 passed — the member projection read and
the member search by the colleague's number (it found the colleague).
The member name search and the admin control stayed green, as expected.
- Restore leg: blob equal to HEAD (`67491e481b64`), `git diff HEAD`
empty, whole-tree `git status --porcelain` clean, rebuilt, marker absent
from all 66 built files (`--absent`); file green again, 18/18.
</details>
<details><summary>Derived gate families</summary>
`node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--commands` on `a77f981d`: 65 commands; 64 exit 0, 1 exit 3. `--ran`
reconciliation: 65 derived, 64 run, 1 NOT-MEASURED, 0 UNRUN. NOT
MEASURED: `check:dual-build-cjs-loads`, reason: PREREQUISITE NOT MET
(seven packages outside this diff's closure have no `dist/` in this
worktree); CI carries it in `Lint & Repo Gates` (success on this head).
`node scripts/check-nul-bytes.mjs` exit 0. Repo-wide scans (`pnpm lint`,
`test:repo`) are left to CI.
</details>
<details><summary>CI on `a77f981d`</summary>
Every non-skipped check-run concluded `success`, the ten required
contexts among them: `Lint & Repo Gates`, `TypeScript Type Check`, `Test
Core`, `Build Core`, `Governed Surface Queue Guard`, `Test UI`; `Lint
UI`, `Type Check UI`, `Build & E2E UI` and `Live E2E UI` were skipped by
their path filter. The earlier `Lint & Repo Gates` red on `99bfe6d4` was
its lint lanes concluding `cancelled` when the push of `a77f981d`
superseded that run (run 40904302735 concluded `cancelled`), not a gate
verdict.
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_017xRBcUoS2G4jgqRskpExS7)_
<sub>Body refreshed by the PM seat 2026-10-29T16:49Z from os-dev report
6502824437.</sub>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_017xRBcVy4VXmkQjn2MGeaWk
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 99aeee6 commit ac9c2f0
5 files changed
Lines changed: 143 additions & 5 deletions
File tree
- .changeset
- packages
- platform-objects/src/identity
- plugins/plugin-security/src
- objects
- qa/dogfood/test
| 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 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
916 | 916 | | |
917 | 917 | | |
918 | 918 | | |
| 919 | + | |
| 920 | + | |
| 921 | + | |
| 922 | + | |
| 923 | + | |
| 924 | + | |
| 925 | + | |
| 926 | + | |
| 927 | + | |
| 928 | + | |
| 929 | + | |
| 930 | + | |
| 931 | + | |
| 932 | + | |
| 933 | + | |
919 | 934 | | |
920 | 935 | | |
921 | 936 | | |
922 | 937 | | |
923 | 938 | | |
924 | 939 | | |
925 | | - | |
| 940 | + | |
926 | 941 | | |
927 | 942 | | |
928 | 943 | | |
| |||
932 | 947 | | |
933 | 948 | | |
934 | 949 | | |
935 | | - | |
| 950 | + | |
936 | 951 | | |
937 | 952 | | |
938 | 953 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
39 | 39 | | |
40 | 40 | | |
41 | 41 | | |
| 42 | + | |
42 | 43 | | |
43 | 44 | | |
44 | 45 | | |
| |||
58 | 59 | | |
59 | 60 | | |
60 | 61 | | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
61 | 68 | | |
62 | 69 | | |
63 | 70 | | |
| |||
90 | 97 | | |
91 | 98 | | |
92 | 99 | | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
93 | 106 | | |
94 | 107 | | |
95 | 108 | | |
| |||
144 | 157 | | |
145 | 158 | | |
146 | 159 | | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
147 | 175 | | |
148 | 176 | | |
149 | 177 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
278 | 278 | | |
279 | 279 | | |
280 | 280 | | |
281 | | - | |
| 281 | + | |
| 282 | + | |
282 | 283 | | |
283 | 284 | | |
284 | | - | |
285 | | - | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
286 | 290 | | |
287 | 291 | | |
288 | 292 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
34 | 34 | | |
35 | 35 | | |
36 | 36 | | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
37 | 40 | | |
38 | 41 | | |
39 | 42 | | |
| |||
63 | 66 | | |
64 | 67 | | |
65 | 68 | | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
66 | 73 | | |
| 74 | + | |
| 75 | + | |
67 | 76 | | |
68 | 77 | | |
69 | 78 | | |
| |||
225 | 234 | | |
226 | 235 | | |
227 | 236 | | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
228 | 284 | | |
229 | 285 | | |
230 | 286 | | |
| |||
0 commit comments