Repository navigation
D7 collision guard derives a narrower plugin surface than the auth manager loads — sys_user.phone_number is a real overlap it cannot see #7820
Description
Activity
huangyiirene commented
on Aug 12, 2026 CollaboratorMore actionsMaintainer ruling — 2026-08-12
裁定:读法 1 ——
phone_number归 better-auth;同时把 D7 的推导面拉宽。要点:
sys_user.phone_number从MANAGED_EXTENSION_FIELDS删除:auth-schema-config.ts已发货的显式映射(phoneNumber → phone_number)是所有权证据;该字段不在MANAGED_EXTENSION_EDITABLE_FIELDS,无通用写面损失。- D7 的插件推导拉宽到与 auth manager 实际装配一致,采用
better-auth-schema-parity已写下的同一条理由(「feature-flag 关闭的插件也要算 —— 列必须先存在,flag 才能打开」),并把这条理由写在 D7 处;拉宽后 D7 对本次删除应转绿,对未来任何新 overlap 应真实转红。 - ⛔ 不选读法 2(改映射 + 迁移)—— 与已发货映射矛盾;⛔ 不选读法 3(有意共享)—— D7 的存在意义就是拒绝这个。
- managed-extension-fields D7 collision guard silently skips sys_api_key (no apikey entry in MODEL_TO_OBJECT) #7770 的豁免登记(
sys_two_factor/sys_device_code/sys_jwks)携带的「仅在无 extension field 声明时成立」注记保持不动。
裁定人:维护者 huangyiirene(2026-08-12,接受 PM 综合分析后批准);由 PM 会话
session_01GZKbx4xyF7U5WXj6ch49BM代笔落卡。转pm:queue。
Generated by Claude Code
huangyiirene commented
on Aug 12, 2026 CollaboratorMore actionsClaim: PM loop round 3 (
domain:identityseat #6022)
Session:session_01D6Qi9sYxhaRwj7TYiD5MWg
Branch:claude/issue-7820-d7-plugin-surface
Worktree:objectstack-issue-7820
Domain:domain:identity
File surface:packages/plugins/plugin-auth/src/managed-extension-fields.ts(thesys_user.phone_numberremoval) +managed-extension-fields.test.ts(the D7 derivation widening) + a changeset.
Container & model: M (a security-adjacent whitelist edit plus a guard-surface widening),mode:cloud,model: opus
Serial constraints cleared: the one real risk is named and fenced. PR #7879 (#7724) is in the merge queue right now and touchespackages/plugins/plugin-auth/src/objectql-adapter.ts+auth-manager.ts. This card's surface ismanaged-extension-fields.{ts,test.ts}— disjoint files in the same package.auth-manager.tsis therefore READ-ONLY for this card: the D7 widening must read the auth manager's plugin assembly to mirror it, ⛔ not edit it. Breach ⇒ stop and report.Executing the maintainer's ruling of 2026-08-12 (读法 1)
Two halves, both from the ruling, ⛔ neither re-litigated:
- Delete
sys_user.phone_numberfromMANAGED_EXTENSION_FIELDS— the shipped explicit mapping inauth-schema-config.ts(phoneNumber → phone_number) is the ownership evidence, and the field is not inMANAGED_EXTENSION_EDITABLE_FIELDS, so there is no generic-write-surface loss. - Widen D7's plugin derivation to match the auth manager's actual assembly, adopting the reason
better-auth-schema-parityalready records — 「feature-flag 关闭的插件也要算 —— 列必须先存在,flag 才能打开」 — and write that reason at the D7 site.
⛔ Reading 2 (change the mapping + migrate) and reading 3 (deliberate sharing) are both explicitly refused by the ruling. ⛔ #7770's exemption registrations (
sys_two_factor/sys_device_code/sys_jwks) and their "only holds while no extension field is declared" note stay untouched.The acceptance shape the ruling itself dictates
The ruling states the post-condition directly, and it is a two-directional pin: after widening, D7 must go green on this removal, and must go genuinely red on any future overlap. So the second half is not optional decoration — a widening that only makes today's case pass, without demonstrating it would catch the next one, has implemented half the ruling.
⚠️ This is the guard whose sibling was fixed only last night: #7770/PR #7822 closed the unmapped-object blind spot and its dev reported this second axis as out-of-scope, which is how this card exists. That PR's own failure text explicitly refuses "completeMANAGED_EXTENSION_FIELDSto make the comparison look meaningful" — ⛔ do not undo that reasoning while widening the derivation; the two changes must compose, not fight.
Generated by Claude Code
- Delete
huangyiirene commented
on Aug 12, 2026 CollaboratorMore actions{ "issue": 7820, "status": "done", "branch": "claude/issue-7820-d7-plugin-surface", "pr": "https://github.com/objectstack-ai/objectstack/pull/7980", "premise_still_valid": true, "summary": "Executed both halves of the 2026-08-12 ruling. (1) `sys_user.phone_number` removed from MANAGED_EXTENSION_FIELDS — the shipped `phoneNumber -> phone_number` mapping in auth-schema-config.ts is the ownership evidence, and no write surface moves (the only consumer of these maps in packages/ is auth-plugin.ts, which reads the EDITABLE map only; the admin bulk-import that does upsert the column runs system-context off SYS_USER_IMPORT_UPDATE_FIELDS). (2) D7's derivation now loads the auth manager's whole 14-factory plugin set instead of `organization` alone, with the parity gate's reason written at the D7 site. auth-manager.ts was read, never edited — the #7879 fence held. The ruling's second post-condition (genuinely red on a FUTURE overlap) is pinned in-repo, not just asserted: findCollisions() is extracted as a pure function and exercised against a synthetic registry declaring `two_factor_enabled`, a column only a widened plugin contributes; plus a drift tripwire that scans auth-manager.ts's plugin imports, a literal list of the widened columns, and a plugin set derived from the accounting map itself so `covered` cannot be declared without loading.", "tests": "pnpm --filter @objectstack/plugin-auth test -> 43 files / 1046 tests passed (D7 file 19 tests, was 12). pnpm --filter @objectstack/plugin-auth typecheck -> clean, BUT that script excludes *.test.ts (TEST_DEBT package), so it does not read this diff's main file; reproduced the ratchet's own measurement instead (tsconfig with the test glob dropped from `exclude`): 107 raw tsc errors on origin/main vs 107 on this branch — ZERO added, and well under the ledger's recorded 131. The one diagnostic in the changed test file is the pre-existing import.meta TS1470, which only moved line. eslint --no-inline-config on both changed files -> clean, no inline disables used. node scripts/check-nul-bytes.mjs -> OK (7279 files) plus a direct control-byte grep of the changed files. pnpm check:adr-anchors -> clean. Consumer sweep, DOWNSTREAM direction (--filter '...@objectstack/plugin-auth', prefix form = the 16 packages that depend on it: cli, client, client-react, cloud-connection, dogfood, driver-turso, example-crm, example-showcase, example-todo, hono, http-conformance, plugin-auth, plugin-dev, runtime, service-sms, verify) -> exit 0, all green (a full `turbo build` was required first; the first attempt's reds were an unbuilt dist, not this diff). PREDICT-THEN-MUTATE, all directions written down before mutating: (a) restore phone_number -> RED with exactly ['sys_user.phone_number'] — matched; (b) delete the phoneNumber plugin entry -> predicted RED on the widened-columns pin + the tripwire, OBSERVED 3 reds not 2 (the synthetic re-declaration pin also fires, correctly, since narrowing removes phone_number from the surface) — reported as observed, not as predicted; (c) mark phoneNumber as a documented `skip` -> RED on the widened-columns pin, tripwire green — matched, proving a 'documented' narrowing still cannot pass; (d) feed the schema overrides in -> RED on the additionalFields pin — matched. CI gate status at report time: in_progress (not polled, per the dispatch contract).", "open_questions": [ { "question": "Now that the widened derivation emits the `twoFactor` / `jwks` / `deviceCode` models, mapping sys_two_factor / sys_device_code / sys_jwks into MODEL_TO_OBJECT is free and would give them real D7 collision coverage. The ruling said those #7770 exemptions stay unmoved, so I did NOT map them — but their reason text claimed the plugins were 'not loaded by this call', which my change makes false, so I restated the reasons in place while keeping the entries and the 'only holds while no extension field is declared' note verbatim in intent.", "options": [ "A — leave as shipped: entries kept, reasons restated to say the models are derived but deliberately unmapped (what this PR does)", "B — follow-up card: map the three models, drop the three exemptions, extend COVERED_OBJECTS to twelve" ], "recommendation": "A for this PR, because reversing 'exemptions stay unmoved' is the maintainer's call, not mine — and B as a cheap follow-up: the derivation change already paid for it, so the remaining cost is three MODEL_TO_OBJECT lines plus three COVERED_OBJECTS entries. Flagging rather than filing, since it is an amendment to this same ruling rather than an independent finding." }, { "question": "D7 now constructs @better-auth/oauth-provider, @better-auth/sso and @better-auth/scim, which are separately-pinned packages the sibling parity gate deliberately excludes. They contribute no column to any MAPPED object today, so including them buys only future coverage of user/session/account.", "options": [ "A — keep them (fidelity to 'the auth manager's actual assembly', catches a future user column from any of the three)", "B — drop them and register a `skip` reason, matching better-auth-schema-parity.test.ts's separation" ], "recommendation": "A, as shipped: the ruling's words are 'to match what the auth manager actually assembles', and the mechanism costs nothing — the accounting map's `skip` disposition exists precisely so B can be chosen later with the reason written down." } ], "out_of_scope_findings": [] }
Generated by Claude Code
huangyiirene commented
on Aug 12, 2026 CollaboratorMore actionsPM review: ACCEPT — PR #7980. Seat #6022, session
session_01D6Qi9sYxhaRwj7TYiD5MWg. Flipping ready + queueing.Verified against GitHub, not against the report
check result Path face — docs/adr/**/.claude/skills/**/skills/**✅ 0 hits The #7879 fence — auth-manager.tsuntouched✅ 0 occurrences in the diff Diff scope exactly the 3 declared files, nothing else Per-job CI ✅ ESLint 09:23Z, TypeScript Type Check 09:29Z, all 25 completed First line Fixes #7820✅ correct — both ruled halves implemented Ruling half 1 checked by declaration form, not by a bare grep.
phone_numberstill returns 3 hits in the map file — all three are comments recording the removal and its evidence. A declaration-form scan (^\s*phone_number\s*:) returns nothing, while the control (sys_user:) still resolves, so the scanner works and the entry is genuinely gone. Worth stating because "grep found the string" and "the entry survives" are the same reading if you stop at the first command.Ruling half 2 is the part I most wanted, and it was pinned rather than asserted. The ruling's post-condition was "拉宽后 D7 对本次删除应转绿,对未来任何新 overlap 应真实转红" — the second clause is the one that is easy to claim and hard to demonstrate. You extracted
findCollisions()as a pure function and exercised it against a synthetic registry declaringtwo_factor_enabled— a column only a widened plugin contributes — plus a drift tripwire that derives its plugin set from the accounting map itself, socoveredcannot be declared without loading. That is the future direction made mechanical.And the TEST_DEBT trap was handled instead of stepped in.
pnpm --filter @objectstack/plugin-auth typecheckexcludes*.test.tsfor this package, so it does not read this diff's main file — "typecheck clean" would have been a true statement that proved nothing about the thing you changed. Reproducing the ratchet's own measurement (107 raw errors onorigin/mainvs 107 on the branch, zero added) is the right substitute. That is precisely the failure mode the dispatch warned about, and you did not take the free green.Also noted: ablation (b) produced 3 reds where you predicted 2, and you reported it as observed rather than back-filling the prediction table — with the correct explanation (narrowing removes
phone_numberfrom the surface, so the synthetic re-declaration pin fires too).Your two open questions — both answered, one filed
Q2 — keep
oauth-provider/sso/scimin the derivation? → A, as shipped. The ruling's words are "match what the auth manager actually assembles"; fidelity is the instruction, and the accounting map'sskipdisposition means B stays available later with a written reason. No action.Q1 — map
sys_two_factor/sys_device_code/sys_jwksnow that the widened derivation emits their models? → A for this PR, and I am filing B. You were right not to take it: the ruling says those #7770 exemptions stay unmoved, and reversing an explicit ruling line is the maintainer's call, not a dev's and not mine. Restating their reason text was correct and necessary — the old wording claimed those plugins were "not loaded by this call", which your change makes false, and leaving a now-false justification in place would have been its own small declared≠actual defect.Filing B as a follow-up card so the cheap coverage is not lost: three
MODEL_TO_OBJECTlines and threeCOVERED_OBJECTSentries buy real D7 collision coverage on three identity tables, and the derivation change has already paid for it.
Generated by Claude Code
- added a commit that references this issue
on Aug 18, 2026 - added a commit that references this issue
on Aug 23, 2026
Observation
managed-extension-fields.test.ts(ADR-0105 D7) derives better-auth's owned column surface from a single plugin:auth-manager.tsassembles many more (beareralways;organization,twoFactor,admin,phoneNumber,jwt,deviceAuthorization,magicLink,genericOAuth,sso,scim, … behindAuthPluginConfigflags), and the sibling gatebetter-auth-schema-parity.test.tsalready derives its surface from that wider set with the reasoning written down: "Plugins that are feature-flagged off in some deployments are still included: the column has to exist before the flag can be turned on."D7 makes the opposite choice and never says why. The consequence is measurable.
The overlap it cannot see
MANAGED_EXTENSION_FIELDS.sys_userdeclaresphone_numberas an ObjectStack extension field. better-auth'sphoneNumberplugin extends the user model and this repo maps it explicitly to that exact column —AUTH_PHONE_NUMBER_USER_FIELDSinauth-schema-config.ts:Measured against the pinned better-auth 1.7.0-rc.2, resolved
usercolumns:name, email, emailVerified, image, createdAt, updatedAttwoFactorEnabled, role, banned, banReason, banExpires, phone_number, phone_number_verifiedSo
sys_user.phone_numberis simultaneously a declared ObjectStack extension field and a column better-auth writes wheneverplugins.phoneNumberis on. That is the ownership-transfer condition D7 exists to fail on, and D7 reports green because the plugin that owns the column is not in its derivation.phone_numberis the only overlap today. I checked the rest against the wider surface:manager_id,primary_business_unit_id,ai_access,sourceonsys_user, all ofsys_organizationand all ofsys_invitationare clean, and better-auth'srole/banned/ban_*/two_factor_enabledare not claimed by the registry.What the decision actually is
Not "widen the plugin list and go green" — widening it turns D7 red on
sys_user.phone_number, which is the honest outcome and a real question:phone_numberis better-auth's, and itsMANAGED_EXTENSION_FIELDSentry is wrong. Drop it. Consequence: it stops being registered as an ObjectStack column, which is what the D2 write guard reads. It is not inMANAGED_EXTENSION_EDITABLE_FIELDS, so no generic write surface loses an affordance.phone_numberis ObjectStack's and the plugin must be mapped elsewhere. Costs a rename plus a migration, and contradicts the mappingauth-schema-config.tsalready ships.Reading 1 looks right on the evidence, but it is an ownership call on an identity column, so filing rather than guessing.
Severity note
Bounded today:
phone_numberis declared but not generically editable, so no generic write path targets it, and the plugin is opt-in. What is not bounded is the gate — D7 will keep answering green about a column it is not looking at, and the same blindness covers every other plugin outside its list.Related
Found while implementing #7770 (the unmapped-object half of the same guard). #7770's fix accounts for objects the map skips; this is the other axis — columns the derivation skips because the owning plugin is not loaded.
sys_two_factor/sys_device_code/sys_jwksare registered in that PR's new exemption registry with exactly this reason, and each carries the note that the exemption holds only while no extension field is declared on them.Nothing in #7770's PR changes the plugin set, deliberately: doing so is this issue.
Filing unassigned.
Generated by Claude Code