feat(auth): AuthPlugin 自派生 app 组织角色 —— 宿主零接线 (#3723 后续, cloud#897) - #3779
Merged
os-zhuang merged 2 commits intoJul 28, 2026
Merged
Conversation
…ing (#3723, cloud#897) Per-host `additionalOrgRoles` wiring was the defect pattern: five hosts boot AuthPlugin from a stack, three of them (verify harness, DevPlugin, cloud's ArtifactKernelFactory) at some point forgot the parameter, and the failure is silent — app-declared roles are simply absent. cloud is also order-hostile: it mounts AuthPlugin before the app metadata exists, so no init-time walk could ever cover it. AuthPlugin now derives the roles in its own kernel:ready hook — the one point that fires after all metadata is registered in every host — via `collectRegisteredOrgRoles` (the late-bound twin of `collectStackOrgRoles`; same dual read as `bootstrapDeclaredPositions`: engine registry first, metadata-service facade as fallback). Both consumers update from the derived union: - better-auth side: `applyConfigPatch` merges the roles; the instance is built lazily (or reset by the patch), so the org-plugin roles map carries the full set on next use. - select side: `sys_invitation` / `sys_member` are re-registered with widened `role` options under the same package id — an explicitly supported registry path (owner contribution replaced, merge cache invalidated). No DDL: options are validator/picker metadata, the column stays TEXT. serve / verify harness / DevPlugin drop their wiring — deliberately, so the dogfood invite gates only stay green if the auto-derivation works. Explicit `additionalOrgRoles` remains as an override for roles outside stack metadata (unioned with the derived set). One bug found the hard way and worth recording: the hook originally acquired the engine via `ctx.getServiceAsync('objectql')` — a method PluginContext does not have. The optional-chain made that silently yield `undefined`, the metadata facade still fed the better-auth half, and the select half was skipped without an error: derivation logged success while invitations kept failing. PluginContext's sync `getService` is the API. Verified: dogfood app-org-role-invite + delegated-admin-invite gates green with ZERO host wiring (10/10); org-roles unit tests 29 (5 new for collectRegisteredOrgRoles); plugin-auth 603; full dogfood 375 passed.
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 4 package(s): 24 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…t is the exception now, not the wiring Both option surfaces still described the parameter as 'the one walk every host shares', naming the three hosts whose wiring this branch just removed. Flagged by re-reading the docs-drift advisory against the actual diff.
os-zhuang
marked this pull request as ready for review
July 28, 2026 04:34
os-zhuang
deleted the
claude/additionalorgroles-registration-mismatch-d9aez7
branch
July 28, 2026 04:34
This was referenced Jul 28, 2026
os-zhuang
added a commit
that referenced
this pull request
Jul 28, 2026
… `sys_member.role` vocabulary (ADR-0108, #3723) (#3802) `sys_member.role` answers "what is your standing in this organization". It does not answer "what may you do" — that is what positions are for. `resolve-authz-context` projects EVERY value stored in `sys_member.role` into `current_user.positions`, so a business role handed out through the membership role was capability, granted with none of ADR-0090 D12's controls: no `granted_by`, no ADR-0091 validity window, no BU-subtree check, no `assignablePermissionSets` allowlist. ADR-0057 D4 ruled that out ("never as the authority for RBAC"), ADR-0090 D3's word ban restates it (distribution = `position`), and ADR-0095 D3 keeps the better-auth role out of the enforcement path. No ADR authorized the widening — it arrived as a bug fix (#3747) and was then made automatic in every host (#3779). The vocabulary is closed to owner / admin / delegated_admin / member. `additionalOrgRoles`, `org-roles.ts` and the `kernel:ready` derivation hook are removed; capability at admission time goes through ADR-0105 D8 invitation placement, which is governed and reaches further (a delegated admin may use it within their subtree, where the membership-role route was org-admin-only). Both reversed changesets were unreleased, so no published version ever offered the behaviour. Downstream `objectstack-ai/cloud` audited at b168e94: no compile-time or runtime impact. Also: lint's MEMBERSHIP_TIERS now derives from BUILTIN_MEMBERSHIP_ROLES. The hand-kept copy carried `guest`, which the select has never offered, so an approver naming it resolved to nobody while the lint whose job is to catch that stayed silent.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
#3723 (PR #3747) 的后续。关联 objectstack-ai/cloud#897(为它解除 framework 侧前提,但不关闭它 —— 注意本描述刻意不写 Closes:cloud#897 的收尾要等 pin bump 后在 cloud 侧验证)。
为什么还要一步
#3747 把「一份角色清单喂两个消费方」做对了,但生产侧仍是按宿主接线:五个从 stack 启动
AuthPlugin的宿主,每个都得记得传additionalOrgRoles。事实证明这就是缺陷模式 —— 五个里三个曾经忘了传(verify harness、DevPlugin、cloud 的 ArtifactKernelFactory),而且失效是安静的:没有报错,app 角色只是凭空缺席。cloud 还额外顺序不友好:它在
:380挂 AuthPlugin、:792才挂 AppPlugin —— 任何 init 时的 stack 遍历都注定拿不到元数据。方案:kernel:ready 自派生
kernel:ready是所有宿主里唯一保证「全部元数据已注册」的时点。AuthPlugin 在自己的 hook 里:collectRegisteredOrgRoles(collectStackOrgRoles的晚绑定孪生;读取路径照抄bootstrapDeclaredPositions的既有范式 —— engine registry 优先,metadata-service facade 兜底)读取已注册的position/permission元数据;additionalOrgRoles取并集(显式项保留为「stack 元数据之外的角色」的覆盖口);applyConfigPatch—— 实例本就 lazy 构建(或被 patch 重置),下次使用即携带完整 roles map;sys_invitation/sys_member—— registry 明确支持的路径(owner contribution 替换 + merge cache 失效)。无 DDL:options 是校验器/picker 元数据,列类型不变。证明方式:宿主接线全部拆除
serve/ verify harness /DevPlugin不再传additionalOrgRoles—— 刻意的:dogfood 邀请闸门(app-org-role-invite + delegated-admin-invite,10 条)现在只有自派生生效才会绿。这是「构造上不可能忘」的端到端证据,而不是又一层「记得接线」。对 cloud#897 的意义:ArtifactKernelFactory 一行都不用改 —— 它的 kernel:ready 同样在 AppPlugin(bundle) 之后触发,positions 经 cloud#898 已能到达托管环境,pin bump 后此路径自动打通。
调试中抓到并修掉的一个真 bug(值得记录)
hook 最初用
ctx.getServiceAsync('objectql')取引擎 —— PluginContext 根本没有这个方法(只有同步getService)。可选链让它静默得到undefined:metadata facade 撑起了 better-auth 半边,select 半边被无声跳过,派生日志照打成功而邀请继续 400。定位靠给SchemaRegistry.prototype.registerObject挂探针记录 boot 全程对sys_member的注册序列,发现 hook 的那次注册从未发生。教训:可选链 + 猜测的 API 名 = 静默半残,恰是本 PR 要消灭的那类失效。验证
org-roles.test.ts29 条(新增 5 条覆盖collectRegisteredOrgRoles:registry 优先、facade 兜底、content包装两种形状、归一化一致、永不抛);turbo build(除 docs)71/71。后续
control-plane-preset.ts:389的顺手确认仍待做);collectStackOrgRoles保留导出(仍是合法的 init 时遍历入口),但不再是任何宿主的必需品。Generated by Claude Code