@jmruthers/pace-core 0.5.109 → 0.5.111

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (240) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/{AuthService-DrHrvXNZ.d.ts → AuthService-CVgsgtaZ.d.ts} +8 -0
  3. package/dist/{DataTable-5HITILXS.js → DataTable-5W2HVLLV.js} +8 -8
  4. package/dist/{UnifiedAuthProvider-A7I23UCN.js → UnifiedAuthProvider-LUM3QLS5.js} +3 -3
  5. package/dist/{api-5I3E47G2.js → api-SIZPFBFX.js} +5 -3
  6. package/dist/{audit-65VNHEV2.js → audit-5JI5T3SL.js} +2 -2
  7. package/dist/{chunk-P72NKAT5.js → chunk-2BIDKXQU.js} +157 -120
  8. package/dist/chunk-2BIDKXQU.js.map +1 -0
  9. package/dist/{chunk-S4D3Z723.js → chunk-ACYQNYHB.js} +7 -7
  10. package/dist/{chunk-D6MEKC27.js → chunk-EFVQBYFN.js} +2 -2
  11. package/dist/{chunk-EZ64QG2I.js → chunk-I5YM5BGS.js} +2 -2
  12. package/dist/{chunk-Q7APDV6H.js → chunk-IWJYNWXN.js} +13 -5
  13. package/dist/chunk-IWJYNWXN.js.map +1 -0
  14. package/dist/{chunk-YFMENCR4.js → chunk-JE2GFA3O.js} +3 -3
  15. package/dist/{chunk-AUXS7XSO.js → chunk-MW73E7SP.js} +35 -11
  16. package/dist/chunk-MW73E7SP.js.map +1 -0
  17. package/dist/{chunk-F6TSYCKP.js → chunk-PXXS26G5.js} +68 -29
  18. package/dist/chunk-PXXS26G5.js.map +1 -0
  19. package/dist/{chunk-UW2DE6JX.js → chunk-TD4BXGPE.js} +4 -4
  20. package/dist/{chunk-EYSXQ756.js → chunk-TDFBX7KJ.js} +2 -2
  21. package/dist/{chunk-WWNOVFDC.js → chunk-UGVU7L7N.js} +52 -90
  22. package/dist/chunk-UGVU7L7N.js.map +1 -0
  23. package/dist/{chunk-2W4WKJVF.js → chunk-X7SPKHYZ.js} +290 -255
  24. package/dist/chunk-X7SPKHYZ.js.map +1 -0
  25. package/dist/{chunk-3TKTL5AZ.js → chunk-ZL45MG76.js} +60 -60
  26. package/dist/chunk-ZL45MG76.js.map +1 -0
  27. package/dist/components.js +10 -10
  28. package/dist/hooks.d.ts +11 -1
  29. package/dist/hooks.js +9 -7
  30. package/dist/hooks.js.map +1 -1
  31. package/dist/index.d.ts +2 -2
  32. package/dist/index.js +13 -13
  33. package/dist/providers.d.ts +2 -2
  34. package/dist/providers.js +2 -2
  35. package/dist/rbac/index.d.ts +46 -29
  36. package/dist/rbac/index.js +9 -9
  37. package/dist/utils.js +1 -1
  38. package/docs/api/classes/ColumnFactory.md +1 -1
  39. package/docs/api/classes/ErrorBoundary.md +1 -1
  40. package/docs/api/classes/InvalidScopeError.md +4 -4
  41. package/docs/api/classes/MissingUserContextError.md +4 -4
  42. package/docs/api/classes/OrganisationContextRequiredError.md +4 -4
  43. package/docs/api/classes/PermissionDeniedError.md +4 -4
  44. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  45. package/docs/api/classes/RBACAuditManager.md +8 -8
  46. package/docs/api/classes/RBACCache.md +8 -8
  47. package/docs/api/classes/RBACEngine.md +9 -8
  48. package/docs/api/classes/RBACError.md +4 -4
  49. package/docs/api/classes/RBACNotInitializedError.md +4 -4
  50. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  51. package/docs/api/classes/StorageUtils.md +1 -1
  52. package/docs/api/enums/FileCategory.md +1 -1
  53. package/docs/api/interfaces/AggregateConfig.md +1 -1
  54. package/docs/api/interfaces/ButtonProps.md +1 -1
  55. package/docs/api/interfaces/CardProps.md +1 -1
  56. package/docs/api/interfaces/ColorPalette.md +1 -1
  57. package/docs/api/interfaces/ColorShade.md +1 -1
  58. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  59. package/docs/api/interfaces/DataRecord.md +1 -1
  60. package/docs/api/interfaces/DataTableAction.md +1 -1
  61. package/docs/api/interfaces/DataTableColumn.md +1 -1
  62. package/docs/api/interfaces/DataTableProps.md +1 -1
  63. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  64. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  65. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  66. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  67. package/docs/api/interfaces/FileMetadata.md +1 -1
  68. package/docs/api/interfaces/FileReference.md +1 -1
  69. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  70. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  71. package/docs/api/interfaces/FileUploadProps.md +1 -1
  72. package/docs/api/interfaces/FooterProps.md +1 -1
  73. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  74. package/docs/api/interfaces/InputProps.md +1 -1
  75. package/docs/api/interfaces/LabelProps.md +1 -1
  76. package/docs/api/interfaces/LoginFormProps.md +1 -1
  77. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  78. package/docs/api/interfaces/NavigationContextType.md +1 -1
  79. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  80. package/docs/api/interfaces/NavigationItem.md +1 -1
  81. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  82. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  83. package/docs/api/interfaces/Organisation.md +1 -1
  84. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  85. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  86. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  87. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  88. package/docs/api/interfaces/PaceAppLayoutProps.md +27 -27
  89. package/docs/api/interfaces/PaceLoginPageProps.md +4 -4
  90. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  91. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  92. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  93. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  94. package/docs/api/interfaces/PaletteData.md +1 -1
  95. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  96. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  97. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  98. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  99. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  100. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  101. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  102. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  103. package/docs/api/interfaces/RBACConfig.md +19 -8
  104. package/docs/api/interfaces/RBACLogger.md +5 -5
  105. package/docs/api/interfaces/RoleBasedRouterContextType.md +8 -8
  106. package/docs/api/interfaces/RoleBasedRouterProps.md +10 -10
  107. package/docs/api/interfaces/RouteAccessRecord.md +10 -10
  108. package/docs/api/interfaces/RouteConfig.md +19 -6
  109. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  110. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  111. package/docs/api/interfaces/StorageConfig.md +1 -1
  112. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  113. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  114. package/docs/api/interfaces/StorageListOptions.md +1 -1
  115. package/docs/api/interfaces/StorageListResult.md +1 -1
  116. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  117. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  118. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  119. package/docs/api/interfaces/StyleImport.md +1 -1
  120. package/docs/api/interfaces/SwitchProps.md +1 -1
  121. package/docs/api/interfaces/ToastActionElement.md +1 -1
  122. package/docs/api/interfaces/ToastProps.md +1 -1
  123. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  124. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  125. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  126. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  127. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  128. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  129. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  130. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  131. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  132. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  133. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  134. package/docs/api/interfaces/UserEventAccess.md +1 -1
  135. package/docs/api/interfaces/UserMenuProps.md +1 -1
  136. package/docs/api/interfaces/UserProfile.md +1 -1
  137. package/docs/api/modules.md +44 -43
  138. package/docs/api-reference/hooks.md +8 -4
  139. package/docs/architecture/rpc-function-standards.md +3 -1
  140. package/docs/best-practices/common-patterns.md +3 -3
  141. package/docs/best-practices/deployment.md +10 -4
  142. package/docs/best-practices/performance.md +11 -3
  143. package/docs/core-concepts/organisations.md +8 -8
  144. package/docs/core-concepts/permissions.md +133 -72
  145. package/docs/documentation-index.md +0 -2
  146. package/docs/migration/rbac-migration.md +65 -66
  147. package/docs/rbac/README.md +114 -38
  148. package/docs/rbac/advanced-patterns.md +15 -22
  149. package/docs/rbac/api-reference.md +63 -16
  150. package/docs/rbac/examples.md +12 -12
  151. package/docs/rbac/getting-started.md +19 -19
  152. package/docs/rbac/quick-start.md +110 -35
  153. package/docs/rbac/troubleshooting.md +127 -3
  154. package/package.json +1 -1
  155. package/src/components/DataTable/components/__tests__/ActionButtons.test.tsx +913 -0
  156. package/src/components/DataTable/components/__tests__/ColumnFilter.test.tsx +609 -0
  157. package/src/components/DataTable/components/__tests__/EmptyState.test.tsx +434 -0
  158. package/src/components/DataTable/components/__tests__/LoadingState.test.tsx +120 -0
  159. package/src/components/DataTable/components/__tests__/PaginationControls.test.tsx +519 -0
  160. package/src/components/DataTable/examples/__tests__/HierarchicalActionsExample.test.tsx +316 -0
  161. package/src/components/DataTable/examples/__tests__/InitialPageSizeExample.test.tsx +211 -0
  162. package/src/components/FileUpload/FileUpload.tsx +2 -8
  163. package/src/components/NavigationMenu/NavigationMenu.test.tsx +38 -4
  164. package/src/components/NavigationMenu/NavigationMenu.tsx +71 -6
  165. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +193 -63
  166. package/src/components/PaceAppLayout/PaceAppLayout.tsx +102 -135
  167. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.accessibility.test.tsx +41 -2
  168. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.integration.test.tsx +61 -6
  169. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +71 -21
  170. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.security.test.tsx +113 -41
  171. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.unit.test.tsx +155 -45
  172. package/src/components/PaceLoginPage/PaceLoginPage.tsx +30 -1
  173. package/src/hooks/__tests__/usePermissionCache.simple.test.ts +63 -5
  174. package/src/hooks/__tests__/usePermissionCache.unit.test.ts +156 -72
  175. package/src/hooks/__tests__/useRBAC.unit.test.ts +4 -38
  176. package/src/hooks/index.ts +1 -1
  177. package/src/hooks/useFileDisplay.ts +51 -0
  178. package/src/hooks/usePermissionCache.test.ts +112 -68
  179. package/src/hooks/usePermissionCache.ts +55 -15
  180. package/src/rbac/README.md +81 -39
  181. package/src/rbac/__tests__/adapters.comprehensive.test.tsx +3 -3
  182. package/src/rbac/__tests__/engine.comprehensive.test.ts +15 -6
  183. package/src/rbac/__tests__/rbac-core.test.tsx +1 -1
  184. package/src/rbac/__tests__/rbac-engine-core-logic.test.ts +57 -4
  185. package/src/rbac/__tests__/rbac-engine-simplified.test.ts +3 -2
  186. package/src/rbac/adapters.tsx +4 -4
  187. package/src/rbac/api.test.ts +39 -15
  188. package/src/rbac/api.ts +27 -9
  189. package/src/rbac/audit.test.ts +2 -2
  190. package/src/rbac/audit.ts +14 -5
  191. package/src/rbac/cache.test.ts +12 -0
  192. package/src/rbac/cache.ts +29 -9
  193. package/src/rbac/components/EnhancedNavigationMenu.test.tsx +1 -1
  194. package/src/rbac/components/NavigationGuard.tsx +14 -14
  195. package/src/rbac/components/NavigationProvider.test.tsx +1 -1
  196. package/src/rbac/components/PagePermissionGuard.tsx +22 -38
  197. package/src/rbac/components/PagePermissionProvider.test.tsx +1 -1
  198. package/src/rbac/components/PermissionEnforcer.tsx +19 -15
  199. package/src/rbac/components/RoleBasedRouter.tsx +16 -9
  200. package/src/rbac/components/__tests__/NavigationGuard.test.tsx +123 -107
  201. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +2 -2
  202. package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +121 -103
  203. package/src/rbac/config.ts +2 -0
  204. package/src/rbac/docs/event-based-apps.md +6 -6
  205. package/src/rbac/engine.ts +27 -7
  206. package/src/rbac/hooks/useCan.test.ts +29 -2
  207. package/src/rbac/hooks/usePermissions.test.ts +25 -25
  208. package/src/rbac/hooks/usePermissions.ts +47 -23
  209. package/src/rbac/hooks/useRBAC.simple.test.ts +1 -8
  210. package/src/rbac/hooks/useRBAC.test.ts +3 -40
  211. package/src/rbac/hooks/useRBAC.ts +0 -55
  212. package/src/rbac/hooks/useResolvedScope.ts +23 -31
  213. package/src/rbac/permissions.test.ts +11 -7
  214. package/src/rbac/security.test.ts +2 -2
  215. package/src/rbac/security.ts +23 -8
  216. package/src/rbac/types.test.ts +2 -2
  217. package/src/rbac/types.ts +1 -2
  218. package/src/services/EventService.ts +41 -13
  219. package/src/services/__tests__/EventService.test.ts +25 -4
  220. package/src/services/interfaces/IEventService.ts +1 -0
  221. package/src/utils/file-reference.ts +9 -0
  222. package/dist/chunk-2W4WKJVF.js.map +0 -1
  223. package/dist/chunk-3TKTL5AZ.js.map +0 -1
  224. package/dist/chunk-AUXS7XSO.js.map +0 -1
  225. package/dist/chunk-F6TSYCKP.js.map +0 -1
  226. package/dist/chunk-P72NKAT5.js.map +0 -1
  227. package/dist/chunk-Q7APDV6H.js.map +0 -1
  228. package/dist/chunk-WWNOVFDC.js.map +0 -1
  229. package/docs/rbac/breaking-changes-v3.md +0 -222
  230. package/docs/rbac/migration-guide.md +0 -260
  231. /package/dist/{DataTable-5HITILXS.js.map → DataTable-5W2HVLLV.js.map} +0 -0
  232. /package/dist/{UnifiedAuthProvider-A7I23UCN.js.map → UnifiedAuthProvider-LUM3QLS5.js.map} +0 -0
  233. /package/dist/{api-5I3E47G2.js.map → api-SIZPFBFX.js.map} +0 -0
  234. /package/dist/{audit-65VNHEV2.js.map → audit-5JI5T3SL.js.map} +0 -0
  235. /package/dist/{chunk-S4D3Z723.js.map → chunk-ACYQNYHB.js.map} +0 -0
  236. /package/dist/{chunk-D6MEKC27.js.map → chunk-EFVQBYFN.js.map} +0 -0
  237. /package/dist/{chunk-EZ64QG2I.js.map → chunk-I5YM5BGS.js.map} +0 -0
  238. /package/dist/{chunk-YFMENCR4.js.map → chunk-JE2GFA3O.js.map} +0 -0
  239. /package/dist/{chunk-UW2DE6JX.js.map → chunk-TD4BXGPE.js.map} +0 -0
  240. /package/dist/{chunk-EYSXQ756.js.map → chunk-TDFBX7KJ.js.map} +0 -0
