@jmruthers/pace-core 0.6.4 → 0.6.6
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/CHANGELOG.md +104 -0
- package/README.md +5 -403
- package/core-usage-manifest.json +93 -0
- package/cursor-rules/00-pace-core-compliance.mdc +128 -26
- package/cursor-rules/01-standards-compliance.mdc +49 -8
- package/cursor-rules/02-project-structure.mdc +6 -0
- package/cursor-rules/03-solid-principles.mdc +2 -0
- package/cursor-rules/04-testing-standards.mdc +2 -0
- package/cursor-rules/05-bug-reports-and-features.mdc +2 -0
- package/cursor-rules/06-code-quality.mdc +2 -0
- package/cursor-rules/07-tech-stack-compliance.mdc +2 -0
- package/cursor-rules/08-markup-quality.mdc +52 -27
- package/cursor-rules/09-rbac-compliance.mdc +462 -0
- package/cursor-rules/10-error-handling-patterns.mdc +179 -0
- package/cursor-rules/11-performance-optimization.mdc +169 -0
- package/cursor-rules/12-ci-cd-integration.mdc +150 -0
- package/dist/{AuthService-Cb34EQs3.d.ts → AuthService-DmfO5rGS.d.ts} +10 -0
- package/dist/{DataTable-BMRU8a1j.d.ts → DataTable-2N_tqbfq.d.ts} +1 -1
- package/dist/DataTable-LRJL4IRV.js +15 -0
- package/dist/{PublicPageProvider-DEMpysFR.d.ts → PublicPageProvider-BBH6Vqg7.d.ts} +72 -139
- package/dist/UnifiedAuthProvider-ZT6TIGM7.js +7 -0
- package/dist/api-Y4MQWOFW.js +4 -0
- package/dist/audit-MYQXYZFU.js +3 -0
- package/dist/{chunk-J36DSWQK.js → chunk-2HGJFNAH.js} +8 -28
- package/dist/{chunk-OEWDTMG7.js → chunk-3O3WHILE.js} +38 -121
- package/dist/{chunk-M43Y4SSO.js → chunk-3QC3KRHK.js} +1 -14
- package/dist/{chunk-DGUM43GV.js → chunk-3RG5ZIWI.js} +1 -4
- package/dist/{chunk-QXHPKYJV.js → chunk-4SXLQIZO.js} +1 -26
- package/dist/chunk-4T7OBVTU.js +62 -0
- package/dist/{chunk-E66EQZE6.js → chunk-6GLLNA6U.js} +3 -9
- package/dist/{chunk-ZSAAAMVR.js → chunk-6QYDGKQY.js} +1 -4
- package/dist/{chunk-NN6WWZ5U.js → chunk-7TYHROIV.js} +579 -563
- package/dist/{chunk-M7MPQISP.js → chunk-A55DK444.js} +9 -16
- package/dist/{chunk-63FOKYGO.js → chunk-AHU7G2R5.js} +2 -11
- package/dist/{chunk-L4OXEN46.js → chunk-BVP2BCJF.js} +2 -16
- package/dist/chunk-C7NSAPTL.js +1 -0
- package/dist/{chunk-YKRAFF5K.js → chunk-FENMYN2U.js} +73 -149
- package/dist/{chunk-AVMLPIM7.js → chunk-FTCRZOG2.js} +284 -432
- package/dist/{chunk-G37KK66H.js → chunk-FYHN4DD5.js} +60 -19
- package/dist/{chunk-VBXEHIUJ.js → chunk-HF6O3O37.js} +6 -88
- package/dist/{chunk-I6DAQMWX.js → chunk-LAZMKTTF.js} +930 -891
- package/dist/{chunk-5EC5MEWX.js → chunk-MAGBIDNS.js} +77 -222
- package/dist/chunk-MBADTM7L.js +64 -0
- package/dist/chunk-OHIK3MIO.js +994 -0
- package/dist/{chunk-6SOIHG6Z.js → chunk-S7DKJPLT.js} +115 -44
- package/dist/{chunk-FMUCXFII.js → chunk-SD6WQY43.js} +1 -5
- package/dist/{chunk-PWLANIRT.js → chunk-TTRFSOKR.js} +1 -7
- package/dist/{chunk-5DRSZLL2.js → chunk-UH3NTO3F.js} +1 -6
- package/dist/{chunk-FFQEQTNW.js → chunk-UIYSCEV7.js} +134 -45
- package/dist/{chunk-3LPHPB62.js → chunk-ZFYPMX46.js} +271 -87
- package/dist/{chunk-7JPAB3T5.js → chunk-ZS5VO5JB.js} +1989 -1283
- package/dist/components.d.ts +6 -6
- package/dist/components.js +57 -267
- package/dist/{database.generated-CzIvgcPu.d.ts → database.generated-CcnC_DRc.d.ts} +4795 -3691
- package/dist/eslint-rules/index.cjs +22 -0
- package/dist/eslint-rules/rules/compliance.cjs +348 -0
- package/dist/eslint-rules/rules/components.cjs +113 -0
- package/dist/eslint-rules/rules/imports.cjs +102 -0
- package/dist/eslint-rules/rules/rbac.cjs +790 -0
- package/dist/eslint-rules/utils/helpers.cjs +42 -0
- package/dist/eslint-rules/utils/manifest-loader.cjs +75 -0
- package/dist/hooks.d.ts +5 -5
- package/dist/hooks.js +62 -270
- package/dist/icons/index.d.ts +1 -0
- package/dist/icons/index.js +1 -0
- package/dist/index.d.ts +36 -26
- package/dist/index.js +87 -690
- package/dist/providers.d.ts +2 -2
- package/dist/providers.js +8 -35
- package/dist/rbac/eslint-rules.d.ts +46 -44
- package/dist/rbac/eslint-rules.js +7 -4
- package/dist/rbac/index.d.ts +124 -594
- package/dist/rbac/index.js +14 -207
- package/dist/styles/index.js +2 -12
- package/dist/theming/runtime.js +3 -19
- package/dist/{timezone-CHhWg6b4.d.ts → timezone-BZe_eUxx.d.ts} +175 -1
- package/dist/{types-CkbwOr4Y.d.ts → types-B-K_5VnO.d.ts} +4 -0
- package/dist/types-t9H8qKRw.d.ts +55 -0
- package/dist/types.d.ts +1 -1
- package/dist/types.js +7 -94
- package/dist/{usePublicRouteParams-i3qtoBgg.d.ts → usePublicRouteParams-COZ28Mvq.d.ts} +9 -9
- package/dist/utils.d.ts +24 -117
- package/dist/utils.js +54 -392
- package/docs/README.md +16 -6
- package/docs/api/README.md +4 -402
- package/docs/api/modules.md +454 -930
- package/docs/api-reference/components.md +3 -1
- package/docs/api-reference/deprecated.md +31 -6
- package/docs/api-reference/rpc-functions.md +78 -3
- package/docs/best-practices/accessibility.md +6 -3
- package/docs/getting-started/cursor-rules.md +3 -23
- package/docs/getting-started/dependencies.md +650 -0
- package/docs/getting-started/installation-guide.md +20 -7
- package/docs/getting-started/quick-start.md +23 -12
- package/docs/implementation-guides/permission-enforcement.md +4 -0
- package/docs/rbac/MIGRATION_GUIDE.md +819 -0
- package/docs/rbac/RBAC_CONTRACT.md +724 -0
- package/docs/rbac/README.md +12 -3
- package/docs/rbac/edge-functions-guide.md +376 -0
- package/docs/rbac/secure-client-protection.md +0 -34
- package/docs/standards/00-pace-core-compliance.md +967 -0
- package/docs/standards/01-standards-compliance.md +188 -0
- package/docs/standards/02-project-structure.md +985 -0
- package/docs/standards/03-solid-principles.md +39 -0
- package/docs/standards/04-testing-standards.md +36 -0
- package/docs/standards/05-bug-reports-and-features.md +27 -0
- package/docs/standards/{04-code-style-standard.md → 06-code-quality.md} +2 -0
- package/docs/standards/07-tech-stack-compliance.md +30 -0
- package/docs/standards/08-markup-quality.md +345 -0
- package/docs/standards/{07-rbac-and-rls-standard.md → 09-rbac-compliance.md} +149 -54
- package/docs/standards/10-error-handling-patterns.md +401 -0
- package/docs/standards/11-performance-optimization.md +348 -0
- package/docs/standards/12-ci-cd-integration.md +370 -0
- package/docs/standards/ALIGNMENT_REVIEW_SUMMARY.md +192 -0
- package/docs/standards/README.md +62 -33
- package/docs/troubleshooting/organisation-context-setup.md +42 -19
- package/eslint-config-pace-core.cjs +20 -4
- package/package.json +31 -21
- package/scripts/audit/audit-compliance.cjs +1295 -0
- package/scripts/audit/audit-components.cjs +260 -0
- package/scripts/audit/audit-dependencies.cjs +395 -0
- package/scripts/audit/audit-rbac.cjs +954 -0
- package/scripts/audit/audit-standards.cjs +1268 -0
- package/scripts/audit/index.cjs +1898 -194
- package/scripts/install-cursor-rules.cjs +259 -8
- package/scripts/validate-master.js +1 -1
- package/src/__tests__/fixtures/supabase.ts +1 -1
- package/src/__tests__/helpers/__tests__/component-test-utils.test.tsx +1 -1
- package/src/__tests__/helpers/__tests__/optimized-test-setup.test.ts +1 -1
- package/src/__tests__/helpers/__tests__/supabaseMock.test.ts +1 -1
- package/src/__tests__/helpers/__tests__/test-utils.test.tsx +3 -3
- package/src/__tests__/helpers/component-test-utils.tsx +1 -1
- package/src/__tests__/helpers/supabaseMock.ts +2 -2
- package/src/__tests__/public-recipe-view.test.ts +38 -9
- package/src/components/Button/Button.tsx +5 -1
- package/src/components/ContextSelector/ContextSelector.tsx +42 -39
- package/src/components/DataTable/__tests__/keyboard.test.tsx +15 -2
- package/src/components/DataTable/components/DataTableBody.tsx +55 -31
- package/src/components/DataTable/components/DataTableCore.tsx +186 -13
- package/src/components/DataTable/components/DataTableLayout.tsx +30 -5
- package/src/components/DataTable/components/EditFields.tsx +23 -3
- package/src/components/DataTable/components/EditableRow.tsx +7 -2
- package/src/components/DataTable/components/ImportModal.tsx +4 -6
- package/src/components/DataTable/components/RowComponent.tsx +12 -0
- package/src/components/DataTable/components/ViewRowModal.tsx +4 -4
- package/src/components/DataTable/components/__tests__/ImportModal.test.tsx +455 -96
- package/src/components/DataTable/components/__tests__/ViewRowModal.test.tsx +122 -58
- package/src/components/DataTable/components/hooks/usePermissionTracking.ts +0 -4
- package/src/components/DataTable/core/DataTableContext.tsx +1 -1
- package/src/components/DataTable/hooks/__tests__/useDataTableState.test.ts +51 -47
- package/src/components/DataTable/hooks/useDataTablePermissions.ts +24 -21
- package/src/components/DataTable/hooks/useDataTableState.ts +125 -9
- package/src/components/DataTable/hooks/useTableColumns.ts +40 -2
- package/src/components/DataTable/hooks/useTableHandlers.ts +11 -0
- package/src/components/DataTable/types.ts +5 -0
- package/src/components/DateTimeField/DateTimeField.tsx +20 -20
- package/src/components/DateTimeField/README.md +5 -2
- package/src/components/Dialog/Dialog.test.tsx +361 -318
- package/src/components/Dialog/Dialog.tsx +1154 -323
- package/src/components/Dialog/index.ts +3 -3
- package/src/components/FileDisplay/FileDisplay.test.tsx +45 -2
- package/src/components/FileDisplay/FileDisplay.tsx +28 -22
- package/src/components/Form/Form.test.tsx +9 -10
- package/src/components/Form/Form.tsx +369 -9
- package/src/components/InactivityWarningModal/InactivityWarningModal.test.tsx +28 -28
- package/src/components/InactivityWarningModal/InactivityWarningModal.tsx +40 -54
- package/src/components/LoginForm/LoginForm.tsx +2 -2
- package/src/components/NavigationMenu/NavigationMenu.test.tsx +14 -13
- package/src/components/NavigationMenu/NavigationMenu.tsx +2 -2
- package/src/components/NavigationMenu/useNavigationFiltering.ts +11 -21
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +6 -4
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +30 -41
- package/src/components/PaceAppLayout/README.md +10 -9
- package/src/components/PaceAppLayout/test-setup.tsx +40 -31
- package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +108 -61
- package/src/components/PaceLoginPage/PaceLoginPage.tsx +27 -3
- package/src/components/PasswordChange/PasswordChangeForm.test.tsx +61 -0
- package/src/components/PasswordChange/PasswordChangeForm.tsx +20 -13
- package/src/components/PublicLayout/PublicLayout.test.tsx +7 -3
- package/src/components/PublicLayout/PublicPageLayout.tsx +5 -8
- package/src/components/Select/Select.tsx +23 -21
- package/src/components/Select/types.ts +1 -1
- package/src/components/UserMenu/UserMenu.test.tsx +38 -6
- package/src/components/UserMenu/UserMenu.tsx +39 -34
- package/src/components/index.ts +3 -4
- package/src/eslint-rules/index.cjs +22 -0
- package/src/eslint-rules/rules/compliance.cjs +348 -0
- package/src/eslint-rules/rules/components.cjs +113 -0
- package/src/eslint-rules/rules/imports.cjs +102 -0
- package/src/eslint-rules/rules/rbac.cjs +790 -0
- package/src/eslint-rules/utils/helpers.cjs +42 -0
- package/src/eslint-rules/utils/manifest-loader.cjs +75 -0
- package/src/hooks/__tests__/hooks.integration.test.tsx +6 -8
- package/src/hooks/__tests__/useAppConfig.unit.test.ts +129 -67
- package/src/hooks/__tests__/usePublicEvent.simple.test.ts +149 -67
- package/src/hooks/__tests__/usePublicEvent.test.ts +149 -79
- package/src/hooks/__tests__/usePublicEvent.unit.test.ts +158 -109
- package/src/hooks/__tests__/useSessionDraft.test.ts +163 -0
- package/src/hooks/__tests__/useSessionRestoration.unit.test.tsx +10 -5
- package/src/hooks/public/usePublicEvent.ts +62 -190
- package/src/hooks/public/usePublicEventLogo.test.ts +70 -17
- package/src/hooks/public/usePublicEventLogo.ts +19 -9
- package/src/hooks/useAppConfig.ts +26 -24
- package/src/hooks/useEventTheme.test.ts +211 -233
- package/src/hooks/useEventTheme.ts +19 -28
- package/src/hooks/useEvents.ts +11 -7
- package/src/hooks/useKeyboardShortcuts.ts +1 -1
- package/src/hooks/useOrganisationPermissions.ts +9 -11
- package/src/hooks/useOrganisations.ts +13 -7
- package/src/hooks/useQueryCache.ts +0 -1
- package/src/hooks/useSessionDraft.ts +380 -0
- package/src/hooks/useSessionRestoration.ts +3 -1
- package/src/icons/index.ts +27 -0
- package/src/index.ts +16 -1
- package/src/providers/OrganisationProvider.tsx +23 -14
- package/src/providers/services/EventServiceProvider.tsx +1 -24
- package/src/providers/services/UnifiedAuthProvider.tsx +5 -48
- package/src/providers/services/__tests__/UnifiedAuthProvider.integration.test.tsx +3 -0
- package/src/rbac/README.md +20 -20
- package/src/rbac/__tests__/adapters.comprehensive.test.tsx +7 -457
- package/src/rbac/__tests__/auth-rbac.e2e.test.tsx +33 -7
- package/src/rbac/adapters.tsx +7 -295
- package/src/rbac/api.test.ts +44 -56
- package/src/rbac/api.ts +10 -17
- package/src/rbac/cache-invalidation.ts +0 -1
- package/src/rbac/compliance/index.ts +10 -0
- package/src/rbac/compliance/pattern-detector.ts +553 -0
- package/src/rbac/compliance/runtime-compliance.ts +22 -0
- package/src/rbac/components/AccessDenied.tsx +150 -0
- package/src/rbac/components/NavigationGuard.tsx +12 -20
- package/src/rbac/components/PagePermissionGuard.tsx +4 -24
- package/src/rbac/components/__tests__/NavigationGuard.test.tsx +21 -8
- package/src/rbac/components/index.ts +3 -41
- package/src/rbac/eslint-rules.js +1 -1
- package/src/rbac/hooks/index.ts +0 -3
- package/src/rbac/hooks/permissions/index.ts +0 -3
- package/src/rbac/hooks/permissions/useAccessLevel.ts +4 -8
- package/src/rbac/hooks/usePermissions.ts +0 -3
- package/src/rbac/hooks/useRBAC.test.ts +21 -3
- package/src/rbac/hooks/useRBAC.ts +4 -3
- package/src/rbac/hooks/useResolvedScope.test.ts +57 -47
- package/src/rbac/hooks/useResolvedScope.ts +58 -140
- package/src/rbac/hooks/useResourcePermissions.test.ts +241 -60
- package/src/rbac/hooks/useResourcePermissions.ts +182 -63
- package/src/rbac/hooks/useRoleManagement.test.ts +65 -22
- package/src/rbac/hooks/useRoleManagement.ts +147 -19
- package/src/rbac/hooks/useSecureSupabase.ts +4 -8
- package/src/rbac/index.ts +7 -9
- package/src/rbac/permissions.ts +17 -17
- package/src/rbac/utils/contextValidator.ts +45 -7
- package/src/services/AuthService.ts +132 -23
- package/src/services/EventService.ts +4 -97
- package/src/services/InactivityService.ts +155 -58
- package/src/services/OrganisationService.ts +7 -44
- package/src/services/__tests__/OrganisationService.test.ts +26 -8
- package/src/services/base/BaseService.ts +0 -3
- package/src/styles/core.css +4 -0
- package/src/types/database.generated.ts +4733 -3809
- package/src/utils/__tests__/organisationContext.unit.test.ts +9 -10
- package/src/utils/context/organisationContext.test.ts +13 -28
- package/src/utils/context/organisationContext.ts +21 -52
- package/src/utils/dynamic/dynamicUtils.ts +1 -1
- package/src/utils/file-reference/index.ts +39 -15
- package/src/utils/formatting/formatDateTime.test.ts +3 -2
- package/src/utils/formatting/formatTime.test.ts +3 -2
- package/src/utils/google-places/loadGoogleMapsScript.ts +29 -4
- package/src/utils/index.ts +4 -1
- package/src/utils/persistence/__tests__/keyDerivation.test.ts +135 -0
- package/src/utils/persistence/__tests__/sensitiveFieldDetection.test.ts +123 -0
- package/src/utils/persistence/keyDerivation.ts +304 -0
- package/src/utils/persistence/sensitiveFieldDetection.ts +212 -0
- package/src/utils/security/secureStorage.ts +5 -5
- package/src/utils/storage/helpers.ts +3 -3
- package/src/utils/supabase/createBaseClient.ts +147 -0
- package/src/utils/timezone/timezone.test.ts +1 -2
- package/src/utils/timezone/timezone.ts +1 -1
- package/src/utils/validation/csrf.ts +4 -4
- package/cursor-rules/CHANGELOG.md +0 -119
- package/cursor-rules/README.md +0 -192
- package/dist/DataTable-E7YQZD7D.js +0 -175
- package/dist/DataTable-E7YQZD7D.js.map +0 -1
- package/dist/UnifiedAuthProvider-QPXO24B4.js +0 -18
- package/dist/UnifiedAuthProvider-QPXO24B4.js.map +0 -1
- package/dist/api-6LVZTHDS.js +0 -52
- package/dist/api-6LVZTHDS.js.map +0 -1
- package/dist/audit-V53FV5AG.js +0 -17
- package/dist/audit-V53FV5AG.js.map +0 -1
- package/dist/chunk-36LVWXB2.js +0 -227
- package/dist/chunk-36LVWXB2.js.map +0 -1
- package/dist/chunk-3LPHPB62.js.map +0 -1
- package/dist/chunk-5DRSZLL2.js.map +0 -1
- package/dist/chunk-5EC5MEWX.js.map +0 -1
- package/dist/chunk-63FOKYGO.js.map +0 -1
- package/dist/chunk-6SOIHG6Z.js.map +0 -1
- package/dist/chunk-7JPAB3T5.js.map +0 -1
- package/dist/chunk-ATKZM7RX.js +0 -2053
- package/dist/chunk-ATKZM7RX.js.map +0 -1
- package/dist/chunk-AVMLPIM7.js.map +0 -1
- package/dist/chunk-DGUM43GV.js.map +0 -1
- package/dist/chunk-E66EQZE6.js.map +0 -1
- package/dist/chunk-FFQEQTNW.js.map +0 -1
- package/dist/chunk-FMUCXFII.js.map +0 -1
- package/dist/chunk-G37KK66H.js.map +0 -1
- package/dist/chunk-I6DAQMWX.js.map +0 -1
- package/dist/chunk-J36DSWQK.js.map +0 -1
- package/dist/chunk-KQCRWDSA.js +0 -1
- package/dist/chunk-KQCRWDSA.js.map +0 -1
- package/dist/chunk-L4OXEN46.js.map +0 -1
- package/dist/chunk-LMC26NLJ.js +0 -84
- package/dist/chunk-LMC26NLJ.js.map +0 -1
- package/dist/chunk-M43Y4SSO.js.map +0 -1
- package/dist/chunk-M7MPQISP.js.map +0 -1
- package/dist/chunk-NN6WWZ5U.js.map +0 -1
- package/dist/chunk-OEWDTMG7.js.map +0 -1
- package/dist/chunk-PWLANIRT.js.map +0 -1
- package/dist/chunk-QXHPKYJV.js.map +0 -1
- package/dist/chunk-VBXEHIUJ.js.map +0 -1
- package/dist/chunk-YKRAFF5K.js.map +0 -1
- package/dist/chunk-ZSAAAMVR.js.map +0 -1
- package/dist/components.js.map +0 -1
- package/dist/contextValidator-OOPCLPZW.js +0 -9
- package/dist/contextValidator-OOPCLPZW.js.map +0 -1
- package/dist/eslint-rules/pace-core-compliance.cjs +0 -510
- package/dist/hooks.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/providers.js.map +0 -1
- package/dist/rbac/eslint-rules.js.map +0 -1
- package/dist/rbac/index.js.map +0 -1
- package/dist/styles/index.js.map +0 -1
- package/dist/theming/runtime.js.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/utils.js.map +0 -1
- package/docs/standards/01-architecture-standard.md +0 -44
- package/docs/standards/02-api-and-rpc-standard.md +0 -39
- package/docs/standards/03-component-standard.md +0 -32
- package/docs/standards/05-security-standard.md +0 -44
- package/docs/standards/06-testing-and-docs-standard.md +0 -29
- package/docs/standards/pace-core-compliance.md +0 -432
- package/scripts/audit/core/checks/accessibility.cjs +0 -197
- package/scripts/audit/core/checks/api-usage.cjs +0 -191
- package/scripts/audit/core/checks/bundle.cjs +0 -142
- package/scripts/audit/core/checks/compliance.cjs +0 -2706
- package/scripts/audit/core/checks/config.cjs +0 -54
- package/scripts/audit/core/checks/coverage.cjs +0 -84
- package/scripts/audit/core/checks/dependencies.cjs +0 -994
- package/scripts/audit/core/checks/documentation.cjs +0 -268
- package/scripts/audit/core/checks/environment.cjs +0 -116
- package/scripts/audit/core/checks/error-handling.cjs +0 -340
- package/scripts/audit/core/checks/forms.cjs +0 -172
- package/scripts/audit/core/checks/heuristics.cjs +0 -68
- package/scripts/audit/core/checks/hooks.cjs +0 -334
- package/scripts/audit/core/checks/imports.cjs +0 -244
- package/scripts/audit/core/checks/performance.cjs +0 -325
- package/scripts/audit/core/checks/routes.cjs +0 -117
- package/scripts/audit/core/checks/state.cjs +0 -130
- package/scripts/audit/core/checks/structure.cjs +0 -65
- package/scripts/audit/core/checks/style.cjs +0 -584
- package/scripts/audit/core/checks/testing.cjs +0 -122
- package/scripts/audit/core/checks/typescript.cjs +0 -61
- package/scripts/audit/core/scanner.cjs +0 -199
- package/scripts/audit/core/utils.cjs +0 -137
- package/scripts/audit/reporters/console.cjs +0 -151
- package/scripts/audit/reporters/json.cjs +0 -54
- package/scripts/audit/reporters/markdown.cjs +0 -124
- package/scripts/audit-consuming-app.cjs +0 -86
- package/src/eslint-rules/pace-core-compliance.cjs +0 -510
- package/src/eslint-rules/pace-core-compliance.js +0 -638
- package/src/rbac/components/EnhancedNavigationMenu.test.tsx +0 -555
- package/src/rbac/components/EnhancedNavigationMenu.tsx +0 -293
- package/src/rbac/components/NavigationProvider.test.tsx +0 -481
- package/src/rbac/components/NavigationProvider.tsx +0 -345
- package/src/rbac/components/PagePermissionProvider.test.tsx +0 -476
- package/src/rbac/components/PagePermissionProvider.tsx +0 -279
- package/src/rbac/components/PermissionEnforcer.tsx +0 -312
- package/src/rbac/components/RoleBasedRouter.tsx +0 -440
- package/src/rbac/components/SecureDataProvider.test.tsx +0 -543
- package/src/rbac/components/SecureDataProvider.tsx +0 -339
- package/src/rbac/components/__tests__/EnhancedNavigationMenu.test.tsx +0 -620
- package/src/rbac/components/__tests__/NavigationProvider.test.tsx +0 -726
- package/src/rbac/components/__tests__/PagePermissionProvider.test.tsx +0 -661
- package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +0 -881
- package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +0 -783
- package/src/rbac/components/__tests__/SecureDataProvider.fixed.test.tsx +0 -645
- package/src/rbac/components/__tests__/SecureDataProvider.test.tsx +0 -659
- package/src/rbac/hooks/permissions/useCachedPermissions.ts +0 -79
- package/src/rbac/hooks/permissions/useHasAllPermissions.ts +0 -90
- package/src/rbac/hooks/permissions/useHasAnyPermission.ts +0 -90
|
@@ -24,16 +24,16 @@ describe('organisationContext', () => {
|
|
|
24
24
|
});
|
|
25
25
|
|
|
26
26
|
describe('setOrganisationContext', () => {
|
|
27
|
-
it('should set organisation context successfully', async () => {
|
|
27
|
+
it('should set organisation context successfully (no-op)', async () => {
|
|
28
28
|
const organisationId = 'org-123';
|
|
29
29
|
const mockRpc = vi.fn().mockResolvedValue({ error: null });
|
|
30
30
|
mockSupabase.rpc = mockRpc;
|
|
31
31
|
|
|
32
|
+
// setOrganisationContext is now a no-op (deprecated)
|
|
32
33
|
await setOrganisationContext(mockSupabase, organisationId);
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
});
|
|
35
|
+
// Should not call rpc since it's a no-op
|
|
36
|
+
expect(mockRpc).not.toHaveBeenCalled();
|
|
37
37
|
});
|
|
38
38
|
|
|
39
39
|
it('should handle missing supabase client gracefully', async () => {
|
|
@@ -153,14 +153,13 @@ describe('organisationContext', () => {
|
|
|
153
153
|
});
|
|
154
154
|
|
|
155
155
|
describe('isOrganisationContextAvailable', () => {
|
|
156
|
-
it('should return
|
|
157
|
-
|
|
158
|
-
mockSupabase.rpc = mockRpc;
|
|
159
|
-
|
|
156
|
+
it('should return false (deprecated function)', async () => {
|
|
157
|
+
// isOrganisationContextAvailable is deprecated and always returns false
|
|
160
158
|
const result = await isOrganisationContextAvailable(mockSupabase);
|
|
161
159
|
|
|
162
|
-
|
|
163
|
-
expect(
|
|
160
|
+
// Should not call RPC since function is deprecated
|
|
161
|
+
expect(mockSupabase.rpc).not.toHaveBeenCalled();
|
|
162
|
+
expect(result).toBe(false);
|
|
164
163
|
});
|
|
165
164
|
|
|
166
165
|
it('should return false when supabase client is missing', async () => {
|
|
@@ -29,17 +29,13 @@ describe('Organisation Context', () => {
|
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
describe('setOrganisationContext', () => {
|
|
32
|
-
it('sets organisation context successfully', async () => {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
error: null
|
|
36
|
-
});
|
|
37
|
-
|
|
32
|
+
it('sets organisation context successfully (no-op)', async () => {
|
|
33
|
+
// setOrganisationContext is now a no-op (deprecated)
|
|
34
|
+
// It should complete without calling rpc
|
|
38
35
|
await setOrganisationContext(mockSupabase, 'org-123');
|
|
39
36
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
});
|
|
37
|
+
// Should not call rpc since it's a no-op
|
|
38
|
+
expect(mockSupabase.rpc).not.toHaveBeenCalled();
|
|
43
39
|
});
|
|
44
40
|
|
|
45
41
|
it('handles missing supabase client gracefully', async () => {
|
|
@@ -180,20 +176,14 @@ describe('Organisation Context', () => {
|
|
|
180
176
|
});
|
|
181
177
|
|
|
182
178
|
it('handles complete workflow', async () => {
|
|
183
|
-
//
|
|
184
|
-
mockSupabase.rpc.mockResolvedValueOnce({
|
|
185
|
-
data: null,
|
|
186
|
-
error: null
|
|
187
|
-
});
|
|
188
|
-
|
|
179
|
+
// setOrganisationContext is now a no-op, so it won't call rpc
|
|
189
180
|
await setOrganisationContext(mockSupabase, 'org-123');
|
|
190
181
|
|
|
191
|
-
//
|
|
182
|
+
// getOrganisationContext always returns null (deprecated)
|
|
192
183
|
const result = await getOrganisationContext(mockSupabase);
|
|
193
|
-
|
|
194
184
|
expect(result).toBeNull();
|
|
195
185
|
|
|
196
|
-
//
|
|
186
|
+
// clearOrganisationContext still calls rpc
|
|
197
187
|
mockSupabase.rpc.mockResolvedValueOnce({
|
|
198
188
|
data: null,
|
|
199
189
|
error: null
|
|
@@ -201,25 +191,20 @@ describe('Organisation Context', () => {
|
|
|
201
191
|
|
|
202
192
|
await clearOrganisationContext(mockSupabase);
|
|
203
193
|
|
|
204
|
-
|
|
194
|
+
// Only clearOrganisationContext should call rpc (once)
|
|
195
|
+
expect(mockSupabase.rpc).toHaveBeenCalledTimes(1);
|
|
205
196
|
});
|
|
206
197
|
|
|
207
198
|
it('handles multiple organisation switches', async () => {
|
|
208
199
|
const organisations = ['org-123', 'org-456', 'org-789'];
|
|
209
200
|
|
|
201
|
+
// setOrganisationContext is now a no-op, so it won't call rpc
|
|
210
202
|
for (const orgId of organisations) {
|
|
211
|
-
mockSupabase.rpc.mockResolvedValue({
|
|
212
|
-
data: null,
|
|
213
|
-
error: null
|
|
214
|
-
});
|
|
215
|
-
|
|
216
203
|
await setOrganisationContext(mockSupabase, orgId);
|
|
217
204
|
}
|
|
218
205
|
|
|
219
|
-
|
|
220
|
-
expect(mockSupabase.rpc).
|
|
221
|
-
expect(mockSupabase.rpc).toHaveBeenCalledWith('set_organisation_context', { org_id: 'org-456' });
|
|
222
|
-
expect(mockSupabase.rpc).toHaveBeenCalledWith('set_organisation_context', { org_id: 'org-789' });
|
|
206
|
+
// setOrganisationContext is deprecated and doesn't call rpc anymore
|
|
207
|
+
expect(mockSupabase.rpc).not.toHaveBeenCalled();
|
|
223
208
|
});
|
|
224
209
|
});
|
|
225
210
|
|
|
@@ -16,47 +16,25 @@ const log = createLogger('organisationContext');
|
|
|
16
16
|
/**
|
|
17
17
|
* Set organisation context in the database session
|
|
18
18
|
*
|
|
19
|
-
* This function
|
|
20
|
-
*
|
|
19
|
+
* @deprecated This function is a no-op. Organisation context is now handled via:
|
|
20
|
+
* - Secure Supabase client headers (useSecureSupabase hook)
|
|
21
|
+
* - Explicit p_organisation_id parameters in RPC calls
|
|
22
|
+
* - RLS policies that use auth.uid() and organisation_id columns
|
|
21
23
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* @
|
|
24
|
+
* This function is kept for backward compatibility but does nothing.
|
|
25
|
+
*
|
|
26
|
+
* @param supabase - Supabase client instance (unused)
|
|
27
|
+
* @param organisationId - The organisation ID (unused)
|
|
28
|
+
* @returns Promise that resolves immediately
|
|
25
29
|
*/
|
|
26
30
|
export async function setOrganisationContext(
|
|
27
31
|
supabase: SupabaseClient,
|
|
28
32
|
organisationId: string
|
|
29
33
|
): Promise<void> {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
try {
|
|
36
|
-
// Add timeout to prevent hanging RPC calls
|
|
37
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
38
|
-
setTimeout(() => reject(new Error('RPC timeout after 3 seconds')), 3000);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
// Call the database function to set organisation context
|
|
42
|
-
const rpcPromise = supabase.rpc('set_organisation_context', {
|
|
43
|
-
org_id: organisationId
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
const { error } = await Promise.race([rpcPromise, timeoutPromise]) as any;
|
|
47
|
-
|
|
48
|
-
if (error) {
|
|
49
|
-
// Function might not exist yet - this is expected during migration
|
|
50
|
-
// Silent fail - will fall back to client-side filtering
|
|
51
|
-
log.debug('RPC function not available or failed, continuing without database context');
|
|
52
|
-
} else {
|
|
53
|
-
log.debug('Organisation context set in database successfully');
|
|
54
|
-
}
|
|
55
|
-
} catch (error) {
|
|
56
|
-
// Handle any other errors gracefully
|
|
57
|
-
// Silent fail - will fall back to client-side filtering
|
|
58
|
-
log.debug('Failed to set database context, continuing without it:', error);
|
|
59
|
-
}
|
|
34
|
+
// No-op: Organisation context is now handled via secure client and explicit parameters
|
|
35
|
+
// This function is kept for backward compatibility
|
|
36
|
+
log.debug('setOrganisationContext called but is a no-op - context handled via secure client');
|
|
37
|
+
return Promise.resolve();
|
|
60
38
|
}
|
|
61
39
|
|
|
62
40
|
/**
|
|
@@ -132,25 +110,16 @@ export async function getOrganisationContext(
|
|
|
132
110
|
/**
|
|
133
111
|
* Check if organisation context functions are available in the database
|
|
134
112
|
*
|
|
135
|
-
* @
|
|
136
|
-
*
|
|
113
|
+
* @deprecated This function always returns false. Organisation context functions have been removed.
|
|
114
|
+
* Organisation context is now handled via secure client and explicit parameters.
|
|
115
|
+
*
|
|
116
|
+
* @param supabase - Supabase client instance (unused)
|
|
117
|
+
* @returns Promise that resolves to false
|
|
137
118
|
*/
|
|
138
119
|
export async function isOrganisationContextAvailable(
|
|
139
120
|
supabase: SupabaseClient
|
|
140
121
|
): Promise<boolean> {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
try {
|
|
146
|
-
const { error } = await supabase.rpc('get_organisation_context');
|
|
147
|
-
|
|
148
|
-
if (error) {
|
|
149
|
-
return false;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return true;
|
|
153
|
-
} catch (error) {
|
|
154
|
-
return false;
|
|
155
|
-
}
|
|
122
|
+
// Always return false - organisation context functions have been removed
|
|
123
|
+
// Context is now handled via secure client and explicit parameters
|
|
124
|
+
return false;
|
|
156
125
|
}
|
|
@@ -73,7 +73,7 @@ export const loadFormUtils = async (): Promise<{
|
|
|
73
73
|
|
|
74
74
|
// Dynamic CSV utilities
|
|
75
75
|
export const loadCSVUtils = async (): Promise<unknown> => {
|
|
76
|
-
// @ts-ignore - papaparse is
|
|
76
|
+
// @ts-ignore - papaparse is a dependency of pace-core, should be available via node_modules
|
|
77
77
|
const papaparse = await import('papaparse');
|
|
78
78
|
return papaparse.default;
|
|
79
79
|
};
|
|
@@ -87,6 +87,26 @@ export class FileReferenceServiceImpl implements FileReferenceService {
|
|
|
87
87
|
log.debug('Using authenticated user ID for user-scoped file upload', { userId: authenticatedUserId });
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
// CRITICAL: Check super admin status in application layer (consistent with pace-core pattern)
|
|
91
|
+
// Super admins bypass all permission checks - this is handled in the application layer,
|
|
92
|
+
// not in RLS policies. The RPC function still validates input and handles the insert,
|
|
93
|
+
// but permission checks are bypassed for super admins.
|
|
94
|
+
let isSuperAdminUser = false;
|
|
95
|
+
const userIdForCheck = authenticatedUserId || options.userId;
|
|
96
|
+
if (userIdForCheck) {
|
|
97
|
+
try {
|
|
98
|
+
// Import isSuperAdmin from rbac/api - this is the standard way to check super admin
|
|
99
|
+
const { isSuperAdmin } = await import('../../rbac/api');
|
|
100
|
+
isSuperAdminUser = await isSuperAdmin(userIdForCheck);
|
|
101
|
+
if (isSuperAdminUser) {
|
|
102
|
+
log.debug('Super admin detected - bypassing permission checks', { userId: userIdForCheck });
|
|
103
|
+
}
|
|
104
|
+
} catch (superAdminCheckError) {
|
|
105
|
+
// If super admin check fails, continue with normal permission flow
|
|
106
|
+
log.warn('Failed to check super-admin status, proceeding with normal permission checks', superAdminCheckError);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
90
110
|
// Step 1: Upload file to storage bucket first
|
|
91
111
|
// This generates a unique path: {orgId}/{folder}/{timestamp-uuid-filename} or users/{auth.uid()}/{folder}/{timestamp-uuid-filename}
|
|
92
112
|
// Bucket is automatically selected based on is_public flag
|
|
@@ -123,6 +143,22 @@ export class FileReferenceServiceImpl implements FileReferenceService {
|
|
|
123
143
|
|
|
124
144
|
// Step 4: Create file reference in database using RPC function
|
|
125
145
|
// This links the storage path to the record in core_file_references table
|
|
146
|
+
// CRITICAL: Always pass the authenticated user ID to SECURITY DEFINER functions
|
|
147
|
+
// In SECURITY DEFINER functions, auth.uid() returns the function owner's ID,
|
|
148
|
+
// not the caller's ID, so we must explicitly pass the user ID
|
|
149
|
+
let rpcUserId: string | null = null;
|
|
150
|
+
if (authenticatedUserId) {
|
|
151
|
+
rpcUserId = authenticatedUserId;
|
|
152
|
+
} else if (options.userId) {
|
|
153
|
+
rpcUserId = options.userId;
|
|
154
|
+
} else {
|
|
155
|
+
// Get authenticated user ID from session as fallback
|
|
156
|
+
const { data: { user: authUser } } = await this.supabase.auth.getUser();
|
|
157
|
+
if (authUser) {
|
|
158
|
+
rpcUserId = authUser.id;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
126
162
|
const { data, error } = await this.supabase
|
|
127
163
|
.rpc('data_file_reference_create', {
|
|
128
164
|
p_table_name: options.table_name,
|
|
@@ -141,7 +177,7 @@ export class FileReferenceServiceImpl implements FileReferenceService {
|
|
|
141
177
|
...options.custom_metadata
|
|
142
178
|
},
|
|
143
179
|
p_is_public: options.is_public || false,
|
|
144
|
-
p_user_id:
|
|
180
|
+
p_user_id: rpcUserId // Always pass authenticated user ID for SECURITY DEFINER functions
|
|
145
181
|
});
|
|
146
182
|
|
|
147
183
|
// Step 5: Rollback - if database insert fails, clean up uploaded file
|
|
@@ -152,19 +188,6 @@ export class FileReferenceServiceImpl implements FileReferenceService {
|
|
|
152
188
|
|
|
153
189
|
// Check if RPC returned null (permission denied or other failure)
|
|
154
190
|
if (!data || data === null) {
|
|
155
|
-
// Before throwing permission error, check if user is super-admin
|
|
156
|
-
// If super-admin, the RPC should have allowed the upload, so this is likely a different issue
|
|
157
|
-
let isSuperAdminUser = false;
|
|
158
|
-
try {
|
|
159
|
-
const { data: { user: authUser } } = await this.supabase.auth.getUser();
|
|
160
|
-
if (authUser) {
|
|
161
|
-
isSuperAdminUser = await isSuperAdmin(authUser.id);
|
|
162
|
-
}
|
|
163
|
-
} catch (superAdminCheckError) {
|
|
164
|
-
// If super-admin check fails, continue with permission error
|
|
165
|
-
log.warn('Failed to check super-admin status', superAdminCheckError);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
191
|
// Clean up the uploaded file since DB insert failed
|
|
169
192
|
await deleteFile(this.supabase, filePath, options.is_public || false);
|
|
170
193
|
|
|
@@ -173,7 +196,8 @@ export class FileReferenceServiceImpl implements FileReferenceService {
|
|
|
173
196
|
const pageContextDisplay = options.pageContext || 'undefined';
|
|
174
197
|
|
|
175
198
|
if (isSuperAdminUser) {
|
|
176
|
-
// Super-admin should have been allowed - this suggests a
|
|
199
|
+
// Super-admin should have been allowed - this suggests a database or RPC function issue
|
|
200
|
+
// Since we already checked super admin in the application layer, this is unexpected
|
|
177
201
|
throw new Error(
|
|
178
202
|
`File upload failed for super-admin user. This may indicate a database issue. ` +
|
|
179
203
|
`Page context: '${pageContextDisplay}', App: '${appName}'. ` +
|
|
@@ -161,8 +161,9 @@ describe('formatDateTime Utility', () => {
|
|
|
161
161
|
}
|
|
162
162
|
const end = performance.now();
|
|
163
163
|
|
|
164
|
-
// Should complete in reasonable time (less than
|
|
165
|
-
|
|
164
|
+
// Should complete in reasonable time (less than 200ms for 1000 calls in test environment)
|
|
165
|
+
// Increased threshold to account for test environment overhead
|
|
166
|
+
expect(end - start).toBeLessThan(200);
|
|
166
167
|
});
|
|
167
168
|
});
|
|
168
169
|
|
|
@@ -161,8 +161,9 @@ describe('formatTime Utility', () => {
|
|
|
161
161
|
}
|
|
162
162
|
const end = performance.now();
|
|
163
163
|
|
|
164
|
-
// Should complete in reasonable time (less than
|
|
165
|
-
|
|
164
|
+
// Should complete in reasonable time (less than 200ms for 1000 calls)
|
|
165
|
+
// Increased threshold to account for test environment variability
|
|
166
|
+
expect(end - start).toBeLessThan(200);
|
|
166
167
|
});
|
|
167
168
|
});
|
|
168
169
|
|
|
@@ -127,13 +127,19 @@ export function loadGoogleMapsScript(
|
|
|
127
127
|
return;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
// Check if script is already being loaded
|
|
130
|
+
// Check if script is already being loaded or exists
|
|
131
131
|
const existingScript = document.querySelector(
|
|
132
132
|
`script[src*="maps.googleapis.com/maps/api/js"]`
|
|
133
133
|
);
|
|
134
134
|
if (existingScript) {
|
|
135
|
+
// If Google Maps is already loaded, resolve immediately
|
|
136
|
+
if (window.google?.maps?.places) {
|
|
137
|
+
resolve();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
135
141
|
// Wait for existing script to load
|
|
136
|
-
|
|
142
|
+
const handleLoad = () => {
|
|
137
143
|
// Wait for the library to initialize with multiple retries
|
|
138
144
|
let attempts = 0;
|
|
139
145
|
const maxAttempts = 20; // 2 seconds total
|
|
@@ -150,10 +156,26 @@ export function loadGoogleMapsScript(
|
|
|
150
156
|
};
|
|
151
157
|
|
|
152
158
|
checkPlaces();
|
|
153
|
-
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// Check if script already loaded
|
|
162
|
+
const scriptElement = existingScript as HTMLScriptElement;
|
|
163
|
+
if (scriptElement.getAttribute('data-loaded') === 'true') {
|
|
164
|
+
handleLoad();
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Check if script is already complete (using type assertion for readyState which exists at runtime)
|
|
169
|
+
const scriptReadyState = (scriptElement as any).readyState;
|
|
170
|
+
if (scriptReadyState === 'complete' || scriptReadyState === 'loaded') {
|
|
171
|
+
handleLoad();
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
existingScript.addEventListener('load', handleLoad, { once: true });
|
|
154
176
|
existingScript.addEventListener('error', () => {
|
|
155
177
|
reject(new Error('Failed to load Google Maps script'));
|
|
156
|
-
});
|
|
178
|
+
}, { once: true });
|
|
157
179
|
return;
|
|
158
180
|
}
|
|
159
181
|
|
|
@@ -164,6 +186,9 @@ export function loadGoogleMapsScript(
|
|
|
164
186
|
script.defer = true;
|
|
165
187
|
|
|
166
188
|
script.onload = () => {
|
|
189
|
+
// Mark script as loaded
|
|
190
|
+
script.setAttribute('data-loaded', 'true');
|
|
191
|
+
|
|
167
192
|
// Wait for the library to initialize with multiple retries
|
|
168
193
|
let attempts = 0;
|
|
169
194
|
const maxAttempts = 20; // 2 seconds total (20 * 100ms)
|
package/src/utils/index.ts
CHANGED
|
@@ -21,7 +21,7 @@ export * from './validation';
|
|
|
21
21
|
// Explicitly re-export commonly used validation utilities to ensure they're available
|
|
22
22
|
// Import from source modules to avoid re-export issues
|
|
23
23
|
export { validateUserInput, usernameSchema } from './validation/validationUtils';
|
|
24
|
-
export { sanitizeUserInput, sanitizeFormData } from './validation/sanitization';
|
|
24
|
+
export { sanitizeUserInput, sanitizeFormData, sanitizeHtml } from './validation/sanitization';
|
|
25
25
|
export { emailSchema, nameSchema, phoneSchema, urlSchema } from './validation/common';
|
|
26
26
|
export { passwordSchema } from './validation/passwordSchema';
|
|
27
27
|
export { pickSchema, combineSchemas } from './validation/schema';
|
|
@@ -178,3 +178,6 @@ export {
|
|
|
178
178
|
getInFlightRequestStats,
|
|
179
179
|
deduplicatedQuery
|
|
180
180
|
} from './request-deduplication';
|
|
181
|
+
|
|
182
|
+
// Supabase client creation (restricted wrapper)
|
|
183
|
+
export { createBaseClient } from './supabase/createBaseClient';
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Key Derivation Tests
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module Utils/Persistence/__tests__
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect } from 'vitest';
|
|
8
|
+
import {
|
|
9
|
+
deriveDataTableKey,
|
|
10
|
+
deriveDialogKey,
|
|
11
|
+
deriveFormKey,
|
|
12
|
+
hashStableFingerprint,
|
|
13
|
+
} from '../keyDerivation';
|
|
14
|
+
|
|
15
|
+
describe('keyDerivation', () => {
|
|
16
|
+
describe('deriveDataTableKey', () => {
|
|
17
|
+
it('should use rbacPageId as primary key', () => {
|
|
18
|
+
const key = deriveDataTableKey({
|
|
19
|
+
rbacPageId: 'user-management',
|
|
20
|
+
});
|
|
21
|
+
expect(key).toBe('datatable:user-management');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should fall back to title when rbacPageId not available', () => {
|
|
25
|
+
const key = deriveDataTableKey({
|
|
26
|
+
title: 'Users Table',
|
|
27
|
+
});
|
|
28
|
+
expect(key).toBe('datatable:users-table');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should use route pathname as fallback', () => {
|
|
32
|
+
const key = deriveDataTableKey(
|
|
33
|
+
{},
|
|
34
|
+
{ pathname: '/users' }
|
|
35
|
+
);
|
|
36
|
+
expect(key).toBe('datatable:/users');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should use column IDs hash as last resort', () => {
|
|
40
|
+
const key = deriveDataTableKey({
|
|
41
|
+
columnIds: ['id', 'name', 'email'],
|
|
42
|
+
});
|
|
43
|
+
expect(key).toMatch(/^datatable:[a-f0-9]+$/);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should return null if no stable key can be determined', () => {
|
|
47
|
+
const key = deriveDataTableKey({});
|
|
48
|
+
expect(key).toBeNull();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('deriveDialogKey', () => {
|
|
53
|
+
it('should use title as primary key', () => {
|
|
54
|
+
const key = deriveDialogKey({
|
|
55
|
+
title: 'Edit User',
|
|
56
|
+
});
|
|
57
|
+
expect(key).toBe('dialog:edit-user');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should use route pathname as fallback', () => {
|
|
61
|
+
const key = deriveDialogKey(
|
|
62
|
+
{},
|
|
63
|
+
{ pathname: '/users/edit' }
|
|
64
|
+
);
|
|
65
|
+
expect(key).toBe('dialog:/users/edit');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should return null if no stable key can be determined', () => {
|
|
69
|
+
const key = deriveDialogKey({});
|
|
70
|
+
expect(key).toBeNull();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('deriveFormKey', () => {
|
|
75
|
+
it('should use parent dialog title when available', () => {
|
|
76
|
+
const key = deriveFormKey(
|
|
77
|
+
{},
|
|
78
|
+
{ dialogTitle: 'Edit User' }
|
|
79
|
+
);
|
|
80
|
+
expect(key).toBe('form:edit-user');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should use route + field names hash as fallback', () => {
|
|
84
|
+
const key = deriveFormKey(
|
|
85
|
+
{
|
|
86
|
+
fieldNames: ['name', 'email'],
|
|
87
|
+
},
|
|
88
|
+
null,
|
|
89
|
+
{ pathname: '/users' }
|
|
90
|
+
);
|
|
91
|
+
expect(key).toMatch(/^form:\/users:[a-f0-9]+$/);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should use route pathname as last resort', () => {
|
|
95
|
+
const key = deriveFormKey(
|
|
96
|
+
{},
|
|
97
|
+
null,
|
|
98
|
+
{ pathname: '/users' }
|
|
99
|
+
);
|
|
100
|
+
expect(key).toBe('form:/users');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should return null if no stable key can be determined', () => {
|
|
104
|
+
const key = deriveFormKey({});
|
|
105
|
+
expect(key).toBeNull();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('hashStableFingerprint', () => {
|
|
110
|
+
it('should create stable hash from array of strings', () => {
|
|
111
|
+
const hash1 = hashStableFingerprint(['a', 'b', 'c']);
|
|
112
|
+
const hash2 = hashStableFingerprint(['a', 'b', 'c']);
|
|
113
|
+
expect(hash1).toBe(hash2);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should be order-independent', () => {
|
|
117
|
+
const hash1 = hashStableFingerprint(['a', 'b', 'c']);
|
|
118
|
+
const hash2 = hashStableFingerprint(['c', 'b', 'a']);
|
|
119
|
+
expect(hash1).toBe(hash2);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should normalize strings (lowercase, trim)', () => {
|
|
123
|
+
const hash1 = hashStableFingerprint(['A', ' B ', 'C']);
|
|
124
|
+
const hash2 = hashStableFingerprint(['a', 'b', 'c']);
|
|
125
|
+
expect(hash1).toBe(hash2);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should filter out empty values', () => {
|
|
129
|
+
const hash1 = hashStableFingerprint(['a', '', 'b', 'c']);
|
|
130
|
+
const hash2 = hashStableFingerprint(['a', 'b', 'c']);
|
|
131
|
+
expect(hash1).toBe(hash2);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Sensitive Field Detection Tests
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module Utils/Persistence/__tests__
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect } from 'vitest';
|
|
8
|
+
import {
|
|
9
|
+
isSensitiveField,
|
|
10
|
+
filterSensitiveFields,
|
|
11
|
+
getSensitiveFieldNames,
|
|
12
|
+
} from '../sensitiveFieldDetection';
|
|
13
|
+
|
|
14
|
+
describe('sensitiveFieldDetection', () => {
|
|
15
|
+
describe('isSensitiveField', () => {
|
|
16
|
+
it('should detect password input type', () => {
|
|
17
|
+
expect(isSensitiveField('password', 'password')).toBe(true);
|
|
18
|
+
expect(isSensitiveField('userPassword', 'password')).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should detect hidden input type', () => {
|
|
22
|
+
expect(isSensitiveField('token', 'hidden')).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should detect sensitive field names by pattern', () => {
|
|
26
|
+
expect(isSensitiveField('password', 'text')).toBe(true);
|
|
27
|
+
expect(isSensitiveField('user_password', 'text')).toBe(true);
|
|
28
|
+
expect(isSensitiveField('api_key', 'text')).toBe(true);
|
|
29
|
+
expect(isSensitiveField('secretToken', 'text')).toBe(true);
|
|
30
|
+
expect(isSensitiveField('credit_card', 'text')).toBe(true);
|
|
31
|
+
expect(isSensitiveField('ssn', 'text')).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should not detect non-sensitive fields', () => {
|
|
35
|
+
expect(isSensitiveField('name', 'text')).toBe(false);
|
|
36
|
+
expect(isSensitiveField('email', 'email')).toBe(false);
|
|
37
|
+
expect(isSensitiveField('age', 'number')).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should be case-insensitive', () => {
|
|
41
|
+
expect(isSensitiveField('PASSWORD', 'text')).toBe(true);
|
|
42
|
+
expect(isSensitiveField('Api_Key', 'text')).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('filterSensitiveFields', () => {
|
|
47
|
+
it('should filter out sensitive fields', () => {
|
|
48
|
+
const data = {
|
|
49
|
+
name: 'John',
|
|
50
|
+
email: 'john@example.com',
|
|
51
|
+
password: 'secret123',
|
|
52
|
+
api_key: 'key123',
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const filtered = filterSensitiveFields(data, ['name', 'email', 'password', 'api_key']);
|
|
56
|
+
|
|
57
|
+
expect(filtered).toEqual({
|
|
58
|
+
name: 'John',
|
|
59
|
+
email: 'john@example.com',
|
|
60
|
+
});
|
|
61
|
+
expect(filtered).not.toHaveProperty('password');
|
|
62
|
+
expect(filtered).not.toHaveProperty('api_key');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should filter by input type', () => {
|
|
66
|
+
const data = {
|
|
67
|
+
name: 'John',
|
|
68
|
+
password: 'secret123',
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const filtered = filterSensitiveFields(data, ['name', 'password'], {
|
|
72
|
+
password: 'password',
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(filtered).toEqual({
|
|
76
|
+
name: 'John',
|
|
77
|
+
});
|
|
78
|
+
expect(filtered).not.toHaveProperty('password');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should return empty object if all fields are sensitive', () => {
|
|
82
|
+
const data = {
|
|
83
|
+
password: 'secret123',
|
|
84
|
+
api_key: 'key123',
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const filtered = filterSensitiveFields(data, ['password', 'api_key']);
|
|
88
|
+
|
|
89
|
+
expect(filtered).toEqual({});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should return all fields if none are sensitive', () => {
|
|
93
|
+
const data = {
|
|
94
|
+
name: 'John',
|
|
95
|
+
email: 'john@example.com',
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const filtered = filterSensitiveFields(data, ['name', 'email']);
|
|
99
|
+
|
|
100
|
+
expect(filtered).toEqual(data);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('getSensitiveFieldNames', () => {
|
|
105
|
+
it('should return list of sensitive field names', () => {
|
|
106
|
+
const sensitive = getSensitiveFieldNames(
|
|
107
|
+
['name', 'email', 'password', 'api_key'],
|
|
108
|
+
{
|
|
109
|
+
password: 'password',
|
|
110
|
+
}
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
expect(sensitive).toEqual(['password', 'api_key']);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should return empty array if no sensitive fields', () => {
|
|
117
|
+
const sensitive = getSensitiveFieldNames(['name', 'email']);
|
|
118
|
+
|
|
119
|
+
expect(sensitive).toEqual([]);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|