@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
|
@@ -0,0 +1,1295 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Compliance Audit Script
|
|
5
|
+
*
|
|
6
|
+
* Audits consuming apps for compliance with pace-core usage patterns.
|
|
7
|
+
*
|
|
8
|
+
* NOTE: Many checks have been migrated to ESLint rules for real-time feedback:
|
|
9
|
+
* - Native HTML elements → ESLint: prefer-pace-core-components
|
|
10
|
+
* - Restricted imports → ESLint: no-restricted-imports
|
|
11
|
+
* - Custom hooks/utils → ESLint: prefer-pace-core-hooks/utils
|
|
12
|
+
* - Inline styles → ESLint: no-inline-styles
|
|
13
|
+
* - RBAC permission loading → ESLint: rbac-permission-loading
|
|
14
|
+
*
|
|
15
|
+
* This script now focuses on file-system and config-based checks:
|
|
16
|
+
* - Secure Supabase client usage (file location checks)
|
|
17
|
+
* - RBAC setup (main.tsx structure)
|
|
18
|
+
* - Provider usage (cross-file nesting)
|
|
19
|
+
* - Core styles import (app.css → core.css chain)
|
|
20
|
+
*
|
|
21
|
+
* Usage:
|
|
22
|
+
* const { runComplianceAudit } = require('./audit-compliance.cjs');
|
|
23
|
+
* const issues = runComplianceAudit(consumingAppPath);
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const path = require('path');
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Recursively find all TypeScript/JavaScript files in a directory
|
|
31
|
+
*/
|
|
32
|
+
function findSourceFiles(dir, fileList = []) {
|
|
33
|
+
if (!fs.existsSync(dir)) {
|
|
34
|
+
return fileList;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const files = fs.readdirSync(dir);
|
|
38
|
+
|
|
39
|
+
files.forEach(file => {
|
|
40
|
+
const filePath = path.join(dir, file);
|
|
41
|
+
const stat = fs.statSync(filePath);
|
|
42
|
+
|
|
43
|
+
if (stat.isDirectory()) {
|
|
44
|
+
// Skip node_modules, dist, build, .git, etc.
|
|
45
|
+
if (!['node_modules', 'dist', 'build', '.git', '.next', '.vite', 'coverage', '.turbo'].includes(file)) {
|
|
46
|
+
findSourceFiles(filePath, fileList);
|
|
47
|
+
}
|
|
48
|
+
} else if (/\.(ts|tsx|js|jsx)$/.test(file)) {
|
|
49
|
+
fileList.push(filePath);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return fileList;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get line number from index in content
|
|
58
|
+
*/
|
|
59
|
+
function getLineNumber(content, index) {
|
|
60
|
+
return content.substring(0, index).split('\n').length;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get code snippet around a match for context
|
|
65
|
+
*/
|
|
66
|
+
function getCodeSnippet(content, index, before = 30, after = 50) {
|
|
67
|
+
const start = Math.max(0, index - before);
|
|
68
|
+
const end = Math.min(content.length, index + after);
|
|
69
|
+
return content.substring(start, end).trim();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Check if content is in a comment or string
|
|
74
|
+
*/
|
|
75
|
+
function isInCommentOrString(content, index) {
|
|
76
|
+
const before = content.substring(0, index);
|
|
77
|
+
|
|
78
|
+
// Check for line comments
|
|
79
|
+
const lastLineComment = before.lastIndexOf('//');
|
|
80
|
+
const lastNewline = before.lastIndexOf('\n');
|
|
81
|
+
if (lastLineComment > lastNewline) {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check for block comments
|
|
86
|
+
const lastBlockCommentStart = before.lastIndexOf('/*');
|
|
87
|
+
const lastBlockCommentEnd = before.lastIndexOf('*/');
|
|
88
|
+
if (lastBlockCommentStart > lastBlockCommentEnd) {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Check for string literals (simple check)
|
|
93
|
+
const singleQuoteMatches = [...before.matchAll(/'/g)];
|
|
94
|
+
const doubleQuoteMatches = [...before.matchAll(/"/g)];
|
|
95
|
+
const backtickMatches = [...before.matchAll(/`/g)];
|
|
96
|
+
|
|
97
|
+
// Simple heuristic: if odd number of quotes before, might be in string
|
|
98
|
+
// This is not perfect but good enough for audit purposes
|
|
99
|
+
const inSingleQuote = singleQuoteMatches.length % 2 === 1;
|
|
100
|
+
const inDoubleQuote = doubleQuoteMatches.length % 2 === 1;
|
|
101
|
+
const inBacktick = backtickMatches.length % 2 === 1;
|
|
102
|
+
|
|
103
|
+
return inSingleQuote || inDoubleQuote || inBacktick;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Load core-usage-manifest.json
|
|
108
|
+
*/
|
|
109
|
+
function loadManifest(consumingAppPath) {
|
|
110
|
+
// Try to find manifest in pace-core package
|
|
111
|
+
const possiblePaths = [
|
|
112
|
+
path.resolve(__dirname, '../../core-usage-manifest.json'),
|
|
113
|
+
path.resolve(__dirname, '../../../core-usage-manifest.json'),
|
|
114
|
+
path.join(consumingAppPath, 'node_modules', '@jmruthers', 'pace-core', 'core-usage-manifest.json'),
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
for (const manifestPath of possiblePaths) {
|
|
118
|
+
if (fs.existsSync(manifestPath)) {
|
|
119
|
+
try {
|
|
120
|
+
return JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
121
|
+
} catch (e) {
|
|
122
|
+
// Continue to next path
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Check if file imports a specific component/hook/util from pace-core
|
|
132
|
+
*/
|
|
133
|
+
function importsFromPaceCore(content, name) {
|
|
134
|
+
// Check for imports like:
|
|
135
|
+
// import { Name } from '@jmruthers/pace-core'
|
|
136
|
+
// import { Name } from '@jmruthers/pace-core/components'
|
|
137
|
+
// import Name from '@jmruthers/pace-core'
|
|
138
|
+
const patterns = [
|
|
139
|
+
new RegExp(`import\\s+.*\\b${name}\\b.*from\\s+['"]@jmruthers/pace-core`),
|
|
140
|
+
new RegExp(`import\\s+.*\\b${name}\\b.*from\\s+['"]@jmruthers/pace-core/components`),
|
|
141
|
+
new RegExp(`import\\s+.*\\b${name}\\b.*from\\s+['"]@jmruthers/pace-core/hooks`),
|
|
142
|
+
new RegExp(`import\\s+.*\\b${name}\\b.*from\\s+['"]@jmruthers/pace-core/utils`),
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
return patterns.some(pattern => pattern.test(content));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Check for native HTML elements when pace-core components exist
|
|
150
|
+
*
|
|
151
|
+
* MIGRATED TO ESLINT: This check is now handled by 'prefer-pace-core-components' ESLint rule.
|
|
152
|
+
* Kept for reference only.
|
|
153
|
+
*/
|
|
154
|
+
function checkNativeElements_MIGRATED_TO_ESLINT(content, filePath, consumingAppPath, manifest) {
|
|
155
|
+
const issues = [];
|
|
156
|
+
|
|
157
|
+
// Map of native elements to pace-core components
|
|
158
|
+
const elementMap = {
|
|
159
|
+
'button': 'Button',
|
|
160
|
+
'input': 'Input',
|
|
161
|
+
'label': 'Label',
|
|
162
|
+
'textarea': 'Textarea',
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
Object.entries(elementMap).forEach(([nativeElement, paceCoreComponent]) => {
|
|
166
|
+
// Pattern to match <element> tags (lowercase only - not <Element> component)
|
|
167
|
+
// Case-sensitive regex (no 'i' flag) ensures we only match native HTML, not pace-core components
|
|
168
|
+
const pattern = new RegExp(`<${nativeElement}(\\s|>|/|\\n)`, 'g');
|
|
169
|
+
let match;
|
|
170
|
+
|
|
171
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
172
|
+
if (isInCommentOrString(content, match.index)) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Check if this file imports the pace-core component
|
|
177
|
+
if (importsFromPaceCore(content, paceCoreComponent)) {
|
|
178
|
+
// They have the import but still using native element - might be intentional
|
|
179
|
+
// Flag it anyway as it should use the component
|
|
180
|
+
const relativePath = path.relative(consumingAppPath || process.cwd(), filePath);
|
|
181
|
+
issues.push({
|
|
182
|
+
type: 'nativeElement',
|
|
183
|
+
file: relativePath,
|
|
184
|
+
line: getLineNumber(content, match.index),
|
|
185
|
+
message: `Native <${nativeElement}> element detected. Use pace-core ${paceCoreComponent} component instead.`,
|
|
186
|
+
code: getCodeSnippet(content, match.index),
|
|
187
|
+
severity: 'error',
|
|
188
|
+
fix: `Replace <${nativeElement}> with <${paceCoreComponent}> from '@jmruthers/pace-core'`,
|
|
189
|
+
});
|
|
190
|
+
} else {
|
|
191
|
+
// No import, definitely should use pace-core component
|
|
192
|
+
const relativePath = path.relative(consumingAppPath || process.cwd(), filePath);
|
|
193
|
+
issues.push({
|
|
194
|
+
type: 'nativeElement',
|
|
195
|
+
file: relativePath,
|
|
196
|
+
line: getLineNumber(content, match.index),
|
|
197
|
+
message: `Native <${nativeElement}> element detected. Use pace-core ${paceCoreComponent} component instead.`,
|
|
198
|
+
code: getCodeSnippet(content, match.index),
|
|
199
|
+
severity: 'error',
|
|
200
|
+
fix: `Import and use ${paceCoreComponent} from '@jmruthers/pace-core'`,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
return issues;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Check for restricted library imports
|
|
211
|
+
*
|
|
212
|
+
* MIGRATED TO ESLINT: This check is now handled by 'no-restricted-imports' ESLint rule.
|
|
213
|
+
* Kept for reference only.
|
|
214
|
+
*/
|
|
215
|
+
function checkRestrictedImports_MIGRATED_TO_ESLINT(content, filePath, consumingAppPath, manifest) {
|
|
216
|
+
const issues = [];
|
|
217
|
+
|
|
218
|
+
if (!manifest || !manifest.restrictedImports) {
|
|
219
|
+
return issues;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
manifest.restrictedImports.forEach(restriction => {
|
|
223
|
+
const moduleName = restriction.module;
|
|
224
|
+
|
|
225
|
+
// Skip react-hook-form - it's handled by audit-components.cjs with more nuanced logic
|
|
226
|
+
// (allows useFormContext when Form is imported from pace-core)
|
|
227
|
+
if (moduleName === 'react-hook-form') {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Pattern to match imports from restricted module
|
|
232
|
+
const importPattern = new RegExp(`from\\s+['"]${moduleName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}['"]`, 'g');
|
|
233
|
+
let match;
|
|
234
|
+
|
|
235
|
+
while ((match = importPattern.exec(content)) !== null) {
|
|
236
|
+
if (isInCommentOrString(content, match.index)) {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Get the import line
|
|
241
|
+
const lineStart = content.lastIndexOf('\n', match.index) + 1;
|
|
242
|
+
const lineEnd = content.indexOf('\n', match.index);
|
|
243
|
+
const importLine = content.substring(lineStart, lineEnd === -1 ? content.length : lineEnd);
|
|
244
|
+
|
|
245
|
+
const relativePath = path.relative(consumingAppPath || process.cwd(), filePath);
|
|
246
|
+
|
|
247
|
+
let fix = restriction.reason;
|
|
248
|
+
if (restriction.importExample) {
|
|
249
|
+
fix += `\n Example: ${restriction.importExample}`;
|
|
250
|
+
}
|
|
251
|
+
if (restriction.usageExample) {
|
|
252
|
+
fix += `\n Usage: ${restriction.usageExample}`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
issues.push({
|
|
256
|
+
type: 'restrictedImport',
|
|
257
|
+
file: relativePath,
|
|
258
|
+
line: getLineNumber(content, match.index),
|
|
259
|
+
message: `Direct import from '${moduleName}' detected. ${restriction.reason}`,
|
|
260
|
+
code: importLine.trim(),
|
|
261
|
+
severity: 'error',
|
|
262
|
+
fix: fix,
|
|
263
|
+
module: moduleName,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
return issues;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Check for custom hooks when pace-core provides them
|
|
273
|
+
*
|
|
274
|
+
* MIGRATED TO ESLINT: This check is now handled by 'prefer-pace-core-hooks' ESLint rule.
|
|
275
|
+
* Kept for reference only.
|
|
276
|
+
*/
|
|
277
|
+
function checkCustomHooks_MIGRATED_TO_ESLINT(content, filePath, consumingAppPath, manifest) {
|
|
278
|
+
const issues = [];
|
|
279
|
+
|
|
280
|
+
if (!manifest || !manifest.hooks) {
|
|
281
|
+
return issues;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Common custom hook patterns that duplicate pace-core functionality
|
|
285
|
+
const problematicHooks = {
|
|
286
|
+
'useAuth': 'useUnifiedAuth',
|
|
287
|
+
'useToast': 'useToast',
|
|
288
|
+
'useDebounce': 'useDebounce',
|
|
289
|
+
'useForm': 'useZodForm',
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
Object.entries(problematicHooks).forEach(([customHook, paceCoreHook]) => {
|
|
293
|
+
// Pattern to match hook definitions
|
|
294
|
+
const patterns = [
|
|
295
|
+
new RegExp(`(function|const)\\s+${customHook}\\s*[=(]`, 'g'),
|
|
296
|
+
new RegExp(`export\\s+(function|const)\\s+${customHook}\\s*[=(]`, 'g'),
|
|
297
|
+
];
|
|
298
|
+
|
|
299
|
+
patterns.forEach(pattern => {
|
|
300
|
+
let match;
|
|
301
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
302
|
+
if (isInCommentOrString(content, match.index)) {
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Check if pace-core hook is imported
|
|
307
|
+
if (importsFromPaceCore(content, paceCoreHook)) {
|
|
308
|
+
// They have the import but also defined custom hook - likely duplicate
|
|
309
|
+
const relativePath = path.relative(consumingAppPath || process.cwd(), filePath);
|
|
310
|
+
issues.push({
|
|
311
|
+
type: 'customHook',
|
|
312
|
+
file: relativePath,
|
|
313
|
+
line: getLineNumber(content, match.index),
|
|
314
|
+
message: `Custom ${customHook} hook detected. Use pace-core ${paceCoreHook} hook instead.`,
|
|
315
|
+
code: getCodeSnippet(content, match.index),
|
|
316
|
+
severity: 'warning',
|
|
317
|
+
fix: `Remove custom ${customHook} and use ${paceCoreHook} from '@jmruthers/pace-core'`,
|
|
318
|
+
});
|
|
319
|
+
} else {
|
|
320
|
+
// No import, might be custom implementation
|
|
321
|
+
const relativePath = path.relative(consumingAppPath || process.cwd(), filePath);
|
|
322
|
+
issues.push({
|
|
323
|
+
type: 'customHook',
|
|
324
|
+
file: relativePath,
|
|
325
|
+
line: getLineNumber(content, match.index),
|
|
326
|
+
message: `Custom ${customHook} hook detected. Consider using pace-core ${paceCoreHook} hook instead.`,
|
|
327
|
+
code: getCodeSnippet(content, match.index),
|
|
328
|
+
severity: 'warning',
|
|
329
|
+
fix: `Use ${paceCoreHook} from '@jmruthers/pace-core' instead of custom implementation`,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
return issues;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Check for custom utilities when pace-core provides them
|
|
341
|
+
*
|
|
342
|
+
* MIGRATED TO ESLINT: This check is now handled by 'prefer-pace-core-utils' ESLint rule.
|
|
343
|
+
* Kept for reference only.
|
|
344
|
+
*/
|
|
345
|
+
function checkCustomUtils_MIGRATED_TO_ESLINT(content, filePath, consumingAppPath, manifest) {
|
|
346
|
+
const issues = [];
|
|
347
|
+
|
|
348
|
+
if (!manifest || !manifest.utils) {
|
|
349
|
+
return issues;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Common utility patterns that duplicate pace-core functionality
|
|
353
|
+
const problematicUtils = {
|
|
354
|
+
'formatDate': 'formatDate',
|
|
355
|
+
'formatTime': 'formatTime',
|
|
356
|
+
'formatDateTime': 'formatDateTime',
|
|
357
|
+
'formatCurrency': 'formatCurrency',
|
|
358
|
+
'formatNumber': 'formatNumber',
|
|
359
|
+
'cn': 'cn',
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
Object.entries(problematicUtils).forEach(([customUtil, paceCoreUtil]) => {
|
|
363
|
+
// Pattern to match function definitions
|
|
364
|
+
const patterns = [
|
|
365
|
+
new RegExp(`(function|const|export\\s+(function|const))\\s+${customUtil}\\s*[=(]`, 'g'),
|
|
366
|
+
];
|
|
367
|
+
|
|
368
|
+
patterns.forEach(pattern => {
|
|
369
|
+
let match;
|
|
370
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
371
|
+
if (isInCommentOrString(content, match.index)) {
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Check if pace-core util is imported
|
|
376
|
+
if (importsFromPaceCore(content, paceCoreUtil)) {
|
|
377
|
+
// They have the import but also defined custom util - likely duplicate
|
|
378
|
+
const relativePath = path.relative(consumingAppPath || process.cwd(), filePath);
|
|
379
|
+
issues.push({
|
|
380
|
+
type: 'customUtil',
|
|
381
|
+
file: relativePath,
|
|
382
|
+
line: getLineNumber(content, match.index),
|
|
383
|
+
message: `Custom ${customUtil} utility detected. Use pace-core ${paceCoreUtil} utility instead.`,
|
|
384
|
+
code: getCodeSnippet(content, match.index),
|
|
385
|
+
severity: 'warning',
|
|
386
|
+
fix: `Remove custom ${customUtil} and use ${paceCoreUtil} from '@jmruthers/pace-core'`,
|
|
387
|
+
});
|
|
388
|
+
} else {
|
|
389
|
+
// No import, might be custom implementation
|
|
390
|
+
const relativePath = path.relative(consumingAppPath || process.cwd(), filePath);
|
|
391
|
+
issues.push({
|
|
392
|
+
type: 'customUtil',
|
|
393
|
+
file: relativePath,
|
|
394
|
+
line: getLineNumber(content, match.index),
|
|
395
|
+
message: `Custom ${customUtil} utility detected. Consider using pace-core ${paceCoreUtil} utility instead.`,
|
|
396
|
+
code: getCodeSnippet(content, match.index),
|
|
397
|
+
severity: 'warning',
|
|
398
|
+
fix: `Use ${paceCoreUtil} from '@jmruthers/pace-core' instead of custom implementation`,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
return issues;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Check for secure Supabase client usage
|
|
410
|
+
*/
|
|
411
|
+
function checkSupabaseUsage(content, filePath, consumingAppPath) {
|
|
412
|
+
const issues = [];
|
|
413
|
+
const relativePath = path.relative(consumingAppPath || process.cwd(), filePath);
|
|
414
|
+
|
|
415
|
+
// Check for createClient() calls
|
|
416
|
+
const createClientPattern = /createClient\s*\(/g;
|
|
417
|
+
let match;
|
|
418
|
+
|
|
419
|
+
while ((match = createClientPattern.exec(content)) !== null) {
|
|
420
|
+
if (isInCommentOrString(content, match.index)) {
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Check if this is from @supabase/supabase-js
|
|
425
|
+
const beforeMatch = content.substring(Math.max(0, match.index - 200), match.index);
|
|
426
|
+
const hasSupabaseImport = /from\s+['"]@supabase\/supabase-js['"]/.test(beforeMatch);
|
|
427
|
+
|
|
428
|
+
if (hasSupabaseImport) {
|
|
429
|
+
// Check if file is in allowed location
|
|
430
|
+
const allowedPatterns = [
|
|
431
|
+
/[\/\\]main\.(tsx?|jsx?)$/i,
|
|
432
|
+
/[\/\\]App\.(tsx?|jsx?)$/i,
|
|
433
|
+
/[\/\\]lib[\/\\]supabase\.(tsx?|jsx?)$/i,
|
|
434
|
+
/[\/\\]src[\/\\]supabase\.(tsx?|jsx?)$/i,
|
|
435
|
+
];
|
|
436
|
+
|
|
437
|
+
const isAllowed = allowedPatterns.some(pattern => pattern.test(filePath));
|
|
438
|
+
|
|
439
|
+
if (!isAllowed) {
|
|
440
|
+
issues.push({
|
|
441
|
+
type: 'supabaseClient',
|
|
442
|
+
file: relativePath,
|
|
443
|
+
line: getLineNumber(content, match.index),
|
|
444
|
+
message: 'createClient() call detected in unauthorized location. Should only be in main.tsx, App.tsx, or lib/supabase.ts',
|
|
445
|
+
code: getCodeSnippet(content, match.index),
|
|
446
|
+
severity: 'error',
|
|
447
|
+
fix: 'Move createClient() call to main.tsx, App.tsx, or lib/supabase.ts. Use useSecureSupabase() hook for queries.',
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Check for .from(), .rpc(), .auth. calls on potentially insecure clients
|
|
454
|
+
// This is a heuristic - we look for these patterns and check if they're on a variable
|
|
455
|
+
// that might be an insecure client
|
|
456
|
+
const queryPatterns = [
|
|
457
|
+
/\.from\s*\(/g,
|
|
458
|
+
/\.rpc\s*\(/g,
|
|
459
|
+
/\.auth\./g,
|
|
460
|
+
];
|
|
461
|
+
|
|
462
|
+
queryPatterns.forEach(pattern => {
|
|
463
|
+
let match;
|
|
464
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
465
|
+
if (isInCommentOrString(content, match.index)) {
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Check if this is on useSecureSupabase() result
|
|
470
|
+
const beforeMatch = content.substring(Math.max(0, match.index - 100), match.index);
|
|
471
|
+
const hasSecureSupabase = /useSecureSupabase\s*\(/.test(beforeMatch) ||
|
|
472
|
+
/secureSupabase/.test(beforeMatch) ||
|
|
473
|
+
/supabase\s*=\s*useSecureSupabase/.test(beforeMatch);
|
|
474
|
+
|
|
475
|
+
if (!hasSecureSupabase) {
|
|
476
|
+
// Might be insecure - check context
|
|
477
|
+
const contextBefore = content.substring(Math.max(0, match.index - 200), match.index);
|
|
478
|
+
const hasCreateClient = /createClient\s*\(/.test(contextBefore);
|
|
479
|
+
|
|
480
|
+
if (hasCreateClient) {
|
|
481
|
+
issues.push({
|
|
482
|
+
type: 'supabaseClient',
|
|
483
|
+
file: relativePath,
|
|
484
|
+
line: getLineNumber(content, match.index),
|
|
485
|
+
message: 'Database query detected on potentially insecure client. Use useSecureSupabase() hook instead.',
|
|
486
|
+
code: getCodeSnippet(content, match.index),
|
|
487
|
+
severity: 'error',
|
|
488
|
+
fix: 'Use useSecureSupabase() hook from @jmruthers/pace-core/rbac for all database queries',
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
// Check for useSecureSupabase() called with parameters (should be called without)
|
|
496
|
+
const useSecureSupabasePattern = /useSecureSupabase\s*\(\s*[^)]/g;
|
|
497
|
+
let secureMatch;
|
|
498
|
+
while ((secureMatch = useSecureSupabasePattern.exec(content)) !== null) {
|
|
499
|
+
if (isInCommentOrString(content, secureMatch.index)) {
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Check if parameter is a base client (acceptable exception)
|
|
504
|
+
const afterMatch = content.substring(secureMatch.index, secureMatch.index + 100);
|
|
505
|
+
const hasBaseClientParam = /useSecureSupabase\s*\(\s*(supabase|baseClient|client)/.test(afterMatch);
|
|
506
|
+
|
|
507
|
+
if (!hasBaseClientParam) {
|
|
508
|
+
// Might be an issue, but base client as parameter is acceptable
|
|
509
|
+
// This is a warning, not an error
|
|
510
|
+
issues.push({
|
|
511
|
+
type: 'supabaseClient',
|
|
512
|
+
file: relativePath,
|
|
513
|
+
line: getLineNumber(content, secureMatch.index),
|
|
514
|
+
message: 'useSecureSupabase() called with parameters. Should be called without parameters (gets client from provider automatically).',
|
|
515
|
+
code: getCodeSnippet(content, secureMatch.index),
|
|
516
|
+
severity: 'warning',
|
|
517
|
+
fix: 'Call useSecureSupabase() without parameters. The hook automatically gets the client from UnifiedAuthProvider.',
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return issues;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Check for RBAC permission usage patterns
|
|
527
|
+
* Detects cases where permission checks are called without waiting for loading state
|
|
528
|
+
*
|
|
529
|
+
* MIGRATED TO ESLINT: This check is now handled by 'rbac-permission-loading' ESLint rule.
|
|
530
|
+
* Kept for reference only.
|
|
531
|
+
*/
|
|
532
|
+
function checkRBACPermissionUsage_MIGRATED_TO_ESLINT(content, filePath, consumingAppPath) {
|
|
533
|
+
const issues = [];
|
|
534
|
+
const relativePath = path.relative(consumingAppPath || process.cwd(), filePath);
|
|
535
|
+
|
|
536
|
+
// Check if file uses useResourcePermissions
|
|
537
|
+
const usesResourcePermissions = /useResourcePermissions\s*\(/i.test(content);
|
|
538
|
+
if (!usesResourcePermissions) {
|
|
539
|
+
return issues;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Find useResourcePermissions calls and check if isLoading is extracted
|
|
543
|
+
// Pattern to match destructuring assignments with useResourcePermissions
|
|
544
|
+
const resourcePermissionsPattern = /(const|let|var)\s*\{[^}]*useResourcePermissions\s*\([^)]*\)[^}]*\}/g;
|
|
545
|
+
let match;
|
|
546
|
+
|
|
547
|
+
while ((match = resourcePermissionsPattern.exec(content)) !== null) {
|
|
548
|
+
if (isInCommentOrString(content, match.index)) {
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const destructuringBlock = match[0];
|
|
553
|
+
const matchIndex = match.index;
|
|
554
|
+
|
|
555
|
+
// Check if isLoading is extracted from useResourcePermissions
|
|
556
|
+
// Match: isLoading, isLoading: permissionsLoading, isLoading: loading, etc.
|
|
557
|
+
const hasIsLoading = /\bisLoading\s*(?::\s*\w+)?\s*[,}]/i.test(destructuringBlock);
|
|
558
|
+
|
|
559
|
+
// Find permission function calls (canCreate, canUpdate, canDelete) in the file
|
|
560
|
+
// Look for calls that might not check loading state first
|
|
561
|
+
const permissionCallPattern = /\b(canCreate|canUpdate|canDelete)\s*\(/g;
|
|
562
|
+
let permMatch;
|
|
563
|
+
|
|
564
|
+
while ((permMatch = permissionCallPattern.exec(content)) !== null) {
|
|
565
|
+
if (isInCommentOrString(content, permMatch.index)) {
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Find the function/block that contains this permission call
|
|
570
|
+
// Look backwards to find the start of the function/block
|
|
571
|
+
const beforeCall = content.substring(0, permMatch.index);
|
|
572
|
+
const functionStart = Math.max(
|
|
573
|
+
beforeCall.lastIndexOf('function'),
|
|
574
|
+
beforeCall.lastIndexOf('const'),
|
|
575
|
+
beforeCall.lastIndexOf('=>'),
|
|
576
|
+
beforeCall.lastIndexOf('async')
|
|
577
|
+
);
|
|
578
|
+
|
|
579
|
+
if (functionStart === -1) continue;
|
|
580
|
+
|
|
581
|
+
// Get the context around the permission call
|
|
582
|
+
const contextStart = Math.max(0, functionStart);
|
|
583
|
+
const contextEnd = Math.min(content.length, permMatch.index + 200);
|
|
584
|
+
const context = content.substring(contextStart, contextEnd);
|
|
585
|
+
|
|
586
|
+
// Check if isLoading is checked before this permission call in the same context
|
|
587
|
+
const isLoadingCheckedBefore = new RegExp(
|
|
588
|
+
`(if\\s*\\(\\s*!?\\s*isLoading|if\\s*\\(\\s*isLoading\\s*===\\s*false|if\\s*\\(\\s*isLoading\\s*!==\\s*true|await|permissionsLoading)`,
|
|
589
|
+
'i'
|
|
590
|
+
).test(context.substring(0, permMatch.index - contextStart));
|
|
591
|
+
|
|
592
|
+
// Check if this is inside a mutation function (where we need to check loading)
|
|
593
|
+
const isInMutation = /mutationFn\s*[:=]\s*async|mutateAsync|mutation\s*[:=]/i.test(context);
|
|
594
|
+
|
|
595
|
+
// If isLoading is not extracted, flag it
|
|
596
|
+
if (!hasIsLoading && isInMutation) {
|
|
597
|
+
issues.push({
|
|
598
|
+
type: 'rbacPermission',
|
|
599
|
+
file: relativePath,
|
|
600
|
+
line: getLineNumber(content, matchIndex),
|
|
601
|
+
message: `useResourcePermissions is used but 'isLoading' is not extracted. Permission checks may fail if scope resolution is still in progress.`,
|
|
602
|
+
code: getCodeSnippet(content, matchIndex, 0, 100),
|
|
603
|
+
severity: 'error',
|
|
604
|
+
fix: `Extract 'isLoading' from useResourcePermissions: const { canCreate, canUpdate, canDelete, isLoading: permissionsLoading } = useResourcePermissions(...);`,
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// If isLoading is extracted but not checked before permission call in mutation
|
|
609
|
+
if (hasIsLoading && isInMutation && !isLoadingCheckedBefore) {
|
|
610
|
+
const permLine = getLineNumber(content, permMatch.index);
|
|
611
|
+
issues.push({
|
|
612
|
+
type: 'rbacPermission',
|
|
613
|
+
file: relativePath,
|
|
614
|
+
line: permLine,
|
|
615
|
+
message: `Permission check '${permMatch[1]}()' is called without checking isLoading first. This can cause false negatives when scope resolution is still in progress.`,
|
|
616
|
+
code: getCodeSnippet(content, permMatch.index, 20, 50),
|
|
617
|
+
severity: 'error',
|
|
618
|
+
fix: `Check isLoading before calling permission function: if (permissionsLoading) { throw new Error("Permission check in progress. Please wait..."); } if (!canCreate(resource)) { ... }`,
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
return issues;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Check for setupRBAC() call in main.tsx
|
|
629
|
+
*/
|
|
630
|
+
function checkRBACSetup(consumingAppPath) {
|
|
631
|
+
const issues = [];
|
|
632
|
+
|
|
633
|
+
// Check main.tsx or main.jsx
|
|
634
|
+
const mainFiles = [
|
|
635
|
+
path.join(consumingAppPath, 'src', 'main.tsx'),
|
|
636
|
+
path.join(consumingAppPath, 'src', 'main.ts'),
|
|
637
|
+
path.join(consumingAppPath, 'src', 'main.jsx'),
|
|
638
|
+
path.join(consumingAppPath, 'src', 'main.js'),
|
|
639
|
+
path.join(consumingAppPath, 'main.tsx'),
|
|
640
|
+
path.join(consumingAppPath, 'main.ts'),
|
|
641
|
+
path.join(consumingAppPath, 'main.jsx'),
|
|
642
|
+
path.join(consumingAppPath, 'main.js'),
|
|
643
|
+
];
|
|
644
|
+
|
|
645
|
+
let mainFile = null;
|
|
646
|
+
for (const file of mainFiles) {
|
|
647
|
+
if (fs.existsSync(file)) {
|
|
648
|
+
mainFile = file;
|
|
649
|
+
break;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
if (!mainFile) {
|
|
654
|
+
issues.push({
|
|
655
|
+
type: 'rbacSetup',
|
|
656
|
+
file: 'src/main.tsx (not found)',
|
|
657
|
+
line: 0,
|
|
658
|
+
message: 'main.tsx file not found. setupRBAC() must be called in main.tsx before app rendering.',
|
|
659
|
+
code: '',
|
|
660
|
+
severity: 'error',
|
|
661
|
+
fix: 'Create src/main.tsx and call setupRBAC(supabase) before rendering the app.',
|
|
662
|
+
});
|
|
663
|
+
return issues;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const content = fs.readFileSync(mainFile, 'utf8');
|
|
667
|
+
const relativePath = path.relative(consumingAppPath || process.cwd(), mainFile);
|
|
668
|
+
|
|
669
|
+
// Check for setupRBAC call
|
|
670
|
+
const setupRBACPattern = /setupRBAC\s*\(/;
|
|
671
|
+
if (!setupRBACPattern.test(content)) {
|
|
672
|
+
issues.push({
|
|
673
|
+
type: 'rbacSetup',
|
|
674
|
+
file: relativePath,
|
|
675
|
+
line: 1,
|
|
676
|
+
message: 'setupRBAC() call not found. Must be called in main.tsx before app rendering.',
|
|
677
|
+
code: '',
|
|
678
|
+
severity: 'error',
|
|
679
|
+
fix: 'Add: import { setupRBAC } from \'@jmruthers/pace-core/rbac\'; setupRBAC(supabase);',
|
|
680
|
+
});
|
|
681
|
+
} else {
|
|
682
|
+
// Check if it's called before React rendering
|
|
683
|
+
const setupRBACIndex = content.indexOf('setupRBAC');
|
|
684
|
+
const renderIndex = content.search(/(createRoot|render)\s*\(/);
|
|
685
|
+
|
|
686
|
+
if (renderIndex !== -1 && setupRBACIndex > renderIndex) {
|
|
687
|
+
issues.push({
|
|
688
|
+
type: 'rbacSetup',
|
|
689
|
+
file: relativePath,
|
|
690
|
+
line: getLineNumber(content, setupRBACIndex),
|
|
691
|
+
message: 'setupRBAC() called after React rendering. Must be called before rendering.',
|
|
692
|
+
code: getCodeSnippet(content, setupRBACIndex),
|
|
693
|
+
severity: 'error',
|
|
694
|
+
fix: 'Move setupRBAC() call before createRoot() or render() call.',
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
return issues;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Check for required providers
|
|
704
|
+
* Providers can be in:
|
|
705
|
+
* - main.tsx or App.tsx (direct)
|
|
706
|
+
* - A provider file (e.g., AppProviders, providers.tsx)
|
|
707
|
+
* - A component imported in App.tsx that wraps the app
|
|
708
|
+
*/
|
|
709
|
+
function checkProviders(consumingAppPath) {
|
|
710
|
+
const issues = [];
|
|
711
|
+
|
|
712
|
+
// First, check main.tsx or App.tsx
|
|
713
|
+
const checkFiles = [
|
|
714
|
+
path.join(consumingAppPath, 'src', 'main.tsx'),
|
|
715
|
+
path.join(consumingAppPath, 'src', 'main.ts'),
|
|
716
|
+
path.join(consumingAppPath, 'src', 'main.jsx'),
|
|
717
|
+
path.join(consumingAppPath, 'src', 'main.js'),
|
|
718
|
+
path.join(consumingAppPath, 'src', 'App.tsx'),
|
|
719
|
+
path.join(consumingAppPath, 'src', 'App.ts'),
|
|
720
|
+
path.join(consumingAppPath, 'src', 'App.jsx'),
|
|
721
|
+
path.join(consumingAppPath, 'src', 'App.js'),
|
|
722
|
+
];
|
|
723
|
+
|
|
724
|
+
let foundFile = null;
|
|
725
|
+
let mainContent = '';
|
|
726
|
+
let appContent = '';
|
|
727
|
+
|
|
728
|
+
// Read main.tsx if it exists
|
|
729
|
+
const mainFile = checkFiles.find(f => fs.existsSync(f));
|
|
730
|
+
if (mainFile) {
|
|
731
|
+
foundFile = mainFile;
|
|
732
|
+
mainContent = fs.readFileSync(mainFile, 'utf8');
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Read App.tsx if it exists
|
|
736
|
+
const appFile = checkFiles.slice(4).find(f => fs.existsSync(f));
|
|
737
|
+
if (appFile) {
|
|
738
|
+
appContent = fs.readFileSync(appFile, 'utf8');
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (!foundFile && !appFile) {
|
|
742
|
+
issues.push({
|
|
743
|
+
type: 'providers',
|
|
744
|
+
file: 'src/main.tsx or src/App.tsx (not found)',
|
|
745
|
+
line: 0,
|
|
746
|
+
message: 'main.tsx or App.tsx not found. Required providers must wrap the app.',
|
|
747
|
+
code: '',
|
|
748
|
+
severity: 'error',
|
|
749
|
+
fix: 'Create src/main.tsx and wrap app with UnifiedAuthProvider and OrganisationProvider.',
|
|
750
|
+
});
|
|
751
|
+
return issues;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Combine content from both files for checking
|
|
755
|
+
const combinedContent = mainContent + '\n' + appContent;
|
|
756
|
+
|
|
757
|
+
// Check for UnifiedAuthProvider in main.tsx or App.tsx
|
|
758
|
+
let hasUnifiedAuthProvider = /<UnifiedAuthProvider/.test(combinedContent) ||
|
|
759
|
+
/UnifiedAuthProvider\s*</.test(combinedContent);
|
|
760
|
+
|
|
761
|
+
// Check for OrganisationProvider in main.tsx or App.tsx
|
|
762
|
+
let hasOrganisationProvider = /<OrganisationProvider/.test(combinedContent) ||
|
|
763
|
+
/OrganisationProvider\s*</.test(combinedContent);
|
|
764
|
+
|
|
765
|
+
// If providers not found in main.tsx/App.tsx, check for provider files
|
|
766
|
+
if (!hasUnifiedAuthProvider || !hasOrganisationProvider) {
|
|
767
|
+
// Look for provider files that might contain the providers
|
|
768
|
+
const providerFilePatterns = [
|
|
769
|
+
'**/providers/**/*.{ts,tsx,js,jsx}',
|
|
770
|
+
'**/app/providers/**/*.{ts,tsx,js,jsx}',
|
|
771
|
+
'**/AppProviders.{ts,tsx,js,jsx}',
|
|
772
|
+
'**/app-providers.{ts,tsx,js,jsx}',
|
|
773
|
+
];
|
|
774
|
+
|
|
775
|
+
// Check common provider file locations
|
|
776
|
+
const providerFiles = [
|
|
777
|
+
path.join(consumingAppPath, 'src', 'app', 'providers', 'app-providers.tsx'),
|
|
778
|
+
path.join(consumingAppPath, 'src', 'app', 'providers', 'app-providers.ts'),
|
|
779
|
+
path.join(consumingAppPath, 'src', 'app', 'providers', 'AppProviders.tsx'),
|
|
780
|
+
path.join(consumingAppPath, 'src', 'app', 'providers', 'AppProviders.ts'),
|
|
781
|
+
path.join(consumingAppPath, 'src', 'providers', 'AppProviders.tsx'),
|
|
782
|
+
path.join(consumingAppPath, 'src', 'providers', 'AppProviders.ts'),
|
|
783
|
+
path.join(consumingAppPath, 'src', 'providers', 'app-providers.tsx'),
|
|
784
|
+
path.join(consumingAppPath, 'src', 'providers', 'app-providers.ts'),
|
|
785
|
+
];
|
|
786
|
+
|
|
787
|
+
for (const providerFile of providerFiles) {
|
|
788
|
+
if (fs.existsSync(providerFile)) {
|
|
789
|
+
try {
|
|
790
|
+
const providerContent = fs.readFileSync(providerFile, 'utf8');
|
|
791
|
+
|
|
792
|
+
// Check for providers in this file
|
|
793
|
+
if (!hasUnifiedAuthProvider && /<UnifiedAuthProvider/.test(providerContent)) {
|
|
794
|
+
hasUnifiedAuthProvider = true;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
if (!hasOrganisationProvider && /<OrganisationProvider/.test(providerContent)) {
|
|
798
|
+
hasOrganisationProvider = true;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// If we found both, we can stop looking
|
|
802
|
+
if (hasUnifiedAuthProvider && hasOrganisationProvider) {
|
|
803
|
+
break;
|
|
804
|
+
}
|
|
805
|
+
} catch (e) {
|
|
806
|
+
// Skip files that can't be read
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// Also check if App.tsx imports a provider component
|
|
812
|
+
if (appContent) {
|
|
813
|
+
// Look for imports like: import { AppProviders } from '@/app/providers/app-providers';
|
|
814
|
+
const providerImportPattern = /import\s+.*\b(AppProviders|app-providers|Providers)\b.*from\s+['"]([^'"]+)['"]/;
|
|
815
|
+
const providerImportMatch = appContent.match(providerImportPattern);
|
|
816
|
+
|
|
817
|
+
if (providerImportMatch) {
|
|
818
|
+
// Try to find the imported file
|
|
819
|
+
const importPath = providerImportMatch[2];
|
|
820
|
+
// Handle path aliases (@/app/providers/app-providers)
|
|
821
|
+
const resolvedPath = importPath.startsWith('@/')
|
|
822
|
+
? path.join(consumingAppPath, 'src', importPath.slice(2))
|
|
823
|
+
: path.join(path.dirname(appFile), importPath);
|
|
824
|
+
|
|
825
|
+
// Try with different extensions
|
|
826
|
+
const possiblePaths = [
|
|
827
|
+
resolvedPath,
|
|
828
|
+
resolvedPath + '.tsx',
|
|
829
|
+
resolvedPath + '.ts',
|
|
830
|
+
resolvedPath + '.jsx',
|
|
831
|
+
resolvedPath + '.js',
|
|
832
|
+
];
|
|
833
|
+
|
|
834
|
+
for (const possiblePath of possiblePaths) {
|
|
835
|
+
if (fs.existsSync(possiblePath)) {
|
|
836
|
+
try {
|
|
837
|
+
const providerContent = fs.readFileSync(possiblePath, 'utf8');
|
|
838
|
+
|
|
839
|
+
if (!hasUnifiedAuthProvider && /<UnifiedAuthProvider/.test(providerContent)) {
|
|
840
|
+
hasUnifiedAuthProvider = true;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
if (!hasOrganisationProvider && /<OrganisationProvider/.test(providerContent)) {
|
|
844
|
+
hasOrganisationProvider = true;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
break;
|
|
848
|
+
} catch (e) {
|
|
849
|
+
// Skip files that can't be read
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
const relativePath = foundFile ? path.relative(consumingAppPath || process.cwd(), foundFile) : 'src/App.tsx';
|
|
858
|
+
|
|
859
|
+
// Check for UnifiedAuthProvider
|
|
860
|
+
if (!hasUnifiedAuthProvider) {
|
|
861
|
+
issues.push({
|
|
862
|
+
type: 'providers',
|
|
863
|
+
file: relativePath,
|
|
864
|
+
line: 1,
|
|
865
|
+
message: 'UnifiedAuthProvider not found. Must wrap the app (can be in main.tsx, App.tsx, or a provider file).',
|
|
866
|
+
code: '',
|
|
867
|
+
severity: 'error',
|
|
868
|
+
fix: 'Import and wrap app with UnifiedAuthProvider from @jmruthers/pace-core',
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// Check for OrganisationProvider
|
|
873
|
+
if (!hasOrganisationProvider) {
|
|
874
|
+
issues.push({
|
|
875
|
+
type: 'providers',
|
|
876
|
+
file: relativePath,
|
|
877
|
+
line: 1,
|
|
878
|
+
message: 'OrganisationProvider not found. Must wrap the app (inside UnifiedAuthProvider, can be in main.tsx, App.tsx, or a provider file).',
|
|
879
|
+
code: '',
|
|
880
|
+
severity: 'error',
|
|
881
|
+
fix: 'Import and wrap app with OrganisationProvider from @jmruthers/pace-core (inside UnifiedAuthProvider)',
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// Check nesting order
|
|
886
|
+
// Find JSX opening tags (not in import/export statements)
|
|
887
|
+
if (hasUnifiedAuthProvider && hasOrganisationProvider) {
|
|
888
|
+
// Find the file that contains the providers for nesting check
|
|
889
|
+
let nestingCheckContent = combinedContent;
|
|
890
|
+
let nestingCheckFile = relativePath;
|
|
891
|
+
|
|
892
|
+
// If providers weren't found in main.tsx/App.tsx, find the provider file
|
|
893
|
+
if (!/<UnifiedAuthProvider/.test(combinedContent) || !/<OrganisationProvider/.test(combinedContent)) {
|
|
894
|
+
// Look for provider files
|
|
895
|
+
const providerFiles = [
|
|
896
|
+
path.join(consumingAppPath, 'src', 'app', 'providers', 'app-providers.tsx'),
|
|
897
|
+
path.join(consumingAppPath, 'src', 'app', 'providers', 'app-providers.ts'),
|
|
898
|
+
path.join(consumingAppPath, 'src', 'app', 'providers', 'AppProviders.tsx'),
|
|
899
|
+
path.join(consumingAppPath, 'src', 'app', 'providers', 'AppProviders.ts'),
|
|
900
|
+
path.join(consumingAppPath, 'src', 'providers', 'AppProviders.tsx'),
|
|
901
|
+
path.join(consumingAppPath, 'src', 'providers', 'AppProviders.ts'),
|
|
902
|
+
path.join(consumingAppPath, 'src', 'providers', 'app-providers.tsx'),
|
|
903
|
+
path.join(consumingAppPath, 'src', 'providers', 'app-providers.ts'),
|
|
904
|
+
];
|
|
905
|
+
|
|
906
|
+
for (const providerFile of providerFiles) {
|
|
907
|
+
if (fs.existsSync(providerFile)) {
|
|
908
|
+
try {
|
|
909
|
+
const providerContent = fs.readFileSync(providerFile, 'utf8');
|
|
910
|
+
if (/<UnifiedAuthProvider/.test(providerContent) && /<OrganisationProvider/.test(providerContent)) {
|
|
911
|
+
nestingCheckContent = providerContent;
|
|
912
|
+
nestingCheckFile = path.relative(consumingAppPath || process.cwd(), providerFile);
|
|
913
|
+
break;
|
|
914
|
+
}
|
|
915
|
+
} catch (e) {
|
|
916
|
+
// Skip files that can't be read
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// Find all occurrences of opening tags
|
|
923
|
+
const unifiedAuthMatches = [];
|
|
924
|
+
const organisationMatches = [];
|
|
925
|
+
|
|
926
|
+
// Find <UnifiedAuthProvider opening tags (not in import statements)
|
|
927
|
+
const unifiedAuthPattern = /<UnifiedAuthProvider\s*(?:[^>]*>|>)/g;
|
|
928
|
+
let match;
|
|
929
|
+
while ((match = unifiedAuthPattern.exec(nestingCheckContent)) !== null) {
|
|
930
|
+
// Check if this is in an import/export statement
|
|
931
|
+
const beforeMatch = nestingCheckContent.substring(Math.max(0, match.index - 100), match.index);
|
|
932
|
+
const isInImport = /import\s+.*from\s+['"]/.test(beforeMatch) ||
|
|
933
|
+
/export\s+.*from\s+['"]/.test(beforeMatch);
|
|
934
|
+
if (!isInImport) {
|
|
935
|
+
unifiedAuthMatches.push(match.index);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// Find <OrganisationProvider opening tags (not in import statements)
|
|
940
|
+
const organisationPattern = /<OrganisationProvider\s*(?:[^>]*>|>)/g;
|
|
941
|
+
while ((match = organisationPattern.exec(nestingCheckContent)) !== null) {
|
|
942
|
+
// Check if this is in an import/export statement
|
|
943
|
+
const beforeMatch = nestingCheckContent.substring(Math.max(0, match.index - 100), match.index);
|
|
944
|
+
const isInImport = /import\s+.*from\s+['"]/.test(beforeMatch) ||
|
|
945
|
+
/export\s+.*from\s+['"]/.test(beforeMatch);
|
|
946
|
+
if (!isInImport) {
|
|
947
|
+
organisationMatches.push(match.index);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// Check nesting order - UnifiedAuthProvider should come before OrganisationProvider
|
|
952
|
+
if (unifiedAuthMatches.length > 0 && organisationMatches.length > 0) {
|
|
953
|
+
const firstUnifiedAuth = unifiedAuthMatches[0];
|
|
954
|
+
const firstOrganisation = organisationMatches[0];
|
|
955
|
+
|
|
956
|
+
// Check if OrganisationProvider appears before UnifiedAuthProvider (wrong order)
|
|
957
|
+
if (firstOrganisation < firstUnifiedAuth) {
|
|
958
|
+
issues.push({
|
|
959
|
+
type: 'providers',
|
|
960
|
+
file: nestingCheckFile,
|
|
961
|
+
line: getLineNumber(nestingCheckContent, firstOrganisation),
|
|
962
|
+
message: 'OrganisationProvider must be nested inside UnifiedAuthProvider, not the other way around.',
|
|
963
|
+
code: getCodeSnippet(nestingCheckContent, firstOrganisation),
|
|
964
|
+
severity: 'error',
|
|
965
|
+
fix: 'Nest OrganisationProvider inside UnifiedAuthProvider: <UnifiedAuthProvider><OrganisationProvider>...</OrganisationProvider></UnifiedAuthProvider>',
|
|
966
|
+
});
|
|
967
|
+
} else {
|
|
968
|
+
// UnifiedAuthProvider comes first - verify proper nesting
|
|
969
|
+
// Find closing tags to check if OrganisationProvider is actually inside UnifiedAuthProvider
|
|
970
|
+
const unifiedAuthClosePattern = /<\/UnifiedAuthProvider>/g;
|
|
971
|
+
const organisationClosePattern = /<\/OrganisationProvider>/g;
|
|
972
|
+
|
|
973
|
+
const unifiedAuthCloses = [];
|
|
974
|
+
const organisationCloses = [];
|
|
975
|
+
|
|
976
|
+
while ((match = unifiedAuthClosePattern.exec(nestingCheckContent)) !== null) {
|
|
977
|
+
unifiedAuthCloses.push(match.index);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
while ((match = organisationClosePattern.exec(nestingCheckContent)) !== null) {
|
|
981
|
+
organisationCloses.push(match.index);
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// Check if OrganisationProvider closing tag comes before UnifiedAuthProvider closing tag
|
|
985
|
+
// This ensures proper nesting: <UnifiedAuthProvider>...<OrganisationProvider>...</OrganisationProvider>...</UnifiedAuthProvider>
|
|
986
|
+
if (unifiedAuthCloses.length > 0 && organisationCloses.length > 0) {
|
|
987
|
+
const firstUnifiedAuthClose = unifiedAuthCloses[0];
|
|
988
|
+
const firstOrganisationClose = organisationCloses[0];
|
|
989
|
+
|
|
990
|
+
// OrganisationProvider should close before UnifiedAuthProvider closes
|
|
991
|
+
if (firstOrganisationClose > firstUnifiedAuthClose) {
|
|
992
|
+
issues.push({
|
|
993
|
+
type: 'providers',
|
|
994
|
+
file: nestingCheckFile,
|
|
995
|
+
line: getLineNumber(nestingCheckContent, firstOrganisation),
|
|
996
|
+
message: 'OrganisationProvider closing tag must come before UnifiedAuthProvider closing tag. Check nesting order.',
|
|
997
|
+
code: getCodeSnippet(nestingCheckContent, firstOrganisation),
|
|
998
|
+
severity: 'error',
|
|
999
|
+
fix: 'Nest OrganisationProvider inside UnifiedAuthProvider: <UnifiedAuthProvider><OrganisationProvider>...</OrganisationProvider></UnifiedAuthProvider>',
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
return issues;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
/**
|
|
1011
|
+
* Check for core.css import (via app.css)
|
|
1012
|
+
* Correct pattern:
|
|
1013
|
+
* - main.tsx imports ./app.css (NOT core.css directly)
|
|
1014
|
+
* - app.css contains @import "@jmruthers/pace-core/styles/core.css";
|
|
1015
|
+
*/
|
|
1016
|
+
function checkCoreStyles(consumingAppPath) {
|
|
1017
|
+
const issues = [];
|
|
1018
|
+
|
|
1019
|
+
// Check main.tsx or App.tsx
|
|
1020
|
+
const checkFiles = [
|
|
1021
|
+
path.join(consumingAppPath, 'src', 'main.tsx'),
|
|
1022
|
+
path.join(consumingAppPath, 'src', 'main.ts'),
|
|
1023
|
+
path.join(consumingAppPath, 'src', 'main.jsx'),
|
|
1024
|
+
path.join(consumingAppPath, 'src', 'main.js'),
|
|
1025
|
+
path.join(consumingAppPath, 'src', 'App.tsx'),
|
|
1026
|
+
path.join(consumingAppPath, 'src', 'App.ts'),
|
|
1027
|
+
path.join(consumingAppPath, 'src', 'App.jsx'),
|
|
1028
|
+
path.join(consumingAppPath, 'src', 'App.js'),
|
|
1029
|
+
];
|
|
1030
|
+
|
|
1031
|
+
let foundFile = null;
|
|
1032
|
+
let content = '';
|
|
1033
|
+
|
|
1034
|
+
for (const file of checkFiles) {
|
|
1035
|
+
if (fs.existsSync(file)) {
|
|
1036
|
+
foundFile = file;
|
|
1037
|
+
content = fs.readFileSync(file, 'utf8');
|
|
1038
|
+
break;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
if (!foundFile) {
|
|
1043
|
+
issues.push({
|
|
1044
|
+
type: 'coreStyles',
|
|
1045
|
+
file: 'src/main.tsx or src/App.tsx (not found)',
|
|
1046
|
+
line: 0,
|
|
1047
|
+
message: 'main.tsx or App.tsx not found. app.css must be imported.',
|
|
1048
|
+
code: '',
|
|
1049
|
+
severity: 'error',
|
|
1050
|
+
fix: 'Create src/main.tsx and import: import \'./app.css\';',
|
|
1051
|
+
});
|
|
1052
|
+
return issues;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
const relativePath = path.relative(consumingAppPath || process.cwd(), foundFile);
|
|
1056
|
+
|
|
1057
|
+
// Check for app.css import (correct pattern)
|
|
1058
|
+
// Matches: import './app.css', import "./app.css", import '@/app.css', import '../app.css', etc.
|
|
1059
|
+
const hasAppCssImport = /import\s+['"](\.\/|@\/|\.\.\/)?app\.css['"]/.test(content);
|
|
1060
|
+
|
|
1061
|
+
// Check for direct core.css import (incorrect pattern - should be in app.css)
|
|
1062
|
+
const coreCssImportMatch = content.match(/@jmruthers\/pace-core\/styles\/core\.css/);
|
|
1063
|
+
const hasDirectCoreCssImport = coreCssImportMatch !== null;
|
|
1064
|
+
|
|
1065
|
+
if (hasDirectCoreCssImport) {
|
|
1066
|
+
const coreCssIndex = coreCssImportMatch.index;
|
|
1067
|
+
issues.push({
|
|
1068
|
+
type: 'coreStyles',
|
|
1069
|
+
file: relativePath,
|
|
1070
|
+
line: getLineNumber(content, coreCssIndex),
|
|
1071
|
+
message: 'Direct core.css import detected. Should import app.css instead, which imports core.css.',
|
|
1072
|
+
code: getCodeSnippet(content, coreCssIndex),
|
|
1073
|
+
severity: 'error',
|
|
1074
|
+
fix: 'Remove direct core.css import and import app.css instead: import \'./app.css\';',
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
if (!hasAppCssImport) {
|
|
1079
|
+
issues.push({
|
|
1080
|
+
type: 'coreStyles',
|
|
1081
|
+
file: relativePath,
|
|
1082
|
+
line: 1,
|
|
1083
|
+
message: 'app.css import not found. Must import app.css (which imports core.css).',
|
|
1084
|
+
code: '',
|
|
1085
|
+
severity: 'error',
|
|
1086
|
+
fix: 'Add: import \'./app.css\';',
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// Check that app.css exists and contains core.css import
|
|
1091
|
+
const appCssFiles = [
|
|
1092
|
+
path.join(consumingAppPath, 'src', 'app.css'),
|
|
1093
|
+
path.join(consumingAppPath, 'app.css'),
|
|
1094
|
+
];
|
|
1095
|
+
|
|
1096
|
+
let appCssFile = null;
|
|
1097
|
+
let appCssContent = '';
|
|
1098
|
+
|
|
1099
|
+
for (const file of appCssFiles) {
|
|
1100
|
+
if (fs.existsSync(file)) {
|
|
1101
|
+
appCssFile = file;
|
|
1102
|
+
appCssContent = fs.readFileSync(file, 'utf8');
|
|
1103
|
+
break;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
if (!appCssFile) {
|
|
1108
|
+
issues.push({
|
|
1109
|
+
type: 'coreStyles',
|
|
1110
|
+
file: 'src/app.css (not found)',
|
|
1111
|
+
line: 0,
|
|
1112
|
+
message: 'app.css file not found. Must create app.css with @import "@jmruthers/pace-core/styles/core.css";',
|
|
1113
|
+
code: '',
|
|
1114
|
+
severity: 'error',
|
|
1115
|
+
fix: 'Create src/app.css with: @import "tailwindcss"; @import "@jmruthers/pace-core/styles/core.css";',
|
|
1116
|
+
});
|
|
1117
|
+
} else {
|
|
1118
|
+
// Check that app.css contains the core.css import
|
|
1119
|
+
const hasCoreCssInAppCss = /@import\s+['"]@jmruthers\/pace-core\/styles\/core\.css['"]/.test(appCssContent);
|
|
1120
|
+
|
|
1121
|
+
if (!hasCoreCssInAppCss) {
|
|
1122
|
+
const appCssRelativePath = path.relative(consumingAppPath || process.cwd(), appCssFile);
|
|
1123
|
+
issues.push({
|
|
1124
|
+
type: 'coreStyles',
|
|
1125
|
+
file: appCssRelativePath,
|
|
1126
|
+
line: 1,
|
|
1127
|
+
message: 'core.css import not found in app.css. Must add @import "@jmruthers/pace-core/styles/core.css";',
|
|
1128
|
+
code: '',
|
|
1129
|
+
severity: 'error',
|
|
1130
|
+
fix: 'Add to app.css: @import "@jmruthers/pace-core/styles/core.css";',
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
return issues;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
/**
|
|
1139
|
+
* Check for inline styles
|
|
1140
|
+
*
|
|
1141
|
+
* MIGRATED TO ESLINT: This check is now handled by 'no-inline-styles' ESLint rule.
|
|
1142
|
+
* Kept for reference only.
|
|
1143
|
+
*/
|
|
1144
|
+
function checkInlineStyles_MIGRATED_TO_ESLINT(content, filePath, consumingAppPath) {
|
|
1145
|
+
const issues = [];
|
|
1146
|
+
|
|
1147
|
+
// Pattern to match style={{...}} or style={...}
|
|
1148
|
+
const stylePattern = /style\s*=\s*\{/g;
|
|
1149
|
+
let match;
|
|
1150
|
+
|
|
1151
|
+
while ((match = stylePattern.exec(content)) !== null) {
|
|
1152
|
+
if (isInCommentOrString(content, match.index)) {
|
|
1153
|
+
continue;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
const relativePath = path.relative(consumingAppPath || process.cwd(), filePath);
|
|
1157
|
+
issues.push({
|
|
1158
|
+
type: 'inlineStyles',
|
|
1159
|
+
file: relativePath,
|
|
1160
|
+
line: getLineNumber(content, match.index),
|
|
1161
|
+
message: 'Inline style detected. Use pace-core components or Tailwind classes instead.',
|
|
1162
|
+
code: getCodeSnippet(content, match.index),
|
|
1163
|
+
severity: 'error',
|
|
1164
|
+
fix: 'Remove inline style and use pace-core component styling or Tailwind utility classes',
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
return issues;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
/**
|
|
1172
|
+
* Main audit function
|
|
1173
|
+
*/
|
|
1174
|
+
function runComplianceAudit(consumingAppPath = process.cwd()) {
|
|
1175
|
+
const srcPath = path.join(consumingAppPath, 'src');
|
|
1176
|
+
const searchPath = fs.existsSync(srcPath) ? srcPath : consumingAppPath;
|
|
1177
|
+
|
|
1178
|
+
if (!fs.existsSync(searchPath)) {
|
|
1179
|
+
return {
|
|
1180
|
+
error: `Source directory not found at ${searchPath}`,
|
|
1181
|
+
issues: {
|
|
1182
|
+
supabaseClient: [],
|
|
1183
|
+
rbacSetup: [],
|
|
1184
|
+
providers: [],
|
|
1185
|
+
coreStyles: [],
|
|
1186
|
+
rbacPermission: [],
|
|
1187
|
+
},
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// Load manifest
|
|
1192
|
+
const manifest = loadManifest(consumingAppPath);
|
|
1193
|
+
|
|
1194
|
+
// Find all source files
|
|
1195
|
+
const sourceFiles = findSourceFiles(searchPath);
|
|
1196
|
+
|
|
1197
|
+
if (sourceFiles.length === 0) {
|
|
1198
|
+
return {
|
|
1199
|
+
error: `No source files found in ${searchPath}`,
|
|
1200
|
+
issues: {
|
|
1201
|
+
supabaseClient: [],
|
|
1202
|
+
rbacSetup: [],
|
|
1203
|
+
providers: [],
|
|
1204
|
+
coreStyles: [],
|
|
1205
|
+
rbacPermission: [],
|
|
1206
|
+
},
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
const issues = {
|
|
1211
|
+
// NOTE: nativeElements, restrictedImports, customHooks, customUtils, inlineStyles
|
|
1212
|
+
// have been migrated to ESLint rules. Only file-system/config checks remain.
|
|
1213
|
+
supabaseClient: [],
|
|
1214
|
+
rbacSetup: [],
|
|
1215
|
+
providers: [],
|
|
1216
|
+
coreStyles: [],
|
|
1217
|
+
// Keep rbacPermission for now (partial migration - complex checks may remain)
|
|
1218
|
+
rbacPermission: [],
|
|
1219
|
+
};
|
|
1220
|
+
|
|
1221
|
+
// Check each file
|
|
1222
|
+
// NOTE: Native elements, restricted imports, custom hooks/utils, inline styles,
|
|
1223
|
+
// and RBAC permission loading checks have been migrated to ESLint rules.
|
|
1224
|
+
// Only file-system and config-based checks remain here.
|
|
1225
|
+
sourceFiles.forEach(filePath => {
|
|
1226
|
+
try {
|
|
1227
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
1228
|
+
|
|
1229
|
+
// Check for Supabase usage (file-system based checks only)
|
|
1230
|
+
// Direct client creation checks moved to ESLint
|
|
1231
|
+
const supabaseIssues = checkSupabaseUsage(content, filePath, consumingAppPath);
|
|
1232
|
+
issues.supabaseClient.push(...supabaseIssues);
|
|
1233
|
+
|
|
1234
|
+
} catch (error) {
|
|
1235
|
+
// Skip files that can't be read
|
|
1236
|
+
console.warn(`Warning: Could not read ${filePath}: ${error.message}`);
|
|
1237
|
+
}
|
|
1238
|
+
});
|
|
1239
|
+
|
|
1240
|
+
// Check setup-specific files
|
|
1241
|
+
const rbacIssues = checkRBACSetup(consumingAppPath);
|
|
1242
|
+
issues.rbacSetup.push(...rbacIssues);
|
|
1243
|
+
|
|
1244
|
+
const providerIssues = checkProviders(consumingAppPath);
|
|
1245
|
+
issues.providers.push(...providerIssues);
|
|
1246
|
+
|
|
1247
|
+
const coreStyleIssues = checkCoreStyles(consumingAppPath);
|
|
1248
|
+
issues.coreStyles.push(...coreStyleIssues);
|
|
1249
|
+
|
|
1250
|
+
return {
|
|
1251
|
+
issues,
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
// Export for use by other scripts
|
|
1256
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
1257
|
+
module.exports = { runComplianceAudit };
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
// If run directly, output results
|
|
1261
|
+
if (require.main === module) {
|
|
1262
|
+
const consumingAppPath = process.argv[2] || process.cwd();
|
|
1263
|
+
const result = runComplianceAudit(consumingAppPath);
|
|
1264
|
+
|
|
1265
|
+
if (result.error) {
|
|
1266
|
+
console.error(`Error: ${result.error}`);
|
|
1267
|
+
process.exit(1);
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
const { issues } = result;
|
|
1271
|
+
|
|
1272
|
+
// Count total issues
|
|
1273
|
+
const totalIssues = Object.values(issues).reduce((sum, arr) => sum + arr.length, 0);
|
|
1274
|
+
|
|
1275
|
+
if (totalIssues === 0) {
|
|
1276
|
+
console.log('✅ No compliance issues found!');
|
|
1277
|
+
process.exit(0);
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
console.log(`\n❌ Found ${totalIssues} compliance issue(s):\n`);
|
|
1281
|
+
|
|
1282
|
+
// Group by type
|
|
1283
|
+
Object.entries(issues).forEach(([type, typeIssues]) => {
|
|
1284
|
+
if (typeIssues.length > 0) {
|
|
1285
|
+
console.log(`\n${type}: ${typeIssues.length} issue(s)`);
|
|
1286
|
+
typeIssues.forEach(issue => {
|
|
1287
|
+
console.log(` ${issue.file}:${issue.line}`);
|
|
1288
|
+
console.log(` ${issue.message}`);
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
});
|
|
1292
|
+
|
|
1293
|
+
process.exit(1);
|
|
1294
|
+
}
|
|
1295
|
+
|