@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.
- package/dist/{AuthService-CVgsgtaZ.d.ts → AuthService-D4646R4b.d.ts} +9 -4
- package/dist/{DataTable-H5KJCAIS.js → DataTable-ZOAKQ3SU.js} +10 -9
- package/dist/{UnifiedAuthProvider-KZZUO27W.js → UnifiedAuthProvider-YFN7YGVN.js} +4 -3
- package/dist/{api-PKU4PUBO.js → api-TNIBJWLM.js} +3 -3
- package/dist/{audit-H4YJJF7R.js → audit-T36HM7IM.js} +2 -2
- package/dist/{chunk-SYXOZQ4P.js → chunk-2GJ5GL77.js} +1 -1
- package/dist/chunk-2GJ5GL77.js.map +1 -0
- package/dist/{chunk-XYRZV7R5.js → chunk-2LM4QQGH.js} +30 -34
- package/dist/chunk-2LM4QQGH.js.map +1 -0
- package/dist/{chunk-3OGQLOJM.js → chunk-3DBFLLLU.js} +30 -1
- package/dist/chunk-3DBFLLLU.js.map +1 -0
- package/dist/{chunk-KTHLNIMA.js → chunk-ECOVPXYS.js} +13 -62
- package/dist/chunk-ECOVPXYS.js.map +1 -0
- package/dist/{chunk-OO3V7W4H.js → chunk-KA3PSVNV.js} +87 -40
- package/dist/chunk-KA3PSVNV.js.map +1 -0
- package/dist/{chunk-HKWQN44G.js → chunk-KMPWND3F.js} +15 -15
- package/dist/{chunk-L36JW4KV.js → chunk-LFS45U62.js} +2 -2
- package/dist/{chunk-NEONKMTU.js → chunk-LZYHAL7Y.js} +9 -4
- package/dist/{chunk-NEONKMTU.js.map → chunk-LZYHAL7Y.js.map} +1 -1
- package/dist/{chunk-BUN7NMV7.js → chunk-O3FTRYEU.js} +2 -2
- package/dist/{chunk-F6QB26OS.js → chunk-P3PUOL6B.js} +80 -8
- package/dist/chunk-P3PUOL6B.js.map +1 -0
- package/dist/{chunk-ZPXWJA4H.js → chunk-PHDAXDHB.js} +131 -5
- package/dist/chunk-PHDAXDHB.js.map +1 -0
- package/dist/chunk-UJI6WSMD.js +201 -0
- package/dist/{chunk-5CDJCTOO.js.map → chunk-UJI6WSMD.js.map} +1 -1
- package/dist/{chunk-OUU3SP6I.js → chunk-UKZWNQMB.js} +50 -7
- package/dist/{chunk-OUU3SP6I.js.map → chunk-UKZWNQMB.js.map} +1 -1
- package/dist/{chunk-7H75SHXZ.js → chunk-VN3OOE35.js} +2 -2
- package/dist/{chunk-QKIVSZ2O.js → chunk-WP5I5GLN.js} +2 -2
- package/dist/components.d.ts +1 -1
- package/dist/components.js +12 -11
- package/dist/components.js.map +1 -1
- package/dist/hooks.d.ts +1 -1
- package/dist/hooks.js +10 -9
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +19 -16
- package/dist/index.js.map +1 -1
- package/dist/providers.d.ts +2 -2
- package/dist/providers.js +3 -2
- package/dist/rbac/index.d.ts +82 -1
- package/dist/rbac/index.js +13 -10
- package/dist/{useToast-DVT4dMtf.d.ts → useToast-Cs_g32bg.d.ts} +1 -1
- package/dist/utils.js +6 -4
- package/dist/utils.js.map +1 -1
- package/dist/validation.js +3 -1
- package/dist/validation.js.map +1 -1
- package/docs/README.md +4 -0
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +1 -1
- package/docs/api/classes/MissingUserContextError.md +1 -1
- package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
- package/docs/api/classes/PermissionDeniedError.md +1 -1
- package/docs/api/classes/PublicErrorBoundary.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +35 -12
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +1 -1
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +1 -1
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CardProps.md +1 -1
- package/docs/api/interfaces/ColorPalette.md +1 -1
- package/docs/api/interfaces/ColorShade.md +1 -1
- package/docs/api/interfaces/DataAccessRecord.md +1 -1
- package/docs/api/interfaces/DataRecord.md +1 -1
- package/docs/api/interfaces/DataTableAction.md +1 -1
- package/docs/api/interfaces/DataTableColumn.md +1 -1
- package/docs/api/interfaces/DataTableProps.md +1 -1
- package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/EventAppRoleData.md +71 -0
- package/docs/api/interfaces/FileDisplayProps.md +1 -1
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/GrantEventAppRoleParams.md +122 -0
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
- package/docs/api/interfaces/NavigationContextType.md +1 -1
- package/docs/api/interfaces/NavigationGuardProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +1 -1
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +27 -27
- package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
- package/docs/api/interfaces/PageAccessRecord.md +1 -1
- package/docs/api/interfaces/PagePermissionContextType.md +1 -1
- package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
- package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RevokeEventAppRoleParams.md +100 -0
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RoleManagementResult.md +52 -0
- package/docs/api/interfaces/RouteAccessRecord.md +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/SwitchProps.md +1 -1
- package/docs/api/interfaces/ToastActionElement.md +1 -1
- package/docs/api/interfaces/ToastProps.md +1 -1
- package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/UserEventAccess.md +1 -1
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +41 -14
- package/docs/architecture/rpc-function-standards.md +193 -0
- package/package.json +1 -1
- package/src/__tests__/TEST_STANDARD.md +244 -2
- package/src/components/DataTable/__tests__/a11y.basic.test.tsx +46 -16
- package/src/components/DataTable/__tests__/keyboard.test.tsx +276 -217
- package/src/components/DataTable/components/DataTableCore.tsx +29 -2
- package/src/components/DataTable/components/DataTableToolbar.tsx +3 -2
- package/src/components/DataTable/components/EditableRow.tsx +18 -1
- package/src/components/DataTable/components/ViewRowModal.tsx +1 -1
- package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +735 -0
- package/src/components/DataTable/components/__tests__/BulkOperationsDropdown.test.tsx +572 -0
- package/src/components/DataTable/components/__tests__/ColumnVisibilityDropdown.test.tsx +708 -0
- package/src/components/DataTable/components/__tests__/DataTableErrorBoundary.test.tsx +451 -0
- package/src/components/DataTable/components/__tests__/DataTableModals.test.tsx +456 -0
- package/src/components/DataTable/components/__tests__/EditableRow.test.tsx +454 -0
- package/src/components/DataTable/components/__tests__/ExpandButton.test.tsx +462 -0
- package/src/components/DataTable/components/__tests__/FilterRow.test.tsx +423 -0
- package/src/components/DataTable/components/__tests__/GroupHeader.test.tsx +393 -0
- package/src/components/DataTable/components/__tests__/GroupingDropdown.test.tsx +617 -0
- package/src/components/DataTable/components/__tests__/ImportModal.test.tsx +734 -0
- package/src/components/DataTable/components/__tests__/ViewRowModal.test.tsx +412 -0
- package/src/components/DataTable/hooks/useTableHandlers.ts +4 -0
- package/src/components/EventSelector/EventSelector.tsx +5 -25
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +12 -7
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +4 -0
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.accessibility.test.tsx +7 -2
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.integration.test.tsx +13 -8
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +109 -100
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.security.test.tsx +18 -13
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.unit.test.tsx +17 -12
- package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +2 -0
- package/src/components/PaceLoginPage/PaceLoginPage.tsx +11 -1
- package/src/components/PasswordReset/PasswordChangeForm.test.tsx +2 -2
- package/src/components/ProtectedRoute/ProtectedRoute.test.tsx +648 -0
- package/src/components/ProtectedRoute/ProtectedRoute.tsx +10 -7
- package/src/components/PublicLayout/__tests__/PublicErrorBoundary.test.tsx +4 -12
- package/src/components/Select/Select.tsx +8 -0
- package/src/components/Toast/Toast.tsx +1 -1
- package/src/hooks/__tests__/usePublicEvent.simple.test.ts +367 -3
- package/src/hooks/__tests__/usePublicFileDisplay.test.ts +916 -0
- package/src/hooks/useEventTheme.ts +49 -18
- package/src/hooks/usePermissionCache.ts +5 -3
- package/src/hooks/useSecureDataAccess.ts +11 -1
- package/src/hooks/useToast.ts +1 -1
- package/src/providers/services/EventServiceProvider.tsx +15 -8
- package/src/rbac/__tests__/cache-invalidation.test.ts +385 -0
- package/src/rbac/audit.test.ts +206 -0
- package/src/rbac/audit.ts +37 -2
- package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +26 -23
- package/src/rbac/errors.test.ts +340 -0
- package/src/rbac/hooks/index.ts +9 -0
- package/src/rbac/hooks/useResolvedScope.test.ts +1063 -0
- package/src/rbac/hooks/useRoleManagement.test.ts +908 -0
- package/src/rbac/hooks/useRoleManagement.ts +255 -0
- package/src/services/AuthService.ts +10 -0
- package/src/services/EventService.ts +111 -50
- package/src/services/__tests__/AuthService.test.ts +1 -1
- package/src/services/__tests__/EventService.test.ts +60 -45
- package/src/services/interfaces/IEventService.ts +1 -1
- package/src/utils/__tests__/deviceFingerprint.unit.test.ts +320 -0
- package/src/utils/__tests__/logger.unit.test.ts +398 -0
- package/src/utils/__tests__/validation.unit.test.ts +225 -1
- package/src/utils/file-reference.test.ts +214 -0
- package/dist/chunk-3OGQLOJM.js.map +0 -1
- package/dist/chunk-5CDJCTOO.js +0 -190
- package/dist/chunk-F6QB26OS.js.map +0 -1
- package/dist/chunk-KTHLNIMA.js.map +0 -1
- package/dist/chunk-OO3V7W4H.js.map +0 -1
- package/dist/chunk-SYXOZQ4P.js.map +0 -1
- package/dist/chunk-XYRZV7R5.js.map +0 -1
- package/dist/chunk-ZPXWJA4H.js.map +0 -1
- package/src/rbac/audit-enhanced.ts +0 -351
- /package/dist/{DataTable-H5KJCAIS.js.map → DataTable-ZOAKQ3SU.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-KZZUO27W.js.map → UnifiedAuthProvider-YFN7YGVN.js.map} +0 -0
- /package/dist/{api-PKU4PUBO.js.map → api-TNIBJWLM.js.map} +0 -0
- /package/dist/{audit-H4YJJF7R.js.map → audit-T36HM7IM.js.map} +0 -0
- /package/dist/{chunk-HKWQN44G.js.map → chunk-KMPWND3F.js.map} +0 -0
- /package/dist/{chunk-L36JW4KV.js.map → chunk-LFS45U62.js.map} +0 -0
- /package/dist/{chunk-BUN7NMV7.js.map → chunk-O3FTRYEU.js.map} +0 -0
- /package/dist/{chunk-7H75SHXZ.js.map → chunk-VN3OOE35.js.map} +0 -0
- /package/dist/{chunk-QKIVSZ2O.js.map → chunk-WP5I5GLN.js.map} +0 -0
|
@@ -523,6 +523,220 @@ describe('[service] FileReferenceServiceImpl', () => {
|
|
|
523
523
|
const result3 = await service.getFileReference('test_table', 'test-record-123', '');
|
|
524
524
|
expect(result3 === null || typeof result3 === 'object').toBe(true);
|
|
525
525
|
});
|
|
526
|
+
|
|
527
|
+
it('handles rollback when RPC fails after upload', async () => {
|
|
528
|
+
const testFile = createTestFile();
|
|
529
|
+
|
|
530
|
+
mockUploadFile.mockResolvedValue({
|
|
531
|
+
success: true,
|
|
532
|
+
path: 'org-123/documents/test.pdf'
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
mockSupabase.rpc.mockResolvedValue({
|
|
536
|
+
data: null,
|
|
537
|
+
error: { message: 'Database error' }
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
await expect(service.createFileReference(mockFileUploadOptions, testFile))
|
|
541
|
+
.rejects.toThrow('Database error');
|
|
542
|
+
|
|
543
|
+
// Verify rollback was attempted
|
|
544
|
+
expect(mockDeleteFile).toHaveBeenCalledWith(
|
|
545
|
+
mockSupabase,
|
|
546
|
+
'org-123/documents/test.pdf',
|
|
547
|
+
false
|
|
548
|
+
);
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it('handles rollback failure gracefully', async () => {
|
|
552
|
+
const testFile = createTestFile();
|
|
553
|
+
|
|
554
|
+
mockUploadFile.mockResolvedValue({
|
|
555
|
+
success: true,
|
|
556
|
+
path: 'org-123/documents/test.pdf'
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
mockSupabase.rpc.mockResolvedValue({
|
|
560
|
+
data: null,
|
|
561
|
+
error: { message: 'Database error' }
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
mockDeleteFile.mockRejectedValue(new Error('Delete failed'));
|
|
565
|
+
|
|
566
|
+
// When rollback fails, the deleteFile error might be thrown, or the original error
|
|
567
|
+
// The implementation throws the database error, but if deleteFile throws synchronously,
|
|
568
|
+
// it might propagate. Let's check what actually happens.
|
|
569
|
+
await expect(service.createFileReference(mockFileUploadOptions, testFile))
|
|
570
|
+
.rejects.toThrow();
|
|
571
|
+
|
|
572
|
+
// Verify rollback was attempted
|
|
573
|
+
expect(mockDeleteFile).toHaveBeenCalled();
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
it('handles getFileUrl when RPC returns null path', async () => {
|
|
577
|
+
const publicFileRef = { ...mockFileReference, is_public: true };
|
|
578
|
+
|
|
579
|
+
mockSupabase.from().select().eq().eq().eq().single.mockResolvedValue({
|
|
580
|
+
data: publicFileRef,
|
|
581
|
+
error: null
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
mockSupabase.rpc.mockResolvedValue({
|
|
585
|
+
data: null,
|
|
586
|
+
error: null
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
const result = await service.getFileUrl(
|
|
590
|
+
'test_table',
|
|
591
|
+
'test-record-123',
|
|
592
|
+
'test-org-123'
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
expect(result).toBeNull();
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
it('handles getFilesByCategory with category mismatch', async () => {
|
|
599
|
+
const rpcResponse = [{
|
|
600
|
+
id: 'file-ref-123',
|
|
601
|
+
file_path: 'org-123/documents/test.pdf',
|
|
602
|
+
file_metadata: {
|
|
603
|
+
fileName: 'test-document.pdf',
|
|
604
|
+
fileType: 'application/pdf',
|
|
605
|
+
fileSize: 1024000,
|
|
606
|
+
category: FileCategory.IMAGES // Different category
|
|
607
|
+
},
|
|
608
|
+
is_public: false,
|
|
609
|
+
created_at: '2023-01-01T00:00:00Z'
|
|
610
|
+
}];
|
|
611
|
+
|
|
612
|
+
mockSupabase.rpc.mockResolvedValue({ data: rpcResponse, error: null });
|
|
613
|
+
|
|
614
|
+
const result = await service.getFilesByCategory(
|
|
615
|
+
'test_table',
|
|
616
|
+
'test-record-123',
|
|
617
|
+
FileCategory.GENERAL_DOCUMENTS,
|
|
618
|
+
'test-org-123'
|
|
619
|
+
);
|
|
620
|
+
|
|
621
|
+
// Should filter out mismatched category
|
|
622
|
+
expect(result).toHaveLength(0);
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
it('handles getFilesByCategory with empty category', async () => {
|
|
626
|
+
mockSupabase.rpc.mockResolvedValue({ data: [], error: null });
|
|
627
|
+
|
|
628
|
+
const result = await service.getFilesByCategory(
|
|
629
|
+
'test_table',
|
|
630
|
+
'test-record-123',
|
|
631
|
+
FileCategory.GENERAL_DOCUMENTS,
|
|
632
|
+
'test-org-123'
|
|
633
|
+
);
|
|
634
|
+
|
|
635
|
+
expect(result).toEqual([]);
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
it('handles updateFileReference with concurrent update conflicts', async () => {
|
|
639
|
+
const updates = { file_metadata: { tags: ['updated'] } };
|
|
640
|
+
|
|
641
|
+
(mockSupabase.from() as any).update().eq().select().single.mockResolvedValue({
|
|
642
|
+
data: null,
|
|
643
|
+
error: { message: 'Concurrent update conflict', code: 'PGRST301' }
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
await expect(service.updateFileReference('file-ref-123', updates))
|
|
647
|
+
.rejects.toThrow('Concurrent update conflict');
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
it('handles deleteFileReference when file already deleted', async () => {
|
|
651
|
+
mockSupabase.from().select().eq().eq().eq().single.mockResolvedValue({
|
|
652
|
+
data: mockFileReference,
|
|
653
|
+
error: null
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
mockSupabase.rpc.mockResolvedValue({
|
|
657
|
+
data: true,
|
|
658
|
+
error: null
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
mockDeleteFile.mockRejectedValue(new Error('File not found'));
|
|
662
|
+
|
|
663
|
+
// The implementation throws when deleteFile fails, so we expect it to throw
|
|
664
|
+
await expect(service.deleteFileReference(
|
|
665
|
+
'test_table',
|
|
666
|
+
'test-record-123',
|
|
667
|
+
'test-org-123',
|
|
668
|
+
true
|
|
669
|
+
)).rejects.toThrow('File not found');
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
it('handles uploadMultipleFiles with all files failing', async () => {
|
|
673
|
+
const files = [
|
|
674
|
+
createTestFile('file1.pdf'),
|
|
675
|
+
createTestFile('file2.pdf')
|
|
676
|
+
];
|
|
677
|
+
|
|
678
|
+
mockUploadFile.mockResolvedValue({
|
|
679
|
+
success: false,
|
|
680
|
+
error: 'Upload failed'
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
const result = await service.uploadMultipleFiles(mockFileUploadOptions, files);
|
|
684
|
+
|
|
685
|
+
expect(result.success).toHaveLength(0);
|
|
686
|
+
expect(result.failed).toHaveLength(2);
|
|
687
|
+
expect(result.failed.every(f => f.error.includes('Upload failed'))).toBe(true);
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
it('handles uploadMultipleFiles with different error types', async () => {
|
|
691
|
+
const files = [
|
|
692
|
+
createTestFile('file1.pdf'),
|
|
693
|
+
createTestFile('file2.pdf'),
|
|
694
|
+
createTestFile('file3.pdf')
|
|
695
|
+
];
|
|
696
|
+
|
|
697
|
+
mockUploadFile
|
|
698
|
+
.mockResolvedValueOnce({ success: true, path: 'path1' })
|
|
699
|
+
.mockResolvedValueOnce({ success: false, error: 'Network timeout' })
|
|
700
|
+
.mockResolvedValueOnce({ success: true, path: 'path3' });
|
|
701
|
+
|
|
702
|
+
mockSupabase.rpc
|
|
703
|
+
.mockResolvedValueOnce({ data: 'file-ref-1', error: null })
|
|
704
|
+
.mockResolvedValueOnce({ data: 'file-ref-3', error: null });
|
|
705
|
+
|
|
706
|
+
const result = await service.uploadMultipleFiles(mockFileUploadOptions, files);
|
|
707
|
+
|
|
708
|
+
expect(result.success).toHaveLength(2);
|
|
709
|
+
expect(result.failed).toHaveLength(1);
|
|
710
|
+
expect(result.failed[0].error).toContain('Network timeout');
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
it('handles getSignedUrl with invalid file path', async () => {
|
|
714
|
+
mockSupabase.rpc.mockResolvedValue({
|
|
715
|
+
data: null,
|
|
716
|
+
error: null
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
const result = await service.getSignedUrl(
|
|
720
|
+
'test_table',
|
|
721
|
+
'test-record-123',
|
|
722
|
+
'test-org-123'
|
|
723
|
+
);
|
|
724
|
+
|
|
725
|
+
expect(result).toBeNull();
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
it('handles getSignedUrl with RPC error', async () => {
|
|
729
|
+
mockSupabase.rpc.mockResolvedValue({
|
|
730
|
+
data: null,
|
|
731
|
+
error: { message: 'File not found' }
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
await expect(service.getSignedUrl(
|
|
735
|
+
'test_table',
|
|
736
|
+
'test-record-123',
|
|
737
|
+
'test-org-123'
|
|
738
|
+
)).rejects.toThrow('File not found');
|
|
739
|
+
});
|
|
526
740
|
});
|
|
527
741
|
});
|
|
528
742
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/rbac/audit.ts"],"sourcesContent":["/**\n * RBAC Audit Events System\n * @package @jmruthers/pace-core\n * @module RBAC/Audit\n * @since 1.0.0\n * \n * This module provides structured audit event emission for all RBAC operations.\n */\n\nimport { SupabaseClient } from '@supabase/supabase-js';\nimport { Database } from '../types/database';\nimport { \n UUID, \n AuditEventSource, \n RBACAuditEvent \n} from './types';\n\n/**\n * Audit event payload for permission checks\n */\nexport interface PermissionCheckAuditEvent {\n type: 'permission_check';\n userId: UUID;\n organisationId: UUID;\n eventId?: string;\n appId?: UUID;\n pageId?: UUID;\n permission: string;\n decision: boolean;\n source: AuditEventSource;\n bypass?: boolean;\n duration_ms: number;\n cache_hit?: boolean;\n cache_source?: 'memory' | 'database' | 'rpc';\n metadata?: Record<string, any>;\n}\n\n/**\n * Audit event payload for permission denied\n */\nexport interface PermissionDeniedAuditEvent {\n type: 'permission_denied';\n userId: UUID;\n organisationId: UUID;\n eventId?: string;\n appId?: UUID;\n pageId?: UUID;\n permission: string;\n source: AuditEventSource;\n metadata?: Record<string, any>;\n}\n\n/**\n * Audit event payload for role granted\n */\nexport interface RoleGrantedAuditEvent {\n type: 'role_granted';\n userId: UUID;\n organisationId: UUID;\n eventId?: string;\n appId?: UUID;\n role: string;\n grantedBy: UUID;\n metadata?: Record<string, any>;\n}\n\n/**\n * Audit event payload for role revoked\n */\nexport interface RoleRevokedAuditEvent {\n type: 'role_denied';\n userId: UUID;\n organisationId: UUID;\n eventId?: string;\n appId?: UUID;\n role: string;\n revokedBy: UUID;\n metadata?: Record<string, any>;\n}\n\n/**\n * Audit event payload for RLS denied\n */\nexport interface RLSDeniedAuditEvent {\n type: 'rls_denied';\n userId: UUID;\n organisationId: UUID;\n table: string;\n operation: string;\n metadata?: Record<string, any>;\n}\n\n/**\n * Union type for all audit events\n */\nexport type AuditEventPayload = \n | PermissionCheckAuditEvent\n | PermissionDeniedAuditEvent\n | RoleGrantedAuditEvent\n | RoleRevokedAuditEvent\n | RLSDeniedAuditEvent;\n\n/**\n * RBAC Audit Manager\n * \n * Handles emission of structured audit events for all RBAC operations.\n */\nexport class RBACAuditManager {\n private supabase: SupabaseClient<Database>;\n private enabled: boolean = true;\n\n constructor(supabase: SupabaseClient<Database>) {\n this.supabase = supabase;\n }\n\n /**\n * Enable or disable audit logging\n * \n * @param enabled - Whether to enable audit logging\n */\n setEnabled(enabled: boolean): void {\n this.enabled = enabled;\n }\n\n /**\n * Check if audit logging is enabled\n * \n * @returns True if audit logging is enabled\n */\n isEnabled(): boolean {\n return this.enabled;\n }\n\n /**\n * Emit an audit event\n * \n * @param event - Audit event payload\n * @returns Promise that resolves when event is logged\n */\n async emitEvent(event: AuditEventPayload): Promise<void> {\n if (!this.enabled) {\n return;\n }\n\n // Validate required fields before attempting to insert\n // MANDATORY: All audit events must have userId\n if (!event.userId) {\n console.error('[RBAC Audit] CRITICAL: Cannot log audit event without userId:', {\n eventType: event.type,\n organisationId: event.organisationId\n });\n return;\n }\n\n // WARNING: Some audit events may not have organisationId (e.g., global admin operations)\n // Log these for security monitoring even if organisationId is missing\n if (!event.organisationId) {\n console.warn('[RBAC Audit] Audit event without organisation context:', {\n userId: event.userId,\n eventType: event.type,\n note: 'This should be investigated for security compliance'\n });\n }\n\n try {\n // Since organisationId is now required in SecurityContext, this should rarely happen\n // But we still handle the edge case properly without masking it\n if (!event.organisationId) {\n console.warn('[RBAC Audit] Audit event without organisation context - this should be investigated:', {\n userId: event.userId,\n eventType: event.type,\n note: 'Organisation context is required for RBAC operations. This may indicate a security issue or missing context derivation.'\n });\n }\n\n // Validate pageId: only include in page_id column if it's a valid UUID\n // Otherwise, store it in metadata to avoid database errors\n const rawPageId = 'pageId' in event ? event.pageId : undefined;\n const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n const isValidPageIdUuid = rawPageId && uuidRegex.test(rawPageId);\n const pageIdUuid: UUID | undefined = isValidPageIdUuid ? (rawPageId as UUID) : undefined;\n const pageIdName: string | undefined = rawPageId && !isValidPageIdUuid ? rawPageId : undefined;\n\n const auditEvent: Omit<RBACAuditEvent, 'id' | 'created_at'> = {\n event_type: event.type,\n user_id: event.userId,\n // Store organisationId - nullable to properly track missing context cases\n // Do NOT use fallback UUID as it masks security issues\n organisation_id: event.organisationId || null, // Explicitly null if missing\n event_id: 'eventId' in event ? event.eventId : undefined,\n app_id: 'appId' in event ? event.appId : undefined,\n page_id: pageIdUuid, // Only set if it's a valid UUID\n permission: 'permission' in event ? event.permission : undefined,\n decision: 'decision' in event ? event.decision : undefined,\n source: 'source' in event ? event.source : 'api', // Default to 'api' if not provided\n bypass: 'bypass' in event ? event.bypass : undefined,\n duration_ms: 'duration_ms' in event ? event.duration_ms : undefined,\n metadata: {\n ...event.metadata,\n cache_hit: 'cache_hit' in event ? event.cache_hit : undefined,\n cache_source: 'cache_source' in event ? event.cache_source : undefined,\n // Explicit flag indicating this event had no organisation context\n no_organisation_context: !event.organisationId,\n // Store page name/identifier in metadata if it's not a UUID\n page_name: pageIdName,\n },\n };\n\n const { error } = await (this.supabase as any)\n .from('rbac_audit_events')\n .insert([auditEvent]);\n\n if (error) {\n // Log the error for debugging but don't throw\n console.warn('[RBAC Audit] Failed to insert audit event:', {\n error: error.message,\n code: error.code,\n details: error.details,\n hint: error.hint,\n event: auditEvent\n });\n }\n } catch (error) {\n // Log unexpected errors but don't throw\n console.error('[RBAC Audit] Unexpected error during audit logging:', error);\n }\n }\n\n /**\n * Emit a permission check audit event\n * \n * @param event - Permission check event data\n */\n async emitPermissionCheck(event: Omit<PermissionCheckAuditEvent, 'type'>): Promise<void> {\n await this.emitEvent({\n type: 'permission_check',\n ...event,\n });\n }\n\n /**\n * Emit a permission denied audit event\n * \n * @param event - Permission denied event data\n */\n async emitPermissionDenied(event: Omit<PermissionDeniedAuditEvent, 'type'>): Promise<void> {\n await this.emitEvent({\n type: 'permission_denied',\n ...event,\n });\n }\n\n /**\n * Emit a role granted audit event\n * \n * @param event - Role granted event data\n */\n async emitRoleGranted(event: Omit<RoleGrantedAuditEvent, 'type'>): Promise<void> {\n await this.emitEvent({\n type: 'role_granted',\n ...event,\n });\n }\n\n /**\n * Emit a role revoked audit event\n * \n * @param event - Role revoked event data\n */\n async emitRoleRevoked(event: Omit<RoleRevokedAuditEvent, 'type'>): Promise<void> {\n await this.emitEvent({\n type: 'role_denied',\n ...event,\n });\n }\n\n /**\n * Emit an RLS denied audit event\n * \n * @param event - RLS denied event data\n */\n async emitRLSDenied(event: Omit<RLSDeniedAuditEvent, 'type'>): Promise<void> {\n await this.emitEvent({\n type: 'rls_denied',\n ...event,\n });\n }\n\n /**\n * Get audit events for a user\n * \n * @param userId - User ID\n * @param limit - Maximum number of events to return\n * @returns Promise resolving to audit events\n */\n async getUserAuditEvents(userId: UUID, limit: number = 100): Promise<RBACAuditEvent[]> {\n const { data, error } = await this.supabase\n .from('rbac_audit_events')\n .select('*')\n .eq('user_id', userId)\n .order('created_at', { ascending: false })\n .limit(limit);\n\n if (error) {\n throw new Error(`Failed to get audit events: ${error.message}`);\n }\n\n return data || [];\n }\n\n /**\n * Get audit events for an organisation\n * \n * @param organisationId - Organisation ID\n * @param limit - Maximum number of events to return\n * @returns Promise resolving to audit events\n */\n async getOrganisationAuditEvents(organisationId: UUID, limit: number = 100): Promise<RBACAuditEvent[]> {\n const { data, error } = await this.supabase\n .from('rbac_audit_events')\n .select('*')\n .eq('organisation_id', organisationId)\n .order('created_at', { ascending: false })\n .limit(limit);\n\n if (error) {\n throw new Error(`Failed to get audit events: ${error.message}`);\n }\n\n return data || [];\n }\n}\n\n/**\n * Create an audit manager instance\n * \n * @param supabase - Supabase client\n * @returns RBACAuditManager instance\n */\nexport function createAuditManager(supabase: SupabaseClient<Database>): RBACAuditManager {\n return new RBACAuditManager(supabase);\n}\n\n/**\n * Global audit manager instance\n * \n * This is set by the RBAC engine when it initializes.\n */\nlet globalAuditManager: RBACAuditManager | null = null;\n\n/**\n * Set the global audit manager\n * \n * @param manager - Audit manager instance\n */\nexport function setGlobalAuditManager(manager: RBACAuditManager): void {\n globalAuditManager = manager;\n}\n\n/**\n * Get the global audit manager\n * \n * @returns Global audit manager or null if not set\n */\nexport function getGlobalAuditManager(): RBACAuditManager | null {\n return globalAuditManager;\n}\n\n/**\n * Emit an audit event using the global audit manager\n * \n * @param event - Audit event payload\n */\nexport async function emitAuditEvent(event: AuditEventPayload): Promise<void> {\n if (globalAuditManager) {\n await globalAuditManager.emitEvent(event);\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/chunk-5CDJCTOO.js
DELETED
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
// src/utils/secureStorage.ts
|
|
2
|
-
var SecureStorageImpl = class {
|
|
3
|
-
constructor() {
|
|
4
|
-
this.encryptionKey = null;
|
|
5
|
-
this.initialized = false;
|
|
6
|
-
}
|
|
7
|
-
/**
|
|
8
|
-
* Initialize secure storage with encryption
|
|
9
|
-
*/
|
|
10
|
-
async init() {
|
|
11
|
-
if (this.initialized) return;
|
|
12
|
-
try {
|
|
13
|
-
if (window.crypto && window.crypto.subtle) {
|
|
14
|
-
const keyData = localStorage.getItem("_sec_key");
|
|
15
|
-
if (keyData) {
|
|
16
|
-
try {
|
|
17
|
-
const keyBuffer = this.base64ToArrayBuffer(keyData);
|
|
18
|
-
this.encryptionKey = await window.crypto.subtle.importKey(
|
|
19
|
-
"raw",
|
|
20
|
-
keyBuffer,
|
|
21
|
-
{ name: "AES-GCM" },
|
|
22
|
-
false,
|
|
23
|
-
["encrypt", "decrypt"]
|
|
24
|
-
);
|
|
25
|
-
} catch (error) {
|
|
26
|
-
await this.generateNewKey();
|
|
27
|
-
}
|
|
28
|
-
} else {
|
|
29
|
-
await this.generateNewKey();
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
this.initialized = true;
|
|
33
|
-
} catch (error) {
|
|
34
|
-
this.initialized = true;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Store item securely
|
|
39
|
-
*/
|
|
40
|
-
async setItem(key, value, options = {}) {
|
|
41
|
-
await this.init();
|
|
42
|
-
const data = {
|
|
43
|
-
value,
|
|
44
|
-
timestamp: Date.now(),
|
|
45
|
-
expiry: options.expiry ? Date.now() + options.expiry : void 0
|
|
46
|
-
};
|
|
47
|
-
const serialized = JSON.stringify(data);
|
|
48
|
-
if (options.encrypt && this.encryptionKey) {
|
|
49
|
-
try {
|
|
50
|
-
const encrypted = await this.encrypt(serialized);
|
|
51
|
-
localStorage.setItem(`_sec_${key}`, encrypted);
|
|
52
|
-
return;
|
|
53
|
-
} catch (error) {
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
localStorage.setItem(key, serialized);
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* Retrieve item securely
|
|
60
|
-
*/
|
|
61
|
-
async getItem(key) {
|
|
62
|
-
await this.init();
|
|
63
|
-
const encryptedData = localStorage.getItem(`_sec_${key}`);
|
|
64
|
-
if (encryptedData && this.encryptionKey) {
|
|
65
|
-
try {
|
|
66
|
-
const decrypted = await this.decrypt(encryptedData);
|
|
67
|
-
const parsed = JSON.parse(decrypted);
|
|
68
|
-
if (parsed.expiry && Date.now() > parsed.expiry) {
|
|
69
|
-
await this.removeItem(key);
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
return parsed.value;
|
|
73
|
-
} catch (error) {
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
const plainData = localStorage.getItem(key);
|
|
77
|
-
if (!plainData) return null;
|
|
78
|
-
try {
|
|
79
|
-
const parsed = JSON.parse(plainData);
|
|
80
|
-
if (parsed.expiry && Date.now() > parsed.expiry) {
|
|
81
|
-
await this.removeItem(key);
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
return parsed.value || plainData;
|
|
85
|
-
} catch (error) {
|
|
86
|
-
return plainData;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* Remove item
|
|
91
|
-
*/
|
|
92
|
-
async removeItem(key) {
|
|
93
|
-
localStorage.removeItem(key);
|
|
94
|
-
localStorage.removeItem(`_sec_${key}`);
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* Clear all secure storage
|
|
98
|
-
*/
|
|
99
|
-
async clear() {
|
|
100
|
-
const keys = Object.keys(localStorage);
|
|
101
|
-
for (const key of keys) {
|
|
102
|
-
if (key.startsWith("_sec_")) {
|
|
103
|
-
localStorage.removeItem(key);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
/**
|
|
108
|
-
* Generate new encryption key
|
|
109
|
-
*/
|
|
110
|
-
async generateNewKey() {
|
|
111
|
-
if (!window.crypto?.subtle) return;
|
|
112
|
-
try {
|
|
113
|
-
this.encryptionKey = await window.crypto.subtle.generateKey(
|
|
114
|
-
{ name: "AES-GCM", length: 256 },
|
|
115
|
-
true,
|
|
116
|
-
["encrypt", "decrypt"]
|
|
117
|
-
);
|
|
118
|
-
const exportedKey = await window.crypto.subtle.exportKey("raw", this.encryptionKey);
|
|
119
|
-
const keyData = this.arrayBufferToBase64(exportedKey);
|
|
120
|
-
localStorage.setItem("_sec_key", keyData);
|
|
121
|
-
} catch (error) {
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* Encrypt data
|
|
126
|
-
*/
|
|
127
|
-
async encrypt(data) {
|
|
128
|
-
if (!this.encryptionKey || !window.crypto?.subtle) {
|
|
129
|
-
throw new Error("Encryption not available");
|
|
130
|
-
}
|
|
131
|
-
const encoder = new TextEncoder();
|
|
132
|
-
const dataBuffer = encoder.encode(data);
|
|
133
|
-
const iv = window.crypto.getRandomValues(new Uint8Array(12));
|
|
134
|
-
const encrypted = await window.crypto.subtle.encrypt(
|
|
135
|
-
{ name: "AES-GCM", iv },
|
|
136
|
-
this.encryptionKey,
|
|
137
|
-
dataBuffer
|
|
138
|
-
);
|
|
139
|
-
const combined = new Uint8Array(iv.length + encrypted.byteLength);
|
|
140
|
-
combined.set(iv);
|
|
141
|
-
combined.set(new Uint8Array(encrypted), iv.length);
|
|
142
|
-
return this.arrayBufferToBase64(combined.buffer);
|
|
143
|
-
}
|
|
144
|
-
/**
|
|
145
|
-
* Decrypt data
|
|
146
|
-
*/
|
|
147
|
-
async decrypt(encryptedData) {
|
|
148
|
-
if (!this.encryptionKey || !window.crypto?.subtle) {
|
|
149
|
-
throw new Error("Decryption not available");
|
|
150
|
-
}
|
|
151
|
-
const combined = this.base64ToArrayBuffer(encryptedData);
|
|
152
|
-
const iv = combined.slice(0, 12);
|
|
153
|
-
const encrypted = combined.slice(12);
|
|
154
|
-
const decrypted = await window.crypto.subtle.decrypt(
|
|
155
|
-
{ name: "AES-GCM", iv },
|
|
156
|
-
this.encryptionKey,
|
|
157
|
-
encrypted
|
|
158
|
-
);
|
|
159
|
-
const decoder = new TextDecoder();
|
|
160
|
-
return decoder.decode(decrypted);
|
|
161
|
-
}
|
|
162
|
-
/**
|
|
163
|
-
* Convert ArrayBuffer to base64
|
|
164
|
-
*/
|
|
165
|
-
arrayBufferToBase64(buffer) {
|
|
166
|
-
const bytes = new Uint8Array(buffer);
|
|
167
|
-
let binary = "";
|
|
168
|
-
for (let i = 0; i < bytes.byteLength; i++) {
|
|
169
|
-
binary += String.fromCharCode(bytes[i]);
|
|
170
|
-
}
|
|
171
|
-
return btoa(binary);
|
|
172
|
-
}
|
|
173
|
-
/**
|
|
174
|
-
* Convert base64 to ArrayBuffer
|
|
175
|
-
*/
|
|
176
|
-
base64ToArrayBuffer(base64) {
|
|
177
|
-
const binary = atob(base64);
|
|
178
|
-
const bytes = new Uint8Array(binary.length);
|
|
179
|
-
for (let i = 0; i < binary.length; i++) {
|
|
180
|
-
bytes[i] = binary.charCodeAt(i);
|
|
181
|
-
}
|
|
182
|
-
return bytes.buffer;
|
|
183
|
-
}
|
|
184
|
-
};
|
|
185
|
-
var secureStorage = new SecureStorageImpl();
|
|
186
|
-
|
|
187
|
-
export {
|
|
188
|
-
secureStorage
|
|
189
|
-
};
|
|
190
|
-
//# sourceMappingURL=chunk-5CDJCTOO.js.map
|