@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
@@ -44,25 +44,15 @@ The RBAC system includes comprehensive security measures:
44
44
  ```typescript
45
45
  import { isPermitted } from '@jmruthers/pace-core/rbac';
46
46
 
47
- // Basic usage (backward compatible)
47
+ // Basic usage - organisationId is required
48
48
  const hasPermission = await isPermitted({
49
49
  userId: 'user-123',
50
- scope: { organisationId: 'org-456' },
51
- permission: 'manage:events'
50
+ scope: { organisationId: 'org-456' }, // organisationId is required
51
+ permission: 'update:events'
52
52
  });
53
53
 
54
- // Enhanced usage with security context
55
- const hasPermissionSecure = await isPermitted({
56
- userId: 'user-123',
57
- scope: { organisationId: 'org-456' },
58
- permission: 'manage:events'
59
- }, {
60
- userId: 'user-123',
61
- organisationId: 'org-456',
62
- ipAddress: '192.168.1.1',
63
- userAgent: 'Mozilla/5.0...',
64
- timestamp: new Date()
65
- });
54
+ // Security context is automatically created internally
55
+ // If organisationId is missing, OrganisationContextRequiredError is thrown
66
56
  ```
67
57
 
68
58
  ### Security Context Examples
@@ -77,17 +67,16 @@ app.get('/api/users', async (req, res) => {
77
67
  const userId = req.user.id;
78
68
  const organisationId = req.user.organisationId;
79
69
 
80
- // Use security context for API routes
70
+ // organisationId is required - if missing, OrganisationContextRequiredError is thrown
71
+ if (!organisationId) {
72
+ return res.status(400).json({ error: 'Organisation context is required' });
73
+ }
74
+
75
+ // Check permission - security context is created automatically
81
76
  const hasAccess = await isPermitted({
82
77
  userId,
83
- scope: { organisationId },
78
+ scope: { organisationId }, // Required
84
79
  permission: 'read:users'
85
- }, {
86
- userId,
87
- organisationId,
88
- ipAddress: req.ip,
89
- userAgent: req.get('User-Agent'),
90
- timestamp: new Date()
91
80
  });
92
81
 
93
82
  if (!hasAccess) {
@@ -111,7 +100,7 @@ function UserManagement() {
111
100
  const { hasPermission, isLoading } = useCan(
112
101
  user.id,
113
102
  { organisationId: user.organisationId },
114
- 'manage:users',
103
+ 'update:users',
115
104
  true, // useCache
116
105
  {
117
106
  userId: user.id,
@@ -172,7 +161,7 @@ function AdminDashboard() {
172
161
  [
173
162
  'read:users',
174
163
  'create:events',
175
- 'manage:organisations',
164
+ 'update:organisations',
176
165
  'delete:data'
177
166
  ],
178
167
  {
@@ -190,7 +179,7 @@ function AdminDashboard() {
190
179
  <div>
191
180
  {permissions['read:users'] && <UserManagement />}
192
181
  {permissions['create:events'] && <EventCreation />}
193
- {permissions['manage:organisations'] && <OrganisationSettings />}
182
+ {permissions['update:organisations'] && <OrganisationSettings />}
194
183
  </div>
195
184
  );
196
185
  }
@@ -283,7 +272,7 @@ function UserActions() {
283
272
  return (
284
273
  <div>
285
274
  {permissions['page-1']?.includes('read') && <ReadButton />}
286
- {permissions['page-1']?.includes('manage') && <ManageButton />}
275
+ {permissions['page-1']?.includes('update') && <UpdateButton />}
287
276
  </div>
288
277
  );
289
278
  }
@@ -308,7 +297,7 @@ function UserActions() {
308
297
  eventId: undefined,
309
298
  appId: undefined
310
299
  },
311
- 'manage:users',
300
+ 'update:users',
312
301
  'page-123' // optional pageId
313
302
  );
314
303
 
@@ -447,7 +436,7 @@ import { isPermitted, getAccessLevel, getPermissionMap } from '@jmruthers/pace-c
447
436
  const canManage = await isPermitted({
448
437
  userId: 'user-123',
449
438
  scope: { organisationId: 'org-456' },
450
- permission: 'manage:events',
439
+ permission: 'update:events',
451
440
  pageId: 'page-789'
452
441
  });
453
442
 
@@ -482,7 +471,7 @@ function MyComponent() {
482
471
  const { can, isLoading: checking } = useCan(
483
472
  'user-123',
484
473
  { organisationId: 'org-456' },
485
- 'manage:events'
474
+ 'update:events'
486
475
  );
487
476
 
488
477
  const { accessLevel } = useAccessLevel(
@@ -511,7 +500,7 @@ function App() {
511
500
  return (
512
501
  <div>
513
502
  <PermissionEnforcer
514
- permissions={['manage:events']}
503
+ permissions={['update:events']}
515
504
  operation="event-management"
516
505
  fallback={<AccessDenied />}
517
506
  >
@@ -537,7 +526,7 @@ import { withPermissionGuard, withAccessLevelGuard } from '@jmruthers/pace-core/
537
526
 
538
527
  // Express middleware
539
528
  app.get('/admin',
540
- withPermissionGuard({ permission: 'manage:admin' },
529
+ withPermissionGuard({ permission: 'update:admin' },
541
530
  (req, res) => {
542
531
  res.json({ message: 'Admin access granted' });
543
532
  }
@@ -566,7 +555,7 @@ Check if a user has a specific permission.
566
555
  const hasPermission = await isPermitted({
567
556
  userId: 'user-123',
568
557
  scope: { organisationId: 'org-456' },
569
- permission: 'manage:events',
558
+ permission: 'update:events',
570
559
  pageId: 'page-789'
571
560
  });
572
561
  ```
@@ -616,7 +605,7 @@ Hook to check if user has a specific permission.
616
605
  const { can, isLoading, error, check } = useCan(
617
606
  'user-123',
618
607
  { organisationId: 'org-456' },
619
- 'manage:events',
608
+ 'update:events',
620
609
  'page-789'
621
610
  );
622
611
  ```
@@ -639,14 +628,34 @@ const { accessLevel, isLoading, error, refetch } = useAccessLevel(
639
628
  Conditionally renders children based on permissions. Works for both organization-based and event-based apps.
640
629
 
641
630
  ```tsx
631
+ // Single permission
642
632
  <PermissionEnforcer
643
- permissions={['manage:events']}
633
+ permissions={['update:events']}
644
634
  operation="event-management"
645
635
  fallback={<AccessDenied />}
646
- onDenied={() => console.log('Access denied')}
647
636
  >
648
637
  <AdminPanel />
649
638
  </PermissionEnforcer>
639
+
640
+ // Multiple permissions - require ALL (default)
641
+ <PermissionEnforcer
642
+ permissions={['read:events', 'update:events']}
643
+ operation="event-management"
644
+ requireAll={true}
645
+ fallback={<AccessDenied />}
646
+ >
647
+ <EventManagementPanel />
648
+ </PermissionEnforcer>
649
+
650
+ // Multiple permissions - require ANY
651
+ <PermissionEnforcer
652
+ permissions={['read:events', 'read:reports']}
653
+ operation="viewing"
654
+ requireAll={false}
655
+ fallback={<AccessDenied />}
656
+ >
657
+ <ViewingPanel />
658
+ </PermissionEnforcer>
650
659
  ```
651
660
 
652
661
  #### `PagePermissionGuard`
@@ -676,6 +685,39 @@ Conditionally renders navigation items based on permissions.
676
685
  </NavigationGuard>
677
686
  ```
678
687
 
688
+ #### `RoleBasedRouter`
689
+
690
+ Centralized routing with permission enforcement and public route support.
691
+
692
+ ```tsx
693
+ const routes: RouteConfig[] = [
694
+ {
695
+ path: '/dashboard',
696
+ component: Dashboard,
697
+ permissions: ['read:dashboard'],
698
+ },
699
+ {
700
+ path: '/login',
701
+ component: Login,
702
+ public: true, // Public route - no permission checks
703
+ },
704
+ {
705
+ path: '/admin',
706
+ component: Admin,
707
+ permissions: ['update:admin'],
708
+ accessLevel: 'admin',
709
+ },
710
+ ];
711
+
712
+ <RoleBasedRouter
713
+ routes={routes}
714
+ fallbackRoute="/unauthorized"
715
+ strictMode={true}
716
+ >
717
+ <App />
718
+ </RoleBasedRouter>
719
+ ```
720
+
679
721
  ### Server Adapters
680
722
 
681
723
  #### `withPermissionGuard(config, handler)`
@@ -684,7 +726,7 @@ Wraps a server handler with permission checking.
684
726
 
685
727
  ```typescript
686
728
  const protectedHandler = withPermissionGuard(
687
- { permission: 'manage:events', pageId: 'page-789' },
729
+ { permission: 'update:events', pageId: 'page-789' },
688
730
  async (req, res) => {
689
731
  // Handler logic here
690
732
  res.json({ success: true });
@@ -711,12 +753,12 @@ const adminHandler = withAccessLevelGuard(
711
753
  ### Core Types
712
754
 
713
755
  ```typescript
714
- type Operation = 'read' | 'create' | 'update' | 'delete' | 'manage';
756
+ type Operation = 'read' | 'create' | 'update' | 'delete';
715
757
  type Permission = `${Operation}:${string}`;
716
758
  type AccessLevel = 'viewer' | 'participant' | 'planner' | 'admin' | 'super';
717
759
 
718
760
  interface Scope {
719
- organisationId?: UUID;
761
+ organisationId: UUID; // Required - can always be derived from event context in event-based apps
720
762
  eventId?: string;
721
763
  appId?: UUID;
722
764
  }
@@ -686,7 +686,7 @@ describe('RBAC Adapters - Comprehensive Tests', () => {
686
686
  it('redirects to fallback URL when permission is denied', async () => {
687
687
  const middleware = createRBACMiddleware({
688
688
  protectedRoutes: [
689
- { path: '/admin', permission: 'manage:admin' as Permission }
689
+ { path: '/admin', permission: 'update:admin' as Permission }
690
690
  ],
691
691
  fallbackUrl: '/access-denied'
692
692
  });
@@ -710,7 +710,7 @@ describe('RBAC Adapters - Comprehensive Tests', () => {
710
710
  it('calls next when permission is granted', async () => {
711
711
  const middleware = createRBACMiddleware({
712
712
  protectedRoutes: [
713
- { path: '/admin', permission: 'manage:admin' as Permission }
713
+ { path: '/admin', permission: 'update:admin' as Permission }
714
714
  ],
715
715
  fallbackUrl: '/access-denied'
716
716
  });
@@ -734,7 +734,7 @@ describe('RBAC Adapters - Comprehensive Tests', () => {
734
734
  it('redirects to login when user context is missing', async () => {
735
735
  const middleware = createRBACMiddleware({
736
736
  protectedRoutes: [
737
- { path: '/admin', permission: 'manage:admin' as Permission }
737
+ { path: '/admin', permission: 'update:admin' as Permission }
738
738
  ],
739
739
  fallbackUrl: '/access-denied'
740
740
  });
@@ -58,9 +58,10 @@ describe('RBACEngine - Comprehensive Tests', () => {
58
58
  });
59
59
 
60
60
  // Helper function to create security context
61
- const createSecurityContext = (userId: string, organisationId?: string) => ({
61
+ // OrganisationId is required - use mockOrgId as default
62
+ const createSecurityContext = (userId: string, organisationId: string = mockOrgId) => ({
62
63
  userId: userId as UUID,
63
- organisationId: organisationId as UUID | undefined,
64
+ organisationId: organisationId as UUID,
64
65
  timestamp: new Date()
65
66
  });
66
67
 
@@ -84,7 +85,7 @@ describe('RBACEngine - Comprehensive Tests', () => {
84
85
  const permissionCheck: PermissionCheck = {
85
86
  userId: '00000000-0000-0000-0000-000000000001' as UUID,
86
87
  scope: { organisationId: '00000000-0000-0000-0000-000000000002' as UUID },
87
- permission: 'manage:everything' as Permission
88
+ permission: 'update:everything' as Permission
88
89
  };
89
90
 
90
91
  const securityContext = createSecurityContext('00000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000002');
@@ -103,7 +104,7 @@ describe('RBACEngine - Comprehensive Tests', () => {
103
104
  const permissionCheck: PermissionCheck = {
104
105
  userId: 'regular-user-123' as UUID,
105
106
  scope: { organisationId: '00000000-0000-0000-0000-000000000002' as UUID },
106
- permission: 'manage:everything' as Permission
107
+ permission: 'update:everything' as Permission
107
108
  };
108
109
 
109
110
  const securityContext = {
@@ -366,6 +367,8 @@ describe('RBACEngine - Comprehensive Tests', () => {
366
367
  select: vi.fn().mockReturnThis(),
367
368
  eq: vi.fn().mockReturnThis(),
368
369
  is: vi.fn().mockReturnThis(),
370
+ lte: vi.fn().mockReturnThis(),
371
+ or: vi.fn().mockReturnThis(),
369
372
  single: vi.fn().mockResolvedValue({
370
373
  data: { role: 'org_admin' },
371
374
  error: null
@@ -427,6 +430,8 @@ describe('RBACEngine - Comprehensive Tests', () => {
427
430
  select: vi.fn().mockReturnThis(),
428
431
  eq: vi.fn().mockReturnThis(),
429
432
  is: vi.fn().mockReturnThis(),
433
+ lte: vi.fn().mockReturnThis(),
434
+ or: vi.fn().mockReturnThis(),
430
435
  single: vi.fn().mockResolvedValue({
431
436
  data: null,
432
437
  error: { code: 'PGRST116' }
@@ -495,6 +500,8 @@ describe('RBACEngine - Comprehensive Tests', () => {
495
500
  select: vi.fn().mockReturnThis(),
496
501
  eq: vi.fn().mockReturnThis(),
497
502
  is: vi.fn().mockReturnThis(),
503
+ lte: vi.fn().mockReturnThis(),
504
+ or: vi.fn().mockReturnThis(),
498
505
  single: vi.fn().mockResolvedValue({
499
506
  data: null,
500
507
  error: { code: 'PGRST116' }
@@ -780,9 +787,11 @@ describe('RBACEngine - Comprehensive Tests', () => {
780
787
  };
781
788
 
782
789
  const securityContext = {
783
- userId: 'user-123',
790
+ userId: 'user-123' as UUID,
791
+ organisationId: '00000000-0000-0000-0000-000000000002' as UUID, // Required
784
792
  ipAddress: '192.168.1.1',
785
- userAgent: 'test-agent'
793
+ userAgent: 'test-agent',
794
+ timestamp: new Date()
786
795
  };
787
796
 
788
797
  const result = await engine.isPermitted(permissionCheck, securityContext);
@@ -216,7 +216,7 @@ describe('RBAC Core Functionality', () => {
216
216
  'create:events',
217
217
  'update:profiles',
218
218
  'delete:comments',
219
- 'manage:organisations',
219
+ 'update:organisations',
220
220
  ];
221
221
 
222
222
  validPermissions.forEach(permission => {
@@ -60,7 +60,7 @@ describe('RBACEngine - Core Logic Tests', () => {
60
60
  const permissionCheck: PermissionCheck = {
61
61
  userId: '00000000-0000-0000-0000-000000000001' as UUID,
62
62
  scope: { organisationId: '00000000-0000-0000-0000-000000000002' as UUID },
63
- permission: 'manage:everything' as Permission
63
+ permission: 'update:everything' as Permission
64
64
  };
65
65
 
66
66
  const securityContext = {
@@ -131,7 +131,7 @@ describe('RBACEngine - Core Logic Tests', () => {
131
131
  const permissionCheck: PermissionCheck = {
132
132
  userId: 'user-123' as UUID,
133
133
  scope: { organisationId: 'org-123' as UUID },
134
- permission: 'manage:everything' as Permission
134
+ permission: 'update:everything' as Permission
135
135
  };
136
136
 
137
137
  const result = await engine.isPermitted(permissionCheck);
@@ -187,6 +187,7 @@ describe('RBACEngine - Core Logic Tests', () => {
187
187
 
188
188
  it('resolves organisation admin access level', async () => {
189
189
  // Mock the from() chain to handle both global roles and organisation roles queries
190
+ let organisationRoleQuery: any;
190
191
  mockSupabase.from.mockImplementation((table: string) => {
191
192
  if (table === 'rbac_global_roles') {
192
193
  return {
@@ -201,15 +202,20 @@ describe('RBACEngine - Core Logic Tests', () => {
201
202
  };
202
203
  }
203
204
  if (table === 'rbac_organisation_roles') {
204
- return {
205
+ const query = {
205
206
  select: vi.fn().mockReturnThis(),
206
207
  eq: vi.fn().mockReturnThis(),
207
208
  is: vi.fn().mockReturnThis(),
209
+ lte: vi.fn().mockReturnThis(),
210
+ or: vi.fn().mockReturnThis(),
208
211
  single: vi.fn().mockResolvedValue({
209
212
  data: { role: 'org_admin' },
210
213
  error: null
211
214
  })
212
215
  };
216
+
217
+ organisationRoleQuery = query;
218
+ return query;
213
219
  }
214
220
  return {
215
221
  select: vi.fn().mockReturnThis(),
@@ -222,8 +228,55 @@ describe('RBACEngine - Core Logic Tests', () => {
222
228
 
223
229
  const scope: Scope = { organisationId: 'org-123' as UUID };
224
230
  const accessLevel = await engine.getAccessLevel({ userId: 'user-123' as UUID, scope });
225
-
231
+
226
232
  expect(accessLevel).toBe('admin');
233
+ expect(organisationRoleQuery.lte).toHaveBeenCalledWith('valid_from', expect.any(String));
234
+ expect(organisationRoleQuery.or).toHaveBeenCalledWith(expect.stringContaining('valid_to.gte.'));
235
+ });
236
+
237
+ it('treats expired organisation roles as viewer access', async () => {
238
+ let organisationRoleQuery: any;
239
+ mockSupabase.from.mockImplementation((table: string) => {
240
+ if (table === 'rbac_global_roles') {
241
+ return {
242
+ select: vi.fn().mockReturnThis(),
243
+ eq: vi.fn().mockReturnThis(),
244
+ lte: vi.fn().mockReturnThis(),
245
+ or: vi.fn().mockReturnThis(),
246
+ limit: vi.fn().mockResolvedValue({
247
+ data: [],
248
+ error: null
249
+ })
250
+ };
251
+ }
252
+ if (table === 'rbac_organisation_roles') {
253
+ const query = {
254
+ select: vi.fn().mockReturnThis(),
255
+ eq: vi.fn().mockReturnThis(),
256
+ is: vi.fn().mockReturnThis(),
257
+ lte: vi.fn().mockReturnThis(),
258
+ or: vi.fn().mockReturnThis(),
259
+ single: vi.fn().mockResolvedValue({ data: null, error: null })
260
+ };
261
+
262
+ organisationRoleQuery = query;
263
+ return query;
264
+ }
265
+ return {
266
+ select: vi.fn().mockReturnThis(),
267
+ eq: vi.fn().mockReturnThis(),
268
+ lte: vi.fn().mockReturnThis(),
269
+ or: vi.fn().mockReturnThis(),
270
+ limit: vi.fn().mockResolvedValue({ data: [], error: null })
271
+ };
272
+ });
273
+
274
+ const scope: Scope = { organisationId: 'org-123' as UUID };
275
+ const accessLevel = await engine.getAccessLevel({ userId: 'user-123' as UUID, scope });
276
+
277
+ expect(accessLevel).toBe('viewer');
278
+ expect(organisationRoleQuery.lte).toHaveBeenCalledWith('valid_from', expect.any(String));
279
+ expect(organisationRoleQuery.or).toHaveBeenCalledWith(expect.stringContaining('valid_to.gte.'));
227
280
  });
228
281
  });
229
282
 
@@ -71,8 +71,7 @@ describe('RBACEngine - Simplified Tests', () => {
71
71
  const invalidPermissions = [
72
72
  'invalid',
73
73
  'read',
74
- 'manage:organisation', // manage is no longer valid
75
- 'manage:*',
74
+ // Note: manage operations have been removed
76
75
  ':users',
77
76
  'read:user-settings', // hyphens not allowed
78
77
  'read:user_settings', // underscores not allowed
@@ -175,6 +174,8 @@ describe('RBACEngine - Simplified Tests', () => {
175
174
  select: vi.fn().mockReturnThis(),
176
175
  eq: vi.fn().mockReturnThis(),
177
176
  is: vi.fn().mockReturnThis(),
177
+ lte: vi.fn().mockReturnThis(),
178
+ or: vi.fn().mockReturnThis(),
178
179
  single: vi.fn().mockResolvedValue({
179
180
  data: { role: 'org_admin' },
180
181
  error: null
@@ -30,7 +30,7 @@ import { useUnifiedAuth } from '../providers/UnifiedAuthProvider';
30
30
  * <PermissionGuard
31
31
  * userId="user-123"
32
32
  * scope={{ organisationId: 'org-456' }}
33
- * permission="manage:events"
33
+ * permission="update:events"
34
34
  * pageId="page-789"
35
35
  * fallback={<AccessDenied />}
36
36
  * >
@@ -39,7 +39,7 @@ import { useUnifiedAuth } from '../providers/UnifiedAuthProvider';
39
39
  *
40
40
  * // With context inference (requires auth context)
41
41
  * <PermissionGuard
42
- * permission="manage:events"
42
+ * permission="update:events"
43
43
  * scope={{ organisationId: 'org-456' }}
44
44
  * fallback={<AccessDenied />}
45
45
  * >
@@ -279,7 +279,7 @@ export function AccessLevelGuard({
279
279
  * @example
280
280
  * ```typescript
281
281
  * const protectedHandler = withPermissionGuard(
282
- * { permission: 'manage:events', pageId: 'page-789' },
282
+ * { permission: 'update:events', pageId: 'page-789' },
283
283
  * async (req, res) => {
284
284
  * // Handler logic here
285
285
  * res.json({ success: true });
@@ -521,7 +521,7 @@ export function withRoleGuard<T extends any[]>(
521
521
  *
522
522
  * export default createRBACMiddleware({
523
523
  * protectedRoutes: [
524
- * { path: '/admin', permission: 'manage:admin' },
524
+ * { path: '/admin', permission: 'update:admin' },
525
525
  * { path: '/events', permission: 'read:events' },
526
526
  * ],
527
527
  * fallbackUrl: '/access-denied',
@@ -37,11 +37,11 @@ vi.mock('./cache', () => ({
37
37
  )
38
38
  },
39
39
  CACHE_PATTERNS: {
40
- PERMISSION: vi.fn((userId, organisationId) => `permission:${userId}:${organisationId}:*`),
41
- USER: vi.fn((userId) => `permission:${userId}:*`),
42
- ORGANISATION: vi.fn((organisationId) => `permission:*:${organisationId}:*`),
43
- EVENT: vi.fn((eventId) => `permission:*:*:${eventId}:*`),
44
- APP: vi.fn((appId) => `permission:*:*:*:${appId}`)
40
+ PERMISSION: vi.fn((userId, organisationId) => `perm:${userId}:${organisationId}:`),
41
+ USER: vi.fn((userId) => `:${userId}:`),
42
+ ORGANISATION: vi.fn((organisationId) => `:${organisationId}:`),
43
+ EVENT: vi.fn((eventId) => `:${eventId}:`),
44
+ APP: vi.fn((appId) => `:${appId}`)
45
45
  }
46
46
  }));
47
47
 
@@ -119,7 +119,7 @@ describe('RBAC API', () => {
119
119
 
120
120
  process.env.NODE_ENV = originalEnv;
121
121
 
122
- expect(mockCreateRBACEngine).toHaveBeenCalledWith(mockSupabase);
122
+ expect(mockCreateRBACEngine).toHaveBeenCalledWith(mockSupabase, undefined);
123
123
  expect(mockCreateAuditManager).toHaveBeenCalledWith(mockSupabase);
124
124
  expect(mockSetGlobalAuditManager).toHaveBeenCalledWith(mockAuditManager);
125
125
  expect(mockLogger.info).toHaveBeenCalledWith('RBAC system initialized successfully');
@@ -203,7 +203,7 @@ describe('RBAC API', () => {
203
203
 
204
204
  setupRBAC(mockSupabase as any);
205
205
 
206
- expect(mockCreateRBACEngine).toHaveBeenCalledWith(mockSupabase);
206
+ expect(mockCreateRBACEngine).toHaveBeenCalledWith(mockSupabase, undefined);
207
207
  });
208
208
 
209
209
  it('handles multiple initialization calls', () => {
@@ -622,6 +622,26 @@ describe('RBAC API', () => {
622
622
  permission: 'read:users'
623
623
  })).rejects.toThrow('Engine error');
624
624
  });
625
+
626
+ it('throws OrganisationContextRequiredError when organisationId is missing', async () => {
627
+ const { isPermitted, OrganisationContextRequiredError } = await import('./api');
628
+
629
+ await expect(isPermitted({
630
+ userId: 'user-123',
631
+ scope: {}, // Missing organisationId
632
+ permission: 'read:users'
633
+ })).rejects.toThrow(OrganisationContextRequiredError);
634
+ });
635
+
636
+ it('throws OrganisationContextRequiredError when organisationId is undefined', async () => {
637
+ const { isPermitted, OrganisationContextRequiredError } = await import('./api');
638
+
639
+ await expect(isPermitted({
640
+ userId: 'user-123',
641
+ scope: { organisationId: undefined },
642
+ permission: 'read:users'
643
+ })).rejects.toThrow(OrganisationContextRequiredError);
644
+ });
625
645
  });
626
646
 
627
647
  describe('isPermittedCached', () => {
@@ -919,28 +939,32 @@ describe('RBAC API', () => {
919
939
  debug: vi.fn()
920
940
  });
921
941
 
942
+ vi.mocked(rbacCache.invalidate).mockClear();
943
+
922
944
  setupRBAC(mockSupabase as any);
923
945
  });
924
946
 
925
947
  describe('invalidateUserCache', () => {
926
948
  it('invalidates user cache with organisation', async () => {
927
949
  const { invalidateUserCache } = await import('./api');
928
-
950
+
929
951
  invalidateUserCache('user-123', 'org-456');
930
952
 
931
- expect(rbacCache.invalidate).toHaveBeenCalledWith(
932
- expect.stringContaining('user-123')
933
- );
953
+ expect(rbacCache.invalidate).toHaveBeenCalledWith('perm:user-123:org-456:');
954
+ expect(rbacCache.invalidate).toHaveBeenCalledWith('access:user-123:org-456:');
955
+ expect(rbacCache.invalidate).toHaveBeenCalledWith('map:user-123:org-456:');
956
+ expect(rbacCache.invalidate).toHaveBeenCalledTimes(3);
934
957
  });
935
958
 
936
959
  it('invalidates user cache without organisation', async () => {
937
960
  const { invalidateUserCache } = await import('./api');
938
-
961
+
939
962
  invalidateUserCache('user-123');
940
963
 
941
- expect(rbacCache.invalidate).toHaveBeenCalledWith(
942
- expect.stringContaining('user-123')
943
- );
964
+ expect(rbacCache.invalidate).toHaveBeenCalledWith('perm:user-123:');
965
+ expect(rbacCache.invalidate).toHaveBeenCalledWith('access:user-123:');
966
+ expect(rbacCache.invalidate).toHaveBeenCalledWith('map:user-123:');
967
+ expect(rbacCache.invalidate).toHaveBeenCalledTimes(3);
944
968
  });
945
969
  });
946
970