From 8539e5f9fea531a3bb60b7785a05e4ef151a1cbc Mon Sep 17 00:00:00 2001 From: zhuangjianguo Date: Thu, 25 Jun 2026 09:38:42 +0800 Subject: [PATCH] feat(identity): align client user-context with ADR-0068 Mirror the framework's unified EvalUser contract on the client so a predicate evaluates identically across client, server-formula and server-RLS. - ExpressionProvider: expose the canonical `current_user` plus the back-compat `user` alias and the server-RLS-parity `ctx.user` alias, all pointing at the same user object (predicate scope + evaluator). - useIsWorkspaceAdmin: accept the canonical org_owner/org_admin role names emitted into user.roles[] by the server customSession, so admin detection survives the removal of the `user.role='admin'` overwrite. No new @objectstack/spec npm exports are consumed, so this is not publish-gated on the framework release. Verified: app-shell build + 766 tests, auth build + 107 tests, console build 33/33, Playwright chromium e2e 10/10 (real-browser render, no JS errors). --- .../src/providers/ExpressionProvider.tsx | 15 ++++++++++++--- packages/auth/src/useIsWorkspaceAdmin.ts | 6 ++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/app-shell/src/providers/ExpressionProvider.tsx b/packages/app-shell/src/providers/ExpressionProvider.tsx index f24f0d7fa..257dce69a 100644 --- a/packages/app-shell/src/providers/ExpressionProvider.tsx +++ b/packages/app-shell/src/providers/ExpressionProvider.tsx @@ -50,7 +50,11 @@ interface ExpressionProviderProps { export function ExpressionProvider({ children, user = {}, app = {}, data = {}, features = {} }: ExpressionProviderProps) { const value = useMemo(() => { - const context = { user, app, data, features }; + // ADR-0068: expose the SAME user object under the canonical `current_user` + // plus the back-compat `user` alias and the server-RLS-parity `ctx.user` + // alias, so a predicate authored against any one form evaluates identically + // on client, server-formula, and server-RLS. + const context = { current_user: user, user, ctx: { user }, app, data, features }; const evaluator = new ExpressionEvaluator(context); return { user, app, data, features, evaluator }; }, [user, app, data, features]); @@ -58,7 +62,11 @@ export function ExpressionProvider({ children, user = {}, app = {}, data = {}, f // Also feed the predicate scope used by useCondition/useExpression in // @object-ui/react so action visibility predicates (e.g. on toolbar // buttons) can see deployment-level flags like features.multiOrgEnabled. - const scope = useMemo(() => ({ user, app, data, features }), [user, app, data, features]); + // Mirror the canonical `current_user`/`user`/`ctx.user` aliases here too. + const scope = useMemo( + () => ({ current_user: user, user, ctx: { user }, app, data, features }), + [user, app, data, features], + ); return ( @@ -76,7 +84,8 @@ export function useExpressionContext(): ExpressionContextValue { if (!ctx) { // Return a safe default so components can be used outside the provider const fallback = { user: {}, app: {}, data: {}, features: {} }; - return { ...fallback, evaluator: new ExpressionEvaluator(fallback) }; + const evalContext = { current_user: {}, ctx: { user: {} }, ...fallback }; + return { ...fallback, evaluator: new ExpressionEvaluator(evalContext) }; } return ctx; } diff --git a/packages/auth/src/useIsWorkspaceAdmin.ts b/packages/auth/src/useIsWorkspaceAdmin.ts index d04fb295f..111690cfa 100644 --- a/packages/auth/src/useIsWorkspaceAdmin.ts +++ b/packages/auth/src/useIsWorkspaceAdmin.ts @@ -15,6 +15,12 @@ const ADMIN_ROLES = new Set([ 'superadmin', 'platform_admin', 'system_admin', + // ADR-0068 canonical role names emitted into `user.roles[]` by the server + // customSession (raw better-auth owner/admin are normalized to org_owner/ + // org_admin). Accept both raw and canonical so admin detection survives the + // removal of the `user.role = 'admin'` overwrite footgun. + 'org_owner', + 'org_admin', ]); function isAdminRole(role: unknown): boolean {