feat(THU-556): Settings → Workspace → Permissions page + resource-CRUD enforcement#978
Conversation
Semgrep Security ScanNo security issues found. |
PR Metrics
Updated Fri, 12 Jun 2026 10:50:45 GMT · run #1874 |
* fix(THU-549): clear auth token after signOut so server can revoke session * fix(THU-549): clear registry before db-closing so reloaded tabs land at ModePicker * fix(THU-549): guard LogoutModal handleLogout against rapid double-click * fix(THU-549): friendlier ZodError for unset/invalid SERVER_ID * fix(THU-549): remove Cloud URL dev-settings input (use ModePicker flow instead) * fix(THU-552): preserve existing membership role on promote-on-insert * fix(THU-552): delete pending row by (workspace_id, email) after promote-on-insert * fix(THU-552): skip onCreated when create-workspace modal dismissed mid-flight * fix(THU-554): preserve slug/icon on PUT when payload omits them * fix(THU-554): reflect remote workspace updates into settings form * fix(THU-554): append random suffix to duplicate workspace slug * fix(THU-555): resolve workspace permission state when user has no membership * fix(THU-555): gate member actions on granular permission keys * fix(THU-551): expose URL workspace id immediately + collapse chat-id on workspace switch * fix(THU-550): wrap BrowserRouter in ErrorBoundary so bootstrap throws surface a screen * chore: strip PR-ref noise from review-followup comments * fix(THU-578): reset isReady before chat session eviction on workspace switch * fix(THU-553): dedupe ModePicker validation between blur and Continue * fix(THU-552): force ctx.userId for invitedByUserId in pending memberships * fix(THU-552): reject malformed emails on pending membership writes * fix(THU-578): persist selectedAgent against session.workspaceId not thread * fix(THU-556): hide chat-skills-bar when no edit perm and no pinned chips * docs: refresh workspace-memberships handler docstring for permission-key model * test(THU-578): seed workspace context in LinkPreviewWidget fallback tests * fix(THU-553): reset isValidating on SET_URL to unstick Continue after stale blur * fix(THU-553): reset stage to picker on stale-URL bail-out in handleContinue * fix(THU-549): pass captured serverId to handleFullWipe after registry clear * fix(THU-555): align pending-row gates + block admin escalation via invite * refactor(THU-555): use isResolved for membership loading distinction * refactor(THU-555): assign openRef in render body, drop useEffect * fix(THU-555): widen duplicate-slug suffix to 8 chars for collision resistance * docs(THU-555): refresh stale comments referencing removed manage_members guard * fix(THU-555): mirror serverUrl ref synchronously in mode-picker onChange * fix(THU-555): sync pending memberships to all workspace members * fix(THU-555): require change_roles for membership PUT that changes existing role * fix(THU-549): keep getAuthToken resolvable through signOut after registry clear * fix(THU-555): add last-admin guard to membership PUT apply path * fix(THU-555): gate pending-row Admin option on change_roles
752cd0f
into
raivieiraadriano92/thu-555-settings-workspace-members-add-remove-pending
…pending) (#974) * feat(THU-555): add user_name/user_email to workspace_memberships * feat(THU-555): enrich membership upload handler with user display info * feat(THU-555): propagate user name/email changes to memberships * feat(THU-555): mirror userName/userEmail on FE workspace_memberships * feat(THU-555): reactive members + pending queries * feat(THU-555): membership mutation DAL * feat(THU-555): useWorkspacePermission hook * feat(THU-555): RequireWorkspacePermission route wrapper * feat(THU-555): Members page scaffold + route * feat(THU-555): Members sidebar entry gated by manage_members * feat(THU-555): render members table with role dropdown * feat(THU-555): wire Remove member with confirmation * feat(THU-555): use globe icon for General sidebar entry * fix(THU-555): rename SCREAMING_SNAKE constant to camelCase * feat(THU-593): gate member management on E2EE-enabled servers * fix(chat-store): dedupe concurrent hydrateChatStore calls * feat(THU-556): Settings → Workspace → Permissions page + resource-CRUD enforcement (#978) * feat(THU-556): setWorkspacePermissionRequiredRole DAL * feat(THU-556): RequireWorkspaceAdmin route guard * refactor(THU-556): consolidate workspace permission keys into shared/workspaces.ts * feat(THU-556): Permissions page (11 keys, labels only) * feat(THU-556): Permissions sidebar entry * feat(THU-556): mount Permissions route * feat(THU-556): refine Permissions page copy and layout * feat(THU-556): scope Permissions to agents/skills/models/mcps * feat(THU-556): use Lock icon for Permissions sidebar entry * feat(THU-556): permissionAllows + BE permission lookup helpers * feat(THU-556): permission-key gating in workspace-scoped handler * feat(THU-556): enforce add_agents/remove_agents permissions * feat(THU-556): treat soft-delete PATCH as remove for permission gating * feat(THU-556): enforce add_skills/remove_skills permissions * feat(THU-556): enforce add_models/remove_models permissions * feat(THU-556): enforce add_mcp_servers/remove_mcp_servers permissions * feat(THU-556): gate edit + chat bypasses, add permission gating tests * fix(workspaces): address review comments across stacked PRs (#988) * fix(THU-549): clear auth token after signOut so server can revoke session * fix(THU-549): clear registry before db-closing so reloaded tabs land at ModePicker * fix(THU-549): guard LogoutModal handleLogout against rapid double-click * fix(THU-549): friendlier ZodError for unset/invalid SERVER_ID * fix(THU-549): remove Cloud URL dev-settings input (use ModePicker flow instead) * fix(THU-552): preserve existing membership role on promote-on-insert * fix(THU-552): delete pending row by (workspace_id, email) after promote-on-insert * fix(THU-552): skip onCreated when create-workspace modal dismissed mid-flight * fix(THU-554): preserve slug/icon on PUT when payload omits them * fix(THU-554): reflect remote workspace updates into settings form * fix(THU-554): append random suffix to duplicate workspace slug * fix(THU-555): resolve workspace permission state when user has no membership * fix(THU-555): gate member actions on granular permission keys * fix(THU-551): expose URL workspace id immediately + collapse chat-id on workspace switch * fix(THU-550): wrap BrowserRouter in ErrorBoundary so bootstrap throws surface a screen * chore: strip PR-ref noise from review-followup comments * fix(THU-578): reset isReady before chat session eviction on workspace switch * fix(THU-553): dedupe ModePicker validation between blur and Continue * fix(THU-552): force ctx.userId for invitedByUserId in pending memberships * fix(THU-552): reject malformed emails on pending membership writes * fix(THU-578): persist selectedAgent against session.workspaceId not thread * fix(THU-556): hide chat-skills-bar when no edit perm and no pinned chips * docs: refresh workspace-memberships handler docstring for permission-key model * test(THU-578): seed workspace context in LinkPreviewWidget fallback tests * fix(THU-553): reset isValidating on SET_URL to unstick Continue after stale blur * fix(THU-553): reset stage to picker on stale-URL bail-out in handleContinue * fix(THU-549): pass captured serverId to handleFullWipe after registry clear * fix(THU-555): align pending-row gates + block admin escalation via invite * refactor(THU-555): use isResolved for membership loading distinction * refactor(THU-555): assign openRef in render body, drop useEffect * fix(THU-555): widen duplicate-slug suffix to 8 chars for collision resistance * docs(THU-555): refresh stale comments referencing removed manage_members guard * fix(THU-555): mirror serverUrl ref synchronously in mode-picker onChange * fix(THU-555): sync pending memberships to all workspace members * fix(THU-555): require change_roles for membership PUT that changes existing role * fix(THU-549): keep getAuthToken resolvable through signOut after registry clear * fix(THU-555): add last-admin guard to membership PUT apply path * fix(THU-555): gate pending-row Admin option on change_roles
…plicate) (#971) * fix(THU-554): align PageHeader actions with content padding * feat(THU-554): allow personal workspace rename via upload handler * feat(THU-554): updateWorkspaceName DAL, default name, createdAt stamps * feat(THU-554): useActiveWorkspaceMembership hook * feat(THU-554): split settings sidebar into Account Settings and Workspace groups * feat(THU-554): Workspace Settings page with autosave rename * feat(THU-554): add slug + icon columns to workspaces * feat(THU-554): workspace slug input with auto-derive from name * feat(THU-554): workspace icon picker (emoji or image upload) * feat(THU-554): render workspace icon in the sidebar selector * feat(THU-554): share WorkspaceFormFields between settings and create modal * feat(THU-554): duplicate workspace action * feat(THU-555): Settings → Workspace → Members (add / remove / role / pending) (#974) * feat(THU-555): add user_name/user_email to workspace_memberships * feat(THU-555): enrich membership upload handler with user display info * feat(THU-555): propagate user name/email changes to memberships * feat(THU-555): mirror userName/userEmail on FE workspace_memberships * feat(THU-555): reactive members + pending queries * feat(THU-555): membership mutation DAL * feat(THU-555): useWorkspacePermission hook * feat(THU-555): RequireWorkspacePermission route wrapper * feat(THU-555): Members page scaffold + route * feat(THU-555): Members sidebar entry gated by manage_members * feat(THU-555): render members table with role dropdown * feat(THU-555): wire Remove member with confirmation * feat(THU-555): use globe icon for General sidebar entry * fix(THU-555): rename SCREAMING_SNAKE constant to camelCase * feat(THU-593): gate member management on E2EE-enabled servers * fix(chat-store): dedupe concurrent hydrateChatStore calls * feat(THU-556): Settings → Workspace → Permissions page + resource-CRUD enforcement (#978) * feat(THU-556): setWorkspacePermissionRequiredRole DAL * feat(THU-556): RequireWorkspaceAdmin route guard * refactor(THU-556): consolidate workspace permission keys into shared/workspaces.ts * feat(THU-556): Permissions page (11 keys, labels only) * feat(THU-556): Permissions sidebar entry * feat(THU-556): mount Permissions route * feat(THU-556): refine Permissions page copy and layout * feat(THU-556): scope Permissions to agents/skills/models/mcps * feat(THU-556): use Lock icon for Permissions sidebar entry * feat(THU-556): permissionAllows + BE permission lookup helpers * feat(THU-556): permission-key gating in workspace-scoped handler * feat(THU-556): enforce add_agents/remove_agents permissions * feat(THU-556): treat soft-delete PATCH as remove for permission gating * feat(THU-556): enforce add_skills/remove_skills permissions * feat(THU-556): enforce add_models/remove_models permissions * feat(THU-556): enforce add_mcp_servers/remove_mcp_servers permissions * feat(THU-556): gate edit + chat bypasses, add permission gating tests * fix(workspaces): address review comments across stacked PRs (#988) * fix(THU-549): clear auth token after signOut so server can revoke session * fix(THU-549): clear registry before db-closing so reloaded tabs land at ModePicker * fix(THU-549): guard LogoutModal handleLogout against rapid double-click * fix(THU-549): friendlier ZodError for unset/invalid SERVER_ID * fix(THU-549): remove Cloud URL dev-settings input (use ModePicker flow instead) * fix(THU-552): preserve existing membership role on promote-on-insert * fix(THU-552): delete pending row by (workspace_id, email) after promote-on-insert * fix(THU-552): skip onCreated when create-workspace modal dismissed mid-flight * fix(THU-554): preserve slug/icon on PUT when payload omits them * fix(THU-554): reflect remote workspace updates into settings form * fix(THU-554): append random suffix to duplicate workspace slug * fix(THU-555): resolve workspace permission state when user has no membership * fix(THU-555): gate member actions on granular permission keys * fix(THU-551): expose URL workspace id immediately + collapse chat-id on workspace switch * fix(THU-550): wrap BrowserRouter in ErrorBoundary so bootstrap throws surface a screen * chore: strip PR-ref noise from review-followup comments * fix(THU-578): reset isReady before chat session eviction on workspace switch * fix(THU-553): dedupe ModePicker validation between blur and Continue * fix(THU-552): force ctx.userId for invitedByUserId in pending memberships * fix(THU-552): reject malformed emails on pending membership writes * fix(THU-578): persist selectedAgent against session.workspaceId not thread * fix(THU-556): hide chat-skills-bar when no edit perm and no pinned chips * docs: refresh workspace-memberships handler docstring for permission-key model * test(THU-578): seed workspace context in LinkPreviewWidget fallback tests * fix(THU-553): reset isValidating on SET_URL to unstick Continue after stale blur * fix(THU-553): reset stage to picker on stale-URL bail-out in handleContinue * fix(THU-549): pass captured serverId to handleFullWipe after registry clear * fix(THU-555): align pending-row gates + block admin escalation via invite * refactor(THU-555): use isResolved for membership loading distinction * refactor(THU-555): assign openRef in render body, drop useEffect * fix(THU-555): widen duplicate-slug suffix to 8 chars for collision resistance * docs(THU-555): refresh stale comments referencing removed manage_members guard * fix(THU-555): mirror serverUrl ref synchronously in mode-picker onChange * fix(THU-555): sync pending memberships to all workspace members * fix(THU-555): require change_roles for membership PUT that changes existing role * fix(THU-549): keep getAuthToken resolvable through signOut after registry clear * fix(THU-555): add last-admin guard to membership PUT apply path * fix(THU-555): gate pending-row Admin option on change_roles
* feat(THU-552): promote pending membership to active on email match * feat(THU-552): createSharedWorkspace DAL function * feat(THU-552): useCanCreateWorkspace hook * feat(THU-552): EmailChipInput component * feat(THU-552): CreateWorkspaceModal component * feat(THU-552): Create workspace button in sidebar selector footer * feat(THU-552): seed defaults into newly-created shared workspace * fix(THU-552): scope cleanupRemovedDefaults to a workspace * feat(THU-552): split create + invite into two modals, single Create CTA * feat(THU-552): workspace selector design polish * fix(THU-552): rename SCREAMING_SNAKE constants to camelCase * feat(THU-554): Settings → Workspace → General (rename, slug, icon, duplicate) (#971) * fix(THU-554): align PageHeader actions with content padding * feat(THU-554): allow personal workspace rename via upload handler * feat(THU-554): updateWorkspaceName DAL, default name, createdAt stamps * feat(THU-554): useActiveWorkspaceMembership hook * feat(THU-554): split settings sidebar into Account Settings and Workspace groups * feat(THU-554): Workspace Settings page with autosave rename * feat(THU-554): add slug + icon columns to workspaces * feat(THU-554): workspace slug input with auto-derive from name * feat(THU-554): workspace icon picker (emoji or image upload) * feat(THU-554): render workspace icon in the sidebar selector * feat(THU-554): share WorkspaceFormFields between settings and create modal * feat(THU-554): duplicate workspace action * feat(THU-555): Settings → Workspace → Members (add / remove / role / pending) (#974) * feat(THU-555): add user_name/user_email to workspace_memberships * feat(THU-555): enrich membership upload handler with user display info * feat(THU-555): propagate user name/email changes to memberships * feat(THU-555): mirror userName/userEmail on FE workspace_memberships * feat(THU-555): reactive members + pending queries * feat(THU-555): membership mutation DAL * feat(THU-555): useWorkspacePermission hook * feat(THU-555): RequireWorkspacePermission route wrapper * feat(THU-555): Members page scaffold + route * feat(THU-555): Members sidebar entry gated by manage_members * feat(THU-555): render members table with role dropdown * feat(THU-555): wire Remove member with confirmation * feat(THU-555): use globe icon for General sidebar entry * fix(THU-555): rename SCREAMING_SNAKE constant to camelCase * feat(THU-593): gate member management on E2EE-enabled servers * fix(chat-store): dedupe concurrent hydrateChatStore calls * feat(THU-556): Settings → Workspace → Permissions page + resource-CRUD enforcement (#978) * feat(THU-556): setWorkspacePermissionRequiredRole DAL * feat(THU-556): RequireWorkspaceAdmin route guard * refactor(THU-556): consolidate workspace permission keys into shared/workspaces.ts * feat(THU-556): Permissions page (11 keys, labels only) * feat(THU-556): Permissions sidebar entry * feat(THU-556): mount Permissions route * feat(THU-556): refine Permissions page copy and layout * feat(THU-556): scope Permissions to agents/skills/models/mcps * feat(THU-556): use Lock icon for Permissions sidebar entry * feat(THU-556): permissionAllows + BE permission lookup helpers * feat(THU-556): permission-key gating in workspace-scoped handler * feat(THU-556): enforce add_agents/remove_agents permissions * feat(THU-556): treat soft-delete PATCH as remove for permission gating * feat(THU-556): enforce add_skills/remove_skills permissions * feat(THU-556): enforce add_models/remove_models permissions * feat(THU-556): enforce add_mcp_servers/remove_mcp_servers permissions * feat(THU-556): gate edit + chat bypasses, add permission gating tests * fix(workspaces): address review comments across stacked PRs (#988) * fix(THU-549): clear auth token after signOut so server can revoke session * fix(THU-549): clear registry before db-closing so reloaded tabs land at ModePicker * fix(THU-549): guard LogoutModal handleLogout against rapid double-click * fix(THU-549): friendlier ZodError for unset/invalid SERVER_ID * fix(THU-549): remove Cloud URL dev-settings input (use ModePicker flow instead) * fix(THU-552): preserve existing membership role on promote-on-insert * fix(THU-552): delete pending row by (workspace_id, email) after promote-on-insert * fix(THU-552): skip onCreated when create-workspace modal dismissed mid-flight * fix(THU-554): preserve slug/icon on PUT when payload omits them * fix(THU-554): reflect remote workspace updates into settings form * fix(THU-554): append random suffix to duplicate workspace slug * fix(THU-555): resolve workspace permission state when user has no membership * fix(THU-555): gate member actions on granular permission keys * fix(THU-551): expose URL workspace id immediately + collapse chat-id on workspace switch * fix(THU-550): wrap BrowserRouter in ErrorBoundary so bootstrap throws surface a screen * chore: strip PR-ref noise from review-followup comments * fix(THU-578): reset isReady before chat session eviction on workspace switch * fix(THU-553): dedupe ModePicker validation between blur and Continue * fix(THU-552): force ctx.userId for invitedByUserId in pending memberships * fix(THU-552): reject malformed emails on pending membership writes * fix(THU-578): persist selectedAgent against session.workspaceId not thread * fix(THU-556): hide chat-skills-bar when no edit perm and no pinned chips * docs: refresh workspace-memberships handler docstring for permission-key model * test(THU-578): seed workspace context in LinkPreviewWidget fallback tests * fix(THU-553): reset isValidating on SET_URL to unstick Continue after stale blur * fix(THU-553): reset stage to picker on stale-URL bail-out in handleContinue * fix(THU-549): pass captured serverId to handleFullWipe after registry clear * fix(THU-555): align pending-row gates + block admin escalation via invite * refactor(THU-555): use isResolved for membership loading distinction * refactor(THU-555): assign openRef in render body, drop useEffect * fix(THU-555): widen duplicate-slug suffix to 8 chars for collision resistance * docs(THU-555): refresh stale comments referencing removed manage_members guard * fix(THU-555): mirror serverUrl ref synchronously in mode-picker onChange * fix(THU-555): sync pending memberships to all workspace members * fix(THU-555): require change_roles for membership PUT that changes existing role * fix(THU-549): keep getAuthToken resolvable through signOut after registry clear * fix(THU-555): add last-admin guard to membership PUT apply path * fix(THU-555): gate pending-row Admin option on change_roles
…pace (#961) * feat(THU-551): reactive active-workspace + URL helpers * feat(THU-551): useWorkspacesQuery hook + DAL test * feat(THU-551): WorkspaceMembershipGate + live membership query * feat(THU-551): mount workspace routes under /w/:workspaceId prefix * feat(THU-551): workspace selector in sidebar header * feat(THU-551): thread workspace prefix through navigation call sites * feat(THU-552): create workspace modal — name + invite by email (#965) * feat(THU-552): promote pending membership to active on email match * feat(THU-552): createSharedWorkspace DAL function * feat(THU-552): useCanCreateWorkspace hook * feat(THU-552): EmailChipInput component * feat(THU-552): CreateWorkspaceModal component * feat(THU-552): Create workspace button in sidebar selector footer * feat(THU-552): seed defaults into newly-created shared workspace * fix(THU-552): scope cleanupRemovedDefaults to a workspace * feat(THU-552): split create + invite into two modals, single Create CTA * feat(THU-552): workspace selector design polish * fix(THU-552): rename SCREAMING_SNAKE constants to camelCase * feat(THU-554): Settings → Workspace → General (rename, slug, icon, duplicate) (#971) * fix(THU-554): align PageHeader actions with content padding * feat(THU-554): allow personal workspace rename via upload handler * feat(THU-554): updateWorkspaceName DAL, default name, createdAt stamps * feat(THU-554): useActiveWorkspaceMembership hook * feat(THU-554): split settings sidebar into Account Settings and Workspace groups * feat(THU-554): Workspace Settings page with autosave rename * feat(THU-554): add slug + icon columns to workspaces * feat(THU-554): workspace slug input with auto-derive from name * feat(THU-554): workspace icon picker (emoji or image upload) * feat(THU-554): render workspace icon in the sidebar selector * feat(THU-554): share WorkspaceFormFields between settings and create modal * feat(THU-554): duplicate workspace action * feat(THU-555): Settings → Workspace → Members (add / remove / role / pending) (#974) * feat(THU-555): add user_name/user_email to workspace_memberships * feat(THU-555): enrich membership upload handler with user display info * feat(THU-555): propagate user name/email changes to memberships * feat(THU-555): mirror userName/userEmail on FE workspace_memberships * feat(THU-555): reactive members + pending queries * feat(THU-555): membership mutation DAL * feat(THU-555): useWorkspacePermission hook * feat(THU-555): RequireWorkspacePermission route wrapper * feat(THU-555): Members page scaffold + route * feat(THU-555): Members sidebar entry gated by manage_members * feat(THU-555): render members table with role dropdown * feat(THU-555): wire Remove member with confirmation * feat(THU-555): use globe icon for General sidebar entry * fix(THU-555): rename SCREAMING_SNAKE constant to camelCase * feat(THU-593): gate member management on E2EE-enabled servers * fix(chat-store): dedupe concurrent hydrateChatStore calls * feat(THU-556): Settings → Workspace → Permissions page + resource-CRUD enforcement (#978) * feat(THU-556): setWorkspacePermissionRequiredRole DAL * feat(THU-556): RequireWorkspaceAdmin route guard * refactor(THU-556): consolidate workspace permission keys into shared/workspaces.ts * feat(THU-556): Permissions page (11 keys, labels only) * feat(THU-556): Permissions sidebar entry * feat(THU-556): mount Permissions route * feat(THU-556): refine Permissions page copy and layout * feat(THU-556): scope Permissions to agents/skills/models/mcps * feat(THU-556): use Lock icon for Permissions sidebar entry * feat(THU-556): permissionAllows + BE permission lookup helpers * feat(THU-556): permission-key gating in workspace-scoped handler * feat(THU-556): enforce add_agents/remove_agents permissions * feat(THU-556): treat soft-delete PATCH as remove for permission gating * feat(THU-556): enforce add_skills/remove_skills permissions * feat(THU-556): enforce add_models/remove_models permissions * feat(THU-556): enforce add_mcp_servers/remove_mcp_servers permissions * feat(THU-556): gate edit + chat bypasses, add permission gating tests * fix(workspaces): address review comments across stacked PRs (#988) * fix(THU-549): clear auth token after signOut so server can revoke session * fix(THU-549): clear registry before db-closing so reloaded tabs land at ModePicker * fix(THU-549): guard LogoutModal handleLogout against rapid double-click * fix(THU-549): friendlier ZodError for unset/invalid SERVER_ID * fix(THU-549): remove Cloud URL dev-settings input (use ModePicker flow instead) * fix(THU-552): preserve existing membership role on promote-on-insert * fix(THU-552): delete pending row by (workspace_id, email) after promote-on-insert * fix(THU-552): skip onCreated when create-workspace modal dismissed mid-flight * fix(THU-554): preserve slug/icon on PUT when payload omits them * fix(THU-554): reflect remote workspace updates into settings form * fix(THU-554): append random suffix to duplicate workspace slug * fix(THU-555): resolve workspace permission state when user has no membership * fix(THU-555): gate member actions on granular permission keys * fix(THU-551): expose URL workspace id immediately + collapse chat-id on workspace switch * fix(THU-550): wrap BrowserRouter in ErrorBoundary so bootstrap throws surface a screen * chore: strip PR-ref noise from review-followup comments * fix(THU-578): reset isReady before chat session eviction on workspace switch * fix(THU-553): dedupe ModePicker validation between blur and Continue * fix(THU-552): force ctx.userId for invitedByUserId in pending memberships * fix(THU-552): reject malformed emails on pending membership writes * fix(THU-578): persist selectedAgent against session.workspaceId not thread * fix(THU-556): hide chat-skills-bar when no edit perm and no pinned chips * docs: refresh workspace-memberships handler docstring for permission-key model * test(THU-578): seed workspace context in LinkPreviewWidget fallback tests * fix(THU-553): reset isValidating on SET_URL to unstick Continue after stale blur * fix(THU-553): reset stage to picker on stale-URL bail-out in handleContinue * fix(THU-549): pass captured serverId to handleFullWipe after registry clear * fix(THU-555): align pending-row gates + block admin escalation via invite * refactor(THU-555): use isResolved for membership loading distinction * refactor(THU-555): assign openRef in render body, drop useEffect * fix(THU-555): widen duplicate-slug suffix to 8 chars for collision resistance * docs(THU-555): refresh stale comments referencing removed manage_members guard * fix(THU-555): mirror serverUrl ref synchronously in mode-picker onChange * fix(THU-555): sync pending memberships to all workspace members * fix(THU-555): require change_roles for membership PUT that changes existing role * fix(THU-549): keep getAuthToken resolvable through signOut after registry clear * fix(THU-555): add last-admin guard to membership PUT apply path * fix(THU-555): gate pending-row Admin option on change_roles
…vity (#958) * feat(THU-553): polish mode-picker UI — layout, selection state, and connectivity * fix(THU-553): DI for mode-picker, drop mock.module('@/lib/http') leakage * feat(THU-551): workspace selector in sidebar + URL-based active workspace (#961) * feat(THU-551): reactive active-workspace + URL helpers * feat(THU-551): useWorkspacesQuery hook + DAL test * feat(THU-551): WorkspaceMembershipGate + live membership query * feat(THU-551): mount workspace routes under /w/:workspaceId prefix * feat(THU-551): workspace selector in sidebar header * feat(THU-551): thread workspace prefix through navigation call sites * feat(THU-552): create workspace modal — name + invite by email (#965) * feat(THU-552): promote pending membership to active on email match * feat(THU-552): createSharedWorkspace DAL function * feat(THU-552): useCanCreateWorkspace hook * feat(THU-552): EmailChipInput component * feat(THU-552): CreateWorkspaceModal component * feat(THU-552): Create workspace button in sidebar selector footer * feat(THU-552): seed defaults into newly-created shared workspace * fix(THU-552): scope cleanupRemovedDefaults to a workspace * feat(THU-552): split create + invite into two modals, single Create CTA * feat(THU-552): workspace selector design polish * fix(THU-552): rename SCREAMING_SNAKE constants to camelCase * feat(THU-554): Settings → Workspace → General (rename, slug, icon, duplicate) (#971) * fix(THU-554): align PageHeader actions with content padding * feat(THU-554): allow personal workspace rename via upload handler * feat(THU-554): updateWorkspaceName DAL, default name, createdAt stamps * feat(THU-554): useActiveWorkspaceMembership hook * feat(THU-554): split settings sidebar into Account Settings and Workspace groups * feat(THU-554): Workspace Settings page with autosave rename * feat(THU-554): add slug + icon columns to workspaces * feat(THU-554): workspace slug input with auto-derive from name * feat(THU-554): workspace icon picker (emoji or image upload) * feat(THU-554): render workspace icon in the sidebar selector * feat(THU-554): share WorkspaceFormFields between settings and create modal * feat(THU-554): duplicate workspace action * feat(THU-555): Settings → Workspace → Members (add / remove / role / pending) (#974) * feat(THU-555): add user_name/user_email to workspace_memberships * feat(THU-555): enrich membership upload handler with user display info * feat(THU-555): propagate user name/email changes to memberships * feat(THU-555): mirror userName/userEmail on FE workspace_memberships * feat(THU-555): reactive members + pending queries * feat(THU-555): membership mutation DAL * feat(THU-555): useWorkspacePermission hook * feat(THU-555): RequireWorkspacePermission route wrapper * feat(THU-555): Members page scaffold + route * feat(THU-555): Members sidebar entry gated by manage_members * feat(THU-555): render members table with role dropdown * feat(THU-555): wire Remove member with confirmation * feat(THU-555): use globe icon for General sidebar entry * fix(THU-555): rename SCREAMING_SNAKE constant to camelCase * feat(THU-593): gate member management on E2EE-enabled servers * fix(chat-store): dedupe concurrent hydrateChatStore calls * feat(THU-556): Settings → Workspace → Permissions page + resource-CRUD enforcement (#978) * feat(THU-556): setWorkspacePermissionRequiredRole DAL * feat(THU-556): RequireWorkspaceAdmin route guard * refactor(THU-556): consolidate workspace permission keys into shared/workspaces.ts * feat(THU-556): Permissions page (11 keys, labels only) * feat(THU-556): Permissions sidebar entry * feat(THU-556): mount Permissions route * feat(THU-556): refine Permissions page copy and layout * feat(THU-556): scope Permissions to agents/skills/models/mcps * feat(THU-556): use Lock icon for Permissions sidebar entry * feat(THU-556): permissionAllows + BE permission lookup helpers * feat(THU-556): permission-key gating in workspace-scoped handler * feat(THU-556): enforce add_agents/remove_agents permissions * feat(THU-556): treat soft-delete PATCH as remove for permission gating * feat(THU-556): enforce add_skills/remove_skills permissions * feat(THU-556): enforce add_models/remove_models permissions * feat(THU-556): enforce add_mcp_servers/remove_mcp_servers permissions * feat(THU-556): gate edit + chat bypasses, add permission gating tests * fix(workspaces): address review comments across stacked PRs (#988) * fix(THU-549): clear auth token after signOut so server can revoke session * fix(THU-549): clear registry before db-closing so reloaded tabs land at ModePicker * fix(THU-549): guard LogoutModal handleLogout against rapid double-click * fix(THU-549): friendlier ZodError for unset/invalid SERVER_ID * fix(THU-549): remove Cloud URL dev-settings input (use ModePicker flow instead) * fix(THU-552): preserve existing membership role on promote-on-insert * fix(THU-552): delete pending row by (workspace_id, email) after promote-on-insert * fix(THU-552): skip onCreated when create-workspace modal dismissed mid-flight * fix(THU-554): preserve slug/icon on PUT when payload omits them * fix(THU-554): reflect remote workspace updates into settings form * fix(THU-554): append random suffix to duplicate workspace slug * fix(THU-555): resolve workspace permission state when user has no membership * fix(THU-555): gate member actions on granular permission keys * fix(THU-551): expose URL workspace id immediately + collapse chat-id on workspace switch * fix(THU-550): wrap BrowserRouter in ErrorBoundary so bootstrap throws surface a screen * chore: strip PR-ref noise from review-followup comments * fix(THU-578): reset isReady before chat session eviction on workspace switch * fix(THU-553): dedupe ModePicker validation between blur and Continue * fix(THU-552): force ctx.userId for invitedByUserId in pending memberships * fix(THU-552): reject malformed emails on pending membership writes * fix(THU-578): persist selectedAgent against session.workspaceId not thread * fix(THU-556): hide chat-skills-bar when no edit perm and no pinned chips * docs: refresh workspace-memberships handler docstring for permission-key model * test(THU-578): seed workspace context in LinkPreviewWidget fallback tests * fix(THU-553): reset isValidating on SET_URL to unstick Continue after stale blur * fix(THU-553): reset stage to picker on stale-URL bail-out in handleContinue * fix(THU-549): pass captured serverId to handleFullWipe after registry clear * fix(THU-555): align pending-row gates + block admin escalation via invite * refactor(THU-555): use isResolved for membership loading distinction * refactor(THU-555): assign openRef in render body, drop useEffect * fix(THU-555): widen duplicate-slug suffix to 8 chars for collision resistance * docs(THU-555): refresh stale comments referencing removed manage_members guard * fix(THU-555): mirror serverUrl ref synchronously in mode-picker onChange * fix(THU-555): sync pending memberships to all workspace members * fix(THU-555): require change_roles for membership PUT that changes existing role * fix(THU-549): keep getAuthToken resolvable through signOut after registry clear * fix(THU-555): add last-admin guard to membership PUT apply path * fix(THU-555): gate pending-row Admin option on change_roles
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Reviewed by Cursor Bugbot for commit 24cc392. Configure here.
| const wouldChangeRole = newRole !== null && existing.role !== newRole | ||
| if ( | ||
| (wouldChangeRole || targetsAdminRole) && | ||
| !(await callerSatisfiesPermission(tx, existing.workspaceId, ctx.userId, 'change_roles')) |
There was a problem hiding this comment.
Admin PUT wrongly needs change_roles
Medium Severity
For workspace_memberships PUTs that omit workspace_id and only target an existing row by op.id, validation treats any role: 'admin' payload as needing change_roles, even when the member is already an admin and the role does not change. The insert-or-update path correctly uses wouldMintNewAdmin only for new rows; this branch still uses targetsAdminRole alone, so callers with invite_users but not change_roles get INSUFFICIENT_PERMISSION on harmless re-upserts.
Reviewed by Cursor Bugbot for commit 24cc392. Configure here.
| isResolved && (requiredRole === 'member' ? userRole === 'admin' || userRole === 'member' : userRole === 'admin') | ||
| isResolved && | ||
| !!membership && | ||
| (requiredRole === 'member' ? userRole === 'admin' || userRole === 'member' : userRole === 'admin') |
There was a problem hiding this comment.
FE duplicates permissionAllows logic
Low Severity
This PR adds shared permissionAllows in shared/workspaces.ts so frontend and backend use the same satisfiability rule, but useWorkspacePermission reimplements that predicate inline instead of calling permissionAllows. Future changes to role rules can update the shared helper while the hook stays wrong, hiding denied actions or showing ones the backend rejects.
Reviewed by Cursor Bugbot for commit 24cc392. Configure here.
* feat(THU-578): scope all FE DAL methods to active workspaceId * fix(THU-578): seed trust domain in eval CLI + scope model-profile upsert by workspace * fix(THU-578): retry hydration when workspaceId resolves after WorkspaceGate * fix(THU-578): evict stale chat session on workspace switch * fix(THU-578): scope prompt join in getTriggerPromptForThread to active workspace * fix(THU-578): fix random test failures from workspaceId resolution races * feat(THU-553): mode picker UI — layout, selection state, and connectivity (#958) * feat(THU-553): polish mode-picker UI — layout, selection state, and connectivity * fix(THU-553): DI for mode-picker, drop mock.module('@/lib/http') leakage * feat(THU-551): workspace selector in sidebar + URL-based active workspace (#961) * feat(THU-551): reactive active-workspace + URL helpers * feat(THU-551): useWorkspacesQuery hook + DAL test * feat(THU-551): WorkspaceMembershipGate + live membership query * feat(THU-551): mount workspace routes under /w/:workspaceId prefix * feat(THU-551): workspace selector in sidebar header * feat(THU-551): thread workspace prefix through navigation call sites * feat(THU-552): create workspace modal — name + invite by email (#965) * feat(THU-552): promote pending membership to active on email match * feat(THU-552): createSharedWorkspace DAL function * feat(THU-552): useCanCreateWorkspace hook * feat(THU-552): EmailChipInput component * feat(THU-552): CreateWorkspaceModal component * feat(THU-552): Create workspace button in sidebar selector footer * feat(THU-552): seed defaults into newly-created shared workspace * fix(THU-552): scope cleanupRemovedDefaults to a workspace * feat(THU-552): split create + invite into two modals, single Create CTA * feat(THU-552): workspace selector design polish * fix(THU-552): rename SCREAMING_SNAKE constants to camelCase * feat(THU-554): Settings → Workspace → General (rename, slug, icon, duplicate) (#971) * fix(THU-554): align PageHeader actions with content padding * feat(THU-554): allow personal workspace rename via upload handler * feat(THU-554): updateWorkspaceName DAL, default name, createdAt stamps * feat(THU-554): useActiveWorkspaceMembership hook * feat(THU-554): split settings sidebar into Account Settings and Workspace groups * feat(THU-554): Workspace Settings page with autosave rename * feat(THU-554): add slug + icon columns to workspaces * feat(THU-554): workspace slug input with auto-derive from name * feat(THU-554): workspace icon picker (emoji or image upload) * feat(THU-554): render workspace icon in the sidebar selector * feat(THU-554): share WorkspaceFormFields between settings and create modal * feat(THU-554): duplicate workspace action * feat(THU-555): Settings → Workspace → Members (add / remove / role / pending) (#974) * feat(THU-555): add user_name/user_email to workspace_memberships * feat(THU-555): enrich membership upload handler with user display info * feat(THU-555): propagate user name/email changes to memberships * feat(THU-555): mirror userName/userEmail on FE workspace_memberships * feat(THU-555): reactive members + pending queries * feat(THU-555): membership mutation DAL * feat(THU-555): useWorkspacePermission hook * feat(THU-555): RequireWorkspacePermission route wrapper * feat(THU-555): Members page scaffold + route * feat(THU-555): Members sidebar entry gated by manage_members * feat(THU-555): render members table with role dropdown * feat(THU-555): wire Remove member with confirmation * feat(THU-555): use globe icon for General sidebar entry * fix(THU-555): rename SCREAMING_SNAKE constant to camelCase * feat(THU-593): gate member management on E2EE-enabled servers * fix(chat-store): dedupe concurrent hydrateChatStore calls * feat(THU-556): Settings → Workspace → Permissions page + resource-CRUD enforcement (#978) * feat(THU-556): setWorkspacePermissionRequiredRole DAL * feat(THU-556): RequireWorkspaceAdmin route guard * refactor(THU-556): consolidate workspace permission keys into shared/workspaces.ts * feat(THU-556): Permissions page (11 keys, labels only) * feat(THU-556): Permissions sidebar entry * feat(THU-556): mount Permissions route * feat(THU-556): refine Permissions page copy and layout * feat(THU-556): scope Permissions to agents/skills/models/mcps * feat(THU-556): use Lock icon for Permissions sidebar entry * feat(THU-556): permissionAllows + BE permission lookup helpers * feat(THU-556): permission-key gating in workspace-scoped handler * feat(THU-556): enforce add_agents/remove_agents permissions * feat(THU-556): treat soft-delete PATCH as remove for permission gating * feat(THU-556): enforce add_skills/remove_skills permissions * feat(THU-556): enforce add_models/remove_models permissions * feat(THU-556): enforce add_mcp_servers/remove_mcp_servers permissions * feat(THU-556): gate edit + chat bypasses, add permission gating tests * fix(workspaces): address review comments across stacked PRs (#988) * fix(THU-549): clear auth token after signOut so server can revoke session * fix(THU-549): clear registry before db-closing so reloaded tabs land at ModePicker * fix(THU-549): guard LogoutModal handleLogout against rapid double-click * fix(THU-549): friendlier ZodError for unset/invalid SERVER_ID * fix(THU-549): remove Cloud URL dev-settings input (use ModePicker flow instead) * fix(THU-552): preserve existing membership role on promote-on-insert * fix(THU-552): delete pending row by (workspace_id, email) after promote-on-insert * fix(THU-552): skip onCreated when create-workspace modal dismissed mid-flight * fix(THU-554): preserve slug/icon on PUT when payload omits them * fix(THU-554): reflect remote workspace updates into settings form * fix(THU-554): append random suffix to duplicate workspace slug * fix(THU-555): resolve workspace permission state when user has no membership * fix(THU-555): gate member actions on granular permission keys * fix(THU-551): expose URL workspace id immediately + collapse chat-id on workspace switch * fix(THU-550): wrap BrowserRouter in ErrorBoundary so bootstrap throws surface a screen * chore: strip PR-ref noise from review-followup comments * fix(THU-578): reset isReady before chat session eviction on workspace switch * fix(THU-553): dedupe ModePicker validation between blur and Continue * fix(THU-552): force ctx.userId for invitedByUserId in pending memberships * fix(THU-552): reject malformed emails on pending membership writes * fix(THU-578): persist selectedAgent against session.workspaceId not thread * fix(THU-556): hide chat-skills-bar when no edit perm and no pinned chips * docs: refresh workspace-memberships handler docstring for permission-key model * test(THU-578): seed workspace context in LinkPreviewWidget fallback tests * fix(THU-553): reset isValidating on SET_URL to unstick Continue after stale blur * fix(THU-553): reset stage to picker on stale-URL bail-out in handleContinue * fix(THU-549): pass captured serverId to handleFullWipe after registry clear * fix(THU-555): align pending-row gates + block admin escalation via invite * refactor(THU-555): use isResolved for membership loading distinction * refactor(THU-555): assign openRef in render body, drop useEffect * fix(THU-555): widen duplicate-slug suffix to 8 chars for collision resistance * docs(THU-555): refresh stale comments referencing removed manage_members guard * fix(THU-555): mirror serverUrl ref synchronously in mode-picker onChange * fix(THU-555): sync pending memberships to all workspace members * fix(THU-555): require change_roles for membership PUT that changes existing role * fix(THU-549): keep getAuthToken resolvable through signOut after registry clear * fix(THU-555): add last-admin guard to membership PUT apply path * fix(THU-555): gate pending-row Admin option on change_roles
* feat(THU-550): BE workspaces tables + post-create bootstrap hook * feat(THU-550): PowerSync upload handler factory + workspace handlers * feat(THU-550): workspace_id on scoped tables + sync rules + ws handler * feat(THU-550): encrypt workspaces.name * feat(THU-550): shared deterministic personal-workspace id helpers * feat(THU-550): BE — FE-owned personal workspace creation * feat(THU-550): FE — workspace-aware DAL + post-auth bootstrap + gate * fix(THU-550): log permanently rejected upload ops * fix(THU-550): pin workspace_id in PATCH/DELETE WHERE clause * fix(THU-550): scope reconcile-defaults lookups and updates by workspace_id * fix(THU-550): force personal workspace name + shared workspace admin bootstrap * fix(THU-550): throw bootstrap error + update stale 400 upload test assertions * fix(THU-550): add workspaceId to model profile defaults + fix getSystemTinfoilClient cloud URL * fix(THU-550): restore BE test isolation broken by upload handler transactions * ci(THU-550): install root deps before BE type-check (shared/ uses uuid) * feat(THU-578): scope all FE DAL methods to active workspaceId (#951) * feat(THU-578): scope all FE DAL methods to active workspaceId * fix(THU-578): seed trust domain in eval CLI + scope model-profile upsert by workspace * fix(THU-578): retry hydration when workspaceId resolves after WorkspaceGate * fix(THU-578): evict stale chat session on workspace switch * fix(THU-578): scope prompt join in getTriggerPromptForThread to active workspace * fix(THU-578): fix random test failures from workspaceId resolution races * feat(THU-553): mode picker UI — layout, selection state, and connectivity (#958) * feat(THU-553): polish mode-picker UI — layout, selection state, and connectivity * fix(THU-553): DI for mode-picker, drop mock.module('@/lib/http') leakage * feat(THU-551): workspace selector in sidebar + URL-based active workspace (#961) * feat(THU-551): reactive active-workspace + URL helpers * feat(THU-551): useWorkspacesQuery hook + DAL test * feat(THU-551): WorkspaceMembershipGate + live membership query * feat(THU-551): mount workspace routes under /w/:workspaceId prefix * feat(THU-551): workspace selector in sidebar header * feat(THU-551): thread workspace prefix through navigation call sites * feat(THU-552): create workspace modal — name + invite by email (#965) * feat(THU-552): promote pending membership to active on email match * feat(THU-552): createSharedWorkspace DAL function * feat(THU-552): useCanCreateWorkspace hook * feat(THU-552): EmailChipInput component * feat(THU-552): CreateWorkspaceModal component * feat(THU-552): Create workspace button in sidebar selector footer * feat(THU-552): seed defaults into newly-created shared workspace * fix(THU-552): scope cleanupRemovedDefaults to a workspace * feat(THU-552): split create + invite into two modals, single Create CTA * feat(THU-552): workspace selector design polish * fix(THU-552): rename SCREAMING_SNAKE constants to camelCase * feat(THU-554): Settings → Workspace → General (rename, slug, icon, duplicate) (#971) * fix(THU-554): align PageHeader actions with content padding * feat(THU-554): allow personal workspace rename via upload handler * feat(THU-554): updateWorkspaceName DAL, default name, createdAt stamps * feat(THU-554): useActiveWorkspaceMembership hook * feat(THU-554): split settings sidebar into Account Settings and Workspace groups * feat(THU-554): Workspace Settings page with autosave rename * feat(THU-554): add slug + icon columns to workspaces * feat(THU-554): workspace slug input with auto-derive from name * feat(THU-554): workspace icon picker (emoji or image upload) * feat(THU-554): render workspace icon in the sidebar selector * feat(THU-554): share WorkspaceFormFields between settings and create modal * feat(THU-554): duplicate workspace action * feat(THU-555): Settings → Workspace → Members (add / remove / role / pending) (#974) * feat(THU-555): add user_name/user_email to workspace_memberships * feat(THU-555): enrich membership upload handler with user display info * feat(THU-555): propagate user name/email changes to memberships * feat(THU-555): mirror userName/userEmail on FE workspace_memberships * feat(THU-555): reactive members + pending queries * feat(THU-555): membership mutation DAL * feat(THU-555): useWorkspacePermission hook * feat(THU-555): RequireWorkspacePermission route wrapper * feat(THU-555): Members page scaffold + route * feat(THU-555): Members sidebar entry gated by manage_members * feat(THU-555): render members table with role dropdown * feat(THU-555): wire Remove member with confirmation * feat(THU-555): use globe icon for General sidebar entry * fix(THU-555): rename SCREAMING_SNAKE constant to camelCase * feat(THU-593): gate member management on E2EE-enabled servers * fix(chat-store): dedupe concurrent hydrateChatStore calls * feat(THU-556): Settings → Workspace → Permissions page + resource-CRUD enforcement (#978) * feat(THU-556): setWorkspacePermissionRequiredRole DAL * feat(THU-556): RequireWorkspaceAdmin route guard * refactor(THU-556): consolidate workspace permission keys into shared/workspaces.ts * feat(THU-556): Permissions page (11 keys, labels only) * feat(THU-556): Permissions sidebar entry * feat(THU-556): mount Permissions route * feat(THU-556): refine Permissions page copy and layout * feat(THU-556): scope Permissions to agents/skills/models/mcps * feat(THU-556): use Lock icon for Permissions sidebar entry * feat(THU-556): permissionAllows + BE permission lookup helpers * feat(THU-556): permission-key gating in workspace-scoped handler * feat(THU-556): enforce add_agents/remove_agents permissions * feat(THU-556): treat soft-delete PATCH as remove for permission gating * feat(THU-556): enforce add_skills/remove_skills permissions * feat(THU-556): enforce add_models/remove_models permissions * feat(THU-556): enforce add_mcp_servers/remove_mcp_servers permissions * feat(THU-556): gate edit + chat bypasses, add permission gating tests * fix(workspaces): address review comments across stacked PRs (#988) * fix(THU-549): clear auth token after signOut so server can revoke session * fix(THU-549): clear registry before db-closing so reloaded tabs land at ModePicker * fix(THU-549): guard LogoutModal handleLogout against rapid double-click * fix(THU-549): friendlier ZodError for unset/invalid SERVER_ID * fix(THU-549): remove Cloud URL dev-settings input (use ModePicker flow instead) * fix(THU-552): preserve existing membership role on promote-on-insert * fix(THU-552): delete pending row by (workspace_id, email) after promote-on-insert * fix(THU-552): skip onCreated when create-workspace modal dismissed mid-flight * fix(THU-554): preserve slug/icon on PUT when payload omits them * fix(THU-554): reflect remote workspace updates into settings form * fix(THU-554): append random suffix to duplicate workspace slug * fix(THU-555): resolve workspace permission state when user has no membership * fix(THU-555): gate member actions on granular permission keys * fix(THU-551): expose URL workspace id immediately + collapse chat-id on workspace switch * fix(THU-550): wrap BrowserRouter in ErrorBoundary so bootstrap throws surface a screen * chore: strip PR-ref noise from review-followup comments * fix(THU-578): reset isReady before chat session eviction on workspace switch * fix(THU-553): dedupe ModePicker validation between blur and Continue * fix(THU-552): force ctx.userId for invitedByUserId in pending memberships * fix(THU-552): reject malformed emails on pending membership writes * fix(THU-578): persist selectedAgent against session.workspaceId not thread * fix(THU-556): hide chat-skills-bar when no edit perm and no pinned chips * docs: refresh workspace-memberships handler docstring for permission-key model * test(THU-578): seed workspace context in LinkPreviewWidget fallback tests * fix(THU-553): reset isValidating on SET_URL to unstick Continue after stale blur * fix(THU-553): reset stage to picker on stale-URL bail-out in handleContinue * fix(THU-549): pass captured serverId to handleFullWipe after registry clear * fix(THU-555): align pending-row gates + block admin escalation via invite * refactor(THU-555): use isResolved for membership loading distinction * refactor(THU-555): assign openRef in render body, drop useEffect * fix(THU-555): widen duplicate-slug suffix to 8 chars for collision resistance * docs(THU-555): refresh stale comments referencing removed manage_members guard * fix(THU-555): mirror serverUrl ref synchronously in mode-picker onChange * fix(THU-555): sync pending memberships to all workspace members * fix(THU-555): require change_roles for membership PUT that changes existing role * fix(THU-549): keep getAuthToken resolvable through signOut after registry clear * fix(THU-555): add last-admin guard to membership PUT apply path * fix(THU-555): gate pending-row Admin option on change_roles
* feat(THU-549): serverId + extended /v1/config payload * feat(THU-549): trust-domain registry + namespaced auth/device/keys * feat(THU-549): boot env vars + trust-domain decision tree * feat(THU-549): scope DB filename by trust domain + lifecycle broadcast * feat(THU-549): logout = unconditional wipe * feat(THU-549): scope cloud URL by active server * refactor(THU-549): collapse registry actions into activateServer/activateStandalone * feat(THU-549): persist per-server session on ServerEntry + wire mirror * refactor(THU-549): drop unused db-reopened event + subscribeDbLifecycle * refactor(THU-549): extract shared sync-worker-name format * fix(THU-549): pin SERVER_ID for e2e backends * fix(THU-549): address PR review — timeout, Tauri OPFS, logout loading state, test SERVER_ID, auth-token let * fix(THU-549): wipe IDB databases on logout + fix SSO redirect race + delete E2EE keys IDB * fix(THU-549): use getActiveCloudUrl in getSystemTinfoilClient * fix(THU-550): fix BE/FE tests broken by THU-549 trust-domain rebase * feat(THU-550): Workspaces data layer (#944) * feat(THU-550): BE workspaces tables + post-create bootstrap hook * feat(THU-550): PowerSync upload handler factory + workspace handlers * feat(THU-550): workspace_id on scoped tables + sync rules + ws handler * feat(THU-550): encrypt workspaces.name * feat(THU-550): shared deterministic personal-workspace id helpers * feat(THU-550): BE — FE-owned personal workspace creation * feat(THU-550): FE — workspace-aware DAL + post-auth bootstrap + gate * fix(THU-550): log permanently rejected upload ops * fix(THU-550): pin workspace_id in PATCH/DELETE WHERE clause * fix(THU-550): scope reconcile-defaults lookups and updates by workspace_id * fix(THU-550): force personal workspace name + shared workspace admin bootstrap * fix(THU-550): throw bootstrap error + update stale 400 upload test assertions * fix(THU-550): add workspaceId to model profile defaults + fix getSystemTinfoilClient cloud URL * fix(THU-550): restore BE test isolation broken by upload handler transactions * ci(THU-550): install root deps before BE type-check (shared/ uses uuid) * feat(THU-578): scope all FE DAL methods to active workspaceId (#951) * feat(THU-578): scope all FE DAL methods to active workspaceId * fix(THU-578): seed trust domain in eval CLI + scope model-profile upsert by workspace * fix(THU-578): retry hydration when workspaceId resolves after WorkspaceGate * fix(THU-578): evict stale chat session on workspace switch * fix(THU-578): scope prompt join in getTriggerPromptForThread to active workspace * fix(THU-578): fix random test failures from workspaceId resolution races * feat(THU-553): mode picker UI — layout, selection state, and connectivity (#958) * feat(THU-553): polish mode-picker UI — layout, selection state, and connectivity * fix(THU-553): DI for mode-picker, drop mock.module('@/lib/http') leakage * feat(THU-551): workspace selector in sidebar + URL-based active workspace (#961) * feat(THU-551): reactive active-workspace + URL helpers * feat(THU-551): useWorkspacesQuery hook + DAL test * feat(THU-551): WorkspaceMembershipGate + live membership query * feat(THU-551): mount workspace routes under /w/:workspaceId prefix * feat(THU-551): workspace selector in sidebar header * feat(THU-551): thread workspace prefix through navigation call sites * feat(THU-552): create workspace modal — name + invite by email (#965) * feat(THU-552): promote pending membership to active on email match * feat(THU-552): createSharedWorkspace DAL function * feat(THU-552): useCanCreateWorkspace hook * feat(THU-552): EmailChipInput component * feat(THU-552): CreateWorkspaceModal component * feat(THU-552): Create workspace button in sidebar selector footer * feat(THU-552): seed defaults into newly-created shared workspace * fix(THU-552): scope cleanupRemovedDefaults to a workspace * feat(THU-552): split create + invite into two modals, single Create CTA * feat(THU-552): workspace selector design polish * fix(THU-552): rename SCREAMING_SNAKE constants to camelCase * feat(THU-554): Settings → Workspace → General (rename, slug, icon, duplicate) (#971) * fix(THU-554): align PageHeader actions with content padding * feat(THU-554): allow personal workspace rename via upload handler * feat(THU-554): updateWorkspaceName DAL, default name, createdAt stamps * feat(THU-554): useActiveWorkspaceMembership hook * feat(THU-554): split settings sidebar into Account Settings and Workspace groups * feat(THU-554): Workspace Settings page with autosave rename * feat(THU-554): add slug + icon columns to workspaces * feat(THU-554): workspace slug input with auto-derive from name * feat(THU-554): workspace icon picker (emoji or image upload) * feat(THU-554): render workspace icon in the sidebar selector * feat(THU-554): share WorkspaceFormFields between settings and create modal * feat(THU-554): duplicate workspace action * feat(THU-555): Settings → Workspace → Members (add / remove / role / pending) (#974) * feat(THU-555): add user_name/user_email to workspace_memberships * feat(THU-555): enrich membership upload handler with user display info * feat(THU-555): propagate user name/email changes to memberships * feat(THU-555): mirror userName/userEmail on FE workspace_memberships * feat(THU-555): reactive members + pending queries * feat(THU-555): membership mutation DAL * feat(THU-555): useWorkspacePermission hook * feat(THU-555): RequireWorkspacePermission route wrapper * feat(THU-555): Members page scaffold + route * feat(THU-555): Members sidebar entry gated by manage_members * feat(THU-555): render members table with role dropdown * feat(THU-555): wire Remove member with confirmation * feat(THU-555): use globe icon for General sidebar entry * fix(THU-555): rename SCREAMING_SNAKE constant to camelCase * feat(THU-593): gate member management on E2EE-enabled servers * fix(chat-store): dedupe concurrent hydrateChatStore calls * feat(THU-556): Settings → Workspace → Permissions page + resource-CRUD enforcement (#978) * feat(THU-556): setWorkspacePermissionRequiredRole DAL * feat(THU-556): RequireWorkspaceAdmin route guard * refactor(THU-556): consolidate workspace permission keys into shared/workspaces.ts * feat(THU-556): Permissions page (11 keys, labels only) * feat(THU-556): Permissions sidebar entry * feat(THU-556): mount Permissions route * feat(THU-556): refine Permissions page copy and layout * feat(THU-556): scope Permissions to agents/skills/models/mcps * feat(THU-556): use Lock icon for Permissions sidebar entry * feat(THU-556): permissionAllows + BE permission lookup helpers * feat(THU-556): permission-key gating in workspace-scoped handler * feat(THU-556): enforce add_agents/remove_agents permissions * feat(THU-556): treat soft-delete PATCH as remove for permission gating * feat(THU-556): enforce add_skills/remove_skills permissions * feat(THU-556): enforce add_models/remove_models permissions * feat(THU-556): enforce add_mcp_servers/remove_mcp_servers permissions * feat(THU-556): gate edit + chat bypasses, add permission gating tests * fix(workspaces): address review comments across stacked PRs (#988) * fix(THU-549): clear auth token after signOut so server can revoke session * fix(THU-549): clear registry before db-closing so reloaded tabs land at ModePicker * fix(THU-549): guard LogoutModal handleLogout against rapid double-click * fix(THU-549): friendlier ZodError for unset/invalid SERVER_ID * fix(THU-549): remove Cloud URL dev-settings input (use ModePicker flow instead) * fix(THU-552): preserve existing membership role on promote-on-insert * fix(THU-552): delete pending row by (workspace_id, email) after promote-on-insert * fix(THU-552): skip onCreated when create-workspace modal dismissed mid-flight * fix(THU-554): preserve slug/icon on PUT when payload omits them * fix(THU-554): reflect remote workspace updates into settings form * fix(THU-554): append random suffix to duplicate workspace slug * fix(THU-555): resolve workspace permission state when user has no membership * fix(THU-555): gate member actions on granular permission keys * fix(THU-551): expose URL workspace id immediately + collapse chat-id on workspace switch * fix(THU-550): wrap BrowserRouter in ErrorBoundary so bootstrap throws surface a screen * chore: strip PR-ref noise from review-followup comments * fix(THU-578): reset isReady before chat session eviction on workspace switch * fix(THU-553): dedupe ModePicker validation between blur and Continue * fix(THU-552): force ctx.userId for invitedByUserId in pending memberships * fix(THU-552): reject malformed emails on pending membership writes * fix(THU-578): persist selectedAgent against session.workspaceId not thread * fix(THU-556): hide chat-skills-bar when no edit perm and no pinned chips * docs: refresh workspace-memberships handler docstring for permission-key model * test(THU-578): seed workspace context in LinkPreviewWidget fallback tests * fix(THU-553): reset isValidating on SET_URL to unstick Continue after stale blur * fix(THU-553): reset stage to picker on stale-URL bail-out in handleContinue * fix(THU-549): pass captured serverId to handleFullWipe after registry clear * fix(THU-555): align pending-row gates + block admin escalation via invite * refactor(THU-555): use isResolved for membership loading distinction * refactor(THU-555): assign openRef in render body, drop useEffect * fix(THU-555): widen duplicate-slug suffix to 8 chars for collision resistance * docs(THU-555): refresh stale comments referencing removed manage_members guard * fix(THU-555): mirror serverUrl ref synchronously in mode-picker onChange * fix(THU-555): sync pending memberships to all workspace members * fix(THU-555): require change_roles for membership PUT that changes existing role * fix(THU-549): keep getAuthToken resolvable through signOut after registry clear * fix(THU-555): add last-admin guard to membership PUT apply path * fix(THU-555): gate pending-row Admin option on change_roles


Summary
Ships PR 8 of the Workspaces v1 plan — the Permissions page under Settings → Workspace, plus end-to-end FE/BE enforcement of those permissions for add/remove × agents / skills / models / mcp_servers (8 keys). Soft-delete (PATCH
deleted_at) is correctly classified as a remove. Stacked on top of #974 (THU-555, Members page); base will move tomainonce #974 merges.Spec called for an 11-row grid; mid-task the surface was narrowed to the 4 resource CRUD pairs (the spec's other keys stay in the enum, stored-only — see Out of scope below).
What this PR does
shared/workspaces.ts) — single source of truth forworkspacePermissionKeys,WorkspacePermissionKey/Roletypes,isWorkspacePermissionKeynarrowing, andpermissionAllows(userRole, requiredRole)so FE + BE agree on satisfiability.setWorkspacePermissionRequiredRole(UPSERT — works without the seeded row),useWorkspacePermissionsQueryreactive hook./w/<id>/settings/workspace/permissions— card+table with 8 rows (Add/Remove × Agents/Skills/Models/MCPs); role selector ("Admin" / "Everyone"). Loads, mutates, persists via PowerSync — no separate save button.RequireWorkspaceAdminroute guard (sibling toRequireWorkspacePermission): admin-only, personal-workspace + E2EE gated. Branch order:personal → e2ee → loading → !admin → render.useWorkspacePermission:+, empty-state Add, row Switch, row Edit (Pen), row Delete (Trash),ModificationIndicatorreset.+, empty-state Add, row Switch, row Trash (Popover).AgentRowSwitch + Trash threaded viacanEditAgents/canRemoveAgentsprops.SkillsListCreate button;LibraryRowSwitch + Edit/Delete menu items;SkillDetailSwitch + Edit/Delete menu items; empty-panel "Create your first skill".skills/ agents / models pickers; gated symmetrically:ChatSkillsBar"Pin a skill" trigger hidden when!add_skills; pinned chips still render butSuggestionChiphides Reorder + Unpin.ChatModelPicker"Add Models" footer suppressed when!add_models.Headeragent selector "Add Agent" footer also gated onadd_agents(in addition toallowCustomAgents).getUserRoleInWorkspace(tx, workspaceId, userId),getRequiredRoleForPermission(tx, workspaceId, key).createWorkspaceScopedHandlerextended with optionaladdPermissionKey/removePermissionKey/softDeleteColumn. NewopIntenthelper classifies PUT/PATCH(nodeleted_at) as "add" and DELETE/PATCH(deleted_atset) as "remove" — gates each on the corresponding key, rejects withpermanent / INSUFFICIENT_PERMISSIONotherwise.agents,skills,models,mcp_serversnow pass all three options (deleted_atis the soft-delete column for all four).Notable decisions
RequireWorkspaceAdmin) rather than gated on thechange_permissionskey — making it configurable creates a self-escalation foot-gun (a member grantedchange_permissions=membercould promote themselves further). The row stays in the enum for forward-compat.'member' = "Everyone"in the UI — the underlying enum stays'admin' | 'member', but the role picker shows "Admin" / "Everyone" since every workspace member satisfies amemberrequirement (admins satisfy it too).deleted_at, sosoftDeleteColumn: 'deleted_at'on each registry entry routes that PATCH toremovePermissionKeyinstead ofaddPermissionKey. Regular edits + restores stay on the add key.add_*(not a separateedit_*key) — toggling enabled, renaming a model, editing a skill all PATCH the row and gate on the resource's add key. Avoids exploding the permission surface.permissionAllowspredicate inshared/workspaces.ts— same satisfiability check on FE (useWorkspacePermission) and BE (createWorkspaceScopedHandler). Adding'admin'always satisfies;'member'is satisfied by both roles.useWorkspacePermissionas a DI seam on each page-level component (ChatSkillsBar,ChatModelPicker,Header,AgentsSettingsPage,McpServersPage,ModelsPage) — matches the existingisStandalone?: () => booleanpattern, lets component-level tests assert "denied → affordance hidden" without seeding the DB.add_skillssince they PATCH the sharedskillsrow (pinned_order). Future work could move pinning to a per-user store; for now the BE enforcement covers it.Out of scope (stored-only keys)
The enum has 12 keys; only 8 are enforced today. The rest:
manage_members— legacy; still backs the Members sidebar gate + route guard for the existing add/remove members flows. Effectively superseded byinvite_users/remove_usersonce those are wired.join_workspace— no user-initiated "join" operation exists yet; reserved for a future invite-link / accept-invite flow.invite_users,change_roles,remove_users— Members page already gates onmanage_members+change_rolesdirectly (membership upload handlers hardcodeisWorkspaceAdmin). These keys are stored but not consulted.change_general_settings— workspaces upload handler hardcodesisWorkspaceAdminfor renames/slug/icon. Stored but not consulted.change_permissions— Permissions page itself is hardcoded admin-only viaRequireWorkspaceAdmin(see Notable decisions).delete_workspace— BE rejects all workspace DELETEs (WORKSPACE_DELETE_DISABLED). Stored but inert.Wiring enforcement for any of these is a one-line registry change (workspace-scoped tables) or a
getRequiredRoleForPermission+permissionAllowscall (bespoke handlers). See the project memory note in this repo for the migration pattern.Caveats
setWorkspacePermissionRequiredRole— works on workspaces that pre-date THU-552's seed-defaults hook. If a row is somehow missing, the first write creates it.Test plan
add_agents/add_skills/add_models/add_mcp_serversto Everyone immediately reflects on the corresponding resource page; member can hit the URL → redirected.add_agents, "Add Custom Agent" hidden, row Switch disabled, Trash hidden. Settingadd_agents=membermakes them appear; row PATCH lands on BE. Header agent-selector footer also follows.+, empty-state Add, row Switch, Edit (Pen), Delete (Trash),ModificationIndicatorreset all gated.+, empty-state Add, row Switch + Trash gated.+ Pin a skillhidden when!add_skills; pinned chips still render but Reorder/Unpin are absent from the chip dropdown.Add Modelsfooter hidden in the model picker.Add Agentfooter hidden in the header's agent selector.{deleted_at: <iso>}from a member without remove permission is rejected withINSUFFICIENT_PERMISSION; granting the remove key allows it.deleted_at) for each resource: rejected on!add_*even if the remove key is granted.bun check(FE) +bun test(FE + BE) green.Spec: https://github.com/thunderbird/thunderbolt-spec/pull/12
Addendum: Decision 10, Decision 11, Decision 25.
What's in the PR
17 commits, ordered for review:
1338e4a5setWorkspacePermissionRequiredRoleDAL09c600fdRequireWorkspaceAdminroute guard6bd9b4dcshared/workspaces.tsf23b18d4c845030af54e70b7796ebf660311c4fbeccd544caa9fde05permissionAllows+ BE permission lookup helpers9f1f6afa29f620b2add_agents/remove_agentspermissionsa9d368c2ab2c8a2aadd_skills/remove_skillspermissionsbfb504cbadd_models/remove_modelspermissions26119f09add_mcp_servers/remove_mcp_serverspermissions967f8e96Note
High Risk
Changes authorization on membership/pending-invite uploads and resource writes, and broadens pending-invite sync visibility to all members—mistakes could allow privilege escalation or block legitimate admins.
Overview
Workspace permissions are centralized in
shared/workspaces.ts(keys, roles,permissionAllows) and wired through FEuseWorkspacePermission/ BEcallerSatisfiesPermission, defaulting missing policy rows to admin-only.Backend upload auth moves membership and pending-invite writes from “workspace admin” to
invite_users/change_roles/remove_users, with guards against admin escalation via invites or membership PUT upserts, last-admin protection on PUT, safer promote-on-insert (no role downgrade, delete pending by workspace+email), email validation, and server-truthinvited_by_user_id. Agents, skills, models, and MCP servers use optionaladd_*/remove_*keys; PATCH settingdeleted_atcounts as remove.Sync:
workspace_pending_membershipssyncs to all members (not an admin-only bucket) so permitted non-admins can see invites; writes stay handler-enforced.Frontend adds the Permissions page (admin-only route),
setWorkspacePermissionRequiredRole, and hides add/edit/remove affordances on settings pages and chat composer paths when the user lacks the matching key. Smaller fixes include Members route withoutmanage_membersguard,AppErrorBoundary, mode-picker stale validation, logout double-click guard, andclearAllKeys(serverId)after registry clear.Reviewed by Cursor Bugbot for commit 24cc392. Bugbot is set up for automated code reviews on this repo. Configure here.