@@ -11,12 +11,12 @@ import { render, screen, waitFor } from '@testing-library/react';
11
11
  import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
12
12
  import { ReactNode } from 'react';
13
13
  import { PermissionEnforcer } from '../PermissionEnforcer';
14
- import { useCan } from '../../hooks';
14
+ import { useMultiplePermissions } from '../../hooks/usePermissions';
15
15
  import { useUnifiedAuth } from '../../../providers/UnifiedAuthProvider';
16
16
 
17
17
  // Mock the RBAC hooks
18
- vi.mock('../../hooks', () => ({
19
- useCan: vi.fn()
18
+ vi.mock('../../hooks/usePermissions', () => ({
19
+ useMultiplePermissions: vi.fn()
20
20
  }));
21
21
 
22
22
  // Mock the auth provider
@@ -43,7 +43,7 @@ const mockScope = {
43
43
  appId: 'app-123'
44
44
  };
45
45
 
46
- const mockPermissions = ['read:events', 'manage:events'] as const;
46
+ const mockPermissions = ['read:events', 'update:events'] as const;
47
47
  const mockOperation = 'event-management';
48
48
 
49
49
  // Test component
@@ -60,7 +60,7 @@ const TestLoading = () => (
60
60
  );
61
61
 
62
62
  describe('PermissionEnforcer Component', () => {
63
- const mockUseCan = vi.mocked(useCan);
63
+ const mockUseMultiplePermissions = vi.mocked(useMultiplePermissions);
64
64
  const mockUseUnifiedAuth = vi.mocked(useUnifiedAuth);
65
65
  const mockCreateScopeFromEvent = vi.mocked(createScopeFromEvent);
66
66
 
@@ -75,19 +75,24 @@ describe('PermissionEnforcer Component', () => {
75
75
  supabase: {} as any
76
76
  });
77
77
 
78
- mockUseCan.mockReturnValue({
79
- can: true,
78
+ mockUseMultiplePermissions.mockReturnValue({
79
+ results: {
80
+ 'read:events': true,
81
+ 'update:events': true
82
+ } as Record<string, boolean>,
80
83
  isLoading: false,
81
- error: null
84
+ error: null,
85
+ refetch: vi.fn()
82
86
  });
83
87
  });
84
88
 
85
89
  describe('Rendering', () => {
86
90
  it('renders children when permission is granted', async () => {
87
- mockUseCan.mockReturnValue({
88
- can: true,
91
+ mockUseMultiplePermissions.mockReturnValue({
92
+ results: { 'read:events': true, 'update:events': true } as Record<string, boolean>,
89
93
  isLoading: false,
90
- error: null
94
+ error: null,
95
+ refetch: vi.fn()
91
96
  });
92
97
 
93
98
  render(
@@ -106,10 +111,11 @@ describe('PermissionEnforcer Component', () => {
106
111
  });
107
112
 
108
113
  it('renders fallback when permission is denied', async () => {
109
- mockUseCan.mockReturnValue({
110
- can: false,
114
+ mockUseMultiplePermissions.mockReturnValue({
115
+ results: { 'read:events': false, 'update:events': false } as Record<string, boolean>,
111
116
  isLoading: false,
112
- error: null
117
+ error: null,
118
+ refetch: vi.fn()
113
119
  });
114
120
 
115
121
  render(
@@ -128,11 +134,12 @@ describe('PermissionEnforcer Component', () => {
128
134
  }, { interval: 10 });
129
135
  });
130
136
 
131
- it('shows loading state during permission check', () => {
132
- mockUseCan.mockReturnValue({
133
- can: false,
137
+ it('shows loading state during permission check', async () => {
138
+ mockUseMultiplePermissions.mockReturnValue({
139
+ results: {} as Record<string, boolean>,
134
140
  isLoading: true,
135
- error: null
141
+ error: null,
142
+ refetch: vi.fn()
136
143
  });
137
144
 
138
145
  render(
@@ -150,10 +157,11 @@ describe('PermissionEnforcer Component', () => {
150
157
  });
151
158
 
152
159
  it('uses default fallback when none provided', async () => {
153
- mockUseCan.mockReturnValue({
154
- can: false,
160
+ mockUseMultiplePermissions.mockReturnValue({
161
+ results: { 'read:events': false, 'update:events': false } as Record<string, boolean>,
155
162
  isLoading: false,
156
- error: null
163
+ error: null,
164
+ refetch: vi.fn()
157
165
  });
158
166
 
159
167
  render(
@@ -172,10 +180,11 @@ describe('PermissionEnforcer Component', () => {
172
180
  });
173
181
 
174
182
  it('uses default loading when none provided', () => {
175
- mockUseCan.mockReturnValue({
176
- can: false,
183
+ mockUseMultiplePermissions.mockReturnValue({
184
+ results: {} as Record<string, boolean>,
177
185
  isLoading: true,
178
- error: null
186
+ error: null,
187
+ refetch: vi.fn()
179
188
  });
180
189
 
181
190
  render(
@@ -195,10 +204,11 @@ describe('PermissionEnforcer Component', () => {
195
204
  it('enforces single permission correctly', async () => {
196
205
  const singlePermission = ['read:events'] as const;
197
206
 
198
- mockUseCan.mockReturnValue({
199
- can: true,
207
+ mockUseMultiplePermissions.mockReturnValue({
208
+ results: { 'read:events': true } as Record<string, boolean>,
200
209
  isLoading: false,
201
- error: null
210
+ error: null,
211
+ refetch: vi.fn()
202
212
  });
203
213
 
204
214
  render(
@@ -214,23 +224,23 @@ describe('PermissionEnforcer Component', () => {
214
224
  expect(screen.getByTestId('test-component')).toBeInTheDocument();
215
225
  }, { interval: 10 });
216
226
 
217
- expect(mockUseCan).toHaveBeenCalledWith(
227
+ expect(mockUseMultiplePermissions).toHaveBeenCalledWith(
218
228
  'user-123',
219
229
  expect.objectContaining({
220
230
  organisationId: 'org-123',
221
231
  eventId: 'event-123'
222
232
  }),
223
- 'read:events',
224
- undefined,
233
+ ['read:events'], // singlePermission only includes read:events
225
234
  true
226
235
  );
227
236
  });
228
237
 
229
- it('enforces multiple permissions with AND logic', async () => {
230
- mockUseCan.mockReturnValue({
231
- can: true,
238
+ it('enforces multiple permissions with AND logic (requireAll=true)', async () => {
239
+ mockUseMultiplePermissions.mockReturnValue({
240
+ results: { 'read:events': true, 'update:events': true } as Record<string, boolean>,
232
241
  isLoading: false,
233
- error: null
242
+ error: null,
243
+ refetch: vi.fn()
234
244
  });
235
245
 
236
246
  render(
@@ -247,25 +257,25 @@ describe('PermissionEnforcer Component', () => {
247
257
  expect(screen.getByTestId('test-component')).toBeInTheDocument();
248
258
  }, { interval: 10 });
249
259
 
250
- // Should check the first permission as representative
251
- expect(mockUseCan).toHaveBeenCalledWith(
260
+ // Should check all permissions when requireAll=true
261
+ expect(mockUseMultiplePermissions).toHaveBeenCalledWith(
252
262
  'user-123',
253
263
  expect.objectContaining({
254
264
  organisationId: 'org-123',
255
265
  eventId: 'event-123'
256
266
  }),
257
- 'read:events',
258
- undefined,
267
+ ['read:events', 'update:events'],
259
268
  true
260
269
  );
261
270
  });
262
271
 
263
272
  it('handles permission checking errors gracefully', async () => {
264
273
  const error = new Error('Permission check failed');
265
- mockUseCan.mockReturnValue({
266
- can: false,
274
+ mockUseMultiplePermissions.mockReturnValue({
275
+ results: {} as Record<string, boolean>,
267
276
  isLoading: false,
268
- error
277
+ error,
278
+ refetch: vi.fn()
269
279
  });
270
280
 
271
281
  render(
@@ -284,10 +294,11 @@ describe('PermissionEnforcer Component', () => {
284
294
  });
285
295
 
286
296
  it('handles empty permissions array', async () => {
287
- mockUseCan.mockReturnValue({
288
- can: true,
297
+ mockUseMultiplePermissions.mockReturnValue({
298
+ results: { 'read:events': true } as Record<string, boolean>,
289
299
  isLoading: false,
290
- error: null
300
+ error: null,
301
+ refetch: vi.fn()
291
302
  });
292
303
 
293
304
  render(
@@ -313,10 +324,11 @@ describe('PermissionEnforcer Component', () => {
313
324
  appId: 'custom-app'
314
325
  };
315
326
 
316
- mockUseCan.mockReturnValue({
317
- can: true,
327
+ mockUseMultiplePermissions.mockReturnValue({
328
+ results: { 'read:events': true } as Record<string, boolean>,
318
329
  isLoading: false,
319
- error: null
330
+ error: null,
331
+ refetch: vi.fn()
320
332
  });
321
333
 
322
334
  render(
@@ -333,20 +345,20 @@ describe('PermissionEnforcer Component', () => {
333
345
  expect(screen.getByTestId('test-component')).toBeInTheDocument();
334
346
  }, { interval: 10 });
335
347
 
336
- expect(mockUseCan).toHaveBeenCalledWith(
348
+ expect(mockUseMultiplePermissions).toHaveBeenCalledWith(
337
349
  'user-123',
338
350
  customScope,
339
- 'read:events',
340
- undefined,
351
+ ['read:events', 'update:events'], // mockPermissions includes both
341
352
  true
342
353
  );
343
354
  });
344
355
 
345
356
  it('resolves scope from organisation and event context', async () => {
346
- mockUseCan.mockReturnValue({
347
- can: true,
357
+ mockUseMultiplePermissions.mockReturnValue({
358
+ results: { 'read:events': true } as Record<string, boolean>,
348
359
  isLoading: false,
349
- error: null
360
+ error: null,
361
+ refetch: vi.fn()
350
362
  });
351
363
 
352
364
  render(
@@ -362,14 +374,13 @@ describe('PermissionEnforcer Component', () => {
362
374
  expect(screen.getByTestId('test-component')).toBeInTheDocument();
363
375
  }, { interval: 10 });
364
376
 
365
- expect(mockUseCan).toHaveBeenCalledWith(
377
+ expect(mockUseMultiplePermissions).toHaveBeenCalledWith(
366
378
  'user-123',
367
379
  expect.objectContaining({
368
380
  organisationId: 'org-123',
369
381
  eventId: 'event-123'
370
382
  }),
371
- 'read:events',
372
- undefined,
383
+ ['read:events', 'update:events'], // mockPermissions includes both
373
384
  true
374
385
  );
375
386
  });
@@ -382,10 +393,11 @@ describe('PermissionEnforcer Component', () => {
382
393
  supabase: {} as any
383
394
  });
384
395
 
385
- mockUseCan.mockReturnValue({
386
- can: true,
396
+ mockUseMultiplePermissions.mockReturnValue({
397
+ results: { 'read:events': true } as Record<string, boolean>,
387
398
  isLoading: false,
388
- error: null
399
+ error: null,
400
+ refetch: vi.fn()
389
401
  });
390
402
 
391
403
  render(
@@ -401,14 +413,13 @@ describe('PermissionEnforcer Component', () => {
401
413
  expect(screen.getByTestId('test-component')).toBeInTheDocument();
402
414
  }, { interval: 10 });
403
415
 
404
- expect(mockUseCan).toHaveBeenCalledWith(
416
+ expect(mockUseMultiplePermissions).toHaveBeenCalledWith(
405
417
  'user-123',
406
418
  expect.objectContaining({
407
419
  organisationId: 'org-123',
408
420
  eventId: undefined
409
421
  }),
410
- 'read:events',
411
- undefined,
422
+ ['read:events', 'update:events'], // mockPermissions includes both
412
423
  true
413
424
  );
414
425
  });
@@ -427,10 +438,11 @@ describe('PermissionEnforcer Component', () => {
427
438
  appId: 'resolved-app'
428
439
  });
429
440
 
430
- mockUseCan.mockReturnValue({
431
- can: true,
441
+ mockUseMultiplePermissions.mockReturnValue({
442
+ results: { 'read:events': true } as Record<string, boolean>,
432
443
  isLoading: false,
433
- error: null
444
+ error: null,
445
+ refetch: vi.fn()
434
446
  });
435
447
 
436
448
  render(
@@ -447,14 +459,13 @@ describe('PermissionEnforcer Component', () => {
447
459
  }, { interval: 10 });
448
460
 
449
461
  expect(mockCreateScopeFromEvent).toHaveBeenCalledWith({}, 'event-123');
450
- expect(mockUseCan).toHaveBeenCalledWith(
462
+ expect(mockUseMultiplePermissions).toHaveBeenCalledWith(
451
463
  'user-123',
452
464
  expect.objectContaining({
453
465
  organisationId: 'resolved-org',
454
466
  eventId: 'event-123'
455
467
  }),
456
- 'read:events',
457
- undefined,
468
+ ['read:events', 'update:events'], // mockPermissions includes both
458
469
  true
459
470
  );
460
471
  });
@@ -513,10 +524,11 @@ describe('PermissionEnforcer Component', () => {
513
524
  it('prevents bypassing in strict mode', async () => {
514
525
  const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
515
526
 
516
- mockUseCan.mockReturnValue({
517
- can: false,
527
+ mockUseMultiplePermissions.mockReturnValue({
528
+ results: { 'read:events': false, 'update:events': false } as Record<string, boolean>,
518
529
  isLoading: false,
519
- error: null
530
+ error: null,
531
+ refetch: vi.fn()
520
532
  });
521
533
 
522
534
  render(
@@ -549,10 +561,11 @@ describe('PermissionEnforcer Component', () => {
549
561
  it('logs security violations for audit', async () => {
550
562
  const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
551
563
 
552
- mockUseCan.mockReturnValue({
553
- can: false,
564
+ mockUseMultiplePermissions.mockReturnValue({
565
+ results: { 'read:events': false, 'update:events': false } as Record<string, boolean>,
554
566
  isLoading: false,
555
- error: null
567
+ error: null,
568
+ refetch: vi.fn()
556
569
  });
557
570
 
558
571
  render(
@@ -586,10 +599,11 @@ describe('PermissionEnforcer Component', () => {
586
599
  it('calls onDenied callback when access is denied', async () => {
587
600
  const onDeniedSpy = vi.fn();
588
601
 
589
- mockUseCan.mockReturnValue({
590
- can: false,
602
+ mockUseMultiplePermissions.mockReturnValue({
603
+ results: { 'read:events': false, 'update:events': false } as Record<string, boolean>,
591
604
  isLoading: false,
592
- error: null
605
+ error: null,
606
+ refetch: vi.fn()
593
607
  });
594
608
 
595
609
  render(
@@ -613,10 +627,11 @@ describe('PermissionEnforcer Component', () => {
613
627
  it('does not call onDenied when access is granted', async () => {
614
628
  const onDeniedSpy = vi.fn();
615
629
 
616
- mockUseCan.mockReturnValue({
617
- can: true,
630
+ mockUseMultiplePermissions.mockReturnValue({
631
+ results: { 'read:events': true } as Record<string, boolean>,
618
632
  isLoading: false,
619
- error: null
633
+ error: null,
634
+ refetch: vi.fn()
620
635
  });
621
636
 
622
637
  render(
@@ -641,10 +656,11 @@ describe('PermissionEnforcer Component', () => {
641
656
  it('respects strictMode setting', async () => {
642
657
  const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
643
658
 
644
- mockUseCan.mockReturnValue({
645
- can: false,
659
+ mockUseMultiplePermissions.mockReturnValue({
660
+ results: { 'read:events': false, 'update:events': false } as Record<string, boolean>,
646
661
  isLoading: false,
647
- error: null
662
+ error: null,
663
+ refetch: vi.fn()
648
664
  });
649
665
 
650
666
  render(
@@ -672,10 +688,11 @@ describe('PermissionEnforcer Component', () => {
672
688
  it('respects auditLog setting', async () => {
673
689
  const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
674
690
 
675
- mockUseCan.mockReturnValue({
676
- can: false,
691
+ mockUseMultiplePermissions.mockReturnValue({
692
+ results: { 'read:events': false, 'update:events': false } as Record<string, boolean>,
677
693
  isLoading: false,
678
- error: null
694
+ error: null,
695
+ refetch: vi.fn()
679
696
  });
680
697
 
681
698
  render(
@@ -700,11 +717,12 @@ describe('PermissionEnforcer Component', () => {
700
717
  consoleSpy.mockRestore();
701
718
  });
702
719
 
703
- it('respects requireAll setting', async () => {
704
- mockUseCan.mockReturnValue({
705
- can: true,
720
+ it('respects requireAll=false (any permission granted)', async () => {
721
+ mockUseMultiplePermissions.mockReturnValue({
722
+ results: { 'read:events': true, 'update:events': false } as Record<string, boolean>,
706
723
  isLoading: false,
707
- error: null
724
+ error: null,
725
+ refetch: vi.fn()
708
726
  });
709
727
 
710
728
  render(
@@ -721,15 +739,14 @@ describe('PermissionEnforcer Component', () => {
721
739
  expect(screen.getByTestId('test-component')).toBeInTheDocument();
722
740
  }, { interval: 10 });
723
741
 
724
- // Should still check the first permission as representative
725
- expect(mockUseCan).toHaveBeenCalledWith(
742
+ // Should check all permissions and allow access if any is granted
743
+ expect(mockUseMultiplePermissions).toHaveBeenCalledWith(
726
744
  'user-123',
727
745
  expect.objectContaining({
728
746
  organisationId: 'org-123',
729
747
  eventId: 'event-123'
730
748
  }),
731
- 'read:events',
732
- undefined,
749
+ ['read:events', 'update:events'],
733
750
  true
734
751
  );
735
752
  });
@@ -744,10 +761,11 @@ describe('PermissionEnforcer Component', () => {
744
761
  supabase: {} as any
745
762
  });
746
763
 
747
- mockUseCan.mockReturnValue({
748
- can: false,
764
+ mockUseMultiplePermissions.mockReturnValue({
765
+ results: { 'read:events': false, 'update:events': false } as Record<string, boolean>,
749
766
  isLoading: false,
750
- error: null
767
+ error: null,
768
+ refetch: vi.fn()
751
769
  });
752
770
 
753
771
  render(
@@ -764,24 +782,24 @@ describe('PermissionEnforcer Component', () => {
764
782
  expect(screen.getByTestId('test-fallback')).toBeInTheDocument();
765
783
  }, { interval: 10 });
766
784
 
767
- expect(mockUseCan).toHaveBeenCalledWith(
785
+ expect(mockUseMultiplePermissions).toHaveBeenCalledWith(
768
786
  '',
769
787
  expect.objectContaining({
770
788
  organisationId: 'org-123',
771
789
  eventId: 'event-123'
772
790
  }),
773
- 'read:events',
774
- undefined,
791
+ ['read:events', 'update:events'], // mockPermissions includes both
775
792
  true
776
793
  );
777
794
  });
778
795
 
779
796
  it('handles permission check errors', async () => {
780
797
  const error = new Error('Database connection failed');
781
- mockUseCan.mockReturnValue({
782
- can: false,
798
+ mockUseMultiplePermissions.mockReturnValue({
799
+ results: {} as Record<string, boolean>,
783
800
  isLoading: false,
784
- error
801
+ error,
802
+ refetch: vi.fn()
785
803
  });
786
804
 
787
805
  render(
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { SupabaseClient } from '@supabase/supabase-js';
11
11
  import { Database } from '../types/database';
12
+ import { RBACSecurityConfig } from './security';
12
13
 
13
14
  export type LogLevel = 'error' | 'warn' | 'info' | 'debug';
14
15
 
@@ -26,6 +27,7 @@ export interface RBACConfig {
26
27
  enabled?: boolean;
27
28
  logLevel?: LogLevel;
28
29
  };
30
+ security?: Partial<RBACSecurityConfig>;
29
31
  }
30
32
 
31
33
  export interface RBACLogger {
@@ -40,7 +40,7 @@ import { PermissionEnforcer } from '@jmruthers/pace-core/rbac';
40
40
  function EventDashboard() {
41
41
  return (
42
42
  <PermissionEnforcer
43
- permissions={['read:events', 'manage:participants']}
43
+ permissions={['read:events', 'update:participants']}
44
44
  operation="dashboard"
45
45
  >
46
46
  <div>Event Dashboard Content</div>
@@ -84,7 +84,7 @@ const navigationItems = [
84
84
  id: 'settings',
85
85
  name: 'Settings',
86
86
  path: '/event/settings',
87
- permissions: ['manage:events']
87
+ permissions: ['update:events']
88
88
  }
89
89
  ];
90
90
 
@@ -118,7 +118,7 @@ function EventComponent() {
118
118
  const { can: canManageEvents } = useCan(
119
119
  userId,
120
120
  { eventId: selectedEventId },
121
- 'manage:events'
121
+ 'update:events'
122
122
  );
123
123
 
124
124
  return (
@@ -178,19 +178,19 @@ These permissions are specific to an event and app combination:
178
178
  ```typescript
179
179
  const EVENT_APP_PERMISSIONS = {
180
180
  // Event management
181
- MANAGE_EVENT: 'manage:event',
181
+ MANAGE_EVENT: 'update:event',
182
182
  READ_EVENT: 'read:event',
183
183
  UPDATE_EVENT: 'update:event',
184
184
 
185
185
  // Participant management
186
- MANAGE_PARTICIPANTS: 'manage:participants',
186
+ MANAGE_PARTICIPANTS: 'update:participants',
187
187
  READ_PARTICIPANTS: 'read:participants',
188
188
  CREATE_PARTICIPANTS: 'create:participants',
189
189
  UPDATE_PARTICIPANTS: 'update:participants',
190
190
  DELETE_PARTICIPANTS: 'delete:participants',
191
191
 
192
192
  // App-specific permissions
193
- MANAGE_APP: 'manage:app',
193
+ MANAGE_APP: 'update:app',
194
194
  READ_APP: 'read:app',
195
195
  UPDATE_APP: 'update:app',
196
196
  };
@@ -36,7 +36,8 @@ import {
36
36
  RBACSecurityValidator,
37
37
  RBACSecurityMiddleware,
38
38
  SecurityContext,
39
- DEFAULT_SECURITY_CONFIG
39
+ DEFAULT_SECURITY_CONFIG,
40
+ RBACSecurityConfig
40
41
  } from './security';
41
42
 
42
43
  /**
@@ -49,9 +50,14 @@ export class RBACEngine {
49
50
  private supabase: SupabaseClient<Database>;
50
51
  private securityMiddleware: RBACSecurityMiddleware;
51
52
 
52
- constructor(supabase: SupabaseClient<Database>) {
53
+ constructor(supabase: SupabaseClient<Database>, securityConfig?: Partial<RBACSecurityConfig>) {
53
54
  this.supabase = supabase;
54
- this.securityMiddleware = new RBACSecurityMiddleware(DEFAULT_SECURITY_CONFIG);
55
+ // Merge provided security config with defaults
56
+ const mergedSecurityConfig: RBACSecurityConfig = {
57
+ ...DEFAULT_SECURITY_CONFIG,
58
+ ...securityConfig,
59
+ };
60
+ this.securityMiddleware = new RBACSecurityMiddleware(mergedSecurityConfig);
55
61
 
56
62
  // Initialize cache invalidation for automatic cache clearing
57
63
  initializeCacheInvalidation(supabase);
@@ -295,6 +301,8 @@ export class RBACEngine {
295
301
  return cached;
296
302
  }
297
303
 
304
+ const now = new Date().toISOString();
305
+
298
306
  // Check super admin
299
307
  const isSuperAdmin = await this.checkSuperAdmin(userId);
300
308
  if (isSuperAdmin) {
@@ -311,6 +319,8 @@ export class RBACEngine {
311
319
  .eq('organisation_id', scope.organisationId)
312
320
  .eq('status', 'active')
313
321
  .is('revoked_at', null)
322
+ .lte('valid_from', now)
323
+ .or(`valid_to.is.null,valid_to.gte.${now}`)
314
324
  .single() as { data: { role: string } | null; error: any };
315
325
 
316
326
  if (orgRole?.role === 'org_admin') {
@@ -321,7 +331,6 @@ export class RBACEngine {
321
331
 
322
332
  // Check event-app role
323
333
  if (scope.eventId && scope.appId) {
324
- const now = new Date().toISOString();
325
334
  const { data: eventRole } = await this.supabase
326
335
  .from('rbac_event_app_roles')
327
336
  .select('role')
@@ -402,10 +411,17 @@ export class RBACEngine {
402
411
  .eq('app_id', scope.appId) as { data: Array<{ id: string; page_name: string }> | null };
403
412
 
404
413
  if (pages) {
414
+ // OrganisationId is required for permission checks
415
+ if (!scope.organisationId) {
416
+ // Return empty permission map if no organisation context
417
+ rbacCache.set(cacheKey, permissionMap, 60000);
418
+ return permissionMap;
419
+ }
420
+
405
421
  // Create a security context for permission checks
406
422
  const securityContext: SecurityContext = {
407
423
  userId,
408
- organisationId: scope.organisationId,
424
+ organisationId: scope.organisationId, // Required
409
425
  timestamp: new Date(),
410
426
  };
411
427
 
@@ -589,9 +605,13 @@ export class RBACEngine {
589
605
  * Create an RBAC engine instance
590
606
  *
591
607
  * @param supabase - Supabase client
608
+ * @param securityConfig - Optional security configuration
592
609
  * @returns RBACEngine instance
593
610
  */
594
- export function createRBACEngine(supabase: SupabaseClient<Database>): RBACEngine {
595
- return new RBACEngine(supabase);
611
+ export function createRBACEngine(
612
+ supabase: SupabaseClient<Database>,
613
+ securityConfig?: Partial<RBACSecurityConfig>
614
+ ): RBACEngine {
615
+ return new RBACEngine(supabase, securityConfig);
596
616
  }
597
617