@jmruthers/pace-core 0.5.164 → 0.5.166

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 (141) hide show
  1. package/dist/{DataTable-HAATGNJY.js → DataTable-Z6IYTME3.js} +3 -3
  2. package/dist/{chunk-DDB2M4XO.js → chunk-5PH366QI.js} +2 -2
  3. package/dist/{chunk-GFSYFEVZ.js → chunk-BPIFVNEO.js} +24 -24
  4. package/dist/chunk-BPIFVNEO.js.map +1 -0
  5. package/dist/{chunk-G3EZSUTR.js → chunk-JIRBF5HR.js} +6 -23
  6. package/dist/chunk-JIRBF5HR.js.map +1 -0
  7. package/dist/{chunk-23YYDN24.js → chunk-SVMR7EVX.js} +2 -2
  8. package/dist/components.js +3 -3
  9. package/dist/index.js +4 -4
  10. package/dist/rbac/index.d.ts +10 -3
  11. package/dist/rbac/index.js +2 -2
  12. package/dist/utils.js +1 -1
  13. package/docs/api/classes/ColumnFactory.md +1 -1
  14. package/docs/api/classes/ErrorBoundary.md +1 -1
  15. package/docs/api/classes/InvalidScopeError.md +1 -1
  16. package/docs/api/classes/MissingUserContextError.md +1 -1
  17. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  18. package/docs/api/classes/PermissionDeniedError.md +1 -1
  19. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  20. package/docs/api/classes/RBACAuditManager.md +1 -1
  21. package/docs/api/classes/RBACCache.md +1 -1
  22. package/docs/api/classes/RBACEngine.md +1 -1
  23. package/docs/api/classes/RBACError.md +1 -1
  24. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  25. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  26. package/docs/api/classes/StorageUtils.md +1 -1
  27. package/docs/api/enums/FileCategory.md +1 -1
  28. package/docs/api/interfaces/AggregateConfig.md +1 -1
  29. package/docs/api/interfaces/BadgeProps.md +1 -1
  30. package/docs/api/interfaces/ButtonProps.md +1 -1
  31. package/docs/api/interfaces/CalendarProps.md +1 -1
  32. package/docs/api/interfaces/CardProps.md +1 -1
  33. package/docs/api/interfaces/ColorPalette.md +1 -1
  34. package/docs/api/interfaces/ColorShade.md +1 -1
  35. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  36. package/docs/api/interfaces/DataRecord.md +1 -1
  37. package/docs/api/interfaces/DataTableAction.md +1 -1
  38. package/docs/api/interfaces/DataTableColumn.md +1 -1
  39. package/docs/api/interfaces/DataTableProps.md +1 -1
  40. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  41. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  42. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  43. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  44. package/docs/api/interfaces/EventLogoProps.md +1 -1
  45. package/docs/api/interfaces/ExportColumn.md +1 -1
  46. package/docs/api/interfaces/ExportOptions.md +1 -1
  47. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  48. package/docs/api/interfaces/FileMetadata.md +1 -1
  49. package/docs/api/interfaces/FileReference.md +1 -1
  50. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  51. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  52. package/docs/api/interfaces/FileUploadProps.md +1 -1
  53. package/docs/api/interfaces/FooterProps.md +1 -1
  54. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  55. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  56. package/docs/api/interfaces/InputProps.md +1 -1
  57. package/docs/api/interfaces/LabelProps.md +1 -1
  58. package/docs/api/interfaces/LoginFormProps.md +1 -1
  59. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  60. package/docs/api/interfaces/NavigationContextType.md +1 -1
  61. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  62. package/docs/api/interfaces/NavigationItem.md +1 -1
  63. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  64. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  65. package/docs/api/interfaces/Organisation.md +1 -1
  66. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  67. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  68. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  69. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  70. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  71. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  72. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  73. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  74. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  75. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  76. package/docs/api/interfaces/PaletteData.md +1 -1
  77. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  78. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  79. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  80. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  81. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  82. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  83. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  84. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  85. package/docs/api/interfaces/RBACConfig.md +1 -1
  86. package/docs/api/interfaces/RBACLogger.md +1 -1
  87. package/docs/api/interfaces/ResourcePermissions.md +1 -1
  88. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  89. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  90. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  91. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  92. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  93. package/docs/api/interfaces/RouteConfig.md +1 -1
  94. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  95. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  96. package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
  97. package/docs/api/interfaces/StorageConfig.md +1 -1
  98. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  99. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  100. package/docs/api/interfaces/StorageListOptions.md +1 -1
  101. package/docs/api/interfaces/StorageListResult.md +1 -1
  102. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  103. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  104. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  105. package/docs/api/interfaces/StyleImport.md +1 -1
  106. package/docs/api/interfaces/SwitchProps.md +1 -1
  107. package/docs/api/interfaces/TabsContentProps.md +1 -1
  108. package/docs/api/interfaces/TabsListProps.md +1 -1
  109. package/docs/api/interfaces/TabsProps.md +1 -1
  110. package/docs/api/interfaces/TabsTriggerProps.md +1 -1
  111. package/docs/api/interfaces/TextareaProps.md +1 -1
  112. package/docs/api/interfaces/ToastActionElement.md +1 -1
  113. package/docs/api/interfaces/ToastProps.md +1 -1
  114. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  115. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  116. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  117. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  118. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  119. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  120. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  121. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  122. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  123. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  124. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  125. package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
  126. package/docs/api/interfaces/UserEventAccess.md +1 -1
  127. package/docs/api/interfaces/UserMenuProps.md +1 -1
  128. package/docs/api/interfaces/UserProfile.md +1 -1
  129. package/docs/api/modules.md +19 -12
  130. package/package.json +2 -2
  131. package/src/__tests__/hooks/usePermissions.test.ts +12 -9
  132. package/src/components/NavigationMenu/NavigationMenu.tsx +4 -26
  133. package/src/hooks/__tests__/ServiceHooks.test.tsx +2 -2
  134. package/src/rbac/hooks/__tests__/usePermissions.integration.test.ts +32 -34
  135. package/src/rbac/hooks/usePermissions.test.ts +34 -32
  136. package/src/rbac/hooks/usePermissions.ts +41 -28
  137. package/dist/chunk-G3EZSUTR.js.map +0 -1
  138. package/dist/chunk-GFSYFEVZ.js.map +0 -1
  139. /package/dist/{DataTable-HAATGNJY.js.map → DataTable-Z6IYTME3.js.map} +0 -0
  140. /package/dist/{chunk-DDB2M4XO.js.map → chunk-5PH366QI.js.map} +0 -0
  141. /package/dist/{chunk-23YYDN24.js.map → chunk-SVMR7EVX.js.map} +0 -0
