@jmruthers/pace-core 0.5.115 → 0.5.116

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 (234) hide show
  1. package/dist/{AuthService-CVgsgtaZ.d.ts → AuthService-D4646R4b.d.ts} +9 -4
  2. package/dist/{DataTable-H5KJCAIS.js → DataTable-ZOAKQ3SU.js} +10 -9
  3. package/dist/{UnifiedAuthProvider-KZZUO27W.js → UnifiedAuthProvider-YFN7YGVN.js} +4 -3
  4. package/dist/{api-PKU4PUBO.js → api-TNIBJWLM.js} +3 -3
  5. package/dist/{audit-H4YJJF7R.js → audit-T36HM7IM.js} +2 -2
  6. package/dist/{chunk-SYXOZQ4P.js → chunk-2GJ5GL77.js} +1 -1
  7. package/dist/chunk-2GJ5GL77.js.map +1 -0
  8. package/dist/{chunk-XYRZV7R5.js → chunk-2LM4QQGH.js} +30 -34
  9. package/dist/chunk-2LM4QQGH.js.map +1 -0
  10. package/dist/{chunk-3OGQLOJM.js → chunk-3DBFLLLU.js} +30 -1
  11. package/dist/chunk-3DBFLLLU.js.map +1 -0
  12. package/dist/{chunk-KTHLNIMA.js → chunk-ECOVPXYS.js} +13 -62
  13. package/dist/chunk-ECOVPXYS.js.map +1 -0
  14. package/dist/{chunk-OO3V7W4H.js → chunk-KA3PSVNV.js} +87 -40
  15. package/dist/chunk-KA3PSVNV.js.map +1 -0
  16. package/dist/{chunk-HKWQN44G.js → chunk-KMPWND3F.js} +15 -15
  17. package/dist/{chunk-L36JW4KV.js → chunk-LFS45U62.js} +2 -2
  18. package/dist/{chunk-NEONKMTU.js → chunk-LZYHAL7Y.js} +9 -4
  19. package/dist/{chunk-NEONKMTU.js.map → chunk-LZYHAL7Y.js.map} +1 -1
  20. package/dist/{chunk-BUN7NMV7.js → chunk-O3FTRYEU.js} +2 -2
  21. package/dist/{chunk-F6QB26OS.js → chunk-P3PUOL6B.js} +80 -8
  22. package/dist/chunk-P3PUOL6B.js.map +1 -0
  23. package/dist/{chunk-ZPXWJA4H.js → chunk-PHDAXDHB.js} +131 -5
  24. package/dist/chunk-PHDAXDHB.js.map +1 -0
  25. package/dist/chunk-UJI6WSMD.js +201 -0
  26. package/dist/{chunk-5CDJCTOO.js.map → chunk-UJI6WSMD.js.map} +1 -1
  27. package/dist/{chunk-OUU3SP6I.js → chunk-UKZWNQMB.js} +50 -7
  28. package/dist/{chunk-OUU3SP6I.js.map → chunk-UKZWNQMB.js.map} +1 -1
  29. package/dist/{chunk-7H75SHXZ.js → chunk-VN3OOE35.js} +2 -2
  30. package/dist/{chunk-QKIVSZ2O.js → chunk-WP5I5GLN.js} +2 -2
  31. package/dist/components.d.ts +1 -1
  32. package/dist/components.js +12 -11
  33. package/dist/components.js.map +1 -1
  34. package/dist/hooks.d.ts +1 -1
  35. package/dist/hooks.js +10 -9
  36. package/dist/hooks.js.map +1 -1
  37. package/dist/index.d.ts +4 -4
  38. package/dist/index.js +19 -16
  39. package/dist/index.js.map +1 -1
  40. package/dist/providers.d.ts +2 -2
  41. package/dist/providers.js +3 -2
  42. package/dist/rbac/index.d.ts +82 -1
  43. package/dist/rbac/index.js +13 -10
  44. package/dist/{useToast-DVT4dMtf.d.ts → useToast-Cs_g32bg.d.ts} +1 -1
  45. package/dist/utils.js +6 -4
  46. package/dist/utils.js.map +1 -1
  47. package/dist/validation.js +3 -1
  48. package/dist/validation.js.map +1 -1
  49. package/docs/README.md +4 -0
  50. package/docs/api/classes/ColumnFactory.md +1 -1
  51. package/docs/api/classes/ErrorBoundary.md +1 -1
  52. package/docs/api/classes/InvalidScopeError.md +1 -1
  53. package/docs/api/classes/MissingUserContextError.md +1 -1
  54. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  55. package/docs/api/classes/PermissionDeniedError.md +1 -1
  56. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  57. package/docs/api/classes/RBACAuditManager.md +35 -12
  58. package/docs/api/classes/RBACCache.md +1 -1
  59. package/docs/api/classes/RBACEngine.md +1 -1
  60. package/docs/api/classes/RBACError.md +1 -1
  61. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  62. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  63. package/docs/api/classes/StorageUtils.md +1 -1
  64. package/docs/api/enums/FileCategory.md +1 -1
  65. package/docs/api/interfaces/AggregateConfig.md +1 -1
  66. package/docs/api/interfaces/ButtonProps.md +1 -1
  67. package/docs/api/interfaces/CardProps.md +1 -1
  68. package/docs/api/interfaces/ColorPalette.md +1 -1
  69. package/docs/api/interfaces/ColorShade.md +1 -1
  70. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  71. package/docs/api/interfaces/DataRecord.md +1 -1
  72. package/docs/api/interfaces/DataTableAction.md +1 -1
  73. package/docs/api/interfaces/DataTableColumn.md +1 -1
  74. package/docs/api/interfaces/DataTableProps.md +1 -1
  75. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  76. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  77. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  78. package/docs/api/interfaces/EventAppRoleData.md +71 -0
  79. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  80. package/docs/api/interfaces/FileMetadata.md +1 -1
  81. package/docs/api/interfaces/FileReference.md +1 -1
  82. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  83. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  84. package/docs/api/interfaces/FileUploadProps.md +1 -1
  85. package/docs/api/interfaces/FooterProps.md +1 -1
  86. package/docs/api/interfaces/GrantEventAppRoleParams.md +122 -0
  87. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  88. package/docs/api/interfaces/InputProps.md +1 -1
  89. package/docs/api/interfaces/LabelProps.md +1 -1
  90. package/docs/api/interfaces/LoginFormProps.md +1 -1
  91. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  92. package/docs/api/interfaces/NavigationContextType.md +1 -1
  93. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  94. package/docs/api/interfaces/NavigationItem.md +1 -1
  95. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  96. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  97. package/docs/api/interfaces/Organisation.md +1 -1
  98. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  99. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  100. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  101. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  102. package/docs/api/interfaces/PaceAppLayoutProps.md +27 -27
  103. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  104. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  105. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  106. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  107. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  108. package/docs/api/interfaces/PaletteData.md +1 -1
  109. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  110. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  111. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  112. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  113. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  114. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  115. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  116. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  117. package/docs/api/interfaces/RBACConfig.md +1 -1
  118. package/docs/api/interfaces/RBACLogger.md +1 -1
  119. package/docs/api/interfaces/RevokeEventAppRoleParams.md +100 -0
  120. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  121. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  122. package/docs/api/interfaces/RoleManagementResult.md +52 -0
  123. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  124. package/docs/api/interfaces/RouteConfig.md +1 -1
  125. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  126. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  127. package/docs/api/interfaces/StorageConfig.md +1 -1
  128. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  129. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  130. package/docs/api/interfaces/StorageListOptions.md +1 -1
  131. package/docs/api/interfaces/StorageListResult.md +1 -1
  132. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  133. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  134. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  135. package/docs/api/interfaces/StyleImport.md +1 -1
  136. package/docs/api/interfaces/SwitchProps.md +1 -1
  137. package/docs/api/interfaces/ToastActionElement.md +1 -1
  138. package/docs/api/interfaces/ToastProps.md +1 -1
  139. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  140. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  141. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  142. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  143. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  144. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  145. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  146. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  147. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  148. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  149. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  150. package/docs/api/interfaces/UserEventAccess.md +1 -1
  151. package/docs/api/interfaces/UserMenuProps.md +1 -1
  152. package/docs/api/interfaces/UserProfile.md +1 -1
  153. package/docs/api/modules.md +41 -14
  154. package/docs/architecture/rpc-function-standards.md +193 -0
  155. package/package.json +1 -1
  156. package/src/__tests__/TEST_STANDARD.md +244 -2
  157. package/src/components/DataTable/__tests__/a11y.basic.test.tsx +46 -16
  158. package/src/components/DataTable/__tests__/keyboard.test.tsx +276 -217
  159. package/src/components/DataTable/components/DataTableCore.tsx +29 -2
  160. package/src/components/DataTable/components/DataTableToolbar.tsx +3 -2
  161. package/src/components/DataTable/components/EditableRow.tsx +18 -1
  162. package/src/components/DataTable/components/ViewRowModal.tsx +1 -1
  163. package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +735 -0
  164. package/src/components/DataTable/components/__tests__/BulkOperationsDropdown.test.tsx +572 -0
  165. package/src/components/DataTable/components/__tests__/ColumnVisibilityDropdown.test.tsx +708 -0
  166. package/src/components/DataTable/components/__tests__/DataTableErrorBoundary.test.tsx +451 -0
  167. package/src/components/DataTable/components/__tests__/DataTableModals.test.tsx +456 -0
  168. package/src/components/DataTable/components/__tests__/EditableRow.test.tsx +454 -0
  169. package/src/components/DataTable/components/__tests__/ExpandButton.test.tsx +462 -0
  170. package/src/components/DataTable/components/__tests__/FilterRow.test.tsx +423 -0
  171. package/src/components/DataTable/components/__tests__/GroupHeader.test.tsx +393 -0
  172. package/src/components/DataTable/components/__tests__/GroupingDropdown.test.tsx +617 -0
  173. package/src/components/DataTable/components/__tests__/ImportModal.test.tsx +734 -0
  174. package/src/components/DataTable/components/__tests__/ViewRowModal.test.tsx +412 -0
  175. package/src/components/DataTable/hooks/useTableHandlers.ts +4 -0
  176. package/src/components/EventSelector/EventSelector.tsx +5 -25
  177. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +12 -7
  178. package/src/components/PaceAppLayout/PaceAppLayout.tsx +4 -0
  179. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.accessibility.test.tsx +7 -2
  180. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.integration.test.tsx +13 -8
  181. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +109 -100
  182. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.security.test.tsx +18 -13
  183. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.unit.test.tsx +17 -12
  184. package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +2 -0
  185. package/src/components/PaceLoginPage/PaceLoginPage.tsx +11 -1
  186. package/src/components/PasswordReset/PasswordChangeForm.test.tsx +2 -2
  187. package/src/components/ProtectedRoute/ProtectedRoute.test.tsx +648 -0
  188. package/src/components/ProtectedRoute/ProtectedRoute.tsx +10 -7
  189. package/src/components/PublicLayout/__tests__/PublicErrorBoundary.test.tsx +4 -12
  190. package/src/components/Select/Select.tsx +8 -0
  191. package/src/components/Toast/Toast.tsx +1 -1
  192. package/src/hooks/__tests__/usePublicEvent.simple.test.ts +367 -3
  193. package/src/hooks/__tests__/usePublicFileDisplay.test.ts +916 -0
  194. package/src/hooks/useEventTheme.ts +49 -18
  195. package/src/hooks/usePermissionCache.ts +5 -3
  196. package/src/hooks/useSecureDataAccess.ts +11 -1
  197. package/src/hooks/useToast.ts +1 -1
  198. package/src/providers/services/EventServiceProvider.tsx +15 -8
  199. package/src/rbac/__tests__/cache-invalidation.test.ts +385 -0
  200. package/src/rbac/audit.test.ts +206 -0
  201. package/src/rbac/audit.ts +37 -2
  202. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +26 -23
  203. package/src/rbac/errors.test.ts +340 -0
  204. package/src/rbac/hooks/index.ts +9 -0
  205. package/src/rbac/hooks/useResolvedScope.test.ts +1063 -0
  206. package/src/rbac/hooks/useRoleManagement.test.ts +908 -0
  207. package/src/rbac/hooks/useRoleManagement.ts +255 -0
  208. package/src/services/AuthService.ts +10 -0
  209. package/src/services/EventService.ts +111 -50
  210. package/src/services/__tests__/AuthService.test.ts +1 -1
  211. package/src/services/__tests__/EventService.test.ts +60 -45
  212. package/src/services/interfaces/IEventService.ts +1 -1
  213. package/src/utils/__tests__/deviceFingerprint.unit.test.ts +320 -0
  214. package/src/utils/__tests__/logger.unit.test.ts +398 -0
  215. package/src/utils/__tests__/validation.unit.test.ts +225 -1
  216. package/src/utils/file-reference.test.ts +214 -0
  217. package/dist/chunk-3OGQLOJM.js.map +0 -1
  218. package/dist/chunk-5CDJCTOO.js +0 -190
  219. package/dist/chunk-F6QB26OS.js.map +0 -1
  220. package/dist/chunk-KTHLNIMA.js.map +0 -1
  221. package/dist/chunk-OO3V7W4H.js.map +0 -1
  222. package/dist/chunk-SYXOZQ4P.js.map +0 -1
  223. package/dist/chunk-XYRZV7R5.js.map +0 -1
  224. package/dist/chunk-ZPXWJA4H.js.map +0 -1
  225. package/src/rbac/audit-enhanced.ts +0 -351
  226. /package/dist/{DataTable-H5KJCAIS.js.map → DataTable-ZOAKQ3SU.js.map} +0 -0
  227. /package/dist/{UnifiedAuthProvider-KZZUO27W.js.map → UnifiedAuthProvider-YFN7YGVN.js.map} +0 -0
  228. /package/dist/{api-PKU4PUBO.js.map → api-TNIBJWLM.js.map} +0 -0
  229. /package/dist/{audit-H4YJJF7R.js.map → audit-T36HM7IM.js.map} +0 -0
  230. /package/dist/{chunk-HKWQN44G.js.map → chunk-KMPWND3F.js.map} +0 -0
  231. /package/dist/{chunk-L36JW4KV.js.map → chunk-LFS45U62.js.map} +0 -0
  232. /package/dist/{chunk-BUN7NMV7.js.map → chunk-O3FTRYEU.js.map} +0 -0
  233. /package/dist/{chunk-7H75SHXZ.js.map → chunk-VN3OOE35.js.map} +0 -0
  234. /package/dist/{chunk-QKIVSZ2O.js.map → chunk-WP5I5GLN.js.map} +0 -0
