Skip to content

feat(auth): AuthPlugin 自派生 app 组织角色 —— 宿主零接线 (#3723 后续, cloud#897) - #3779

Merged
os-zhuang merged 2 commits into
mainfrom
claude/additionalorgroles-registration-mismatch-d9aez7
Jul 28, 2026
Merged

feat(auth): AuthPlugin 自派生 app 组织角色 —— 宿主零接线 (#3723 后续, cloud#897)#3779
os-zhuang merged 2 commits into
mainfrom
claude/additionalorgroles-registration-mismatch-d9aez7

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

#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 里:

  1. 经新增的 collectRegisteredOrgRoles(collectStackOrgRoles 的晚绑定孪生;读取路径照抄 bootstrapDeclaredPositions 的既有范式 —— engine registry 优先,metadata-service facade 兜底)读取已注册的 position / permission 元数据;
  2. 与显式传入的 additionalOrgRoles 取并集(显式项保留为「stack 元数据之外的角色」的覆盖口);
  3. 两个消费方从同一并集更新:
    • better-auth 侧:applyConfigPatch —— 实例本就 lazy 构建(或被 patch 重置),下次使用即携带完整 roles map;
    • select 侧:以同 packageId 重注册 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 要消灭的那类失效。

验证

  • dogfood 邀请闸门 10/10,宿主零接线(harness 不传任何角色参数);
  • org-roles.test.ts 29 条(新增 5 条覆盖 collectRegisteredOrgRoles:registry 优先、facade 兜底、content 包装两种形状、归一化一致、永不抛);
  • plugin-auth 603 / cli 667 / verify 7 / plugin-dev 7 / spec 6736 全过;全量 dogfood 375 passed;
  • 全量 turbo build(除 docs)71/71。

后续

  • cloud 侧:pin bump 到含本 PR 的 SHA 后,在托管环境验证邀请路径,然后关 cloud#897(其 control-plane-preset.ts:389 的顺手确认仍待做);
  • collectStackOrgRoles 保留导出(仍是合法的 init 时遍历入口),但不再是任何宿主的必需品。

Generated by Claude Code

…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.
@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 28, 2026 4:19am

Request Review

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Jul 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/cli, @objectstack/plugin-auth, @objectstack/plugin-dev, @objectstack/verify.

24 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/skills-reference.mdx (via packages/cli)
  • content/docs/api/client-sdk.mdx (via @objectstack/cli)
  • content/docs/api/data-flow.mdx (via @objectstack/cli)
  • content/docs/api/environment-routing.mdx (via @objectstack/cli)
  • content/docs/api/error-catalog.mdx (via @objectstack/cli)
  • content/docs/automation/hook-bodies.mdx (via packages/cli)
  • content/docs/deployment/backup-restore.mdx (via @objectstack/cli)
  • content/docs/deployment/cli.mdx (via @objectstack/cli, @objectstack/plugin-auth)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/plugin-auth)
  • content/docs/deployment/self-hosting.mdx (via @objectstack/cli)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/cli)
  • content/docs/kernel/runtime-services/data-service.mdx (via packages/cli)
  • content/docs/kernel/runtime-services/index.mdx (via packages/cli)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/plugin-auth)
  • content/docs/permissions/authentication.mdx (via @objectstack/cli, @objectstack/plugin-auth)
  • content/docs/permissions/sso.mdx (via @objectstack/plugin-auth)
  • content/docs/plugins/index.mdx (via @objectstack/plugin-auth)
  • content/docs/plugins/packages.mdx (via @objectstack/cli, @objectstack/plugin-auth, @objectstack/plugin-dev)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/cli)
  • content/docs/protocol/kernel/realtime-protocol.mdx (via @objectstack/cli)
  • content/docs/releases/implementation-status.mdx (via @objectstack/cli, @objectstack/plugin-auth, @objectstack/verify)
  • content/docs/releases/v15.mdx (via @objectstack/verify)
  • content/docs/releases/v16.mdx (via @objectstack/cli)
  • content/docs/releases/v9.mdx (via @objectstack/plugin-auth)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

…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
os-zhuang marked this pull request as ready for review July 28, 2026 04:34
@os-zhuang
os-zhuang merged commit 042537c into main Jul 28, 2026
16 checks passed
@os-zhuang
os-zhuang deleted the claude/additionalorgroles-registration-mismatch-d9aez7 branch July 28, 2026 04:34
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants