import { ReactNode } from 'react'; import { UseQueryResult } from '@tanstack/react-query'; /** * Effective permissions returned from RBAC service */ export interface EffectivePermissions { actorId: string; scopeId: string; /** Role names assigned to the actor */ roles: string[]; /** Detailed permissions by resource type */ permissions: EffectiveResourcePermission[]; /** Flat permission strings for easy matching (e.g., "project:read", "task:*") */ permissionStrings: string[]; /** When this permission set should be refreshed */ expiresAt: string; /** Cache key for debugging */ cacheKey?: string; } export interface EffectiveResourcePermission { resourceType: string; level: 'ADMIN' | 'EDITOR' | 'VIEWER' | 'NONE'; allowedActions: string[]; source: string; } /** * Lightweight role-only result */ export interface MyRolesResult { roles: RoleInfo[]; isAdmin: boolean; isOwner: boolean; } export interface RoleInfo { id: string; name: string; priority: number; } /** * Permission check options */ export interface PermissionCheckOptions { /** Require all permissions (AND logic). Default: false (OR logic) */ requireAll?: boolean; } /** * Permission context value */ export interface PermissionContextValue { /** Current permissions data */ permissions: EffectivePermissions | null; /** Loading state */ isLoading: boolean; /** Error state */ error: Error | null; /** Check if user has a specific permission (supports wildcards) */ hasPermission: (permission: string) => boolean; /** Check multiple permissions */ hasPermissions: (permissions: string[], options?: PermissionCheckOptions) => boolean; /** Check if user has a specific role */ hasRole: (roleName: string) => boolean; /** Check if user has any of the specified roles */ hasAnyRole: (roleNames: string[]) => boolean; /** Check if user has all of the specified roles */ hasAllRoles: (roleNames: string[]) => boolean; /** Refresh permissions manually */ refreshPermissions: () => Promise; /** Invalidate and refetch permissions */ invalidatePermissions: () => Promise; /** Current scope ID */ scopeId: string | null; /** * True when the authenticated actor is a platform super admin and every * check above short-circuits to `true`. Consumers that need to know the * difference between "allowed by role" and "allowed by bypass" (e.g. the * persona-surface role simulator) read this instead of re-deriving it. */ isSuperAdmin: boolean; /** Raw query result for advanced usage */ query: UseQueryResult; } /** * Role identifier for platform super admins. * * This is the server-signed JWT claim emitted by global-auth-svc for the * `super_admin` actor type. It must stay in sync with the backend — if the * identifier ever changes, update both this constant and the auth-svc * contract. */ export declare const SUPER_ADMIN_ROLE: "SUPER_ADMIN"; export declare const SUPER_ADMIN_ACTOR_TYPE: "super_admin"; /** * Match a permission string against a pattern with wildcard support. * * Patterns: * - Exact match: "project:read" matches "project:read" * - Wildcard action: "project:*" matches "project:read", "project:write", etc. * - Global wildcard: "*" matches everything * * @example * matchPermission("project:read", "project:read") // true * matchPermission("project:*", "project:read") // true * matchPermission("project:read", "project:*") // true (user has wildcard) * matchPermission("*", "project:read") // true */ /** * Tracks when the current scope first came back with no access at all, so the * catch-up poll can be bounded. `null` means "not currently empty". */ export type EmptyPermissionsTracker = { scopeId: string | null; at: number; } | null; export interface PermissionsRefetchIntervalInput { data: EffectivePermissions | undefined; scopeId: string | null; tracker: EmptyPermissionsTracker; now: number; refetchInterval: number; emptyPermissionsRefetchInterval: number; emptyPermissionsCatchupWindow: number; } /** * Decide how soon to re-fetch permissions, and carry the empty-since tracker. * * An authenticated actor resolving to zero roles AND zero permissions in their * own scope is a transient state, not a steady one: workspace provisioning is * asynchronous (the RBAC layer clones the role catalog off `workspace.created`, * ~90-130s after signup in production). On the normal interval the UI would sit * with no product nav and blocked pages for up to five minutes after the * backend had already caught up. Poll quickly until access appears, then revert. * * The window is bounded so an actor who genuinely has no access in this scope — * an invited member of someone else's workspace, say — settles back onto the * normal cadence instead of polling forever. */ export declare function resolvePermissionsRefetchInterval(input: PermissionsRefetchIntervalInput): { interval: number; tracker: EmptyPermissionsTracker; }; export declare function matchPermission(userPermission: string, requiredPermission: string): boolean; /** * Check if any user permission matches the required permission */ export declare function hasMatchingPermission(userPermissions: string[], requiredPermission: string): boolean; /** * Determine whether a user should bypass RBAC checks based on their * authenticated role. Exported as a pure function so it can be unit * tested without mounting the full React provider. */ export declare function isSuperAdminBypass(isAuthenticated: boolean, role: string | undefined | null, actorType?: string | undefined | null): boolean; export interface PermissionProviderProps { children: ReactNode; /** * Scope ID for permission fetching. * - For workspace layer: workspaceId * - For global layer: tenantId or billingAccountId */ scopeId: string | null; /** * Gateway to use for fetching permissions. * - 'workspace': Use workspace gateway (wspace-rbac-svc) * - 'global': Use global gateway (global-rbac-svc) * @default 'workspace' */ gateway?: 'workspace' | 'global'; /** * Stale time in milliseconds. Permissions are considered fresh for this duration. * @default 300000 (5 minutes) */ staleTime?: number; /** * Refetch interval in milliseconds. Set to 0 to disable. * @default 300000 (5 minutes) */ refetchInterval?: number; /** * Refetch on window focus if data is stale for this duration (ms). * @default 120000 (2 minutes) */ refetchOnFocusStaleTime?: number; /** * How often to re-poll while the actor resolves to NO roles at all (ms). * * A brand-new workspace is provisioned asynchronously: the tenant layer * creates it, publishes `workspace.created`, and the RBAC layer clones the * role catalog and assigns the owner off that event. Measured in production * that lands ~90-130s after signup. A user who lands on the app inside that * window fetches an empty permission set, and on the normal `refetchInterval` * would keep a stripped-down UI (no product nav, blocked pages) for up to * five minutes with nothing prompting a retry — the backend catches up but * the UI never notices until a manual reload. * * Empty is only ever a transient state for an authenticated actor in their * own scope, so poll quickly until roles appear, then fall back to the normal * interval. Bounded by `emptyPermissionsCatchupWindow` so a genuinely * permissionless actor does not poll forever. * @default 5000 (5 seconds) */ emptyPermissionsRefetchInterval?: number; /** * How long to keep fast-polling an empty permission set before giving up and * reverting to `refetchInterval` (ms). * @default 180000 (3 minutes) */ emptyPermissionsCatchupWindow?: number; /** * Enable debug logging * @default false */ debug?: boolean; } /** * PermissionProvider - Manages permission state for UI blocking * * Features: * - Fetches permissions from RBAC service on mount and scope change * - Caches permissions with React Query * - Auto-refreshes based on expiresAt from server * - Supports wildcard permission matching * - Provides hooks for permission and role checks * * SECURITY NOTE: `hasPermission` / `hasPermissions` / `hasRole` / * `hasAnyRole` / `hasAllRoles` short-circuit to `true` when * `useAuth().user?.role === SUPER_ADMIN_ROLE` / `actorType === "super_admin"`, * matching the platform rule that super_admin actors bypass all RBAC checks. * This is UI * gating only — backend enforcement at the API gateway / RBAC service * is unchanged. The `permissions` context field (raw fetched RBAC * payload) stays `null` for bypassed super admins, so any consumer * that reads `permissions?.permissionStrings` directly instead of * calling `hasPermission()` will still see an empty set. Prefer the * helper methods. * * @example * ```tsx * // In your app root or shell * * * * * // In components * const { hasPermission } = usePermissions(); * if (hasPermission('project:write')) { * // Show edit button * } * ``` */ export declare function PermissionProvider({ children, scopeId, gateway, staleTime, // 5 minutes refetchInterval, // 5 minutes refetchOnFocusStaleTime, // 2 minutes emptyPermissionsRefetchInterval, // 5 seconds emptyPermissionsCatchupWindow, // 3 minutes debug, }: PermissionProviderProps): import("react/jsx-runtime").JSX.Element; /** * Hook to access permission context * * @throws Error if used outside PermissionProvider * * @example * ```tsx * const { hasPermission, hasRole, isLoading } = usePermissions(); * * if (isLoading) return ; * * if (hasPermission('project:write')) { * return ; * } * ``` */ export declare function usePermissions(): PermissionContextValue; /** * Like `usePermissions`, but returns `null` outside a PermissionProvider * instead of throwing. For chrome components (e.g. ProductSideNav) that must * keep working in shells that haven't mounted RBAC yet. */ export declare function useSafePermissions(): PermissionContextValue | null; /** * Hook to check a single permission * * @example * ```tsx * const canEdit = usePermission('project:write'); * const canAdmin = usePermission('project:*'); * ``` */ export declare function usePermission(permission: string): boolean; /** * Hook to check a single role * * @example * ```tsx * const isAdmin = useRole('Admin'); * const isOwner = useRole('Owner'); * ``` */ export declare function useRole(roleName: string): boolean; /** * Hook to get permission loading state */ export declare function usePermissionLoading(): boolean; //# sourceMappingURL=PermissionProvider.d.ts.map