@@ -349,6 +349,212 @@ describe('RBACAuditManager', () => {
349
349
  });
350
350
  });
351
351
 
352
+ describe('Fallback Logging', () => {
353
+ let consoleLogSpy: any;
354
+ let consoleWarnSpy: any;
355
+
356
+ beforeEach(() => {
357
+ consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
358
+ consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
359
+ });
360
+
361
+ afterEach(() => {
362
+ consoleLogSpy.mockRestore();
363
+ consoleWarnSpy.mockRestore();
364
+ });
365
+
366
+ it('uses fallback logging when database insert fails', async () => {
367
+ const dbError = {
368
+ message: 'Database connection failed',
369
+ code: 'PGRST116',
370
+ details: 'Connection timeout',
371
+ hint: 'Check network connectivity'
372
+ };
373
+
374
+ mockSupabase.from().insert.mockResolvedValue({
375
+ data: null,
376
+ error: dbError
377
+ });
378
+
379
+ const event: PermissionCheckAuditEvent = {
380
+ type: 'permission_check',
381
+ userId: 'user-123' as UUID,
382
+ organisationId: 'org-456' as UUID,
383
+ permission: 'read:users',
384
+ decision: true,
385
+ source: 'api' as AuditEventSource,
386
+ duration_ms: 100
387
+ };
388
+
389
+ await auditManager.emitPermissionCheck(event);
390
+
391
+ // Verify fallback logging was called
392
+ expect(consoleLogSpy).toHaveBeenCalledWith(
393
+ '[RBAC Audit Fallback]',
394
+ expect.objectContaining({
395
+ timestamp: expect.any(String),
396
+ event: expect.objectContaining({
397
+ type: 'permission_check',
398
+ userId: 'user-123',
399
+ organisationId: 'org-456',
400
+ permission: 'read:users'
401
+ }),
402
+ error: 'Database connection failed',
403
+ note: 'Database audit logging failed, using console fallback'
404
+ })
405
+ );
406
+ });
407
+
408
+ it('uses fallback logging when exception occurs', async () => {
409
+ const networkError = new Error('Network error');
410
+ mockSupabase.from().insert.mockRejectedValue(networkError);
411
+
412
+ const event: PermissionCheckAuditEvent = {
413
+ type: 'permission_check',
414
+ userId: 'user-123' as UUID,
415
+ organisationId: 'org-456' as UUID,
416
+ permission: 'read:users',
417
+ decision: true,
418
+ source: 'api' as AuditEventSource,
419
+ duration_ms: 100
420
+ };
421
+
422
+ await auditManager.emitPermissionCheck(event);
423
+
424
+ // Verify fallback logging was called
425
+ expect(consoleLogSpy).toHaveBeenCalledWith(
426
+ '[RBAC Audit Fallback]',
427
+ expect.objectContaining({
428
+ timestamp: expect.any(String),
429
+ event: expect.objectContaining({
430
+ type: 'permission_check',
431
+ userId: 'user-123',
432
+ organisationId: 'org-456',
433
+ permission: 'read:users',
434
+ decision: true,
435
+ source: 'api',
436
+ duration_ms: 100
437
+ }),
438
+ error: 'Network error', // Error is converted to string message
439
+ note: 'Database audit logging failed, using console fallback'
440
+ })
441
+ );
442
+ });
443
+
444
+ it('can disable fallback logging', async () => {
445
+ auditManager.setFallbackEnabled(false);
446
+
447
+ const dbError = {
448
+ message: 'Database error',
449
+ code: 'PGRST116'
450
+ };
451
+
452
+ mockSupabase.from().insert.mockResolvedValue({
453
+ data: null,
454
+ error: dbError
455
+ });
456
+
457
+ const event: PermissionCheckAuditEvent = {
458
+ type: 'permission_check',
459
+ userId: 'user-123' as UUID,
460
+ organisationId: 'org-456' as UUID,
461
+ permission: 'read:users',
462
+ decision: true,
463
+ source: 'api' as AuditEventSource,
464
+ duration_ms: 100
465
+ };
466
+
467
+ await auditManager.emitPermissionCheck(event);
468
+
469
+ // Verify fallback logging was NOT called
470
+ expect(consoleLogSpy).not.toHaveBeenCalled();
471
+
472
+ // But warning should still be logged
473
+ expect(consoleWarnSpy).toHaveBeenCalled();
474
+ });
475
+
476
+ it('can re-enable fallback logging', async () => {
477
+ auditManager.setFallbackEnabled(false);
478
+ auditManager.setFallbackEnabled(true);
479
+
480
+ const dbError = {
481
+ message: 'Database error',
482
+ code: 'PGRST116'
483
+ };
484
+
485
+ mockSupabase.from().insert.mockResolvedValue({
486
+ data: null,
487
+ error: dbError
488
+ });
489
+
490
+ const event: PermissionCheckAuditEvent = {
491
+ type: 'permission_check',
492
+ userId: 'user-123' as UUID,
493
+ organisationId: 'org-456' as UUID,
494
+ permission: 'read:users',
495
+ decision: true,
496
+ source: 'api' as AuditEventSource,
497
+ duration_ms: 100
498
+ };
499
+
500
+ await auditManager.emitPermissionCheck(event);
501
+
502
+ // Verify fallback logging was called after re-enabling
503
+ expect(consoleLogSpy).toHaveBeenCalled();
504
+ });
505
+
506
+ it('fallback logging includes correct event data', async () => {
507
+ const dbError = {
508
+ message: 'Database error'
509
+ };
510
+
511
+ mockSupabase.from().insert.mockResolvedValue({
512
+ data: null,
513
+ error: dbError
514
+ });
515
+
516
+ const event: PermissionCheckAuditEvent = {
517
+ type: 'permission_check',
518
+ userId: 'user-123' as UUID,
519
+ organisationId: 'org-456' as UUID,
520
+ eventId: 'event-789',
521
+ appId: 'app-101' as UUID,
522
+ permission: 'read:users',
523
+ decision: false,
524
+ source: 'ui' as AuditEventSource,
525
+ bypass: true,
526
+ duration_ms: 250,
527
+ cache_hit: true,
528
+ cache_source: 'memory',
529
+ metadata: { test: 'data' }
530
+ };
531
+
532
+ await auditManager.emitPermissionCheck(event);
533
+
534
+ const fallbackCall = consoleLogSpy.mock.calls[0];
535
+ expect(fallbackCall[0]).toBe('[RBAC Audit Fallback]');
536
+ expect(fallbackCall[1]).toMatchObject({
537
+ event: {
538
+ type: 'permission_check',
539
+ userId: 'user-123',
540
+ organisationId: 'org-456',
541
+ eventId: 'event-789',
542
+ appId: 'app-101',
543
+ permission: 'read:users',
544
+ decision: false,
545
+ source: 'ui',
546
+ bypass: true,
547
+ duration_ms: 250,
548
+ cache_hit: true,
549
+ cache_source: 'memory',
550
+ metadata: { test: 'data' }
551
+ },
552
+ error: 'Database error',
553
+ note: 'Database audit logging failed, using console fallback'
554
+ });
555
+ });
556
+ });
557
+
352
558
  describe('Event Retrieval', () => {
353
559
  it('retrieves audit events for user', async () => {
354
560
  const mockEvents = [
package/src/rbac/audit.ts CHANGED
@@ -108,6 +108,7 @@ export type AuditEventPayload =
108
108
  export class RBACAuditManager {
109
109
  private supabase: SupabaseClient<Database>;
110
110
  private enabled: boolean = true;
111
+ private fallbackEnabled: boolean = true;
111
112
 
112
113
  constructor(supabase: SupabaseClient<Database>) {
113
114
  this.supabase = supabase;
@@ -131,6 +132,15 @@ export class RBACAuditManager {
131
132
  return this.enabled;
132
133
  }
133
134
 
135
+ /**
136
+ * Enable or disable fallback logging (console logging when database fails)
137
+ *
138
+ * @param enabled - Whether to enable fallback logging
139
+ */
140
+ setFallbackEnabled(enabled: boolean): void {
141
+ this.fallbackEnabled = enabled;
142
+ }
143
+
134
144
  /**
135
145
  * Emit an audit event
136
146
  *
@@ -211,7 +221,7 @@ export class RBACAuditManager {
211
221
  .insert([auditEvent]);
212
222
 
213
223
  if (error) {
214
- // Log the error for debugging but don't throw
224
+ // Log the error for debugging
215
225
  console.warn('[RBAC Audit] Failed to insert audit event:', {
216
226
  error: error.message,
217
227
  code: error.code,
@@ -219,13 +229,38 @@ export class RBACAuditManager {
219
229
  hint: error.hint,
220
230
  event: auditEvent
221
231
  });
232
+
233
+ // Use fallback logging if enabled
234
+ if (this.fallbackEnabled) {
235
+ this.logFallbackEvent(event, error);
236
+ }
222
237
  }
223
238
  } catch (error) {
224
- // Log unexpected errors but don't throw
239
+ // Log unexpected errors
225
240
  console.error('[RBAC Audit] Unexpected error during audit logging:', error);
241
+
242
+ // Use fallback logging if enabled
243
+ if (this.fallbackEnabled) {
244
+ this.logFallbackEvent(event, error);
245
+ }
226
246
  }
227
247
  }
228
248
 
249
+ /**
250
+ * Log event to console as fallback when database logging fails
251
+ *
252
+ * @param event - Audit event payload
253
+ * @param error - The error that occurred
254
+ */
255
+ private logFallbackEvent(event: AuditEventPayload, error: any): void {
256
+ console.log('[RBAC Audit Fallback]', {
257
+ timestamp: new Date().toISOString(),
258
+ event,
259
+ error: error?.message || error,
260
+ note: 'Database audit logging failed, using console fallback'
261
+ });
262
+ }
263
+
229
264
  /**
230
265
  * Emit a permission check audit event
231
266
  *
@@ -711,9 +711,10 @@ describe('PagePermissionGuard Component', () => {
711
711
  });
712
712
 
713
713
  describe('Security Features', () => {
714
- it('prevents bypassing in strict mode', async () => {
714
+ it('invokes onDenied callback when strict mode blocks access', async () => {
715
715
  const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
716
-
716
+ const onDenied = vi.fn();
717
+
717
718
  mockUseCan.mockReturnValue({
718
719
  can: false,
719
720
  isLoading: false,
@@ -726,61 +727,63 @@ describe('PagePermissionGuard Component', () => {
726
727
  operation={mockOperation}
727
728
  strictMode={true}
728
729
  fallback={<TestFallback />}
730
+ onDenied={onDenied}
729
731
  >
730
732
  <TestComponent>Protected Page</TestComponent>
731
733
  </PagePermissionGuard>
732
734
  );
733
735
 
734
736
  await waitFor(() => {
735
- expect(screen.getByText('Checking permissions...')).toBeInTheDocument();
737
+ expect(screen.getByTestId('test-fallback')).toBeInTheDocument();
736
738
  }, { interval: 10 });
737
739
 
738
- expect(consoleSpy).toHaveBeenCalledWith(
739
- expect.stringContaining('STRICT MODE VIOLATION'),
740
- expect.objectContaining({
741
- pageName: mockPageName,
742
- operation: mockOperation,
743
- userId: 'user-123'
744
- })
745
- );
740
+ expect(onDenied).toHaveBeenCalledWith(mockPageName, mockOperation);
746
741
 
747
742
  consoleSpy.mockRestore();
748
743
  });
749
744
 
750
- it('logs page access attempts for audit', async () => {
745
+ it('does not call onDenied multiple times when audit logging rerenders', async () => {
751
746
  const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
752
-
747
+ const onDenied = vi.fn();
748
+
753
749
  mockUseCan.mockReturnValue({
754
750
  can: false,
755
751
  isLoading: false,
756
752
  error: null
757
753
  });
758
754
 
759
- render(
755
+ const { rerender } = render(
760
756
  <PagePermissionGuard
761
757
  pageName={mockPageName}
762
758
  operation={mockOperation}
763
759
  auditLog={true}
764
760
  fallback={<TestFallback />}
761
+ onDenied={onDenied}
765
762
  >
766
763
  <TestComponent>Protected Page</TestComponent>
767
764
  </PagePermissionGuard>
768
765
  );
769
766
 
770
767
  await waitFor(() => {
771
- expect(screen.getByText('Checking permissions...')).toBeInTheDocument();
768
+ expect(screen.getByTestId('test-fallback')).toBeInTheDocument();
772
769
  }, { interval: 10 });
773
770
 
774
- expect(consoleSpy).toHaveBeenCalledWith(
775
- expect.stringContaining('Page access attempt'),
776
- expect.objectContaining({
777
- pageName: mockPageName,
778
- operation: mockOperation,
779
- userId: 'user-123',
780
- allowed: false
781
- })
771
+ rerender(
772
+ <PagePermissionGuard
773
+ pageName={mockPageName}
774
+ operation={mockOperation}
775
+ auditLog={true}
776
+ fallback={<TestFallback />}
777
+ onDenied={onDenied}
778
+ >
779
+ <TestComponent>Protected Page</TestComponent>
780
+ </PagePermissionGuard>
782
781
  );
783
782
 
783
+ await waitFor(() => {
784
+ expect(onDenied).toHaveBeenCalledTimes(1);
785
+ }, { interval: 10 });
786
+
784
787
  consoleSpy.mockRestore();
785
788
  });
786
789