@@ -27,9 +27,13 @@ vi.mock('../../api', () => ({
27
27
  import { getPermissionMap } from '../../api';
28
28
 
29
29
  const mockUserId = 'user-123';
30
+ const mockOrgId = 'org-123';
31
+ const mockEventId = 'event-123';
32
+ const mockAppId = 'app-123';
30
33
  const mockScope = {
31
- organisationId: 'org-123',
32
- eventId: 'event-123'
34
+ organisationId: mockOrgId,
35
+ eventId: mockEventId,
36
+ appId: mockAppId
33
37
  };
34
38
 
35
39
  const mockPermissionMap = {
@@ -54,9 +58,9 @@ describe('usePermissions Integration Tests', () => {
54
58
  describe('Cache Behavior', () => {
55
59
  it('fetches permissions only once for same dependencies', async () => {
56
60
  const { result, rerender } = renderHook(
57
- ({ userId, scope }) => usePermissions(userId, scope),
61
+ ({ userId, orgId, eventId, appId }) => usePermissions(userId, orgId, eventId, appId),
58
62
  {
59
- initialProps: { userId: mockUserId, scope: mockScope }
63
+ initialProps: { userId: mockUserId, orgId: mockOrgId, eventId: mockEventId, appId: mockAppId }
60
64
  }
61
65
  );
62
66
 
@@ -75,9 +79,9 @@ describe('usePermissions Integration Tests', () => {
75
79
 
76
80
  it('refetches when organisationId changes', async () => {
77
81
  const { result, rerender } = renderHook(
78
- ({ userId, scope }) => usePermissions(userId, scope),
82
+ ({ userId, orgId, eventId, appId }) => usePermissions(userId, orgId, eventId, appId),
79
83
  {
80
- initialProps: { userId: mockUserId, scope: mockScope }
84
+ initialProps: { userId: mockUserId, orgId: mockOrgId, eventId: mockEventId, appId: mockAppId }
81
85
  }
82
86
  );
83
87
 
@@ -95,9 +99,9 @@ describe('usePermissions Integration Tests', () => {
95
99
 
96
100
  it('refetches when eventId changes', async () => {
97
101
  const { result, rerender } = renderHook(
98
- ({ userId, scope }) => usePermissions(userId, scope),
102
+ ({ userId, orgId, eventId, appId }) => usePermissions(userId, orgId, eventId, appId),
99
103
  {
100
- initialProps: { userId: mockUserId, scope: mockScope }
104
+ initialProps: { userId: mockUserId, orgId: mockOrgId, eventId: mockEventId, appId: mockAppId }
101
105
  }
102
106
  );
103
107
 
@@ -114,11 +118,10 @@ describe('usePermissions Integration Tests', () => {
114
118
  });
115
119
 
116
120
  it('refetches when appId changes', async () => {
117
- const scopeWithApp = { ...mockScope, appId: 'app-1' };
118
121
  const { result, rerender } = renderHook(
119
- ({ userId, scope }) => usePermissions(userId, scope),
122
+ ({ userId, orgId, eventId, appId }) => usePermissions(userId, orgId, eventId, appId),
120
123
  {
121
- initialProps: { userId: mockUserId, scope: scopeWithApp }
124
+ initialProps: { userId: mockUserId, orgId: mockOrgId, eventId: mockEventId, appId: 'app-1' }
122
125
  }
123
126
  );
124
127
 
@@ -137,7 +140,7 @@ describe('usePermissions Integration Tests', () => {
137
140
 
138
141
  describe('Refetch Scenarios', () => {
139
142
  it('refetch updates permissions after external change', async () => {
140
- const { result } = renderHook(() => usePermissions(mockUserId, mockScope));
143
+ const { result } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
141
144
 
142
145
  await waitFor(() => {
143
146
  expect(result.current.isLoading).toBe(false);
@@ -159,7 +162,7 @@ describe('usePermissions Integration Tests', () => {
159
162
  it('refetch clears error state', async () => {
160
163
  // First render with error
161
164
  mockGetPermissionMap.mockRejectedValueOnce(new Error('Network error'));
162
- const { result } = renderHook(() => usePermissions(mockUserId, mockScope));
165
+ const { result } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
163
166
 
164
167
  await waitFor(() => {
165
168
  expect(result.current.error).toBeDefined();
@@ -176,7 +179,7 @@ describe('usePermissions Integration Tests', () => {
176
179
  });
177
180
 
178
181
  it('refetch triggers API call', async () => {
179
- const { result } = renderHook(() => usePermissions(mockUserId, mockScope));
182
+ const { result } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
180
183
 
181
184
  await waitFor(() => {
182
185
  expect(result.current.isLoading).toBe(false);
@@ -192,7 +195,7 @@ describe('usePermissions Integration Tests', () => {
192
195
  });
193
196
 
194
197
  it('refetch does nothing when userId is empty', async () => {
195
- const { result } = renderHook(() => usePermissions('', mockScope));
198
+ const { result } = renderHook(() => usePermissions('', mockOrgId, mockEventId, mockAppId));
196
199
 
197
200
  await waitFor(() => {
198
201
  expect(result.current.isLoading).toBe(false);
@@ -207,8 +210,7 @@ describe('usePermissions Integration Tests', () => {
207
210
  });
208
211
 
209
212
  it('refetch does nothing when organisationId is empty', async () => {
210
- const invalidScope = { ...mockScope, organisationId: '' };
211
- const { result } = renderHook(() => usePermissions(mockUserId, invalidScope));
213
+ const { result } = renderHook(() => usePermissions(mockUserId, '', mockEventId, mockAppId));
212
214
 
213
215
  await waitFor(() => {
214
216
  expect(result.current.isLoading).toBe(true);
@@ -225,7 +227,7 @@ describe('usePermissions Integration Tests', () => {
225
227
 
226
228
  describe('Concurrent Permission Checks', () => {
227
229
  it('handles rapid permission checks correctly', async () => {
228
- const { result } = renderHook(() => usePermissions(mockUserId, mockScope));
230
+ const { result } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
229
231
 
230
232
  await waitFor(() => {
231
233
  expect(result.current.isLoading).toBe(false);
@@ -250,8 +252,8 @@ describe('usePermissions Integration Tests', () => {
250
252
  });
251
253
 
252
254
  it('handles multiple hooks with same scope', async () => {
253
- const { result: result1 } = renderHook(() => usePermissions(mockUserId, mockScope));
254
- const { result: result2 } = renderHook(() => usePermissions(mockUserId, mockScope));
255
+ const { result: result1 } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
256
+ const { result: result2 } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
255
257
 
256
258
  await waitFor(() => {
257
259
  expect(result1.current.isLoading).toBe(false);
@@ -266,8 +268,8 @@ describe('usePermissions Integration Tests', () => {
266
268
  });
267
269
 
268
270
  it('handles refetch from multiple components', async () => {
269
- const { result: result1 } = renderHook(() => usePermissions(mockUserId, mockScope));
270
- const { result: result2 } = renderHook(() => usePermissions(mockUserId, mockScope));
271
+ const { result: result1 } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
272
+ const { result: result2 } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
271
273
 
272
274
  await waitFor(() => {
273
275
  expect(result1.current.isLoading).toBe(false);
@@ -286,7 +288,7 @@ describe('usePermissions Integration Tests', () => {
286
288
 
287
289
  describe('Permission Array Operations', () => {
288
290
  it('hasAnyPermission handles complex arrays', async () => {
289
- const { result } = renderHook(() => usePermissions(mockUserId, mockScope));
291
+ const { result } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
290
292
 
291
293
  await waitFor(() => {
292
294
  expect(result.current.isLoading).toBe(false);
@@ -298,7 +300,7 @@ describe('usePermissions Integration Tests', () => {
298
300
  });
299
301
 
300
302
  it('hasAllPermissions handles complex arrays', async () => {
301
- const { result } = renderHook(() => usePermissions(mockUserId, mockScope));
303
+ const { result } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
302
304
 
303
305
  await waitFor(() => {
304
306
  expect(result.current.isLoading).toBe(false);
@@ -316,7 +318,7 @@ describe('usePermissions Integration Tests', () => {
316
318
  }
317
319
 
318
320
  mockGetPermissionMap.mockResolvedValue(largePermissionMap);
319
- const { result } = renderHook(() => usePermissions(mockUserId, mockScope));
321
+ const { result } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
320
322
 
321
323
  await waitFor(() => {
322
324
  expect(result.current.isLoading).toBe(false);
@@ -371,8 +373,7 @@ describe('usePermissions Integration Tests', () => {
371
373
 
372
374
  describe('Invalid Scope Handling', () => {
373
375
  it('handles empty organisationId gracefully', async () => {
374
- const invalidScope = { ...mockScope, organisationId: '' };
375
- const { result } = renderHook(() => usePermissions(mockUserId, invalidScope));
376
+ const { result } = renderHook(() => usePermissions(mockUserId, '', mockEventId, mockAppId));
376
377
 
377
378
  // Should not fetch with invalid scope
378
379
  await waitFor(() => {
@@ -381,8 +382,7 @@ describe('usePermissions Integration Tests', () => {
381
382
  });
382
383
 
383
384
  it('handles whitespace-only organisationId', async () => {
384
- const invalidScope = { ...mockScope, organisationId: ' ' };
385
- const { result } = renderHook(() => usePermissions(mockUserId, invalidScope));
385
+ const { result } = renderHook(() => usePermissions(mockUserId, ' ', mockEventId, mockAppId));
386
386
 
387
387
  // Should not fetch with invalid scope
388
388
  await waitFor(() => {
@@ -391,9 +391,7 @@ describe('usePermissions Integration Tests', () => {
391
391
  });
392
392
 
393
393
  it('handles missing organisationId', async () => {
394
- const invalidScope = { ...mockScope };
395
- delete (invalidScope as any).organisationId;
396
- const { result } = renderHook(() => usePermissions(mockUserId, invalidScope));
394
+ const { result } = renderHook(() => usePermissions(mockUserId, undefined, mockEventId, mockAppId));
397
395
 
398
396
  // Should not fetch with invalid scope
399
397
  await waitFor(() => {
@@ -405,9 +403,9 @@ describe('usePermissions Integration Tests', () => {
405
403
  describe('Memoization', () => {
406
404
  it('returns stable references for helpers', async () => {
407
405
  const { result, rerender } = renderHook(
408
- ({ userId, scope }) => usePermissions(userId, scope),
406
+ ({ userId, orgId, eventId, appId }) => usePermissions(userId, orgId, eventId, appId),
409
407
  {
410
- initialProps: { userId: mockUserId, scope: mockScope }
408
+ initialProps: { userId: mockUserId, orgId: mockOrgId, eventId: mockEventId, appId: mockAppId }
411
409
  }
412
410
  );
413
411
 
@@ -23,9 +23,13 @@ import { getPermissionMap } from '../api';
23
23
 
24
24
  // Mock data
25
25
  const mockUserId = 'user-123';
26
+ const mockOrgId = 'org-123';
27
+ const mockEventId = 'event-123';
28
+ const mockAppId = 'app-123';
26
29
  const mockScope = {
27
- organisationId: 'org-123',
28
- eventId: 'event-123'
30
+ organisationId: mockOrgId,
31
+ eventId: mockEventId,
32
+ appId: mockAppId
29
33
  };
30
34
 
31
35
  const mockPermissionMap = {
@@ -47,7 +51,7 @@ describe('usePermissions Hook', () => {
47
51
  describe('Permission Fetching', () => {
48
52
  it('fetches permissions for valid user', async () => {
49
53
 
50
- const { result } = renderHook(() => usePermissions(mockUserId, mockScope));
54
+ const { result } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
51
55
 
52
56
  expect(result.current.isLoading).toBe(true);
53
57
  expect(result.current.permissions).toEqual({});
@@ -63,7 +67,7 @@ describe('usePermissions Hook', () => {
63
67
  const error = new Error('Failed to fetch permissions');
64
68
  mockGetPermissionMap.mockRejectedValue(error);
65
69
 
66
- const { result } = renderHook(() => usePermissions(mockUserId, mockScope));
70
+ const { result } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
67
71
 
68
72
  await waitFor(() => {
69
73
  expect(result.current.isLoading).toBe(false);
@@ -73,7 +77,7 @@ describe('usePermissions Hook', () => {
73
77
  });
74
78
 
75
79
  it('returns empty permissions for invalid user', async () => {
76
- const { result } = renderHook(() => usePermissions('', mockScope));
80
+ const { result } = renderHook(() => usePermissions('', mockOrgId, mockEventId, mockAppId));
77
81
 
78
82
  await waitFor(() => {
79
83
  expect(result.current.isLoading).toBe(false);
@@ -112,7 +116,7 @@ describe('usePermissions Hook', () => {
112
116
  });
113
117
 
114
118
  it('hasPermission returns correct boolean', async () => {
115
- const { result } = renderHook(() => usePermissions(mockUserId, mockScope));
119
+ const { result } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
116
120
 
117
121
  await waitFor(() => {
118
122
  expect(result.current.isLoading).toBe(false);
@@ -126,7 +130,7 @@ describe('usePermissions Hook', () => {
126
130
  });
127
131
 
128
132
  it('hasAnyPermission works with multiple permissions', async () => {
129
- const { result } = renderHook(() => usePermissions(mockUserId, mockScope));
133
+ const { result } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
130
134
 
131
135
  await waitFor(() => {
132
136
  expect(result.current.isLoading).toBe(false);
@@ -138,7 +142,7 @@ describe('usePermissions Hook', () => {
138
142
  });
139
143
 
140
144
  it('hasAllPermissions works with multiple permissions', async () => {
141
- const { result } = renderHook(() => usePermissions(mockUserId, mockScope));
145
+ const { result } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
142
146
 
143
147
  await waitFor(() => {
144
148
  expect(result.current.isLoading).toBe(false);
@@ -150,7 +154,7 @@ describe('usePermissions Hook', () => {
150
154
  });
151
155
 
152
156
  it('handles empty permission lists', async () => {
153
- const { result } = renderHook(() => usePermissions(mockUserId, mockScope));
157
+ const { result } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
154
158
 
155
159
  await waitFor(() => {
156
160
  expect(result.current.isLoading).toBe(false);
@@ -165,7 +169,7 @@ describe('usePermissions Hook', () => {
165
169
  it('refetch function works correctly', async () => {
166
170
  mockGetPermissionMap.mockResolvedValue(mockPermissionMap);
167
171
 
168
- const { result } = renderHook(() => usePermissions(mockUserId, mockScope));
172
+ const { result } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
169
173
 
170
174
  await waitFor(() => {
171
175
  expect(result.current.isLoading).toBe(false);
@@ -184,7 +188,7 @@ describe('usePermissions Hook', () => {
184
188
  .mockResolvedValueOnce(mockPermissionMap)
185
189
  .mockRejectedValueOnce(new Error('Refetch error'));
186
190
 
187
- const { result } = renderHook(() => usePermissions(mockUserId, mockScope));
191
+ const { result } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
188
192
 
189
193
  await waitFor(() => {
190
194
  expect(result.current.isLoading).toBe(false);
@@ -203,7 +207,7 @@ describe('usePermissions Hook', () => {
203
207
  it('shows loading state during initial fetch', () => {
204
208
  mockGetPermissionMap.mockImplementation(() => new Promise(() => {})); // Never resolves
205
209
 
206
- const { result } = renderHook(() => usePermissions(mockUserId, mockScope));
210
+ const { result } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
207
211
 
208
212
  expect(result.current.isLoading).toBe(true);
209
213
  expect(result.current.permissions).toEqual({});
@@ -213,7 +217,7 @@ describe('usePermissions Hook', () => {
213
217
  it('shows loading state during refetch', async () => {
214
218
  mockGetPermissionMap.mockResolvedValue(mockPermissionMap);
215
219
 
216
- const { result } = renderHook(() => usePermissions(mockUserId, mockScope));
220
+ const { result } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
217
221
 
218
222
  await waitFor(() => {
219
223
  expect(result.current.isLoading).toBe(false);
@@ -233,7 +237,7 @@ describe('usePermissions Hook', () => {
233
237
  it('handles non-Error exceptions', async () => {
234
238
  mockGetPermissionMap.mockRejectedValue('String error');
235
239
 
236
- const { result } = renderHook(() => usePermissions(mockUserId, mockScope));
240
+ const { result } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
237
241
 
238
242
  await waitFor(() => {
239
243
  expect(result.current.error).toBeInstanceOf(Error);
@@ -246,7 +250,7 @@ describe('usePermissions Hook', () => {
246
250
  .mockRejectedValueOnce(new Error('Initial error'))
247
251
  .mockResolvedValueOnce(mockPermissionMap);
248
252
 
249
- const { result } = renderHook(() => usePermissions(mockUserId, mockScope));
253
+ const { result } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
250
254
 
251
255
  await waitFor(() => {
252
256
  expect(result.current.error).toBeDefined();
@@ -275,7 +279,7 @@ describe('usePermissions Hook', () => {
275
279
 
276
280
  mockGetPermissionMap.mockResolvedValue(superAdminPermissions);
277
281
 
278
- const { result } = renderHook(() => usePermissions(mockUserId, mockScope));
282
+ const { result } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
279
283
 
280
284
  await waitFor(() => {
281
285
  expect(result.current.isLoading).toBe(false);
@@ -304,7 +308,7 @@ describe('usePermissions Hook', () => {
304
308
 
305
309
  mockGetPermissionMap.mockResolvedValue(orgAdminPermissions);
306
310
 
307
- const { result } = renderHook(() => usePermissions(mockUserId, mockScope));
311
+ const { result } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
308
312
 
309
313
  await waitFor(() => {
310
314
  expect(result.current.isLoading).toBe(false);
@@ -333,7 +337,7 @@ describe('usePermissions Hook', () => {
333
337
 
334
338
  mockGetPermissionMap.mockResolvedValue(eventAdminPermissions);
335
339
 
336
- const { result } = renderHook(() => usePermissions(mockUserId, mockScope));
340
+ const { result } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
337
341
 
338
342
  await waitFor(() => {
339
343
  expect(result.current.isLoading).toBe(false);
@@ -363,7 +367,7 @@ describe('usePermissions Hook', () => {
363
367
 
364
368
  mockGetPermissionMap.mockResolvedValue(memberPermissions);
365
369
 
366
- const { result } = renderHook(() => usePermissions(mockUserId, mockScope));
370
+ const { result } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
367
371
 
368
372
  await waitFor(() => {
369
373
  expect(result.current.isLoading).toBe(false);
@@ -393,7 +397,7 @@ describe('usePermissions Hook', () => {
393
397
 
394
398
  mockGetPermissionMap.mockResolvedValue(participantPermissions);
395
399
 
396
- const { result } = renderHook(() => usePermissions(mockUserId, mockScope));
400
+ const { result } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
397
401
 
398
402
  await waitFor(() => {
399
403
  expect(result.current.isLoading).toBe(false);
@@ -465,7 +469,7 @@ describe('usePermissions Hook', () => {
465
469
 
466
470
  describe('Edge Cases', () => {
467
471
  it('handles null userId', async () => {
468
- const { result } = renderHook(() => usePermissions(null as any, mockScope));
472
+ const { result } = renderHook(() => usePermissions(null as any, mockOrgId, mockEventId, mockAppId));
469
473
 
470
474
  await waitFor(() => {
471
475
  expect(result.current.isLoading).toBe(false);
@@ -475,7 +479,7 @@ describe('usePermissions Hook', () => {
475
479
  });
476
480
 
477
481
  it('handles undefined userId', async () => {
478
- const { result } = renderHook(() => usePermissions(undefined as any, mockScope));
482
+ const { result } = renderHook(() => usePermissions(undefined as any, mockOrgId, mockEventId, mockAppId));
479
483
 
480
484
  await waitFor(() => {
481
485
  expect(result.current.isLoading).toBe(false);
@@ -487,7 +491,7 @@ describe('usePermissions Hook', () => {
487
491
  it('handles empty scope', async () => {
488
492
  mockGetPermissionMap.mockResolvedValue(mockPermissionMap);
489
493
 
490
- const { result } = renderHook(() => usePermissions(mockUserId, {} as any));
494
+ const { result } = renderHook(() => usePermissions(mockUserId, '', undefined, undefined));
491
495
 
492
496
  // Wait for the hook to complete loading
493
497
  await waitFor(() => {
@@ -506,7 +510,7 @@ describe('usePermissions Hook', () => {
506
510
 
507
511
  mockGetPermissionMap.mockResolvedValue(largePermissionMap);
508
512
 
509
- const { result } = renderHook(() => usePermissions(mockUserId, mockScope));
513
+ const { result } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
510
514
 
511
515
  await waitFor(() => {
512
516
  expect(result.current.isLoading).toBe(false);
@@ -517,7 +521,7 @@ describe('usePermissions Hook', () => {
517
521
  it('handles empty permission map', async () => {
518
522
  mockGetPermissionMap.mockResolvedValue({});
519
523
 
520
- const { result } = renderHook(() => usePermissions(mockUserId, mockScope));
524
+ const { result } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
521
525
 
522
526
  await waitFor(() => {
523
527
  expect(result.current.isLoading).toBe(false);
@@ -532,9 +536,8 @@ describe('usePermissions Hook', () => {
532
536
  });
533
537
 
534
538
  it('handles scope with empty organisationId', async () => {
535
- const invalidScope = { organisationId: '', eventId: 'event-123' };
536
539
 
537
- const { result } = renderHook(() => usePermissions(mockUserId, invalidScope));
540
+ const { result } = renderHook(() => usePermissions(mockUserId, '', 'event-123', undefined));
538
541
 
539
542
  await waitFor(() => {
540
543
  expect(result.current.isLoading).toBe(true);
@@ -544,9 +547,8 @@ describe('usePermissions Hook', () => {
544
547
  });
545
548
 
546
549
  it('handles scope with whitespace-only organisationId', async () => {
547
- const invalidScope = { organisationId: ' ', eventId: 'event-123' };
548
550
 
549
- const { result } = renderHook(() => usePermissions(mockUserId, invalidScope));
551
+ const { result } = renderHook(() => usePermissions(mockUserId, ' ', 'event-123', undefined));
550
552
 
551
553
  await waitFor(() => {
552
554
  expect(result.current.isLoading).toBe(true);
@@ -560,7 +562,7 @@ describe('usePermissions Hook', () => {
560
562
  it('maintains stable references for same permissions', async () => {
561
563
  mockGetPermissionMap.mockResolvedValue(mockPermissionMap);
562
564
 
563
- const { result, rerender } = renderHook(() => usePermissions(mockUserId, mockScope));
565
+ const { result, rerender } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
564
566
 
565
567
  await waitFor(() => {
566
568
  expect(result.current.isLoading).toBe(false);
@@ -580,7 +582,7 @@ describe('usePermissions Hook', () => {
580
582
  it('handles rapid permission checks efficiently', async () => {
581
583
  mockGetPermissionMap.mockResolvedValue(mockPermissionMap);
582
584
 
583
- const { result } = renderHook(() => usePermissions(mockUserId, mockScope));
585
+ const { result } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
584
586
 
585
587
  await waitFor(() => {
586
588
  expect(result.current.isLoading).toBe(false);
@@ -601,7 +603,7 @@ describe('usePermissions Hook', () => {
601
603
 
602
604
  describe('[integration] Cache Invalidation', () => {
603
605
  it('refetches after cache invalidation', async () => {
604
- const { result } = renderHook(() => usePermissions(mockUserId, mockScope));
606
+ const { result } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
605
607
 
606
608
  await waitFor(() => {
607
609
  expect(result.current.isLoading).toBe(false);
@@ -27,13 +27,20 @@ import { getRBACLogger } from '../config';
27
27
  * Hook to get user's permissions in a scope
28
28
  *
29
29
  * @param userId - User ID
30
- * @param scope - Scope for permission checking
30
+ * @param organisationId - Organisation ID
31
+ * @param eventId - Event ID (optional)
32
+ * @param appId - Application ID (optional)
31
33
  * @returns Permission state and methods
32
34
  *
33
35
  * @example
34
36
  * ```tsx
35
37
  * function MyComponent() {
36
- * const { permissions, isLoading, error } = usePermissions(userId, scope);
38
+ * const { permissions, isLoading, error } = usePermissions(
39
+ * userId,
40
+ * organisationId,
41
+ * eventId,
42
+ * appId
43
+ * );
37
44
  *
38
45
  * if (isLoading) return <div>Loading...</div>;
39
46
  * if (error) return <div>Error: {error.message}</div>;
@@ -47,29 +54,28 @@ import { getRBACLogger } from '../config';
47
54
  * }
48
55
  * ```
49
56
  */
50
- export function usePermissions(userId: UUID, scope: Scope) {
57
+ export function usePermissions(
58
+ userId: UUID,
59
+ organisationId: string | undefined,
60
+ eventId: string | undefined,
61
+ appId: string | undefined
62
+ ) {
51
63
  const [permissions, setPermissions] = useState<PermissionMap>({} as PermissionMap);
52
64
  const [isLoading, setIsLoading] = useState(true);
53
65
  const [error, setError] = useState<Error | null>(null);
54
66
  const isFetchingRef = useRef(false);
55
67
  const logger = getRBACLogger();
56
68
 
57
- // Extract individual values to ensure React detects changes
58
- // This is critical - React compares these values, not the object reference
59
- const orgId = scope.organisationId || '';
60
- const eventId = scope.eventId;
61
- const appId = scope.appId;
69
+ // Normalize organisationId to empty string if undefined
70
+ const orgId = organisationId || '';
62
71
 
63
72
  // Log when hook is called with new scope (every render)
64
73
  // This helps us see if React is calling the hook with updated scope
65
74
  logger.warn('[usePermissions] Hook called with scope', {
66
75
  userId,
67
- extractedOrgId: orgId,
68
- extractedEventId: eventId,
69
- extractedAppId: appId,
70
- scopeOrgId: scope.organisationId,
71
- scopeEventId: scope.eventId,
72
- scopeAppId: scope.appId,
76
+ organisationId: orgId,
77
+ eventId,
78
+ appId,
73
79
  hasAppId: !!appId,
74
80
  hasOrganisationId: !!orgId
75
81
  });
@@ -78,17 +84,17 @@ export function usePermissions(userId: UUID, scope: Scope) {
78
84
  React.useEffect(() => {
79
85
  logger.warn('[usePermissions] Scope changed (useEffect)', {
80
86
  userId,
81
- organisationId: scope.organisationId,
82
- eventId: scope.eventId,
83
- appId: scope.appId,
84
- hasAppId: !!scope.appId,
85
- hasOrganisationId: !!scope.organisationId
87
+ organisationId: orgId,
88
+ eventId,
89
+ appId,
90
+ hasAppId: !!appId,
91
+ hasOrganisationId: !!orgId
86
92
  });
87
- }, [scope.organisationId, scope.eventId, scope.appId, userId]);
93
+ }, [userId, orgId, eventId, appId]);
88
94
 
89
95
  // Add timeout for missing organisation context (3 seconds)
90
96
  useEffect(() => {
91
- if (!scope.organisationId || scope.organisationId === null || (typeof scope.organisationId === 'string' && scope.organisationId.trim() === '')) {
97
+ if (!orgId || orgId === null || (typeof orgId === 'string' && orgId.trim() === '')) {
92
98
  const timeoutId = setTimeout(() => {
93
99
  setError(new Error('Organisation context is required for permission checks'));
94
100
  setIsLoading(false);
@@ -100,7 +106,7 @@ export function usePermissions(userId: UUID, scope: Scope) {
100
106
  if (error?.message === 'Organisation context is required for permission checks') {
101
107
  setError(null);
102
108
  }
103
- }, [scope.organisationId, error]);
109
+ }, [orgId, error]);
104
110
 
105
111
  useEffect(() => {
106
112
  const fetchPermissions = async () => {
@@ -176,15 +182,15 @@ export function usePermissions(userId: UUID, scope: Scope) {
176
182
  setIsLoading(true);
177
183
  setError(null);
178
184
 
179
- // Build scope object from extracted values
180
- const scopeForFetch: Scope = {
185
+ // Build scope object for API call
186
+ const scope: Scope = {
181
187
  organisationId: orgId,
182
188
  eventId: eventId,
183
189
  appId: appId
184
190
  };
185
191
 
186
192
  // Fetch new permissions - don't clear old ones until we have new ones
187
- const permissionMap = await getPermissionMap({ userId, scope: scopeForFetch });
193
+ const permissionMap = await getPermissionMap({ userId, scope });
188
194
 
189
195
  logger.warn('[usePermissions] Permissions fetched successfully', {
190
196
  permissionCount: Object.keys(permissionMap).length,
@@ -211,7 +217,7 @@ export function usePermissions(userId: UUID, scope: Scope) {
211
217
  };
212
218
 
213
219
  fetchPermissions();
214
- }, [userId, scope.organisationId, scope.eventId, scope.appId]);
220
+ }, [userId, orgId, eventId, appId]);
215
221
 
216
222
  const hasPermission = useCallback((permission: Permission): boolean => {
217
223
  if (permissions['*']) {
@@ -248,7 +254,7 @@ export function usePermissions(userId: UUID, scope: Scope) {
248
254
 
249
255
  // Don't fetch permissions if scope is invalid (e.g., organisationId is null/empty)
250
256
  // IMPORTANT: Don't clear existing permissions - keep them until we have new ones
251
- if (!scope.organisationId || scope.organisationId === null || (typeof scope.organisationId === 'string' && scope.organisationId.trim() === '')) {
257
+ if (!orgId || orgId === null || (typeof orgId === 'string' && orgId.trim() === '')) {
252
258
  // Keep existing permissions, just mark as loading
253
259
  setIsLoading(true);
254
260
  setError(null);
@@ -260,6 +266,13 @@ export function usePermissions(userId: UUID, scope: Scope) {
260
266
  setIsLoading(true);
261
267
  setError(null);
262
268
 
269
+ // Build scope object for API call
270
+ const scope: Scope = {
271
+ organisationId: orgId,
272
+ eventId: eventId,
273
+ appId: appId
274
+ };
275
+
263
276
  // Fetch new permissions - don't clear old ones until we have new ones
264
277
  const permissionMap = await getPermissionMap({ userId, scope });
265
278
 
@@ -276,7 +289,7 @@ export function usePermissions(userId: UUID, scope: Scope) {
276
289
  setIsLoading(false);
277
290
  isFetchingRef.current = false;
278
291
  }
279
- }, [userId, scope.organisationId, scope.eventId, scope.appId]);
292
+ }, [userId, orgId, eventId, appId]);
280
293
 
281
294
  // Memoize the return object to prevent unnecessary re-renders
282
295
  return useMemo(() => ({