@jmruthers/pace-core 0.6.5 → 0.6.7
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/audit-tool/00-dependencies.cjs +394 -0
- package/audit-tool/audits/01-pace-core-compliance.cjs +556 -0
- package/audit-tool/audits/02-project-structure.cjs +255 -0
- package/audit-tool/audits/03-architecture.cjs +196 -0
- package/audit-tool/audits/04-code-quality.cjs +149 -0
- package/audit-tool/audits/05-styling.cjs +224 -0
- package/audit-tool/audits/06-security-rbac.cjs +544 -0
- package/audit-tool/audits/07-api-tech-stack.cjs +301 -0
- package/audit-tool/audits/08-testing-documentation.cjs +202 -0
- package/audit-tool/audits/09-operations.cjs +208 -0
- package/audit-tool/index.cjs +291 -0
- package/audit-tool/utils/code-utils.cjs +218 -0
- package/audit-tool/utils/file-utils.cjs +230 -0
- package/audit-tool/utils/report-utils.cjs +241 -0
- package/core-usage-manifest.json +93 -0
- package/cursor-rules/00-standards-overview.mdc +156 -0
- package/cursor-rules/01-pace-core-compliance.mdc +586 -0
- package/cursor-rules/02-project-structure.mdc +42 -4
- package/cursor-rules/{03-solid-principles.mdc → 03-architecture.mdc} +126 -10
- package/cursor-rules/04-code-quality.mdc +419 -0
- package/cursor-rules/{08-markup-quality.mdc → 05-styling.mdc} +104 -34
- package/cursor-rules/06-security-rbac.mdc +518 -0
- package/cursor-rules/07-api-tech-stack.mdc +377 -0
- package/cursor-rules/08-testing-documentation.mdc +324 -0
- package/cursor-rules/09-operations.mdc +365 -0
- package/dist/{AuthService-Cb34EQs3.d.ts → AuthService-DmfO5rGS.d.ts} +10 -0
- package/dist/DataTable-7PMH7XN7.js +15 -0
- package/dist/{DataTable-BMRU8a1j.d.ts → DataTable-DRUIgtUH.d.ts} +1 -1
- package/dist/{PublicPageProvider-QTFVrL-Z.d.ts → PublicPageProvider-DlsCaR5v.d.ts} +33 -72
- package/dist/UnifiedAuthProvider-ZT6TIGM7.js +7 -0
- package/dist/api-Y4MQWOFW.js +4 -0
- package/dist/audit-MYQXYZFU.js +3 -0
- package/dist/{chunk-DGUM43GV.js → chunk-3RG5ZIWI.js} +1 -4
- package/dist/{chunk-QXHPKYJV.js → chunk-4SXLQIZO.js} +1 -26
- package/dist/{chunk-UPPMRMYG.js → chunk-5X4QLXRG.js} +73 -151
- package/dist/chunk-6F3IILHI.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-FMUCXFII.js → chunk-7ILTDCL2.js} +9 -5
- package/dist/{chunk-M43Y4SSO.js → chunk-A3W6LW53.js} +15 -13
- package/dist/{chunk-63FOKYGO.js → chunk-AHU7G2R5.js} +2 -11
- package/dist/{chunk-HU2C6SSC.js → chunk-BM4CQ5P3.js} +606 -559
- package/dist/chunk-C7NSAPTL.js +1 -0
- package/dist/{chunk-J36DSWQK.js → chunk-FEJLJNWA.js} +7 -41
- package/dist/{chunk-IHB5DR3H.js → chunk-FTCRZOG2.js} +188 -387
- package/dist/{chunk-G37KK66H.js → chunk-FYHN4DD5.js} +60 -19
- package/dist/chunk-GHYHJTYV.js +994 -0
- package/dist/{chunk-VBXEHIUJ.js → chunk-HF6O3O37.js} +6 -88
- package/dist/{chunk-FFQEQTNW.js → chunk-IUBRCBSY.js} +134 -45
- package/dist/{chunk-6COVEUS7.js → chunk-JGWDVX64.js} +983 -1034
- package/dist/{chunk-RGAWHO7N.js → chunk-L4XMVJKY.js} +77 -222
- package/dist/chunk-MBADTM7L.js +64 -0
- package/dist/{chunk-M7MPQISP.js → chunk-OJ4SKRSV.js} +3 -16
- package/dist/{chunk-IVOFDYWT.js → chunk-Q7Q7V5NV.js} +2109 -1604
- package/dist/{chunk-JGRYX5UX.js → chunk-S7DKJPLT.js} +29 -58
- package/dist/{chunk-PWLANIRT.js → chunk-TTRFSOKR.js} +1 -7
- package/dist/{chunk-5DRSZLL2.js → chunk-UH3NTO3F.js} +1 -6
- package/dist/{chunk-NTM7ZSB6.js → chunk-VBCS3DUA.js} +261 -168
- package/dist/{chunk-EFN2EIMK.js → chunk-ZFYPMX46.js} +271 -87
- package/dist/{chunk-L4OXEN46.js → chunk-ZKAWKYT4.js} +10 -24
- package/dist/components.d.ts +7 -5
- package/dist/components.js +46 -257
- package/dist/{database.generated-CzIvgcPu.d.ts → database.generated-CcnC_DRc.d.ts} +4795 -3691
- package/dist/eslint-rules/index.cjs +35 -0
- package/{src/eslint-rules/pace-core-compliance.cjs → dist/eslint-rules/rules/01-pace-core-compliance.cjs} +234 -235
- package/dist/eslint-rules/rules/04-code-quality.cjs +290 -0
- package/dist/eslint-rules/rules/05-styling.cjs +61 -0
- package/dist/eslint-rules/rules/06-security-rbac.cjs +806 -0
- package/dist/eslint-rules/rules/07-api-tech-stack.cjs +263 -0
- package/dist/eslint-rules/rules/08-testing.cjs +94 -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 +6 -6
- package/dist/hooks.js +62 -172
- package/dist/icons/index.d.ts +1 -0
- package/dist/icons/index.js +1 -0
- package/dist/index.d.ts +12 -11
- package/dist/index.js +67 -660
- 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 +109 -586
- package/dist/rbac/index.js +14 -207
- package/dist/styles/index.js +2 -12
- package/dist/theming/runtime.d.ts +14 -1
- 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-DXstZpNI.d.ts} +4 -17
- package/dist/types-t9H8qKRw.d.ts +55 -0
- package/dist/types.d.ts +1 -1
- package/dist/types.js +7 -94
- package/dist/{usePublicRouteParams-ClnV4tnv.d.ts → usePublicRouteParams-MamNgwqe.d.ts} +20 -20
- package/dist/utils.d.ts +24 -117
- package/dist/utils.js +54 -392
- package/docs/README.md +17 -7
- package/docs/api/README.md +4 -402
- package/docs/api/modules.md +301 -871
- package/docs/api-reference/components.md +21 -21
- package/docs/api-reference/deprecated.md +31 -6
- package/docs/api-reference/hooks.md +80 -80
- package/docs/api-reference/rpc-functions.md +78 -3
- package/docs/api-reference/types.md +1 -1
- package/docs/api-reference/utilities.md +1 -1
- package/docs/architecture/README.md +1 -1
- package/docs/core-concepts/events.md +3 -3
- package/docs/core-concepts/organisations.md +6 -6
- package/docs/core-concepts/permissions.md +6 -6
- package/docs/documentation-index.md +12 -18
- package/docs/getting-started/cursor-rules.md +3 -23
- package/docs/getting-started/dependencies.md +650 -0
- package/docs/getting-started/documentation-index.md +1 -1
- package/docs/getting-started/examples/README.md +4 -4
- package/docs/getting-started/examples/full-featured-app.md +1 -1
- package/docs/getting-started/faq.md +2 -2
- package/docs/getting-started/installation-guide.md +20 -7
- package/docs/getting-started/quick-reference.md +4 -4
- package/docs/getting-started/quick-start.md +23 -12
- package/docs/implementation-guides/authentication.md +15 -15
- package/docs/implementation-guides/component-styling.md +1 -1
- package/docs/implementation-guides/data-tables.md +126 -33
- package/docs/implementation-guides/datatable-rbac-usage.md +1 -1
- package/docs/implementation-guides/dynamic-colors.md +3 -3
- package/docs/implementation-guides/file-upload-storage.md +2 -2
- package/docs/implementation-guides/hierarchical-datatable.md +40 -60
- package/docs/implementation-guides/inactivity-tracking.md +3 -3
- package/docs/implementation-guides/large-datasets.md +3 -2
- package/docs/implementation-guides/organisation-security.md +2 -2
- package/docs/implementation-guides/performance.md +2 -2
- package/docs/implementation-guides/permission-enforcement.md +5 -1
- package/docs/migration/V0.3.44_organisation-context-timing-fix.md +1 -1
- package/docs/migration/V0.4.0_rbac-migration.md +6 -6
- package/docs/rbac/MIGRATION_GUIDE.md +819 -0
- package/docs/rbac/RBAC_CONTRACT.md +724 -0
- package/docs/rbac/README.md +17 -8
- package/docs/rbac/advanced-patterns.md +6 -6
- package/docs/rbac/api-reference.md +20 -20
- package/docs/rbac/edge-functions-guide.md +376 -0
- package/docs/rbac/event-based-apps.md +3 -3
- package/docs/rbac/examples.md +41 -41
- package/docs/rbac/getting-started.md +37 -37
- package/docs/rbac/performance.md +1 -1
- package/docs/rbac/quick-start.md +52 -52
- package/docs/rbac/secure-client-protection.md +1 -35
- package/docs/rbac/troubleshooting.md +1 -1
- package/docs/security/README.md +5 -5
- package/docs/standards/0-standards-overview.md +220 -0
- package/docs/standards/1-pace-core-compliance-standards.md +986 -0
- package/docs/standards/2-project-structure-standards.md +949 -0
- package/docs/standards/3-architecture-standards.md +606 -0
- package/docs/standards/4-code-quality-standards.md +728 -0
- package/docs/standards/5-styling-standards.md +348 -0
- package/docs/standards/{07-rbac-and-rls-standard.md → 6-security-rbac-standards.md} +269 -66
- package/docs/standards/7-api-tech-stack-standards.md +662 -0
- package/docs/standards/8-testing-documentation-standards.md +401 -0
- package/docs/standards/9-operations-standards.md +1102 -0
- package/docs/standards/README.md +185 -57
- package/docs/troubleshooting/README.md +4 -4
- package/docs/troubleshooting/common-issues.md +2 -2
- package/docs/troubleshooting/debugging.md +9 -9
- package/docs/troubleshooting/migration.md +4 -4
- package/docs/troubleshooting/organisation-context-setup.md +42 -19
- package/eslint-config-pace-core.cjs +33 -6
- package/package.json +35 -23
- package/scripts/install-cursor-rules.cjs +25 -6
- package/scripts/install-eslint-config.cjs +284 -0
- package/src/__tests__/fixtures/supabase.ts +1 -1
- package/src/__tests__/helpers/__tests__/component-test-utils.test.tsx +3 -3
- 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-providers.test.tsx +2 -2
- package/src/__tests__/helpers/__tests__/test-utils.test.tsx +13 -13
- package/src/__tests__/helpers/component-test-utils.tsx +1 -1
- package/src/__tests__/helpers/supabaseMock.ts +2 -2
- package/src/__tests__/integration/UserProfile.test.tsx +14 -14
- package/src/__tests__/public-recipe-view.test.ts +38 -9
- package/src/__tests__/rbac/PagePermissionGuard.test.tsx +6 -6
- package/src/__tests__/templates/accessibility.test.template.tsx +9 -9
- package/src/__tests__/templates/component.test.template.tsx +18 -15
- package/src/components/Button/Button.tsx +5 -1
- package/src/components/Calendar/Calendar.tsx +201 -47
- package/src/components/ContextSelector/ContextSelector.tsx +106 -119
- package/src/components/DataTable/AUDIT_REPORT.md +293 -0
- package/src/components/DataTable/__tests__/DataTableCore.test.tsx +10 -2
- package/src/components/DataTable/__tests__/a11y.basic.test.tsx +10 -4
- package/src/components/DataTable/__tests__/test-utils/sharedTestUtils.tsx +9 -9
- package/src/components/DataTable/components/ColumnFilter.tsx +63 -74
- package/src/components/DataTable/components/ColumnVisibilityDropdown.tsx +43 -41
- package/src/components/DataTable/components/DataTableCore.tsx +186 -13
- package/src/components/DataTable/components/DataTableErrorBoundary.tsx +9 -11
- package/src/components/DataTable/components/DataTableLayout.tsx +35 -21
- package/src/components/DataTable/components/EditFields.tsx +23 -3
- package/src/components/DataTable/components/EditableRow.tsx +12 -9
- package/src/components/DataTable/components/EmptyState.tsx +10 -9
- package/src/components/DataTable/components/FilterRow.tsx +2 -4
- package/src/components/DataTable/components/ImportModal.tsx +124 -126
- package/src/components/DataTable/components/LoadingState.tsx +5 -6
- package/src/components/DataTable/components/RowComponent.tsx +12 -0
- package/src/components/DataTable/components/SortIndicator.tsx +50 -0
- package/src/components/DataTable/components/__tests__/COVERAGE_NOTE.md +4 -4
- package/src/components/DataTable/components/__tests__/ColumnFilter.test.tsx +23 -82
- package/src/components/DataTable/components/__tests__/DataTableErrorBoundary.test.tsx +37 -9
- package/src/components/DataTable/components/__tests__/EmptyState.test.tsx +7 -4
- package/src/components/DataTable/components/__tests__/FilterRow.test.tsx +12 -4
- package/src/components/DataTable/components/__tests__/LoadingState.test.tsx +41 -27
- package/src/components/DataTable/components/hooks/usePermissionTracking.ts +0 -4
- package/src/components/DataTable/components/index.ts +2 -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 -18
- package/src/components/DataTable/utils/a11yUtils.ts +17 -0
- package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +2 -1
- package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.tsx +11 -15
- package/src/components/DateTimeField/DateTimeField.tsx +10 -9
- package/src/components/Dialog/Dialog.test.tsx +128 -104
- package/src/components/Dialog/Dialog.tsx +742 -24
- package/src/components/ErrorBoundary/ErrorBoundary.tsx +77 -79
- package/src/components/FileDisplay/FileDisplay.test.tsx +4 -2
- package/src/components/FileDisplay/FileDisplay.tsx +23 -17
- package/src/components/FileUpload/FileUpload.test.tsx +52 -14
- package/src/components/FileUpload/FileUpload.tsx +112 -130
- package/src/components/Form/Form.test.tsx +6 -8
- package/src/components/Form/Form.tsx +365 -4
- package/src/components/NavigationMenu/NavigationMenu.test.tsx +14 -13
- package/src/components/NavigationMenu/useNavigationFiltering.ts +11 -21
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +6 -4
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +11 -15
- package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +108 -61
- package/src/components/PaceLoginPage/PaceLoginPage.tsx +27 -3
- package/src/components/Progress/Progress.tsx +2 -4
- package/src/components/ProtectedRoute/ProtectedRoute.tsx +8 -8
- package/src/components/Select/Select.tsx +109 -98
- package/src/components/Select/types.ts +4 -1
- package/src/components/UserMenu/UserMenu.tsx +9 -6
- package/src/hooks/__tests__/ServiceHooks.test.tsx +16 -16
- package/src/hooks/__tests__/hooks.integration.test.tsx +55 -57
- package/src/hooks/__tests__/useAppConfig.unit.test.ts +129 -67
- package/src/hooks/__tests__/useFocusTrap.unit.test.tsx +97 -97
- 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 +67 -195
- package/src/hooks/public/usePublicEventLogo.test.ts +70 -17
- package/src/hooks/public/usePublicEventLogo.ts +24 -14
- package/src/hooks/public/usePublicFileDisplay.ts +2 -2
- package/src/hooks/public/usePublicRouteParams.ts +5 -5
- package/src/hooks/useAppConfig.ts +28 -26
- package/src/hooks/useEventTheme.test.ts +217 -239
- package/src/hooks/useEventTheme.ts +16 -28
- package/src/hooks/useFileDisplay.ts +2 -2
- package/src/hooks/useOrganisationPermissions.ts +5 -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 +5 -0
- package/src/providers/OrganisationProvider.tsx +23 -14
- package/src/providers/UnifiedAuthProvider.smoke.test.tsx +21 -21
- package/src/providers/__tests__/AuthProvider.test.tsx +21 -21
- package/src/providers/__tests__/EventProvider.test.tsx +61 -61
- package/src/providers/__tests__/InactivityProvider.test.tsx +56 -56
- package/src/providers/__tests__/OrganisationProvider.test.tsx +75 -75
- package/src/providers/__tests__/ProviderLifecycle.test.tsx +37 -37
- package/src/providers/__tests__/UnifiedAuthProvider.test.tsx +103 -103
- package/src/providers/services/EventServiceProvider.tsx +1 -24
- package/src/providers/services/UnifiedAuthProvider.tsx +5 -48
- package/src/providers/services/__tests__/AuthServiceProvider.integration.test.tsx +7 -7
- package/src/providers/services/__tests__/UnifiedAuthProvider.integration.test.tsx +13 -10
- 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/useResolvedScope.test.ts +57 -47
- package/src/rbac/hooks/useResolvedScope.ts +58 -140
- package/src/rbac/hooks/useResourcePermissions.test.ts +124 -38
- package/src/rbac/hooks/useResourcePermissions.ts +139 -48
- 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/utils/contextValidator.ts +9 -7
- package/src/services/AuthService.ts +130 -18
- package/src/services/EventService.ts +4 -97
- package/src/services/InactivityService.ts +16 -0
- 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 +7 -0
- package/src/theming/__tests__/parseEventColours.test.ts +9 -3
- package/src/theming/parseEventColours.ts +22 -10
- package/src/types/database.generated.ts +4733 -3809
- package/src/utils/__tests__/lazyLoad.unit.test.tsx +42 -39
- 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/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/README.md +1 -1
- 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/00-pace-core-compliance.mdc +0 -331
- package/cursor-rules/01-standards-compliance.mdc +0 -244
- package/cursor-rules/04-testing-standards.mdc +0 -268
- package/cursor-rules/05-bug-reports-and-features.mdc +0 -246
- package/cursor-rules/06-code-quality.mdc +0 -309
- package/cursor-rules/07-tech-stack-compliance.mdc +0 -214
- package/cursor-rules/CHANGELOG.md +0 -119
- package/cursor-rules/README.md +0 -192
- package/dist/DataTable-AOVNCPTX.js +0 -175
- package/dist/DataTable-AOVNCPTX.js.map +0 -1
- package/dist/UnifiedAuthProvider-4SBX4LU5.js +0 -18
- package/dist/UnifiedAuthProvider-4SBX4LU5.js.map +0 -1
- package/dist/api-O6HTBX5Y.js +0 -52
- package/dist/api-O6HTBX5Y.js.map +0 -1
- package/dist/audit-V53FV5AG.js +0 -17
- package/dist/audit-V53FV5AG.js.map +0 -1
- package/dist/chunk-5DRSZLL2.js.map +0 -1
- package/dist/chunk-63FOKYGO.js.map +0 -1
- package/dist/chunk-6COVEUS7.js.map +0 -1
- package/dist/chunk-AFVQODI2.js +0 -263
- package/dist/chunk-AFVQODI2.js.map +0 -1
- package/dist/chunk-DGUM43GV.js.map +0 -1
- package/dist/chunk-E66EQZE6.js.map +0 -1
- package/dist/chunk-EFN2EIMK.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-G7QEZTYQ.js +0 -2053
- package/dist/chunk-G7QEZTYQ.js.map +0 -1
- package/dist/chunk-HU2C6SSC.js.map +0 -1
- package/dist/chunk-IHB5DR3H.js.map +0 -1
- package/dist/chunk-IVOFDYWT.js.map +0 -1
- package/dist/chunk-J36DSWQK.js.map +0 -1
- package/dist/chunk-JGRYX5UX.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-NTM7ZSB6.js.map +0 -1
- package/dist/chunk-PWLANIRT.js.map +0 -1
- package/dist/chunk-QXHPKYJV.js.map +0 -1
- package/dist/chunk-RGAWHO7N.js.map +0 -1
- package/dist/chunk-UPPMRMYG.js.map +0 -1
- package/dist/chunk-VBXEHIUJ.js.map +0 -1
- package/dist/chunk-ZSAAAMVR.js.map +0 -1
- package/dist/components.js.map +0 -1
- package/dist/contextValidator-5OGXSPKS.js +0 -9
- package/dist/contextValidator-5OGXSPKS.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/best-practices/README.md +0 -472
- package/docs/best-practices/accessibility.md +0 -601
- package/docs/best-practices/common-patterns.md +0 -516
- package/docs/best-practices/deployment.md +0 -1103
- package/docs/best-practices/performance.md +0 -1328
- package/docs/best-practices/security.md +0 -940
- package/docs/best-practices/testing.md +0 -1034
- package/docs/rbac/compliance/compliance-guide.md +0 -544
- 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/04-code-style-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/index.cjs +0 -223
- 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/components/DataTable/components/DataTableBody.tsx +0 -454
- package/src/components/DataTable/components/DraggableColumnHeader.tsx +0 -156
- package/src/components/DataTable/components/ExpandButton.tsx +0 -113
- package/src/components/DataTable/components/GroupHeader.tsx +0 -54
- package/src/components/DataTable/components/ViewRowModal.tsx +0 -68
- package/src/components/DataTable/components/VirtualizedDataTable.tsx +0 -525
- package/src/components/DataTable/components/__tests__/ExpandButton.test.tsx +0 -462
- package/src/components/DataTable/components/__tests__/GroupHeader.test.tsx +0 -393
- package/src/components/DataTable/components/__tests__/ViewRowModal.test.tsx +0 -476
- package/src/components/DataTable/components/__tests__/VirtualizedDataTable.test.tsx +0 -128
- package/src/components/DataTable/core/DataTableContext.tsx +0 -216
- package/src/components/DataTable/core/__tests__/DataTableContext.test.tsx +0 -136
- package/src/components/DataTable/hooks/__tests__/useColumnReordering.test.ts +0 -570
- package/src/components/DataTable/hooks/useColumnReordering.ts +0 -123
- package/src/components/DataTable/utils/debugTools.ts +0 -514
- 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
|
@@ -1,2706 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* pace-core Compliance Check Module
|
|
5
|
-
* @package @jmruthers/pace-core
|
|
6
|
-
* @module Audit/Checks/Compliance
|
|
7
|
-
*
|
|
8
|
-
* Scans codebase for pace-core compliance violations including:
|
|
9
|
-
* - Restricted imports
|
|
10
|
-
* - Duplicate components/hooks/utils
|
|
11
|
-
* - Custom auth/RBAC code
|
|
12
|
-
* - Provider setup issues
|
|
13
|
-
* - Direct Supabase usage
|
|
14
|
-
* - Unnecessary wrappers
|
|
15
|
-
* - App discovery issues
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
const fs = require('fs');
|
|
19
|
-
const path = require('path');
|
|
20
|
-
const { getLineNumber, getRelativePath } = require('../utils.cjs');
|
|
21
|
-
|
|
22
|
-
// Load manifest
|
|
23
|
-
function loadManifest() {
|
|
24
|
-
const manifestPath = path.join(__dirname, '../../../../core-usage-manifest.json');
|
|
25
|
-
if (!fs.existsSync(manifestPath)) {
|
|
26
|
-
throw new Error(`core-usage-manifest.json not found at ${manifestPath}`);
|
|
27
|
-
}
|
|
28
|
-
return JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function scanProviderSetup(filePath, content, relativePath) {
|
|
32
|
-
const issues = [];
|
|
33
|
-
|
|
34
|
-
// Check for UnifiedAuthProvider
|
|
35
|
-
const hasUnifiedAuthProvider = /UnifiedAuthProvider/.test(content);
|
|
36
|
-
if (!hasUnifiedAuthProvider) {
|
|
37
|
-
issues.push({
|
|
38
|
-
file: relativePath,
|
|
39
|
-
line: 1,
|
|
40
|
-
type: 'missing-provider',
|
|
41
|
-
provider: 'UnifiedAuthProvider',
|
|
42
|
-
reason: 'UnifiedAuthProvider is required. Wrap your app with UnifiedAuthProvider from @jmruthers/pace-core.',
|
|
43
|
-
recommendation: 'Import UnifiedAuthProvider from @jmruthers/pace-core and wrap your app component with it.'
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Check for OrganisationProvider
|
|
48
|
-
const hasOrganisationProvider = /OrganisationProvider/.test(content);
|
|
49
|
-
if (hasUnifiedAuthProvider && !hasOrganisationProvider) {
|
|
50
|
-
issues.push({
|
|
51
|
-
file: relativePath,
|
|
52
|
-
line: 1,
|
|
53
|
-
type: 'missing-provider',
|
|
54
|
-
provider: 'OrganisationProvider',
|
|
55
|
-
reason: 'OrganisationProvider is recommended when using UnifiedAuthProvider. It provides organisation context.',
|
|
56
|
-
recommendation: 'Import OrganisationProvider from @jmruthers/pace-core and wrap your app inside UnifiedAuthProvider.'
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Check provider nesting order
|
|
61
|
-
if (hasUnifiedAuthProvider) {
|
|
62
|
-
// Normalize content for better pattern matching (handle multiline JSX)
|
|
63
|
-
const normalizedContent = content.replace(/\s+/g, ' ');
|
|
64
|
-
|
|
65
|
-
const hasBrowserRouter = /BrowserRouter/.test(content);
|
|
66
|
-
const hasQueryClientProvider = /QueryClientProvider/.test(content);
|
|
67
|
-
|
|
68
|
-
// Only check nesting if we have both BrowserRouter and UnifiedAuthProvider
|
|
69
|
-
if (hasBrowserRouter) {
|
|
70
|
-
// Check if BrowserRouter is inside UnifiedAuthProvider (wrong order)
|
|
71
|
-
// Pattern: <UnifiedAuthProvider ...> ... <BrowserRouter
|
|
72
|
-
// This is a clear wrong pattern - only report if we see this directly
|
|
73
|
-
// BUT first check if there's a wrapper component that uses Router hooks
|
|
74
|
-
const wrapperFunctionMatch = content.match(/(?:function|const)\s+(\w*UnifiedAuthProvider\w*Wrapper)\s*[=\(]/);
|
|
75
|
-
let wrapperUsesRouterHooks = false;
|
|
76
|
-
if (wrapperFunctionMatch) {
|
|
77
|
-
const wrapperName = wrapperFunctionMatch[1];
|
|
78
|
-
const wrapperStart = content.indexOf(wrapperFunctionMatch[0]);
|
|
79
|
-
const afterWrapper = content.substring(wrapperStart);
|
|
80
|
-
let braceCount = 0;
|
|
81
|
-
let bodyStart = -1;
|
|
82
|
-
let bodyEnd = -1;
|
|
83
|
-
for (let i = 0; i < afterWrapper.length; i++) {
|
|
84
|
-
if (afterWrapper[i] === '{') {
|
|
85
|
-
if (braceCount === 0) bodyStart = i;
|
|
86
|
-
braceCount++;
|
|
87
|
-
} else if (afterWrapper[i] === '}') {
|
|
88
|
-
braceCount--;
|
|
89
|
-
if (braceCount === 0 && bodyStart !== -1) {
|
|
90
|
-
bodyEnd = i;
|
|
91
|
-
break;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
if (bodyStart !== -1 && bodyEnd !== -1) {
|
|
96
|
-
const functionBody = afterWrapper.substring(bodyStart, bodyEnd + 1);
|
|
97
|
-
wrapperUsesRouterHooks = /useNavigate|useLocation|useParams|useSearchParams/.test(functionBody);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const wrongNestingPattern = /<UnifiedAuthProvider[^>]*>[\s\S]*?<BrowserRouter/g;
|
|
102
|
-
// Only flag wrong nesting if there's no wrapper using Router hooks
|
|
103
|
-
if (wrongNestingPattern.test(content) && !wrapperUsesRouterHooks) {
|
|
104
|
-
const match = content.match(wrongNestingPattern);
|
|
105
|
-
issues.push({
|
|
106
|
-
file: relativePath,
|
|
107
|
-
line: getLineNumber(content, match[0]),
|
|
108
|
-
type: 'wrong-nesting',
|
|
109
|
-
reason: 'BrowserRouter should wrap UnifiedAuthProvider, not the other way around. This causes Router context errors.',
|
|
110
|
-
recommendation: 'Correct nesting order: QueryClientProvider → BrowserRouter → UnifiedAuthProvider → OrganisationProvider → App'
|
|
111
|
-
});
|
|
112
|
-
} else {
|
|
113
|
-
// Check if BrowserRouter wraps UnifiedAuthProvider (correct pattern)
|
|
114
|
-
// Look for BrowserRouter wrapping UnifiedAuthProvider or any component that might contain it
|
|
115
|
-
// Pattern: <BrowserRouter ...> ... <UnifiedAuthProvider (or wrapper component)
|
|
116
|
-
const correctNestingPattern = /<BrowserRouter[^>]*>[\s\S]*?<UnifiedAuthProvider/g;
|
|
117
|
-
|
|
118
|
-
// Also check for wrapper components (e.g., UnifiedAuthProviderWrapper)
|
|
119
|
-
// Pattern: <BrowserRouter ...> ... <UnifiedAuthProviderWrapper ...> ... <UnifiedAuthProvider
|
|
120
|
-
const wrapperPattern = /<BrowserRouter[^>]*>[\s\S]*?(?:<UnifiedAuthProviderWrapper|<UnifiedAuthProvider)/g;
|
|
121
|
-
|
|
122
|
-
// wrapperUsesRouterHooks is already set above, reuse it
|
|
123
|
-
const browserRouterIndex = content.indexOf('<BrowserRouter');
|
|
124
|
-
const unifiedAuthIndex = content.indexOf('UnifiedAuthProvider');
|
|
125
|
-
const wrapperIndex = content.indexOf('UnifiedAuthProviderWrapper');
|
|
126
|
-
|
|
127
|
-
// Only report if we can clearly see a wrong pattern
|
|
128
|
-
// If BrowserRouter wraps UnifiedAuthProvider (directly or via wrapper), that's correct
|
|
129
|
-
if (browserRouterIndex !== -1 && unifiedAuthIndex !== -1) {
|
|
130
|
-
// If wrapper uses Router hooks, it must be inside BrowserRouter (correct pattern)
|
|
131
|
-
// Check if BrowserRouter comes before UnifiedAuthProviderWrapper in the JSX
|
|
132
|
-
if (wrapperUsesRouterHooks && wrapperPattern.test(content) &&
|
|
133
|
-
(browserRouterIndex < wrapperIndex || browserRouterIndex < unifiedAuthIndex || wrapperIndex === -1)) {
|
|
134
|
-
// This is correct - wrapper uses Router hooks and is inside BrowserRouter
|
|
135
|
-
// Don't report - skip to next check
|
|
136
|
-
} else if (unifiedAuthIndex < browserRouterIndex && !correctNestingPattern.test(content) && !wrapperPattern.test(content)) {
|
|
137
|
-
// Only report if we see UnifiedAuthProvider directly wrapping BrowserRouter (clear wrong pattern)
|
|
138
|
-
// AND wrapper doesn't use Router hooks (which would require BrowserRouter)
|
|
139
|
-
const unifiedWrappingRouter = /<UnifiedAuthProvider[^>]*>[\s\S]{0,200}<BrowserRouter/g;
|
|
140
|
-
if (unifiedWrappingRouter.test(content) && !wrapperUsesRouterHooks) {
|
|
141
|
-
issues.push({
|
|
142
|
-
file: relativePath,
|
|
143
|
-
line: getLineNumber(content, /BrowserRouter/.exec(content)?.index || 0),
|
|
144
|
-
type: 'wrong-nesting',
|
|
145
|
-
reason: 'UnifiedAuthProvider should be inside BrowserRouter to provide Router context.',
|
|
146
|
-
recommendation: 'Correct nesting order: QueryClientProvider → BrowserRouter → UnifiedAuthProvider → OrganisationProvider → App'
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
// If it's a wrapper component that uses Router hooks, don't report - that's acceptable
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Check if QueryClientProvider wraps BrowserRouter
|
|
156
|
-
if (hasQueryClientProvider && hasBrowserRouter) {
|
|
157
|
-
// Check if QueryClientProvider comes before BrowserRouter in JSX structure
|
|
158
|
-
const queryBeforeRouter = /<QueryClientProvider[^>]*>[\s\S]*?<BrowserRouter/g;
|
|
159
|
-
if (!queryBeforeRouter.test(content)) {
|
|
160
|
-
// Only report if we can clearly see BrowserRouter comes before QueryClientProvider
|
|
161
|
-
const routerBeforeQuery = /<BrowserRouter[^>]*>[\s\S]*?<QueryClientProvider/g;
|
|
162
|
-
if (routerBeforeQuery.test(content)) {
|
|
163
|
-
const queryMatch = /QueryClientProvider/.exec(content);
|
|
164
|
-
issues.push({
|
|
165
|
-
file: relativePath,
|
|
166
|
-
line: getLineNumber(content, queryMatch?.index || 0),
|
|
167
|
-
type: 'wrong-nesting',
|
|
168
|
-
reason: 'QueryClientProvider should wrap BrowserRouter.',
|
|
169
|
-
recommendation: 'Correct nesting order: QueryClientProvider → BrowserRouter → UnifiedAuthProvider → OrganisationProvider → App'
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
// If we can't find either pattern, don't report - might be in wrapper components
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
return issues;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Scan Vite configuration for required settings
|
|
181
|
-
function scanViteConfig(filePath, content, relativePath) {
|
|
182
|
-
const issues = [];
|
|
183
|
-
|
|
184
|
-
// Check if @jmruthers/pace-core is in optimizeDeps.exclude
|
|
185
|
-
const hasOptimizeDepsExclude = /optimizeDeps\s*:\s*\{[\s\S]*?exclude/.test(content);
|
|
186
|
-
if (hasOptimizeDepsExclude) {
|
|
187
|
-
const excludeArrayMatch = content.match(/exclude\s*:\s*\[([^\]]*)\]/);
|
|
188
|
-
if (excludeArrayMatch) {
|
|
189
|
-
const excludeContent = excludeArrayMatch[1];
|
|
190
|
-
if (!excludeContent.includes('@jmruthers/pace-core')) {
|
|
191
|
-
issues.push({
|
|
192
|
-
file: relativePath,
|
|
193
|
-
line: getLineNumber(content, excludeArrayMatch[0]),
|
|
194
|
-
type: 'missing-exclude',
|
|
195
|
-
reason: '@jmruthers/pace-core should be excluded from Vite pre-bundling to prevent React context mismatches.',
|
|
196
|
-
recommendation: "Add '@jmruthers/pace-core' to optimizeDeps.exclude array."
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
} else {
|
|
200
|
-
// Check if it's a single string
|
|
201
|
-
const excludeStringMatch = content.match(/exclude\s*:\s*['"]@jmruthers\/pace-core['"]/);
|
|
202
|
-
if (!excludeStringMatch) {
|
|
203
|
-
issues.push({
|
|
204
|
-
file: relativePath,
|
|
205
|
-
line: getLineNumber(content, /exclude\s*:/.exec(content)?.index || 0),
|
|
206
|
-
type: 'missing-exclude',
|
|
207
|
-
reason: '@jmruthers/pace-core should be excluded from Vite pre-bundling.',
|
|
208
|
-
recommendation: "Add '@jmruthers/pace-core' to optimizeDeps.exclude."
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
} else {
|
|
213
|
-
issues.push({
|
|
214
|
-
file: relativePath,
|
|
215
|
-
line: getLineNumber(content, /optimizeDeps/.exec(content)?.index || 1),
|
|
216
|
-
type: 'missing-optimize-deps',
|
|
217
|
-
reason: 'optimizeDeps.exclude is missing. This can cause React context mismatch errors.',
|
|
218
|
-
recommendation: "Add optimizeDeps: { exclude: ['@jmruthers/pace-core'], include: ['react', 'react-dom', 'react-router-dom'] } to your Vite config."
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Check if @jmruthers/pace-core is in optimizeDeps.include (should NOT be)
|
|
223
|
-
const includeArrayMatch = content.match(/include\s*:\s*\[([^\]]*)\]/);
|
|
224
|
-
if (includeArrayMatch) {
|
|
225
|
-
const includeContent = includeArrayMatch[1];
|
|
226
|
-
if (includeContent.includes('@jmruthers/pace-core')) {
|
|
227
|
-
issues.push({
|
|
228
|
-
file: relativePath,
|
|
229
|
-
line: getLineNumber(content, includeArrayMatch[0]),
|
|
230
|
-
type: 'should-exclude',
|
|
231
|
-
reason: '@jmruthers/pace-core should NOT be in optimizeDeps.include. It should be in exclude instead.',
|
|
232
|
-
recommendation: "Remove '@jmruthers/pace-core' from optimizeDeps.include and add it to optimizeDeps.exclude."
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// Check for react-router-dom in dedupe (recommended)
|
|
238
|
-
const hasDedupe = /dedupe\s*:\s*\[/.test(content);
|
|
239
|
-
if (hasDedupe) {
|
|
240
|
-
const dedupeArrayMatch = content.match(/dedupe\s*:\s*\[([^\]]*)\]/);
|
|
241
|
-
if (dedupeArrayMatch) {
|
|
242
|
-
const dedupeContent = dedupeArrayMatch[1];
|
|
243
|
-
if (!dedupeContent.includes('react-router-dom')) {
|
|
244
|
-
issues.push({
|
|
245
|
-
file: relativePath,
|
|
246
|
-
line: getLineNumber(content, dedupeArrayMatch[0]),
|
|
247
|
-
type: 'recommendation',
|
|
248
|
-
reason: 'Adding react-router-dom to resolve.dedupe is recommended to prevent Router context issues.',
|
|
249
|
-
recommendation: "Add 'react-router-dom' to resolve.dedupe array: dedupe: ['react', 'react-dom', 'react-router-dom']"
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
} else {
|
|
254
|
-
// Check if resolve exists
|
|
255
|
-
if (/resolve\s*:\s*\{/.test(content)) {
|
|
256
|
-
issues.push({
|
|
257
|
-
file: relativePath,
|
|
258
|
-
line: getLineNumber(content, /resolve\s*:/.exec(content)?.index || 1),
|
|
259
|
-
type: 'recommendation',
|
|
260
|
-
reason: 'Adding resolve.dedupe is recommended to prevent React context issues.',
|
|
261
|
-
recommendation: "Add dedupe: ['react', 'react-dom', 'react-router-dom'] to resolve config."
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// Check for react-router-dom in optimizeDeps.include (should be included, not excluded)
|
|
267
|
-
const hasOptimizeDepsInclude = /optimizeDeps\s*:\s*\{[\s\S]*?include/.test(content);
|
|
268
|
-
if (hasOptimizeDepsInclude) {
|
|
269
|
-
const includeArrayMatch = content.match(/include\s*:\s*\[([^\]]*)\]/);
|
|
270
|
-
if (includeArrayMatch) {
|
|
271
|
-
const includeContent = includeArrayMatch[1];
|
|
272
|
-
if (!includeContent.includes('react-router-dom')) {
|
|
273
|
-
issues.push({
|
|
274
|
-
file: relativePath,
|
|
275
|
-
line: getLineNumber(content, includeArrayMatch[0]),
|
|
276
|
-
type: 'recommendation',
|
|
277
|
-
reason: 'Including react-router-dom in pre-bundling is recommended to prevent module resolution issues.',
|
|
278
|
-
recommendation: "Add 'react-router-dom' to optimizeDeps.include array."
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// Warn if react-router-dom is in exclude (this causes "module is not defined" errors)
|
|
285
|
-
if (hasOptimizeDepsExclude) {
|
|
286
|
-
const excludeArrayMatch = content.match(/exclude\s*:\s*\[([^\]]*)\]/);
|
|
287
|
-
if (excludeArrayMatch) {
|
|
288
|
-
const excludeContent = excludeArrayMatch[1];
|
|
289
|
-
if (excludeContent.includes('react-router-dom')) {
|
|
290
|
-
issues.push({
|
|
291
|
-
file: relativePath,
|
|
292
|
-
line: getLineNumber(content, excludeArrayMatch[0]),
|
|
293
|
-
type: 'should-exclude',
|
|
294
|
-
reason: 'react-router-dom should NOT be excluded from pre-bundling. This causes "module is not defined" errors.',
|
|
295
|
-
recommendation: "Remove 'react-router-dom' from optimizeDeps.exclude array and add it to optimizeDeps.include instead."
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
return issues;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// Scan Router setup in main entry files
|
|
305
|
-
function scanRouterSetup(filePath, content, relativePath) {
|
|
306
|
-
const issues = [];
|
|
307
|
-
|
|
308
|
-
// Only check main.tsx for BrowserRouter setup
|
|
309
|
-
// App.tsx might use Routes but doesn't define BrowserRouter
|
|
310
|
-
const isMainFile = relativePath.match(/^(src\/)?main\.(tsx?|jsx?)$/);
|
|
311
|
-
|
|
312
|
-
if (isMainFile) {
|
|
313
|
-
// Check for BrowserRouter
|
|
314
|
-
const hasBrowserRouter = /BrowserRouter/.test(content);
|
|
315
|
-
if (!hasBrowserRouter) {
|
|
316
|
-
issues.push({
|
|
317
|
-
file: relativePath,
|
|
318
|
-
line: 1,
|
|
319
|
-
type: 'missing-router',
|
|
320
|
-
reason: 'BrowserRouter is required for pace-core components that use React Router hooks.',
|
|
321
|
-
recommendation: 'Import BrowserRouter from react-router-dom and wrap your app with it.'
|
|
322
|
-
});
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// Don't check nesting order here - that's handled in scanProviderSetup
|
|
326
|
-
// This function just checks if BrowserRouter exists
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// Check for Routes usage in any file (indicates Router context is needed)
|
|
330
|
-
// But only warn if it's in main.tsx and BrowserRouter is missing
|
|
331
|
-
const hasRoutes = /<Routes/.test(content) || /Routes/.test(content);
|
|
332
|
-
if (hasRoutes && isMainFile) {
|
|
333
|
-
const hasBrowserRouter = /BrowserRouter/.test(content);
|
|
334
|
-
if (!hasBrowserRouter) {
|
|
335
|
-
issues.push({
|
|
336
|
-
file: relativePath,
|
|
337
|
-
line: getLineNumber(content, /Routes/.exec(content)?.index || 0),
|
|
338
|
-
type: 'missing-router',
|
|
339
|
-
reason: 'Routes component requires BrowserRouter to provide Router context.',
|
|
340
|
-
recommendation: 'Wrap your app with BrowserRouter from react-router-dom.'
|
|
341
|
-
});
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
return issues;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// Helper function to provide migration recommendations
|
|
349
|
-
function getMigrationRecommendation(method, operation) {
|
|
350
|
-
const recommendations = {
|
|
351
|
-
secureQuery: `Replace with: const supabase = useSecureSupabase(); const { data } = await supabase.from('table').select('*');`,
|
|
352
|
-
secureInsert: `Replace with: const supabase = useSecureSupabase(); const { data } = await supabase.from('table').insert(data).select().single();`,
|
|
353
|
-
secureUpdate: `Replace with: const supabase = useSecureSupabase(); const { data } = await supabase.from('table').update(data).eq('id', id).select().single();`,
|
|
354
|
-
secureDelete: `Replace with: const supabase = useSecureSupabase(); await supabase.from('table').delete().eq('id', id);`,
|
|
355
|
-
secureRpc: `Replace with: const supabase = useSecureSupabase(); const { data } = await supabase.rpc('function_name', params);`
|
|
356
|
-
};
|
|
357
|
-
|
|
358
|
-
return recommendations[method] || `Replace with useSecureSupabase() and use standard Supabase ${operation} API`;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// Scan for unnecessary wrappers around pace-core components and local components
|
|
362
|
-
function scanUnnecessaryWrappers(content, relativePath, manifest) {
|
|
363
|
-
const issues = [];
|
|
364
|
-
|
|
365
|
-
// Check if file imports from pace-core
|
|
366
|
-
const paceCoreImportPattern = /import\s+{([^}]+)}\s+from\s+['"]@jmruthers\/pace-core['"]/;
|
|
367
|
-
const paceCoreImportMatch = content.match(paceCoreImportPattern);
|
|
368
|
-
|
|
369
|
-
// Extract imported pace-core component names
|
|
370
|
-
let importedPaceCoreComponents = [];
|
|
371
|
-
if (paceCoreImportMatch) {
|
|
372
|
-
importedPaceCoreComponents = (paceCoreImportMatch[1] || '')
|
|
373
|
-
.split(',')
|
|
374
|
-
.map(name => name.trim().replace(/\s+as\s+\w+/, '')) // Remove aliases
|
|
375
|
-
.filter(name => manifest.components.includes(name));
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
// Find exported component definitions
|
|
379
|
-
const componentPattern = /export\s+(default\s+)?(function|const)\s+(\w+)\s*[=\(]/g;
|
|
380
|
-
const componentMatches = [...content.matchAll(componentPattern)];
|
|
381
|
-
|
|
382
|
-
componentMatches.forEach(match => {
|
|
383
|
-
const componentName = match[3];
|
|
384
|
-
const matchIndex = match.index;
|
|
385
|
-
|
|
386
|
-
// Skip if it's a test file or example file
|
|
387
|
-
if (relativePath.includes('.test.') || relativePath.includes('.spec.') ||
|
|
388
|
-
relativePath.includes('example') || relativePath.includes('Example')) {
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// Find the component body (simplified - just check if it's a simple wrapper)
|
|
393
|
-
const afterMatch = content.substring(matchIndex + match[0].length, Math.min(content.length, matchIndex + match[0].length + 500));
|
|
394
|
-
|
|
395
|
-
// Check if body has significant logic
|
|
396
|
-
const hasHooks = /use[A-Z]\w+/.test(afterMatch);
|
|
397
|
-
const hasState = /useState|useReducer|useRef/.test(afterMatch);
|
|
398
|
-
const hasConditionals = /if\s*\(|&&|\?|switch/.test(afterMatch);
|
|
399
|
-
const hasMultipleReturns = (afterMatch.match(/return/g) || []).length > 1;
|
|
400
|
-
const hasLoops = /for\s*\(|while\s*\(|\.map\s*\(/.test(afterMatch);
|
|
401
|
-
|
|
402
|
-
// Find JSX components used
|
|
403
|
-
const jsxComponentPattern = /<([A-Z][a-zA-Z0-9]*)[\s>\/]/g;
|
|
404
|
-
const jsxComponents = [];
|
|
405
|
-
let jsxMatch;
|
|
406
|
-
while ((jsxMatch = jsxComponentPattern.exec(afterMatch)) !== null) {
|
|
407
|
-
const jsxComponentName = jsxMatch[1];
|
|
408
|
-
if (jsxComponentName !== 'Fragment' &&
|
|
409
|
-
jsxComponentName !== componentName &&
|
|
410
|
-
!jsxComponents.includes(jsxComponentName)) {
|
|
411
|
-
jsxComponents.push(jsxComponentName);
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// Check if it's a simple wrapper
|
|
416
|
-
if (jsxComponents.length === 1) {
|
|
417
|
-
const wrappedComponent = jsxComponents[0];
|
|
418
|
-
const wrappedComponentCount = (afterMatch.match(new RegExp(`<${wrappedComponent}`, 'gi')) || []).length;
|
|
419
|
-
|
|
420
|
-
const isSimpleWrapper =
|
|
421
|
-
wrappedComponentCount <= 2 &&
|
|
422
|
-
!hasState &&
|
|
423
|
-
!hasLoops &&
|
|
424
|
-
(!hasMultipleReturns || (hasMultipleReturns && !hasConditionals)) &&
|
|
425
|
-
(!hasHooks || /use(UnifiedAuth|Permissions|Can|RBAC)/.test(afterMatch));
|
|
426
|
-
|
|
427
|
-
if (isSimpleWrapper) {
|
|
428
|
-
const isPaceCoreComponent = importedPaceCoreComponents.includes(wrappedComponent);
|
|
429
|
-
|
|
430
|
-
let reason, recommendation;
|
|
431
|
-
if (isPaceCoreComponent) {
|
|
432
|
-
reason = `Component '${componentName}' appears to be an unnecessary wrapper around pace-core's '${wrappedComponent}'.`;
|
|
433
|
-
recommendation = `Use '${wrappedComponent}' directly from '@jmruthers/pace-core' instead.`;
|
|
434
|
-
} else {
|
|
435
|
-
reason = `Component '${componentName}' appears to be an unnecessary wrapper around '${wrappedComponent}'.`;
|
|
436
|
-
recommendation = `Remove the wrapper and use '${wrappedComponent}' directly.`;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
issues.push({
|
|
440
|
-
component: componentName,
|
|
441
|
-
wrappedComponent: wrappedComponent,
|
|
442
|
-
file: relativePath,
|
|
443
|
-
line: getLineNumber(content, match[0]),
|
|
444
|
-
reason: reason,
|
|
445
|
-
recommendation: recommendation
|
|
446
|
-
});
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
return issues;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
// Scan file for violations
|
|
455
|
-
function scanFile(filePath, manifest, projectRoot) {
|
|
456
|
-
const violations = {
|
|
457
|
-
restrictedImports: [],
|
|
458
|
-
duplicateComponents: [],
|
|
459
|
-
duplicateHooks: [],
|
|
460
|
-
duplicateUtils: [],
|
|
461
|
-
suggestions: [],
|
|
462
|
-
customAuthCode: [],
|
|
463
|
-
duplicateConfig: [],
|
|
464
|
-
unprotectedPages: [],
|
|
465
|
-
directSupabaseAuth: [],
|
|
466
|
-
directSupabaseClient: [], // Direct Supabase client usage instead of useSecureSupabase
|
|
467
|
-
deprecatedSecureDataAccess: [], // Deprecated useSecureDataAccess with secureQuery/secureInsert/etc
|
|
468
|
-
providerSetupIssues: [],
|
|
469
|
-
viteConfigIssues: [],
|
|
470
|
-
routerSetupIssues: [],
|
|
471
|
-
unnecessaryWrappers: [],
|
|
472
|
-
appDiscoveryIssues: []
|
|
473
|
-
};
|
|
474
|
-
|
|
475
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
476
|
-
const relativePath = getRelativePath(filePath, projectRoot);
|
|
477
|
-
|
|
478
|
-
// Normalize path for cross-platform compatibility (handle both forward and backslash paths)
|
|
479
|
-
const normalizedPath = relativePath.replace(/\\/g, '/');
|
|
480
|
-
|
|
481
|
-
// Skip Edge Functions - they run in Deno, not React, so React hooks aren't available
|
|
482
|
-
// Direct Supabase auth calls are the correct approach in Edge Functions
|
|
483
|
-
const isEdgeFunction = normalizedPath.includes('supabase/functions/');
|
|
484
|
-
|
|
485
|
-
// Skip pace-core package files - compliance checks are for consuming applications, not the library itself
|
|
486
|
-
// The library must import these dependencies to build its components, and its own components/hooks/utils
|
|
487
|
-
// are the source of truth, not duplicates
|
|
488
|
-
const isPaceCorePackage = normalizedPath.includes('packages/core/') || normalizedPath.startsWith('packages/core/');
|
|
489
|
-
if (isPaceCorePackage) {
|
|
490
|
-
return violations; // Return empty violations for pace-core package files
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
// Skip scripts directory - these are utility/setup scripts, not application code
|
|
494
|
-
// Scripts may legitimately need direct database access for admin operations
|
|
495
|
-
const isScript = normalizedPath.startsWith('scripts/') || normalizedPath.includes('/scripts/');
|
|
496
|
-
if (isScript) {
|
|
497
|
-
return violations; // Return empty violations for script files
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
// Skip root-level src directory - in pace-core repository, this is a demo/showcase app, not a consuming app
|
|
501
|
-
// The audit is designed for consuming applications, not demo apps in the library repository
|
|
502
|
-
const isRootSrc = normalizedPath.startsWith('src/') && !normalizedPath.includes('packages/');
|
|
503
|
-
if (isRootSrc) {
|
|
504
|
-
return violations; // Return empty violations for root-level src files (demo apps)
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// Check for restricted imports
|
|
508
|
-
manifest.restrictedImports.forEach(({ module, reason }) => {
|
|
509
|
-
const importPattern = new RegExp(`from\\s+['"]${module.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}['"]`, 'g');
|
|
510
|
-
if (importPattern.test(content)) {
|
|
511
|
-
violations.restrictedImports.push({
|
|
512
|
-
module,
|
|
513
|
-
reason,
|
|
514
|
-
file: relativePath,
|
|
515
|
-
line: getLineNumber(content, content.match(importPattern)[0])
|
|
516
|
-
});
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
// Also check for @radix-ui/* pattern
|
|
520
|
-
if (module.startsWith('@radix-ui/')) {
|
|
521
|
-
const radixPattern = /from\s+['"]@radix-ui\/[^'"]+['"]/g;
|
|
522
|
-
const matches = content.match(radixPattern);
|
|
523
|
-
if (matches) {
|
|
524
|
-
matches.forEach(match => {
|
|
525
|
-
const matchedModule = match.match(/['"]([^'"]+)['"]/)[1];
|
|
526
|
-
if (!manifest.restrictedImports.find(ri => ri.module === matchedModule)) {
|
|
527
|
-
violations.restrictedImports.push({
|
|
528
|
-
module: matchedModule,
|
|
529
|
-
reason: 'Use pace-core component instead of direct Radix UI import',
|
|
530
|
-
file: relativePath,
|
|
531
|
-
line: getLineNumber(content, match)
|
|
532
|
-
});
|
|
533
|
-
}
|
|
534
|
-
});
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
});
|
|
538
|
-
|
|
539
|
-
// Check for duplicate component names
|
|
540
|
-
const filename = path.basename(filePath);
|
|
541
|
-
const componentName = filename.replace(/\.(tsx?|jsx?)$/, '').replace(/\.(test|spec)$/, '');
|
|
542
|
-
|
|
543
|
-
if (manifest.components.includes(componentName)) {
|
|
544
|
-
// Check if this file exports a component
|
|
545
|
-
if (content.match(/export\s+(default\s+)?(function|const|class)\s+(\w+)?/)) {
|
|
546
|
-
violations.duplicateComponents.push({
|
|
547
|
-
component: componentName,
|
|
548
|
-
file: relativePath
|
|
549
|
-
});
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
// Check for duplicate hook names
|
|
554
|
-
if (filename.startsWith('use') && filename.endsWith('.ts') || filename.endsWith('.tsx')) {
|
|
555
|
-
const hookName = filename.replace(/\.(tsx?|jsx?)$/, '').replace(/\.(test|spec)$/, '');
|
|
556
|
-
if (manifest.hooks.includes(hookName)) {
|
|
557
|
-
if (content.match(/export\s+(default\s+)?(function|const)\s+(\w+)?/)) {
|
|
558
|
-
violations.duplicateHooks.push({
|
|
559
|
-
hook: hookName,
|
|
560
|
-
file: relativePath
|
|
561
|
-
});
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
// Check for duplicate util names
|
|
567
|
-
const utilName = filename.replace(/\.(ts|js)$/, '').replace(/\.(test|spec)$/, '');
|
|
568
|
-
if (manifest.utils.includes(utilName)) {
|
|
569
|
-
if (content.match(/export\s+(default\s+)?(function|const)\s+(\w+)?/)) {
|
|
570
|
-
violations.duplicateUtils.push({
|
|
571
|
-
util: utilName,
|
|
572
|
-
file: relativePath
|
|
573
|
-
});
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
// Check for native HTML elements that should use pace-core components
|
|
578
|
-
const nativeElementPatterns = {
|
|
579
|
-
'<button': { suggestion: 'Use Button from @jmruthers/pace-core' },
|
|
580
|
-
'<input': { suggestion: 'Use Input from @jmruthers/pace-core' },
|
|
581
|
-
'<textarea': { suggestion: 'Use Textarea from @jmruthers/pace-core' },
|
|
582
|
-
'<label': { suggestion: 'Use Label from @jmruthers/pace-core' }
|
|
583
|
-
};
|
|
584
|
-
|
|
585
|
-
Object.entries(nativeElementPatterns).forEach(([pattern, { suggestion }]) => {
|
|
586
|
-
if (content.includes(pattern) && !content.includes('from \'@jmruthers/pace-core\'')) {
|
|
587
|
-
violations.suggestions.push({
|
|
588
|
-
type: 'native-element',
|
|
589
|
-
suggestion,
|
|
590
|
-
file: relativePath,
|
|
591
|
-
pattern
|
|
592
|
-
});
|
|
593
|
-
}
|
|
594
|
-
});
|
|
595
|
-
|
|
596
|
-
// ============================================
|
|
597
|
-
// RBAC/Auth Compliance Checks
|
|
598
|
-
// ============================================
|
|
599
|
-
|
|
600
|
-
// Check if file imports from pace-core for auth/rbac (define at function scope)
|
|
601
|
-
const hasPaceCoreImport = /from\s+['"]@jmruthers\/pace-core/.test(content) ||
|
|
602
|
-
/from\s+['"]@jmruthers\/pace-core\/rbac/.test(content) ||
|
|
603
|
-
/from\s+['"]@jmruthers\/pace-core\/providers/.test(content);
|
|
604
|
-
|
|
605
|
-
// Check for custom auth/rbac/permission code that doesn't import from pace-core
|
|
606
|
-
const authRbacPatterns = [
|
|
607
|
-
// Custom auth hooks
|
|
608
|
-
{ pattern: /export\s+(default\s+)?(function|const)\s+useAuth\s*[=\(]/g, name: 'useAuth', type: 'hook' },
|
|
609
|
-
{ pattern: /export\s+(default\s+)?(function|const)\s+useCurrentUser\s*[=\(]/g, name: 'useCurrentUser', type: 'hook', replacement: 'useUnifiedAuth' },
|
|
610
|
-
{ pattern: /export\s+(default\s+)?(function|const)\s+useLogin\s*[=\(]/g, name: 'useLogin', type: 'hook' },
|
|
611
|
-
{ pattern: /export\s+(default\s+)?(function|const)\s+useLogout\s*[=\(]/g, name: 'useLogout', type: 'hook' },
|
|
612
|
-
{ pattern: /export\s+(default\s+)?(function|const)\s+useSession\s*[=\(]/g, name: 'useSession', type: 'hook' },
|
|
613
|
-
{ pattern: /export\s+(default\s+)?(function|const)\s+useUser\s*[=\(]/g, name: 'useUser', type: 'hook' },
|
|
614
|
-
{ pattern: /export\s+(default\s+)?(function|const)\s+useAuthentication\s*[=\(]/g, name: 'useAuthentication', type: 'hook' },
|
|
615
|
-
// Custom RBAC hooks (that query RBAC tables directly)
|
|
616
|
-
{ pattern: /export\s+(default\s+)?(function|const)\s+useUserOrganisationRoles\s*[=\(]/g, name: 'useUserOrganisationRoles', type: 'hook', replacement: 'useOrganisations or pace-core RBAC hooks' },
|
|
617
|
-
{ pattern: /export\s+(default\s+)?(function|const)\s+useUserRoles\s*[=\(]/g, name: 'useUserRoles', type: 'hook', replacement: 'pace-core RBAC hooks' },
|
|
618
|
-
{ pattern: /export\s+(default\s+)?(function|const)\s+usePermissions\s*[=\(]/g, name: 'usePermissions', type: 'hook' },
|
|
619
|
-
{ pattern: /export\s+(default\s+)?(function|const)\s+useCan\s*[=\(]/g, name: 'useCan', type: 'hook' },
|
|
620
|
-
{ pattern: /export\s+(default\s+)?(function|const)\s+useAccessLevel\s*[=\(]/g, name: 'useAccessLevel', type: 'hook' },
|
|
621
|
-
{ pattern: /export\s+(default\s+)?(function|const)\s+useRole\s*[=\(]/g, name: 'useRole', type: 'hook' },
|
|
622
|
-
// Custom RBAC components
|
|
623
|
-
{ pattern: /export\s+(default\s+)?(function|const)\s+PermissionGuard\s*[=\(]/g, name: 'PermissionGuard', type: 'component' },
|
|
624
|
-
{ pattern: /export\s+(default\s+)?(function|const)\s+AuthGuard\s*[=\(]/g, name: 'AuthGuard', type: 'component' },
|
|
625
|
-
{ pattern: /export\s+(default\s+)?(function|const)\s+RoleGuard\s*[=\(]/g, name: 'RoleGuard', type: 'component' },
|
|
626
|
-
{ pattern: /export\s+(default\s+)?(function|const)\s+AccessGuard\s*[=\(]/g, name: 'AccessGuard', type: 'component' },
|
|
627
|
-
// Custom permission utilities
|
|
628
|
-
{ pattern: /export\s+(default\s+)?(function|const)\s+checkPermission\s*[=\(]/g, name: 'checkPermission', type: 'util' },
|
|
629
|
-
{ pattern: /export\s+(default\s+)?(function|const)\s+hasPermission\s*[=\(]/g, name: 'hasPermission', type: 'util' },
|
|
630
|
-
{ pattern: /export\s+(default\s+)?(function|const)\s+hasAccess\s*[=\(]/g, name: 'hasAccess', type: 'util' },
|
|
631
|
-
{ pattern: /export\s+(default\s+)?(function|const)\s+canAccess\s*[=\(]/g, name: 'canAccess', type: 'util' },
|
|
632
|
-
{ pattern: /export\s+(default\s+)?(function|const)\s+isPermitted\s*[=\(]/g, name: 'isPermitted', type: 'util' }
|
|
633
|
-
];
|
|
634
|
-
|
|
635
|
-
authRbacPatterns.forEach(({ pattern, name, type, replacement }) => {
|
|
636
|
-
// Create a new regex instance to avoid state issues
|
|
637
|
-
const testPattern = new RegExp(pattern.source, pattern.flags);
|
|
638
|
-
if (testPattern.test(content)) {
|
|
639
|
-
// For custom RBAC hooks, check if they're actually using pace-core APIs
|
|
640
|
-
// If they use pace-core RBAC APIs (useRoleManagement, useSecureSupabase, etc.), they're compliant
|
|
641
|
-
const isCustomRBACHook = [
|
|
642
|
-
'useUserRoles',
|
|
643
|
-
'useUserOrganisationRoles',
|
|
644
|
-
'useRoleMutations',
|
|
645
|
-
'usePermissions',
|
|
646
|
-
'useCan',
|
|
647
|
-
'useAccessLevel',
|
|
648
|
-
'useRole'
|
|
649
|
-
].includes(name);
|
|
650
|
-
|
|
651
|
-
// Check if the hook uses pace-core RBAC APIs (use a fresh regex to avoid state issues)
|
|
652
|
-
const rbacApiPattern = /useRoleManagement|useSecureSupabase|useRBAC|usePermissions|useCan|rbac_role_grant|rbac_role_revoke|rbac_roles_list|data_rbac_apps_list/;
|
|
653
|
-
const usesPaceCoreRBAC = isCustomRBACHook && rbacApiPattern.test(content);
|
|
654
|
-
|
|
655
|
-
// Check for @pace-core-compliant comment (use a fresh regex)
|
|
656
|
-
const compliancePattern = /@pace-core-compliant|pace-core-compliant/i;
|
|
657
|
-
const hasComplianceComment = compliancePattern.test(content);
|
|
658
|
-
|
|
659
|
-
// If it's a custom RBAC hook but uses pace-core APIs or has compliance comment, skip it
|
|
660
|
-
if (isCustomRBACHook && (usesPaceCoreRBAC || hasComplianceComment)) {
|
|
661
|
-
// This hook is compliant - it uses pace-core APIs
|
|
662
|
-
return; // Skip to next pattern
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
// Flag custom RBAC hooks even if they import from pace-core (they're still duplicating functionality)
|
|
666
|
-
// For other hooks, only flag if they don't import from pace-core
|
|
667
|
-
if (isCustomRBACHook || !hasPaceCoreImport) {
|
|
668
|
-
const replacementName = replacement || name;
|
|
669
|
-
const reason = isCustomRBACHook
|
|
670
|
-
? `Custom ${type} '${name}' detected that duplicates pace-core functionality. Even though it may use some pace-core utilities, it implements custom role/permission management logic that should use pace-core APIs instead.`
|
|
671
|
-
: `Custom ${type} '${name}' detected. Use pace-core's ${replacementName} instead.`;
|
|
672
|
-
|
|
673
|
-
violations.customAuthCode.push({
|
|
674
|
-
name,
|
|
675
|
-
type,
|
|
676
|
-
file: relativePath,
|
|
677
|
-
line: getLineNumber(content, content.match(pattern)[0]),
|
|
678
|
-
reason: reason,
|
|
679
|
-
replacement: replacementName,
|
|
680
|
-
severity: 'error'
|
|
681
|
-
});
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
});
|
|
685
|
-
|
|
686
|
-
// Check for duplicate Supabase client configurations
|
|
687
|
-
const supabaseCreateClientPattern = /createClient\s*\(/g;
|
|
688
|
-
const supabaseCreateClientMatches = content.match(supabaseCreateClientPattern);
|
|
689
|
-
if (supabaseCreateClientMatches && supabaseCreateClientMatches.length > 1) {
|
|
690
|
-
violations.duplicateConfig.push({
|
|
691
|
-
type: 'supabase-client',
|
|
692
|
-
file: relativePath,
|
|
693
|
-
count: supabaseCreateClientMatches.length,
|
|
694
|
-
reason: `Multiple Supabase client instantiations found (${supabaseCreateClientMatches.length}). Consolidate to a single client configuration.`
|
|
695
|
-
});
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
// Check for Supabase URL/key configuration in multiple places
|
|
699
|
-
// This check is designed to catch actual duplication, not centralized config files.
|
|
700
|
-
// A centralized config file may legitimately reference env vars multiple times:
|
|
701
|
-
// - Reading from import.meta.env or process.env
|
|
702
|
-
// - Validation/error messages
|
|
703
|
-
// - Using in createClient
|
|
704
|
-
// - Comments/documentation
|
|
705
|
-
const supabaseUrlPattern = /(SUPABASE_URL|VITE_SUPABASE_URL|NEXT_PUBLIC_SUPABASE_URL|REACT_APP_SUPABASE_URL)/g;
|
|
706
|
-
const supabaseKeyPattern = /(SUPABASE_ANON_KEY|VITE_SUPABASE_PUBLISHABLE_KEY|NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY|REACT_APP_SUPABASE_PUBLISHABLE_KEY)/g;
|
|
707
|
-
const urlMatches = content.match(supabaseUrlPattern);
|
|
708
|
-
const keyMatches = content.match(supabaseKeyPattern);
|
|
709
|
-
|
|
710
|
-
// Check if this looks like a centralized config file
|
|
711
|
-
// Config files typically:
|
|
712
|
-
// - Have "config" or "supabase" or "client" in the path
|
|
713
|
-
// - Have a single createClient call (or none, if it's just exporting env vars)
|
|
714
|
-
// - Export the env vars or the client (indicating it's the centralized location)
|
|
715
|
-
const isConfigFile = /config|supabase|client/i.test(relativePath);
|
|
716
|
-
const hasSingleCreateClient = supabaseCreateClientMatches && supabaseCreateClientMatches.length === 1;
|
|
717
|
-
const hasNoCreateClient = !supabaseCreateClientMatches || supabaseCreateClientMatches.length === 0;
|
|
718
|
-
|
|
719
|
-
// Check if file exports env vars or client (strong indicator of centralized config)
|
|
720
|
-
// Matches: export const SUPABASE_URL = ...; export { SUPABASE_URL }; export const supabase = ...
|
|
721
|
-
const exportsEnvVars = /export\s+(const|let|var|{)\s*(SUPABASE_URL|SUPABASE_ANON_KEY|supabase)/.test(content) ||
|
|
722
|
-
/export\s*{\s*[^}]*\b(SUPABASE_URL|SUPABASE_ANON_KEY|supabase)\b/.test(content);
|
|
723
|
-
const exportsClient = /export\s+(const|let|var)\s+supabase\s*=/.test(content) ||
|
|
724
|
-
/export\s*{\s*[^}]*\bsupabase\b/.test(content);
|
|
725
|
-
|
|
726
|
-
// A file is considered a centralized config if:
|
|
727
|
-
// 1. It's in a config/supabase/client path, AND
|
|
728
|
-
// 2. Has a single createClient call (typical centralized config), OR
|
|
729
|
-
// 3. Has no createClient but references env vars (env-only config file), OR
|
|
730
|
-
// 4. Exports env vars or the client (indicating it's the centralized location)
|
|
731
|
-
// The path name is the primary indicator - if it's in a config path, trust it
|
|
732
|
-
const looksLikeCentralizedConfig = isConfigFile && (
|
|
733
|
-
hasSingleCreateClient ||
|
|
734
|
-
(hasNoCreateClient && (urlMatches || keyMatches)) ||
|
|
735
|
-
exportsEnvVars ||
|
|
736
|
-
exportsClient
|
|
737
|
-
);
|
|
738
|
-
|
|
739
|
-
// Only flag if:
|
|
740
|
-
// 1. Multiple createClient calls (already handled above), OR
|
|
741
|
-
// 2. Many references (10+) in a file that doesn't look like a centralized config
|
|
742
|
-
// Config files are allowed to have many references (read, validate, use, errors, comments, etc.)
|
|
743
|
-
const threshold = looksLikeCentralizedConfig ? 20 : 5;
|
|
744
|
-
|
|
745
|
-
if (!looksLikeCentralizedConfig &&
|
|
746
|
-
((urlMatches && urlMatches.length > threshold) ||
|
|
747
|
-
(keyMatches && keyMatches.length > threshold))) {
|
|
748
|
-
violations.duplicateConfig.push({
|
|
749
|
-
type: 'supabase-env',
|
|
750
|
-
file: relativePath,
|
|
751
|
-
reason: `Supabase environment variables referenced many times (${urlMatches?.length || keyMatches?.length || 0}). If this is a centralized config file, consider moving it to a file with 'config', 'supabase', or 'client' in the path, or export the values to indicate it's the centralized location.`
|
|
752
|
-
});
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
// Check for unprotected pages/routes
|
|
756
|
-
// Look for route definitions without PagePermissionGuard
|
|
757
|
-
const routePatterns = [
|
|
758
|
-
/<Route\s+path=["'][^"']+["']/g,
|
|
759
|
-
/<Route\s+element\s*=/g,
|
|
760
|
-
/createBrowserRouter\s*\(/g,
|
|
761
|
-
/createRoutesFromElements/g
|
|
762
|
-
];
|
|
763
|
-
|
|
764
|
-
const isRouteFile = routePatterns.some(pattern => pattern.test(content));
|
|
765
|
-
const hasPagePermissionGuard = /PagePermissionGuard/.test(content) ||
|
|
766
|
-
/from\s+['"]@jmruthers\/pace-core\/rbac['"]/.test(content);
|
|
767
|
-
|
|
768
|
-
if (isRouteFile && !hasPagePermissionGuard && !relativePath.includes('test') && !relativePath.includes('spec')) {
|
|
769
|
-
violations.unprotectedPages.push({
|
|
770
|
-
file: relativePath,
|
|
771
|
-
reason: 'Route file found without PagePermissionGuard. All routes should be protected with PagePermissionGuard from pace-core.'
|
|
772
|
-
});
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
// Check for direct Supabase auth usage (should use UnifiedAuthProvider)
|
|
776
|
-
// This includes all variations: supabase.auth.getUser(), client.auth.getUser(), etc.
|
|
777
|
-
// Priority patterns - these are the most common violations
|
|
778
|
-
// Using multiple patterns to catch all variations
|
|
779
|
-
|
|
780
|
-
// Skip Edge Functions - they run in Deno, not React, so React hooks are not available
|
|
781
|
-
// Edge Functions MUST use direct supabase.auth.getUser() calls - this is correct and required
|
|
782
|
-
// isEdgeFunction is already defined above
|
|
783
|
-
|
|
784
|
-
// Other auth patterns - defined here so it's accessible in all scopes
|
|
785
|
-
const otherAuthPatterns = [
|
|
786
|
-
{ pattern: /\.auth\.signIn\s*\(/g, method: 'signIn' },
|
|
787
|
-
{ pattern: /\.auth\.signUp\s*\(/g, method: 'signUp' },
|
|
788
|
-
{ pattern: /\.auth\.signOut\s*\(/g, method: 'signOut' },
|
|
789
|
-
{ pattern: /\.auth\.onAuthStateChange\s*\(/g, method: 'onAuthStateChange' }
|
|
790
|
-
];
|
|
791
|
-
|
|
792
|
-
// Check if file actually uses useUnifiedAuth hook (not just imports it)
|
|
793
|
-
// Define these at function scope so they're available throughout
|
|
794
|
-
const usesUnifiedAuthHook = /useUnifiedAuth\s*\(/.test(content);
|
|
795
|
-
const hasUnifiedAuthImport = /UnifiedAuthProvider/.test(content) ||
|
|
796
|
-
/useUnifiedAuth/.test(content) ||
|
|
797
|
-
/from\s+['"]@jmruthers\/pace-core\/providers/.test(content);
|
|
798
|
-
|
|
799
|
-
// Skip all auth checks for Edge Functions - they cannot use React hooks
|
|
800
|
-
if (isEdgeFunction) {
|
|
801
|
-
// Edge Functions use service role client or direct auth calls - correct pattern
|
|
802
|
-
// Don't flag these as violations
|
|
803
|
-
} else {
|
|
804
|
-
// Only check for direct auth usage in client-side code (React components, hooks, etc.)
|
|
805
|
-
const priorityAuthPatterns = [
|
|
806
|
-
// Most specific patterns first
|
|
807
|
-
{ pattern: /supabase\.auth\.getUser\s*\(/g, method: 'getUser', specific: true },
|
|
808
|
-
{ pattern: /supabase\.auth\.getSession\s*\(/g, method: 'getSession', specific: true },
|
|
809
|
-
// Also catch with await or const destructuring
|
|
810
|
-
{ pattern: /await\s+supabase\.auth\.getUser\s*\(/g, method: 'getUser', specific: true },
|
|
811
|
-
{ pattern: /await\s+supabase\.auth\.getSession\s*\(/g, method: 'getSession', specific: true },
|
|
812
|
-
{ pattern: /const\s+[^=]*=\s*supabase\.auth\.getUser\s*\(/g, method: 'getUser', specific: true },
|
|
813
|
-
{ pattern: /const\s+[^=]*=\s*supabase\.auth\.getSession\s*\(/g, method: 'getSession', specific: true },
|
|
814
|
-
// Generic patterns for other variable names
|
|
815
|
-
{ pattern: /\w+\.auth\.getUser\s*\(/g, method: 'getUser', specific: false },
|
|
816
|
-
{ pattern: /\w+\.auth\.getSession\s*\(/g, method: 'getSession', specific: false }
|
|
817
|
-
];
|
|
818
|
-
|
|
819
|
-
// Check for usage of useCurrentUser hook (even if imported from local file)
|
|
820
|
-
// This catches both local imports and direct usage
|
|
821
|
-
const useCurrentUserImportPattern = /import\s+.*useCurrentUser.*from\s+['"][^'"]*['"]/g;
|
|
822
|
-
const useCurrentUserUsagePattern = /useCurrentUser\s*\(/g;
|
|
823
|
-
|
|
824
|
-
// Check for local import (not from pace-core)
|
|
825
|
-
const useCurrentUserImportMatch = content.match(useCurrentUserImportPattern);
|
|
826
|
-
if (useCurrentUserImportMatch) {
|
|
827
|
-
const isFromPaceCore = useCurrentUserImportMatch.some(match => match.includes('@jmruthers/pace-core'));
|
|
828
|
-
if (!isFromPaceCore) {
|
|
829
|
-
useCurrentUserImportMatch.forEach(match => {
|
|
830
|
-
violations.customAuthCode.push({
|
|
831
|
-
name: 'useCurrentUser import',
|
|
832
|
-
type: 'hook import',
|
|
833
|
-
file: relativePath,
|
|
834
|
-
line: getLineNumber(content, match),
|
|
835
|
-
reason: 'useCurrentUser imported from local file. Replace with useUnifiedAuth from pace-core.',
|
|
836
|
-
replacement: 'useUnifiedAuth from @jmruthers/pace-core'
|
|
837
|
-
});
|
|
838
|
-
});
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
// Check for usage (even if imported)
|
|
843
|
-
const useCurrentUserUsageMatches = content.match(useCurrentUserUsagePattern);
|
|
844
|
-
if (useCurrentUserUsageMatches && !usesUnifiedAuthHook) {
|
|
845
|
-
useCurrentUserUsageMatches.forEach(match => {
|
|
846
|
-
violations.customAuthCode.push({
|
|
847
|
-
name: 'useCurrentUser',
|
|
848
|
-
type: 'hook usage',
|
|
849
|
-
file: relativePath,
|
|
850
|
-
line: getLineNumber(content, match),
|
|
851
|
-
reason: 'useCurrentUser hook usage detected. Replace with useUnifiedAuth from pace-core.',
|
|
852
|
-
replacement: 'useUnifiedAuth'
|
|
853
|
-
});
|
|
854
|
-
});
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
// Check priority patterns first (getUser, getSession) - these should always be flagged
|
|
858
|
-
// Use exec in a loop to get all matches with their correct positions
|
|
859
|
-
priorityAuthPatterns.forEach(({ pattern, method, specific }) => {
|
|
860
|
-
// Reset regex for each pattern
|
|
861
|
-
const regex = new RegExp(pattern.source, pattern.flags);
|
|
862
|
-
regex.lastIndex = 0; // Reset to start
|
|
863
|
-
|
|
864
|
-
let match;
|
|
865
|
-
while ((match = regex.exec(content)) !== null) {
|
|
866
|
-
// Prevent infinite loops on zero-length matches
|
|
867
|
-
if (match.index === regex.lastIndex) {
|
|
868
|
-
regex.lastIndex++;
|
|
869
|
-
continue;
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
const matchIndex = match.index;
|
|
873
|
-
const matchText = match[0];
|
|
874
|
-
|
|
875
|
-
// Simple comment check - only skip if clearly in a line comment
|
|
876
|
-
const lineStart = content.lastIndexOf('\n', matchIndex) + 1;
|
|
877
|
-
const lineUpToMatch = content.substring(lineStart, matchIndex);
|
|
878
|
-
const isInLineComment = /\/\/[^\n]*$/.test(lineUpToMatch);
|
|
879
|
-
|
|
880
|
-
// Only skip if clearly in a comment - be conservative
|
|
881
|
-
// Also skip Edge Functions - they run in Deno, not React, so React hooks aren't available
|
|
882
|
-
if (!isInLineComment && !isEdgeFunction) {
|
|
883
|
-
violations.directSupabaseAuth.push({
|
|
884
|
-
file: relativePath,
|
|
885
|
-
line: getLineNumber(content, matchText),
|
|
886
|
-
reason: `Direct Supabase auth usage detected (${method}). Use useUnifiedAuth hook from pace-core instead.`,
|
|
887
|
-
method,
|
|
888
|
-
recommendation: specific
|
|
889
|
-
? method === 'getUser'
|
|
890
|
-
? `Replace with: const { user } = useUnifiedAuth(); then use user?.id instead of calling supabase.auth.getUser()`
|
|
891
|
-
: `Replace with: const { session } = useUnifiedAuth(); then use session?.access_token instead of calling supabase.auth.getSession()`
|
|
892
|
-
: `Use useUnifiedAuth hook from @jmruthers/pace-core instead of direct auth calls`
|
|
893
|
-
});
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
});
|
|
897
|
-
} // End of else block for non-Edge Functions
|
|
898
|
-
|
|
899
|
-
// Additional simple pattern check as fallback - look for literal strings
|
|
900
|
-
// Skip for Edge Functions
|
|
901
|
-
if (!isEdgeFunction) {
|
|
902
|
-
// This catches cases where the regex might miss due to formatting
|
|
903
|
-
const simpleAuthPatterns = [
|
|
904
|
-
{ search: 'supabase.auth.getUser(', method: 'getUser' },
|
|
905
|
-
{ search: 'supabase.auth.getSession(', method: 'getSession' }
|
|
906
|
-
];
|
|
907
|
-
|
|
908
|
-
simpleAuthPatterns.forEach(({ search, method }) => {
|
|
909
|
-
let searchIndex = 0;
|
|
910
|
-
while ((searchIndex = content.indexOf(search, searchIndex)) !== -1) {
|
|
911
|
-
// Skip if in a line comment
|
|
912
|
-
const lineStart = content.lastIndexOf('\n', searchIndex) + 1;
|
|
913
|
-
const lineUpToMatch = content.substring(lineStart, searchIndex);
|
|
914
|
-
const isInLineComment = /\/\/[^\n]*$/.test(lineUpToMatch);
|
|
915
|
-
|
|
916
|
-
// Skip Edge Functions - they run in Deno, not React, so React hooks aren't available
|
|
917
|
-
if (!isInLineComment && !isEdgeFunction) {
|
|
918
|
-
// Calculate line number from index
|
|
919
|
-
const lineNum = content.substring(0, searchIndex).split('\n').length;
|
|
920
|
-
|
|
921
|
-
// Check if we already reported this (might overlap with regex matches)
|
|
922
|
-
const alreadyReported = violations.directSupabaseAuth.some(v =>
|
|
923
|
-
v.file === relativePath &&
|
|
924
|
-
Math.abs(v.line - lineNum) <= 2
|
|
925
|
-
);
|
|
926
|
-
|
|
927
|
-
if (!alreadyReported) {
|
|
928
|
-
violations.directSupabaseAuth.push({
|
|
929
|
-
file: relativePath,
|
|
930
|
-
line: lineNum,
|
|
931
|
-
reason: `Direct Supabase auth usage detected (${method}). Use useUnifiedAuth hook from pace-core instead.`,
|
|
932
|
-
method,
|
|
933
|
-
recommendation: method === 'getUser'
|
|
934
|
-
? `Replace with: const { user } = useUnifiedAuth(); then use user?.id instead of calling supabase.auth.getUser()`
|
|
935
|
-
: `Replace with: const { session } = useUnifiedAuth(); then use session?.access_token instead of calling supabase.auth.getSession()`
|
|
936
|
-
});
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
searchIndex += search.length; // Move past this match
|
|
941
|
-
}
|
|
942
|
-
});
|
|
943
|
-
} // End of Edge Function check for simple patterns
|
|
944
|
-
|
|
945
|
-
// Check other auth patterns - flag if not using useUnifiedAuth
|
|
946
|
-
// Skip for Edge Functions
|
|
947
|
-
if (!isEdgeFunction) {
|
|
948
|
-
otherAuthPatterns.forEach(({ pattern, method }) => {
|
|
949
|
-
let match;
|
|
950
|
-
const regex = new RegExp(pattern.source, pattern.flags);
|
|
951
|
-
|
|
952
|
-
while ((match = regex.exec(content)) !== null) {
|
|
953
|
-
// Prevent infinite loops on zero-length matches
|
|
954
|
-
if (match.index === regex.lastIndex) {
|
|
955
|
-
regex.lastIndex++;
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
const matchIndex = match.index;
|
|
959
|
-
const matchText = match[0];
|
|
960
|
-
|
|
961
|
-
// Skip if this is in a comment or string literal
|
|
962
|
-
const lineStart = content.lastIndexOf('\n', matchIndex) + 1;
|
|
963
|
-
const lineBeforeMatch = content.substring(lineStart, matchIndex);
|
|
964
|
-
const isInLineComment = /\/\/[^\n]*$/.test(lineBeforeMatch);
|
|
965
|
-
const beforeMatch = content.substring(Math.max(0, matchIndex - 100), matchIndex);
|
|
966
|
-
const afterMatch = content.substring(matchIndex, Math.min(content.length, matchIndex + matchText.length + 50));
|
|
967
|
-
const isInBlockComment = /\/\*[\s\S]*?\*\//.test(beforeMatch + matchText + afterMatch);
|
|
968
|
-
|
|
969
|
-
// Check if it's in a string literal
|
|
970
|
-
const beforeQuotes = content.substring(0, matchIndex);
|
|
971
|
-
const singleQuotes = (beforeQuotes.match(/'/g) || []).length;
|
|
972
|
-
const doubleQuotes = (beforeQuotes.match(/"/g) || []).length;
|
|
973
|
-
const backticks = (beforeQuotes.match(/`/g) || []).length;
|
|
974
|
-
const isInString = (singleQuotes % 2 === 1 && beforeMatch.endsWith("'")) ||
|
|
975
|
-
(doubleQuotes % 2 === 1 && beforeMatch.endsWith('"')) ||
|
|
976
|
-
(backticks % 2 === 1 && beforeMatch.endsWith('`'));
|
|
977
|
-
|
|
978
|
-
// Skip Edge Functions - they run in Deno, not React, so React hooks aren't available
|
|
979
|
-
if (!isInLineComment && !isInBlockComment && !isInString && !usesUnifiedAuthHook && !isEdgeFunction) {
|
|
980
|
-
violations.directSupabaseAuth.push({
|
|
981
|
-
file: relativePath,
|
|
982
|
-
line: getLineNumber(content, matchText),
|
|
983
|
-
reason: `Direct Supabase auth usage detected (${method}). Use UnifiedAuthProvider and useUnifiedAuth from pace-core instead.`,
|
|
984
|
-
method
|
|
985
|
-
});
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
});
|
|
989
|
-
} // End of Edge Function check for other auth patterns
|
|
990
|
-
|
|
991
|
-
// Check for direct RBAC table queries (should use pace-core RBAC APIs/RPC functions)
|
|
992
|
-
// List of all RBAC tables with specific recommendations
|
|
993
|
-
const rbacTables = [
|
|
994
|
-
// Core RBAC tables - should use pace-core APIs
|
|
995
|
-
{ name: 'rbac_organisation_roles', type: 'role', recommendation: 'Use rbac_role_grant/rbac_role_revoke RPC functions for mutations, or useOrganisations hook for queries' },
|
|
996
|
-
{ name: 'rbac_event_app_roles', type: 'role', recommendation: 'Use rbac_role_grant/rbac_role_revoke RPC functions for mutations, or pace-core RBAC APIs (useRBAC, usePermissions) for queries' },
|
|
997
|
-
{ name: 'rbac_global_roles', type: 'role', recommendation: 'Use rbac_role_grant/rbac_role_revoke RPC functions for mutations, or pace-core RBAC APIs (useRBAC) for queries' },
|
|
998
|
-
{ name: 'rbac_apps', type: 'config', recommendation: 'For admin operations, use useSecureSupabase. For application use, use pace-core RBAC APIs' },
|
|
999
|
-
{ name: 'rbac_app_pages', type: 'config', recommendation: 'For admin operations, use useSecureSupabase. For application use, use pace-core permission management APIs or PagePermissionGuard' },
|
|
1000
|
-
{ name: 'rbac_page_permissions', type: 'config', recommendation: 'For admin operations, use useSecureSupabase. For application use, use pace-core permission management APIs or PagePermissionGuard' },
|
|
1001
|
-
{ name: 'rbac_user_units', type: 'user_data', recommendation: 'Use useSecureSupabase. For reading, consider data_user_unit_get RPC function' },
|
|
1002
|
-
// User data tables - acceptable to query but must use secure methods
|
|
1003
|
-
{ name: 'rbac_user_profiles', type: 'user_data', recommendation: 'Use useSecureSupabase from pace-core to ensure organisation context is enforced' },
|
|
1004
|
-
{ name: 'rbac_user_login_history', type: 'audit', recommendation: 'Use useSecureSupabase from pace-core. Login history is automatically tracked by UnifiedAuthProvider, but queries should use secure methods' },
|
|
1005
|
-
{ name: 'rbac_user_sessions', type: 'session', recommendation: 'Use useSecureSupabase from pace-core to ensure organisation context is enforced' }
|
|
1006
|
-
];
|
|
1007
|
-
|
|
1008
|
-
// Detect admin/management context
|
|
1009
|
-
const isAdminContext =
|
|
1010
|
-
relativePath.includes('/admin/') ||
|
|
1011
|
-
relativePath.includes('/superadmin/') ||
|
|
1012
|
-
relativePath.includes('/permissions/') ||
|
|
1013
|
-
relativePath.includes('/management/') ||
|
|
1014
|
-
/(Admin|Manager|Management|Permissions)[^/]*\.(tsx?|jsx?)$/.test(relativePath);
|
|
1015
|
-
|
|
1016
|
-
// Multiple patterns to catch all variations
|
|
1017
|
-
const rbacTablePatterns = [
|
|
1018
|
-
/\.from\s*\(\s*['"]rbac_/g, // .from('rbac_ or .from("rbac_
|
|
1019
|
-
/from\s*\(\s*['"]rbac_/g, // from('rbac_ (without dot)
|
|
1020
|
-
/\.select\s*\([^)]*from\s+['"]rbac_/g, // .select(...).from('rbac_
|
|
1021
|
-
/supabase\.from\s*\(\s*['"]rbac_/g // supabase.from('rbac_
|
|
1022
|
-
];
|
|
1023
|
-
|
|
1024
|
-
// Check if file uses pace-core RBAC APIs (if it does, direct queries might be acceptable in some cases)
|
|
1025
|
-
// But we still want to flag them as they should use the APIs
|
|
1026
|
-
const hasRBACImport = /from\s+['"]@jmruthers\/pace-core\/rbac/.test(content) ||
|
|
1027
|
-
/useRBAC/.test(content) ||
|
|
1028
|
-
/usePermissions/.test(content) ||
|
|
1029
|
-
/useSecureSupabase/.test(content) ||
|
|
1030
|
-
/PagePermissionGuard/.test(content);
|
|
1031
|
-
|
|
1032
|
-
// Check if file destructures secure methods (deprecated secureQuery/secureInsert/etc from useSecureDataAccess)
|
|
1033
|
-
const hasSecureMethods = /(const|let)\s*\{[^}]*secure(Query|Update|Insert|Delete)/.test(content) ||
|
|
1034
|
-
/secure(Query|Update|Insert|Delete)\s*\(/.test(content);
|
|
1035
|
-
|
|
1036
|
-
// First, identify all variables assigned from secure hooks
|
|
1037
|
-
// Match patterns like: const supabase = useSecureSupabase();
|
|
1038
|
-
// Also detect fromSupabaseClient and wrapper patterns
|
|
1039
|
-
const secureVariablePatterns = [
|
|
1040
|
-
/const\s+(\w+)\s*=\s*useSecureSupabase\s*\(/g,
|
|
1041
|
-
/let\s+(\w+)\s*=\s*useSecureSupabase\s*\(/g,
|
|
1042
|
-
/(\w+)\s*=\s*useSecureSupabase\s*\(/g,
|
|
1043
|
-
// Detect fromSupabaseClient usage
|
|
1044
|
-
/const\s+(\w+)\s*=\s*fromSupabaseClient\s*\(/g,
|
|
1045
|
-
/let\s+(\w+)\s*=\s*fromSupabaseClient\s*\(/g,
|
|
1046
|
-
/(\w+)\s*=\s*fromSupabaseClient\s*\(/g
|
|
1047
|
-
];
|
|
1048
|
-
|
|
1049
|
-
// Check for fromSupabaseClient import
|
|
1050
|
-
const hasFromSupabaseClientImport = /import.*fromSupabaseClient.*from\s+['"]@jmruthers\/pace-core\/rbac/.test(content);
|
|
1051
|
-
|
|
1052
|
-
// Check for wrapper functions that use fromSupabaseClient
|
|
1053
|
-
// Pattern: function/hook that imports fromSupabaseClient and returns a client
|
|
1054
|
-
const wrapperFunctionPattern = /(export\s+)?(function|const)\s+(\w+)\s*[=\(][\s\S]{0,500}fromSupabaseClient/g;
|
|
1055
|
-
const wrapperMatches = content.match(wrapperFunctionPattern);
|
|
1056
|
-
const wrapperFunctionNames = new Set();
|
|
1057
|
-
if (wrapperMatches) {
|
|
1058
|
-
wrapperMatches.forEach(match => {
|
|
1059
|
-
const funcMatch = match.match(/(?:function|const)\s+(\w+)\s*[=\(]/);
|
|
1060
|
-
if (funcMatch && funcMatch[1]) {
|
|
1061
|
-
wrapperFunctionNames.add(funcMatch[1]);
|
|
1062
|
-
}
|
|
1063
|
-
});
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
// Also check for useSecureClient or similar wrapper patterns
|
|
1067
|
-
const useSecureClientPattern = /(const|let)\s+(\w+)\s*=\s*useSecureClient\s*\(/g;
|
|
1068
|
-
let useSecureClientMatch;
|
|
1069
|
-
while ((useSecureClientMatch = useSecureClientPattern.exec(content)) !== null) {
|
|
1070
|
-
if (useSecureClientMatch[2] && hasFromSupabaseClientImport) {
|
|
1071
|
-
// If useSecureClient is used and file imports fromSupabaseClient, it's likely a wrapper
|
|
1072
|
-
wrapperFunctionNames.add(useSecureClientMatch[2]);
|
|
1073
|
-
}
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
const secureVariables = new Set();
|
|
1077
|
-
secureVariablePatterns.forEach(pattern => {
|
|
1078
|
-
let match;
|
|
1079
|
-
const regex = new RegExp(pattern.source, pattern.flags);
|
|
1080
|
-
regex.lastIndex = 0;
|
|
1081
|
-
while ((match = regex.exec(content)) !== null) {
|
|
1082
|
-
if (match[1]) {
|
|
1083
|
-
secureVariables.add(match[1]);
|
|
1084
|
-
// Debug: log detected secure variables (can be removed later)
|
|
1085
|
-
// console.log(`Detected secure variable: ${match[1]} from pattern ${pattern.source}`);
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
|
-
});
|
|
1089
|
-
|
|
1090
|
-
// Also check for variables assigned from useSecureSupabase with different names
|
|
1091
|
-
// Pattern: const <anyName> = useSecureSupabase();
|
|
1092
|
-
const anySecureSupabasePattern = /(const|let)\s+(\w+)\s*=\s*useSecureSupabase\s*\(/g;
|
|
1093
|
-
let anySecureMatch;
|
|
1094
|
-
while ((anySecureMatch = anySecureSupabasePattern.exec(content)) !== null) {
|
|
1095
|
-
if (anySecureMatch[2]) {
|
|
1096
|
-
secureVariables.add(anySecureMatch[2]);
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
// Add wrapper function return values to secure variables
|
|
1101
|
-
wrapperFunctionNames.forEach(wrapperName => {
|
|
1102
|
-
// Find variables assigned from wrapper functions
|
|
1103
|
-
const wrapperUsagePattern = new RegExp(`(const|let)\\s+(\\w+)\\s*=\\s*${wrapperName}\\s*\\(`, 'g');
|
|
1104
|
-
let wrapperUsageMatch;
|
|
1105
|
-
while ((wrapperUsageMatch = wrapperUsagePattern.exec(content)) !== null) {
|
|
1106
|
-
if (wrapperUsageMatch[2]) {
|
|
1107
|
-
secureVariables.add(wrapperUsageMatch[2]);
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
});
|
|
1111
|
-
|
|
1112
|
-
// Debug: Log detected secure variables (only in verbose mode if needed)
|
|
1113
|
-
// For now, we'll trust the detection works
|
|
1114
|
-
|
|
1115
|
-
// Check each RBAC table specifically
|
|
1116
|
-
rbacTables.forEach(({ name: tableName, type, recommendation }) => {
|
|
1117
|
-
// Pattern to match the table name in a .from() call
|
|
1118
|
-
// Match: variable.from('table_name') or variable\n.from('table_name') (handles newlines)
|
|
1119
|
-
// First, find all .from('table_name') calls
|
|
1120
|
-
const fromPattern = new RegExp(`\\.from\\s*\\(\\s*['"]${tableName.replace(/_/g, '\\_')}['"]`, 'g');
|
|
1121
|
-
let match;
|
|
1122
|
-
const regex = new RegExp(fromPattern.source, fromPattern.flags);
|
|
1123
|
-
regex.lastIndex = 0;
|
|
1124
|
-
|
|
1125
|
-
// Also check for secureQuery/secureUpdate/secureInsert/secureDelete calls
|
|
1126
|
-
// Pattern: secureQuery('table_name', ...) or secureUpdate('table_name', ...)
|
|
1127
|
-
const secureQueryPattern = new RegExp(`(secureQuery|secureUpdate|secureInsert|secureDelete)\\s*\\(\\s*['"]${tableName.replace(/_/g, '\\_')}['"]`, 'g');
|
|
1128
|
-
let secureMatch;
|
|
1129
|
-
const secureRegex = new RegExp(secureQueryPattern.source, secureQueryPattern.flags);
|
|
1130
|
-
secureRegex.lastIndex = 0;
|
|
1131
|
-
|
|
1132
|
-
while ((match = regex.exec(content)) !== null) {
|
|
1133
|
-
if (match.index === regex.lastIndex) {
|
|
1134
|
-
regex.lastIndex++;
|
|
1135
|
-
continue;
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
|
-
const matchIndex = match.index;
|
|
1139
|
-
|
|
1140
|
-
// Look backwards to find the variable name (handle newlines and whitespace)
|
|
1141
|
-
// Look back up to 200 characters to find the variable
|
|
1142
|
-
const beforeMatch = content.substring(Math.max(0, matchIndex - 200), matchIndex);
|
|
1143
|
-
// Find the last word/identifier before .from
|
|
1144
|
-
// Split by .from and get the last part, then extract the last word
|
|
1145
|
-
const parts = beforeMatch.split('.from');
|
|
1146
|
-
let variableName = null;
|
|
1147
|
-
if (parts.length > 0) {
|
|
1148
|
-
const beforeFrom = parts[parts.length - 1].trim();
|
|
1149
|
-
// Extract the last word (identifier) from the string before .from
|
|
1150
|
-
// Handle cases like: "supabase\n " or "await supabase\n " or "supabase"
|
|
1151
|
-
const words = beforeFrom.match(/\b\w+\b/g);
|
|
1152
|
-
if (words && words.length > 0) {
|
|
1153
|
-
variableName = words[words.length - 1];
|
|
1154
|
-
}
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
// Skip if in a line comment
|
|
1158
|
-
const lineStart = content.lastIndexOf('\n', matchIndex) + 1;
|
|
1159
|
-
const lineUpToMatch = content.substring(lineStart, matchIndex);
|
|
1160
|
-
const isInLineComment = /\/\/[^\n]*$/.test(lineUpToMatch);
|
|
1161
|
-
|
|
1162
|
-
if (!isInLineComment && variableName) {
|
|
1163
|
-
const lineNumber = content.substring(0, matchIndex).split('\n').length;
|
|
1164
|
-
const isUserDataOrAudit = type === 'user_data' || type === 'audit';
|
|
1165
|
-
const isConfigTable = type === 'config';
|
|
1166
|
-
|
|
1167
|
-
// Check if the variable comes from a secure hook
|
|
1168
|
-
// Check both the secureVariables set and also check if the variable is assigned from useSecureSupabase
|
|
1169
|
-
// Escape special regex characters in variable name and use multiline flag to handle newlines
|
|
1170
|
-
const escapedVarName = variableName ? variableName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') : '';
|
|
1171
|
-
|
|
1172
|
-
// Check if variable is declared locally (const/let = useSecureSupabase())
|
|
1173
|
-
const isDeclaredSecure = secureVariables.has(variableName) ||
|
|
1174
|
-
(variableName && new RegExp(`(const|let)\\s+${escapedVarName}\\s*=\\s*useSecureSupabase\\s*\\(`, 'm').test(content));
|
|
1175
|
-
|
|
1176
|
-
// Check if variable is passed as function parameter with secure type annotation
|
|
1177
|
-
// Look for function signatures with type annotations indicating secure client
|
|
1178
|
-
const isParameterSecure = variableName && (
|
|
1179
|
-
// Check in beforeMatch (200 chars before) for parameter type annotations
|
|
1180
|
-
new RegExp(`\\b${escapedVarName}\\s*:\\s*.*(SecureSupabaseClient|useSecureSupabase|ReturnType.*useSecureSupabase)`, 'm').test(beforeMatch) ||
|
|
1181
|
-
// Check full content for function signatures with secure type annotations
|
|
1182
|
-
new RegExp(`(function|=>|async\\s+function|const\\s+\\w+\\s*=\\s*(async\\s+)?(function|=>))[^(]*\\([^)]*\\b${escapedVarName}\\s*:\\s*.*(SecureSupabaseClient|useSecureSupabase|ReturnType.*useSecureSupabase)`, 'm').test(content) ||
|
|
1183
|
-
// Check for ReturnType<typeof import pattern (common in TypeScript)
|
|
1184
|
-
new RegExp(`\\b${escapedVarName}\\s*:\\s*.*ReturnType.*useSecureSupabase`, 'm').test(content) ||
|
|
1185
|
-
// Check for NonNullable<ReturnType<typeof useSecureSupabase>> pattern
|
|
1186
|
-
new RegExp(`\\b${escapedVarName}\\s*:\\s*.*NonNullable.*ReturnType.*useSecureSupabase`, 'm').test(content)
|
|
1187
|
-
);
|
|
1188
|
-
|
|
1189
|
-
// Check for common secure variable names (heuristic for secure usage)
|
|
1190
|
-
const isSecureVariableName = variableName && /^(secureSupabase|secureClient|secureDb|secure)$/i.test(variableName);
|
|
1191
|
-
|
|
1192
|
-
// Check for comments indicating secure usage (pace-core-compliant, useSecureSupabase, etc.)
|
|
1193
|
-
// Also check for @pace-core-compliant annotation which can suppress false positives
|
|
1194
|
-
const hasSecureComment = variableName && (
|
|
1195
|
-
// Check in beforeMatch for comments
|
|
1196
|
-
new RegExp(`(pace-core-compliant|useSecureSupabase|secureSupabase|@pace-core-compliant)`, 'i').test(beforeMatch) ||
|
|
1197
|
-
// Check for comments on previous lines (up to 10 lines back for better coverage)
|
|
1198
|
-
(() => {
|
|
1199
|
-
const lines = content.substring(0, matchIndex).split('\n');
|
|
1200
|
-
const recentLines = lines.slice(Math.max(0, lines.length - 10)).join('\n');
|
|
1201
|
-
return new RegExp(`(pace-core-compliant|useSecureSupabase|secureSupabase|@pace-core-compliant)`, 'i').test(recentLines);
|
|
1202
|
-
})()
|
|
1203
|
-
);
|
|
1204
|
-
|
|
1205
|
-
// Check for @pace-core-compliant annotation on the same line or previous lines (suppression mechanism)
|
|
1206
|
-
const hasComplianceAnnotation = (() => {
|
|
1207
|
-
const lines = content.substring(0, matchIndex).split('\n');
|
|
1208
|
-
const currentLineIdx = lines.length - 1;
|
|
1209
|
-
// Check current line and up to 3 previous lines for @pace-core-compliant
|
|
1210
|
-
const checkLines = lines.slice(Math.max(0, currentLineIdx - 3), currentLineIdx + 1);
|
|
1211
|
-
return new RegExp(`@pace-core-compliant`, 'i').test(checkLines.join('\n'));
|
|
1212
|
-
})();
|
|
1213
|
-
|
|
1214
|
-
// Combine all checks
|
|
1215
|
-
// If @pace-core-compliant annotation is present, trust it (suppression mechanism)
|
|
1216
|
-
const isUsingSecureVariable = hasComplianceAnnotation || isDeclaredSecure || isParameterSecure || (isSecureVariableName && hasSecureComment);
|
|
1217
|
-
|
|
1218
|
-
// Determine severity based on context
|
|
1219
|
-
let severity = 'error';
|
|
1220
|
-
let reason = '';
|
|
1221
|
-
|
|
1222
|
-
if (isUserDataOrAudit) {
|
|
1223
|
-
if (isUsingSecureVariable) {
|
|
1224
|
-
// Correct usage - skip it
|
|
1225
|
-
continue;
|
|
1226
|
-
} else {
|
|
1227
|
-
severity = 'error';
|
|
1228
|
-
reason = `Direct query to RBAC table '${tableName}' detected. This table can be queried, but you MUST use useSecureSupabase to ensure organisation context is enforced and RLS policies are respected.`;
|
|
1229
|
-
}
|
|
1230
|
-
} else if (isConfigTable) {
|
|
1231
|
-
if (isUsingSecureVariable) {
|
|
1232
|
-
// Using secure methods for config tables - acceptable (admin operations or hooks using secure methods)
|
|
1233
|
-
// Don't flag if using secure methods, regardless of admin context
|
|
1234
|
-
continue;
|
|
1235
|
-
} else if (isAdminContext) {
|
|
1236
|
-
// Admin operations without secure methods - warning
|
|
1237
|
-
severity = 'warning';
|
|
1238
|
-
reason = `Admin operation on configuration table '${tableName}' detected. Ensure you're using useSecureSupabase for security.`;
|
|
1239
|
-
} else {
|
|
1240
|
-
// Not admin context and not using secure methods - error
|
|
1241
|
-
severity = 'error';
|
|
1242
|
-
reason = `Direct query to RBAC configuration table '${tableName}' detected. These are system configuration tables. For admin operations, use useSecureSupabase. For application use, use pace-core RBAC APIs.`;
|
|
1243
|
-
}
|
|
1244
|
-
} else {
|
|
1245
|
-
// Role/permission tables - always error, even in admin context
|
|
1246
|
-
// These should use pace-core APIs or RPC functions, not direct queries
|
|
1247
|
-
severity = 'error';
|
|
1248
|
-
reason = `Direct query to RBAC role/permission table '${tableName}' detected. Use pace-core RBAC APIs (useRBAC, usePermissions) or RPC functions (rbac_role_grant, rbac_role_revoke, rbac_roles_list) instead of direct queries, even in admin contexts.`;
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1251
|
-
violations.customAuthCode.push({
|
|
1252
|
-
name: `Direct RBAC table query (${tableName})`,
|
|
1253
|
-
type: 'rbac query',
|
|
1254
|
-
file: relativePath,
|
|
1255
|
-
line: lineNumber,
|
|
1256
|
-
reason: reason,
|
|
1257
|
-
replacement: recommendation,
|
|
1258
|
-
severity: severity
|
|
1259
|
-
});
|
|
1260
|
-
}
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
// Check for secureQuery/secureUpdate/secureInsert/secureDelete calls
|
|
1264
|
-
while ((secureMatch = secureRegex.exec(content)) !== null) {
|
|
1265
|
-
if (secureMatch.index === secureRegex.lastIndex) {
|
|
1266
|
-
secureRegex.lastIndex++;
|
|
1267
|
-
continue;
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
|
-
const secureMatchIndex = secureMatch.index;
|
|
1271
|
-
const secureMethod = secureMatch[1]; // secureQuery, secureUpdate, etc.
|
|
1272
|
-
|
|
1273
|
-
// Skip if in a line comment
|
|
1274
|
-
const secureLineStart = content.lastIndexOf('\n', secureMatchIndex) + 1;
|
|
1275
|
-
const secureLineUpToMatch = content.substring(secureLineStart, secureMatchIndex);
|
|
1276
|
-
const isSecureInLineComment = /\/\/[^\n]*$/.test(secureLineUpToMatch);
|
|
1277
|
-
|
|
1278
|
-
if (!isSecureInLineComment) {
|
|
1279
|
-
const secureLineNumber = content.substring(0, secureMatchIndex).split('\n').length;
|
|
1280
|
-
const isUserDataOrAudit = type === 'user_data' || type === 'audit';
|
|
1281
|
-
const isConfigTable = type === 'config';
|
|
1282
|
-
const isRoleTable = type === 'role';
|
|
1283
|
-
|
|
1284
|
-
// Determine severity - role tables should always be errors (should use pace-core APIs)
|
|
1285
|
-
// Config tables in admin context with secure methods are acceptable
|
|
1286
|
-
// User data/audit tables with secure methods are acceptable
|
|
1287
|
-
let severity = 'error';
|
|
1288
|
-
let reason = '';
|
|
1289
|
-
|
|
1290
|
-
if (isRoleTable) {
|
|
1291
|
-
// Role tables should use pace-core APIs, not secureQuery
|
|
1292
|
-
severity = 'error';
|
|
1293
|
-
reason = `Direct query to RBAC role table '${tableName}' using ${secureMethod} detected. Role tables should use pace-core RBAC APIs (useRBAC, usePermissions) or RPC functions (rbac_role_grant, rbac_role_revoke, rbac_roles_list) instead of direct queries.`;
|
|
1294
|
-
} else if (isUserDataOrAudit) {
|
|
1295
|
-
// User data/audit tables with secure methods are acceptable
|
|
1296
|
-
continue; // Skip - this is correct usage
|
|
1297
|
-
} else if (isConfigTable) {
|
|
1298
|
-
// Config tables using secure methods - acceptable for admin operations
|
|
1299
|
-
// If using secureQuery/secureUpdate/etc., it's already using secure methods
|
|
1300
|
-
if (isAdminContext) {
|
|
1301
|
-
// Config tables in admin context - acceptable
|
|
1302
|
-
continue; // Skip - this is correct usage for admin operations
|
|
1303
|
-
} else {
|
|
1304
|
-
severity = 'error';
|
|
1305
|
-
reason = `Direct query to RBAC configuration table '${tableName}' using ${secureMethod} detected. These are system configuration tables. For admin operations, use useSecureSupabase. For application use, use pace-core RBAC APIs.`;
|
|
1306
|
-
}
|
|
1307
|
-
} else {
|
|
1308
|
-
severity = 'error';
|
|
1309
|
-
reason = `Direct query to RBAC table '${tableName}' using ${secureMethod} detected. Use pace-core RBAC APIs instead.`;
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
|
-
violations.customAuthCode.push({
|
|
1313
|
-
name: `Direct RBAC table query via ${secureMethod} (${tableName})`,
|
|
1314
|
-
type: 'rbac query',
|
|
1315
|
-
file: relativePath,
|
|
1316
|
-
line: secureLineNumber,
|
|
1317
|
-
reason: reason,
|
|
1318
|
-
replacement: recommendation,
|
|
1319
|
-
severity: severity
|
|
1320
|
-
});
|
|
1321
|
-
}
|
|
1322
|
-
}
|
|
1323
|
-
});
|
|
1324
|
-
|
|
1325
|
-
// Also check generic pattern as fallback (for patterns that might not match the specific table patterns)
|
|
1326
|
-
rbacTablePatterns.forEach(pattern => {
|
|
1327
|
-
let match;
|
|
1328
|
-
const regex = new RegExp(pattern.source, pattern.flags);
|
|
1329
|
-
regex.lastIndex = 0;
|
|
1330
|
-
|
|
1331
|
-
while ((match = regex.exec(content)) !== null) {
|
|
1332
|
-
if (match.index === regex.lastIndex) {
|
|
1333
|
-
regex.lastIndex++;
|
|
1334
|
-
continue;
|
|
1335
|
-
}
|
|
1336
|
-
|
|
1337
|
-
const matchIndex = match.index;
|
|
1338
|
-
const matchText = match[0];
|
|
1339
|
-
|
|
1340
|
-
// Check if this is an edge function (supabase/functions directory) - check early to skip
|
|
1341
|
-
// Handle both forward and backslash paths (Windows vs Unix)
|
|
1342
|
-
const normalizedPath = relativePath.replace(/\\/g, '/');
|
|
1343
|
-
const isEdgeFunction = normalizedPath.includes('supabase/functions/');
|
|
1344
|
-
|
|
1345
|
-
// Skip edge functions - they use service role client which is correct for server-side operations
|
|
1346
|
-
if (isEdgeFunction) {
|
|
1347
|
-
continue; // Edge functions use service role client - correct pattern
|
|
1348
|
-
}
|
|
1349
|
-
|
|
1350
|
-
// Extract table name
|
|
1351
|
-
const afterMatch = content.substring(matchIndex, Math.min(content.length, matchIndex + 100));
|
|
1352
|
-
const tableMatch = afterMatch.match(/['"]rbac_([^'"]+)['"]/);
|
|
1353
|
-
const tableName = tableMatch ? `rbac_${tableMatch[1]}` : 'rbac_*';
|
|
1354
|
-
|
|
1355
|
-
// Find the table config to check if it's user_data or audit
|
|
1356
|
-
const tableConfig = rbacTables.find(t => t.name === tableName);
|
|
1357
|
-
const isUserDataOrAudit = tableConfig && (tableConfig.type === 'user_data' || tableConfig.type === 'audit');
|
|
1358
|
-
const isConfigTable = tableConfig && tableConfig.type === 'config';
|
|
1359
|
-
|
|
1360
|
-
// Extract variable name if pattern matches variable.from() (handle newlines)
|
|
1361
|
-
let variableName = null;
|
|
1362
|
-
// Use wider context to find function signatures (up to 500 chars before)
|
|
1363
|
-
const beforeMatch = content.substring(Math.max(0, matchIndex - 500), matchIndex);
|
|
1364
|
-
// Find the last word/identifier before .from (same logic as main pattern)
|
|
1365
|
-
const parts = beforeMatch.split('.from');
|
|
1366
|
-
if (parts.length > 0) {
|
|
1367
|
-
const beforeFrom = parts[parts.length - 1].trim();
|
|
1368
|
-
const words = beforeFrom.match(/\b\w+\b/g);
|
|
1369
|
-
if (words && words.length > 0) {
|
|
1370
|
-
variableName = words[words.length - 1];
|
|
1371
|
-
}
|
|
1372
|
-
}
|
|
1373
|
-
|
|
1374
|
-
// Check if using secure variable (check both set and direct pattern match)
|
|
1375
|
-
// Escape special regex characters in variable name and use multiline flag to handle newlines
|
|
1376
|
-
const escapedVarName = variableName ? variableName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') : '';
|
|
1377
|
-
// Check if variable is declared with useSecureSupabase
|
|
1378
|
-
const isDeclaredSecure = (variableName && secureVariables.has(variableName)) ||
|
|
1379
|
-
(variableName && new RegExp(`(const|let)\\s+${escapedVarName}\\s*=\\s*useSecureSupabase\\s*\\(`, 'm').test(content));
|
|
1380
|
-
// Check if variable is passed as parameter with useSecureSupabase type annotation
|
|
1381
|
-
// Look for the parameter in function signatures (check both beforeMatch and full content)
|
|
1382
|
-
const isParameterSecure = variableName && (
|
|
1383
|
-
// Check in beforeMatch (500 chars before)
|
|
1384
|
-
new RegExp(`\\b${escapedVarName}\\s*:\\s*.*useSecureSupabase`, 'm').test(beforeMatch) ||
|
|
1385
|
-
new RegExp(`\\b${escapedVarName}\\s*:\\s*.*ReturnType`, 'm').test(beforeMatch) ||
|
|
1386
|
-
new RegExp(`\\b${escapedVarName}\\s*:\\s*.*secureSupabase`, 'i').test(beforeMatch) ||
|
|
1387
|
-
// Also check the full function signature area (wider context in full content)
|
|
1388
|
-
new RegExp(`(function|=>|async\\s+function)[^(]*\\([^)]*\\b${escapedVarName}\\s*:\\s*.*(useSecureSupabase|ReturnType|secureSupabase)`, 'm').test(content) ||
|
|
1389
|
-
// Check for ReturnType<typeof import pattern (common in TypeScript)
|
|
1390
|
-
new RegExp(`\\b${escapedVarName}\\s*:\\s*.*ReturnType.*useSecureSupabase`, 'm').test(content)
|
|
1391
|
-
);
|
|
1392
|
-
// Check for comments indicating secureSupabase usage
|
|
1393
|
-
const hasSecureComment = variableName && (
|
|
1394
|
-
new RegExp(`secureSupabase|useSecureSupabase`, 'i').test(beforeMatch) ||
|
|
1395
|
-
new RegExp(`COMPLIANCE.*secureSupabase|pace-core.*secureSupabase`, 'i').test(beforeMatch)
|
|
1396
|
-
);
|
|
1397
|
-
const isUsingSecureVariable = isDeclaredSecure || isParameterSecure || hasSecureComment;
|
|
1398
|
-
|
|
1399
|
-
// Skip if we already reported this specific table
|
|
1400
|
-
const alreadyReported = violations.customAuthCode.some(v =>
|
|
1401
|
-
v.file === relativePath &&
|
|
1402
|
-
v.name && v.name.includes(tableName) &&
|
|
1403
|
-
Math.abs(v.line - content.substring(0, matchIndex).split('\n').length) <= 2
|
|
1404
|
-
);
|
|
1405
|
-
|
|
1406
|
-
// Skip if in a line comment
|
|
1407
|
-
const lineStart = content.lastIndexOf('\n', matchIndex) + 1;
|
|
1408
|
-
const lineUpToMatch = content.substring(lineStart, matchIndex);
|
|
1409
|
-
const isInLineComment = /\/\/[^\n]*$/.test(lineUpToMatch);
|
|
1410
|
-
|
|
1411
|
-
// Skip if using secure variable for user_data/audit tables (correct usage)
|
|
1412
|
-
// isUsingSecureVariable is already declared above
|
|
1413
|
-
if (isUsingSecureVariable && isUserDataOrAudit) {
|
|
1414
|
-
continue; // This is correct usage, don't flag it
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
// Determine severity for config tables in admin context
|
|
1418
|
-
let severity = 'error';
|
|
1419
|
-
let reason = '';
|
|
1420
|
-
|
|
1421
|
-
if (isConfigTable && isAdminContext && (isUsingSecureVariable || hasSecureMethods)) {
|
|
1422
|
-
// Admin operations with secure methods - acceptable, skip
|
|
1423
|
-
continue;
|
|
1424
|
-
} else if (isConfigTable && isAdminContext && !isUsingSecureVariable && !hasSecureMethods) {
|
|
1425
|
-
severity = 'warning';
|
|
1426
|
-
reason = `Admin operation on configuration table '${tableName}' detected. Ensure you're using useSecureSupabase for security.`;
|
|
1427
|
-
} else if (isConfigTable && !isAdminContext) {
|
|
1428
|
-
severity = 'error';
|
|
1429
|
-
reason = `Direct query to RBAC configuration table '${tableName}' detected. These are system configuration tables. For admin operations, use useSecureSupabase. For application use, use pace-core RBAC APIs.`;
|
|
1430
|
-
} else if (isUserDataOrAudit) {
|
|
1431
|
-
if (isUsingSecureVariable || hasSecureMethods) {
|
|
1432
|
-
// User data/audit tables with secure methods - acceptable, skip
|
|
1433
|
-
continue;
|
|
1434
|
-
}
|
|
1435
|
-
severity = 'error';
|
|
1436
|
-
reason = `Direct query to RBAC table '${tableName}' detected. This table can be queried, but you MUST use useSecureSupabase to ensure organisation context is enforced and RLS policies are respected.`;
|
|
1437
|
-
} else {
|
|
1438
|
-
severity = 'error';
|
|
1439
|
-
reason = `Direct query to RBAC table '${tableName}' detected. Use pace-core RBAC hooks, RPC functions, or useSecureSupabase instead.`;
|
|
1440
|
-
}
|
|
1441
|
-
|
|
1442
|
-
if (!alreadyReported && !isInLineComment) {
|
|
1443
|
-
violations.customAuthCode.push({
|
|
1444
|
-
name: `Direct RBAC table query (${tableName})`,
|
|
1445
|
-
type: 'rbac query',
|
|
1446
|
-
file: relativePath,
|
|
1447
|
-
line: content.substring(0, matchIndex).split('\n').length,
|
|
1448
|
-
reason: reason,
|
|
1449
|
-
replacement: tableConfig ? tableConfig.recommendation : 'pace-core RBAC APIs (useRBAC, usePermissions, RPC functions)',
|
|
1450
|
-
severity: severity
|
|
1451
|
-
});
|
|
1452
|
-
}
|
|
1453
|
-
}
|
|
1454
|
-
});
|
|
1455
|
-
|
|
1456
|
-
// Check for direct permission table CRUD operations
|
|
1457
|
-
// This includes both .from().insert/update/delete patterns AND secureUpdate/secureInsert/secureDelete calls
|
|
1458
|
-
const permissionCrudPatterns = [
|
|
1459
|
-
// .from() patterns
|
|
1460
|
-
{
|
|
1461
|
-
pattern: /\.from\s*\(\s*['"]rbac_page_permissions['"]\s*\)\s*\.(insert|update|delete|upsert)\s*\(/g,
|
|
1462
|
-
table: 'rbac_page_permissions',
|
|
1463
|
-
operation: 'CRUD',
|
|
1464
|
-
isConfig: true,
|
|
1465
|
-
method: 'from'
|
|
1466
|
-
},
|
|
1467
|
-
{
|
|
1468
|
-
pattern: /\.from\s*\(\s*['"]rbac_app_pages['"]\s*\)\s*\.(insert|update|delete|upsert)\s*\(/g,
|
|
1469
|
-
table: 'rbac_app_pages',
|
|
1470
|
-
operation: 'CRUD',
|
|
1471
|
-
isConfig: true,
|
|
1472
|
-
method: 'from'
|
|
1473
|
-
},
|
|
1474
|
-
{
|
|
1475
|
-
pattern: /\.from\s*\(\s*['"]rbac_apps['"]\s*\)\s*\.(insert|update|delete|upsert)\s*\(/g,
|
|
1476
|
-
table: 'rbac_apps',
|
|
1477
|
-
operation: 'CRUD',
|
|
1478
|
-
isConfig: true,
|
|
1479
|
-
method: 'from'
|
|
1480
|
-
},
|
|
1481
|
-
{
|
|
1482
|
-
pattern: /\.from\s*\(\s*['"]rbac_organisation_roles['"]\s*\)\s*\.(insert|update|delete|upsert)\s*\(/g,
|
|
1483
|
-
table: 'rbac_organisation_roles',
|
|
1484
|
-
operation: 'CRUD',
|
|
1485
|
-
isRole: true,
|
|
1486
|
-
roleType: 'organisation',
|
|
1487
|
-
method: 'from'
|
|
1488
|
-
},
|
|
1489
|
-
{
|
|
1490
|
-
pattern: /\.from\s*\(\s*['"]rbac_event_app_roles['"]\s*\)\s*\.(insert|update|delete|upsert)\s*\(/g,
|
|
1491
|
-
table: 'rbac_event_app_roles',
|
|
1492
|
-
operation: 'CRUD',
|
|
1493
|
-
isRole: true,
|
|
1494
|
-
roleType: 'event_app',
|
|
1495
|
-
method: 'from'
|
|
1496
|
-
},
|
|
1497
|
-
{
|
|
1498
|
-
pattern: /\.from\s*\(\s*['"]rbac_global_roles['"]\s*\)\s*\.(insert|update|delete|upsert)\s*\(/g,
|
|
1499
|
-
table: 'rbac_global_roles',
|
|
1500
|
-
operation: 'CRUD',
|
|
1501
|
-
isRole: true,
|
|
1502
|
-
roleType: 'global',
|
|
1503
|
-
method: 'from'
|
|
1504
|
-
},
|
|
1505
|
-
{
|
|
1506
|
-
pattern: /\.from\s*\(\s*['"]rbac_user_units['"]\s*\)\s*\.(insert|update|delete|upsert)\s*\(/g,
|
|
1507
|
-
table: 'rbac_user_units',
|
|
1508
|
-
operation: 'CRUD',
|
|
1509
|
-
isUserData: true,
|
|
1510
|
-
method: 'from'
|
|
1511
|
-
},
|
|
1512
|
-
// secureUpdate/secureInsert/secureDelete patterns
|
|
1513
|
-
{
|
|
1514
|
-
pattern: /secureUpdate\s*\(\s*['"]rbac_page_permissions['"]/g,
|
|
1515
|
-
table: 'rbac_page_permissions',
|
|
1516
|
-
operation: 'update',
|
|
1517
|
-
isConfig: true,
|
|
1518
|
-
method: 'secureUpdate'
|
|
1519
|
-
},
|
|
1520
|
-
{
|
|
1521
|
-
pattern: /secureInsert\s*\(\s*['"]rbac_page_permissions['"]/g,
|
|
1522
|
-
table: 'rbac_page_permissions',
|
|
1523
|
-
operation: 'insert',
|
|
1524
|
-
isConfig: true,
|
|
1525
|
-
method: 'secureInsert'
|
|
1526
|
-
},
|
|
1527
|
-
{
|
|
1528
|
-
pattern: /secureUpdate\s*\(\s*['"]rbac_app_pages['"]/g,
|
|
1529
|
-
table: 'rbac_app_pages',
|
|
1530
|
-
operation: 'update',
|
|
1531
|
-
isConfig: true,
|
|
1532
|
-
method: 'secureUpdate'
|
|
1533
|
-
},
|
|
1534
|
-
{
|
|
1535
|
-
pattern: /secureInsert\s*\(\s*['"]rbac_app_pages['"]/g,
|
|
1536
|
-
table: 'rbac_app_pages',
|
|
1537
|
-
operation: 'insert',
|
|
1538
|
-
isConfig: true,
|
|
1539
|
-
method: 'secureInsert'
|
|
1540
|
-
},
|
|
1541
|
-
{
|
|
1542
|
-
pattern: /secureUpdate\s*\(\s*['"]rbac_apps['"]/g,
|
|
1543
|
-
table: 'rbac_apps',
|
|
1544
|
-
operation: 'update',
|
|
1545
|
-
isConfig: true,
|
|
1546
|
-
method: 'secureUpdate'
|
|
1547
|
-
},
|
|
1548
|
-
{
|
|
1549
|
-
pattern: /secureInsert\s*\(\s*['"]rbac_apps['"]/g,
|
|
1550
|
-
table: 'rbac_apps',
|
|
1551
|
-
operation: 'insert',
|
|
1552
|
-
isConfig: true,
|
|
1553
|
-
method: 'secureInsert'
|
|
1554
|
-
},
|
|
1555
|
-
{
|
|
1556
|
-
pattern: /secureUpdate\s*\(\s*['"]rbac_organisation_roles['"]/g,
|
|
1557
|
-
table: 'rbac_organisation_roles',
|
|
1558
|
-
operation: 'update',
|
|
1559
|
-
isRole: true,
|
|
1560
|
-
roleType: 'organisation',
|
|
1561
|
-
method: 'secureUpdate'
|
|
1562
|
-
},
|
|
1563
|
-
{
|
|
1564
|
-
pattern: /secureInsert\s*\(\s*['"]rbac_organisation_roles['"]/g,
|
|
1565
|
-
table: 'rbac_organisation_roles',
|
|
1566
|
-
operation: 'insert',
|
|
1567
|
-
isRole: true,
|
|
1568
|
-
roleType: 'organisation',
|
|
1569
|
-
method: 'secureInsert'
|
|
1570
|
-
},
|
|
1571
|
-
{
|
|
1572
|
-
pattern: /secureUpdate\s*\(\s*['"]rbac_event_app_roles['"]/g,
|
|
1573
|
-
table: 'rbac_event_app_roles',
|
|
1574
|
-
operation: 'update',
|
|
1575
|
-
isRole: true,
|
|
1576
|
-
roleType: 'event_app',
|
|
1577
|
-
method: 'secureUpdate'
|
|
1578
|
-
},
|
|
1579
|
-
{
|
|
1580
|
-
pattern: /secureInsert\s*\(\s*['"]rbac_event_app_roles['"]/g,
|
|
1581
|
-
table: 'rbac_event_app_roles',
|
|
1582
|
-
operation: 'insert',
|
|
1583
|
-
isRole: true,
|
|
1584
|
-
roleType: 'event_app',
|
|
1585
|
-
method: 'secureInsert'
|
|
1586
|
-
},
|
|
1587
|
-
{
|
|
1588
|
-
pattern: /secureUpdate\s*\(\s*['"]rbac_global_roles['"]/g,
|
|
1589
|
-
table: 'rbac_global_roles',
|
|
1590
|
-
operation: 'update',
|
|
1591
|
-
isRole: true,
|
|
1592
|
-
roleType: 'global',
|
|
1593
|
-
method: 'secureUpdate'
|
|
1594
|
-
},
|
|
1595
|
-
{
|
|
1596
|
-
pattern: /secureInsert\s*\(\s*['"]rbac_global_roles['"]/g,
|
|
1597
|
-
table: 'rbac_global_roles',
|
|
1598
|
-
operation: 'insert',
|
|
1599
|
-
isRole: true,
|
|
1600
|
-
roleType: 'global',
|
|
1601
|
-
method: 'secureInsert'
|
|
1602
|
-
}
|
|
1603
|
-
];
|
|
1604
|
-
|
|
1605
|
-
permissionCrudPatterns.forEach(({ pattern, table, operation, isConfig, isRole, roleType, isUserData, method }) => {
|
|
1606
|
-
let match;
|
|
1607
|
-
const regex = new RegExp(pattern.source, pattern.flags);
|
|
1608
|
-
regex.lastIndex = 0;
|
|
1609
|
-
|
|
1610
|
-
while ((match = regex.exec(content)) !== null) {
|
|
1611
|
-
if (match.index === regex.lastIndex) {
|
|
1612
|
-
regex.lastIndex++;
|
|
1613
|
-
continue;
|
|
1614
|
-
}
|
|
1615
|
-
|
|
1616
|
-
const matchIndex = match.index;
|
|
1617
|
-
const matchText = match[0];
|
|
1618
|
-
// For .from() patterns, match[1] is the CRUD method; for secure* patterns, operation is already set
|
|
1619
|
-
const crudMethod = method === 'from' ? match[1] : operation;
|
|
1620
|
-
|
|
1621
|
-
// Extract variable name for .from() patterns (for secure* patterns, we skip variable detection)
|
|
1622
|
-
let variableName = null;
|
|
1623
|
-
if (method === 'from') {
|
|
1624
|
-
// Look backwards to find the variable name before .from()
|
|
1625
|
-
const beforeMatch = content.substring(Math.max(0, matchIndex - 200), matchIndex);
|
|
1626
|
-
const parts = beforeMatch.split('.from');
|
|
1627
|
-
if (parts.length > 0) {
|
|
1628
|
-
const beforeFrom = parts[parts.length - 1].trim();
|
|
1629
|
-
const words = beforeFrom.match(/\b\w+\b/g);
|
|
1630
|
-
if (words && words.length > 0) {
|
|
1631
|
-
variableName = words[words.length - 1];
|
|
1632
|
-
}
|
|
1633
|
-
}
|
|
1634
|
-
}
|
|
1635
|
-
|
|
1636
|
-
// Check if using secure variable (only for .from() patterns)
|
|
1637
|
-
let isUsingSecureVariable = false;
|
|
1638
|
-
if (method === 'from' && variableName) {
|
|
1639
|
-
const escapedVarName = variableName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1640
|
-
|
|
1641
|
-
// Check if variable is declared locally
|
|
1642
|
-
const isDeclaredSecure = secureVariables.has(variableName) ||
|
|
1643
|
-
new RegExp(`(const|let)\\s+${escapedVarName}\\s*=\\s*useSecureSupabase\\s*\\(`, 'm').test(content);
|
|
1644
|
-
|
|
1645
|
-
// Check if variable is passed as function parameter with secure type annotation
|
|
1646
|
-
const beforeMatch = content.substring(Math.max(0, matchIndex - 200), matchIndex);
|
|
1647
|
-
const isParameterSecure =
|
|
1648
|
-
new RegExp(`\\b${escapedVarName}\\s*:\\s*.*(SecureSupabaseClient|useSecureSupabase|ReturnType.*useSecureSupabase)`, 'm').test(beforeMatch) ||
|
|
1649
|
-
new RegExp(`(function|=>|async\\s+function|const\\s+\\w+\\s*=\\s*(async\\s+)?(function|=>))[^(]*\\([^)]*\\b${escapedVarName}\\s*:\\s*.*(SecureSupabaseClient|useSecureSupabase|ReturnType.*useSecureSupabase)`, 'm').test(content) ||
|
|
1650
|
-
new RegExp(`\\b${escapedVarName}\\s*:\\s*.*ReturnType.*useSecureSupabase`, 'm').test(content) ||
|
|
1651
|
-
new RegExp(`\\b${escapedVarName}\\s*:\\s*.*NonNullable.*ReturnType.*useSecureSupabase`, 'm').test(content);
|
|
1652
|
-
|
|
1653
|
-
// Check for common secure variable names
|
|
1654
|
-
const isSecureVariableName = /^(secureSupabase|secureClient|secureDb|secure)$/i.test(variableName);
|
|
1655
|
-
|
|
1656
|
-
// Check for comments indicating secure usage
|
|
1657
|
-
const hasSecureComment =
|
|
1658
|
-
new RegExp(`(pace-core-compliant|useSecureSupabase|secureSupabase|@pace-core-compliant)`, 'i').test(beforeMatch) ||
|
|
1659
|
-
(() => {
|
|
1660
|
-
const lines = content.substring(0, matchIndex).split('\n');
|
|
1661
|
-
const recentLines = lines.slice(Math.max(0, lines.length - 10)).join('\n');
|
|
1662
|
-
return new RegExp(`(pace-core-compliant|useSecureSupabase|secureSupabase|@pace-core-compliant)`, 'i').test(recentLines);
|
|
1663
|
-
})();
|
|
1664
|
-
|
|
1665
|
-
// Check for @pace-core-compliant annotation (suppression mechanism)
|
|
1666
|
-
const hasComplianceAnnotation = (() => {
|
|
1667
|
-
const lines = content.substring(0, matchIndex).split('\n');
|
|
1668
|
-
const currentLineIdx = lines.length - 1;
|
|
1669
|
-
const checkLines = lines.slice(Math.max(0, currentLineIdx - 3), currentLineIdx + 1);
|
|
1670
|
-
return new RegExp(`@pace-core-compliant`, 'i').test(checkLines.join('\n'));
|
|
1671
|
-
})();
|
|
1672
|
-
|
|
1673
|
-
isUsingSecureVariable = hasComplianceAnnotation || isDeclaredSecure || isParameterSecure || (isSecureVariableName && hasSecureComment);
|
|
1674
|
-
}
|
|
1675
|
-
|
|
1676
|
-
// Skip if in a line comment
|
|
1677
|
-
const lineStart = content.lastIndexOf('\n', matchIndex) + 1;
|
|
1678
|
-
const lineUpToMatch = content.substring(lineStart, matchIndex);
|
|
1679
|
-
const isInLineComment = /\/\/[^\n]*$/.test(lineUpToMatch);
|
|
1680
|
-
|
|
1681
|
-
if (!isInLineComment) {
|
|
1682
|
-
let reason = '';
|
|
1683
|
-
let replacement = '';
|
|
1684
|
-
let severity = 'error';
|
|
1685
|
-
let example = '';
|
|
1686
|
-
|
|
1687
|
-
// If using secure variable for user_data tables, skip (correct usage)
|
|
1688
|
-
if (isUserData && isUsingSecureVariable) {
|
|
1689
|
-
continue;
|
|
1690
|
-
}
|
|
1691
|
-
|
|
1692
|
-
// If using secure variable for config tables, skip (correct usage)
|
|
1693
|
-
if (isConfig && isUsingSecureVariable) {
|
|
1694
|
-
continue;
|
|
1695
|
-
}
|
|
1696
|
-
|
|
1697
|
-
if (isRole) {
|
|
1698
|
-
// Role mutations should use RPC functions
|
|
1699
|
-
const isGrant = crudMethod === 'insert' || crudMethod === 'upsert';
|
|
1700
|
-
const isRevoke = crudMethod === 'delete' || crudMethod === 'update';
|
|
1701
|
-
|
|
1702
|
-
if (isGrant) {
|
|
1703
|
-
reason = `Direct ${crudMethod} operation on '${table}' detected. Use rbac_role_grant RPC function instead.`;
|
|
1704
|
-
replacement = `Use rbac_role_grant RPC function with p_role_type='${roleType}'`;
|
|
1705
|
-
example = `const { data, error } = await supabase.rpc('rbac_role_grant', {\n p_user_id: userId,\n p_role_type: '${roleType}',\n p_role_name: 'role_name',\n p_context_id: contextId, // org_id for organisation, 'event_id:app_id' for event_app\n p_granted_by: currentUserId\n});`;
|
|
1706
|
-
} else if (isRevoke) {
|
|
1707
|
-
reason = `Direct ${crudMethod} operation on '${table}' detected. Use rbac_role_revoke RPC function instead.`;
|
|
1708
|
-
replacement = `Use rbac_role_revoke RPC function with p_role_type='${roleType}'`;
|
|
1709
|
-
example = `const { data, error } = await supabase.rpc('rbac_role_revoke', {\n p_user_id: userId,\n p_role_type: '${roleType}',\n p_role_name: 'role_name',\n p_context_id: contextId, // org_id for organisation, 'event_id:app_id' for event_app\n p_revoked_by: currentUserId\n});`;
|
|
1710
|
-
} else {
|
|
1711
|
-
reason = `Direct ${crudMethod} operation on '${table}' detected. Use rbac_role_grant or rbac_role_revoke RPC functions instead.`;
|
|
1712
|
-
replacement = `Use rbac_role_grant/rbac_role_revoke RPC functions with p_role_type='${roleType}'`;
|
|
1713
|
-
}
|
|
1714
|
-
} else if (isConfig) {
|
|
1715
|
-
// Config table mutations - check if using secure methods
|
|
1716
|
-
if (method && method.startsWith('secure')) {
|
|
1717
|
-
// Using secureInsert/secureUpdate/secureDelete - this is correct, don't flag
|
|
1718
|
-
continue;
|
|
1719
|
-
} else if (isAdminContext) {
|
|
1720
|
-
// Admin context - check if using secure methods
|
|
1721
|
-
// If the operation uses secureQuery/secureUpdate/etc, it's already handled above
|
|
1722
|
-
// This case is for direct .from() calls in admin context
|
|
1723
|
-
severity = 'warning';
|
|
1724
|
-
reason = `Admin operation (${crudMethod}) on configuration table '${table}' detected. Ensure you're using useSecureSupabase for security.`;
|
|
1725
|
-
replacement = 'Use useSecureSupabase from pace-core for admin operations on configuration tables';
|
|
1726
|
-
} else {
|
|
1727
|
-
reason = `Direct ${crudMethod} operation on configuration table '${table}' detected. These are system configuration tables. For admin operations, use useSecureSupabase.`;
|
|
1728
|
-
replacement = 'Use useSecureSupabase from pace-core for admin operations';
|
|
1729
|
-
}
|
|
1730
|
-
} else if (isUserData) {
|
|
1731
|
-
// User data tables - should use secure methods
|
|
1732
|
-
if (isUsingSecureVariable) {
|
|
1733
|
-
// Already handled above - skip
|
|
1734
|
-
continue;
|
|
1735
|
-
}
|
|
1736
|
-
reason = `Direct ${crudMethod} operation on '${table}' detected. Use useSecureSupabase to ensure organisation context is enforced.`;
|
|
1737
|
-
replacement = 'Use useSecureSupabase from pace-core';
|
|
1738
|
-
} else {
|
|
1739
|
-
reason = `Direct ${crudMethod} operation on '${table}' detected. Use pace-core permission management APIs or documented RPC functions instead.`;
|
|
1740
|
-
replacement = 'pace-core permission management APIs or RPC functions';
|
|
1741
|
-
}
|
|
1742
|
-
|
|
1743
|
-
violations.customAuthCode.push({
|
|
1744
|
-
name: `Direct ${operation} (${table})`,
|
|
1745
|
-
type: 'permission management',
|
|
1746
|
-
file: relativePath,
|
|
1747
|
-
line: content.substring(0, matchIndex).split('\n').length,
|
|
1748
|
-
reason: reason,
|
|
1749
|
-
replacement: replacement,
|
|
1750
|
-
severity: severity,
|
|
1751
|
-
example: example
|
|
1752
|
-
});
|
|
1753
|
-
}
|
|
1754
|
-
}
|
|
1755
|
-
});
|
|
1756
|
-
|
|
1757
|
-
// Check for custom role management hooks/components
|
|
1758
|
-
// Analyze hook operations to distinguish data fetching from permission management
|
|
1759
|
-
const customRoleManagementPatterns = [
|
|
1760
|
-
{ pattern: /export\s+(default\s+)?(function|const)\s+useRoleMutations\s*[=\(]/g, name: 'useRoleMutations', type: 'hook', replacement: 'pace-core RBAC APIs (rbac_role_grant, rbac_role_revoke RPC functions or useRoleManagement hook)' },
|
|
1761
|
-
{ pattern: /export\s+(default\s+)?(function|const)\s+useUserRoles\s*[=\(]/g, name: 'useUserRoles', type: 'hook', replacement: 'pace-core RBAC hooks (useRBAC, usePermissions) or rbac_roles_list RPC function' },
|
|
1762
|
-
{ pattern: /export\s+(default\s+)?(function|const)\s+useUserOrganisationRoles\s*[=\(]/g, name: 'useUserOrganisationRoles', type: 'hook', replacement: 'useOrganisations hook from pace-core or data_user_organisation_roles_get RPC function' },
|
|
1763
|
-
{ pattern: /export\s+(default\s+)?(function|const)\s+useRoleSupportingData\s*[=\(]/g, name: 'useRoleSupportingData', type: 'hook', replacement: 'pace-core RBAC APIs' },
|
|
1764
|
-
{ pattern: /export\s+(default\s+)?(function|const)\s+useAppAccess\s*[=\(]/g, name: 'useAppAccess', type: 'hook', replacement: 'pace-core RBAC APIs' },
|
|
1765
|
-
{ pattern: /export\s+(default\s+)?(function|const)\s+useEventForm\s*[=\(]/g, name: 'useEventForm', type: 'hook', replacement: 'pace-core RBAC APIs' },
|
|
1766
|
-
{ pattern: /export\s+(default\s+)?(function|const)\s+useUnifiedStats\s*[=\(]/g, name: 'useUnifiedStats', type: 'hook', replacement: 'pace-core RBAC APIs' }
|
|
1767
|
-
];
|
|
1768
|
-
|
|
1769
|
-
customRoleManagementPatterns.forEach(({ pattern, name, type, replacement }) => {
|
|
1770
|
-
if (pattern.test(content)) {
|
|
1771
|
-
// Analyze hook operations to determine if it's data fetching or permission management
|
|
1772
|
-
const hookStartMatch = content.match(pattern);
|
|
1773
|
-
if (!hookStartMatch) {
|
|
1774
|
-
return; // Exit early if no match
|
|
1775
|
-
}
|
|
1776
|
-
|
|
1777
|
-
const hookStartIndex = content.indexOf(hookStartMatch[0]);
|
|
1778
|
-
// Find the end of the hook (next export or end of file, or closing brace at same level)
|
|
1779
|
-
const afterHookStart = content.substring(hookStartIndex);
|
|
1780
|
-
const hookContent = afterHookStart.split(/\n\s*export\s+/)[0]; // Get content until next export
|
|
1781
|
-
|
|
1782
|
-
// Count operations
|
|
1783
|
-
const readOperations = (
|
|
1784
|
-
(hookContent.match(/secureQuery\s*\(/g) || []).length +
|
|
1785
|
-
(hookContent.match(/\.select\s*\(/g) || []).length +
|
|
1786
|
-
(hookContent.match(/\.rpc\s*\(\s*['"](get_|data_|rbac_roles_list|rbac_permissions_get|data_user_)/g) || []).length
|
|
1787
|
-
);
|
|
1788
|
-
|
|
1789
|
-
const writeOperations = (
|
|
1790
|
-
(hookContent.match(/secureUpdate\s*\(/g) || []).length +
|
|
1791
|
-
(hookContent.match(/secureInsert\s*\(/g) || []).length +
|
|
1792
|
-
(hookContent.match(/secureDelete\s*\(/g) || []).length +
|
|
1793
|
-
(hookContent.match(/\.(insert|update|delete|upsert)\s*\(/g) || []).length +
|
|
1794
|
-
(hookContent.match(/\.rpc\s*\(\s*['"](rbac_role_grant|rbac_role_revoke|app_)/g) || []).length
|
|
1795
|
-
);
|
|
1796
|
-
|
|
1797
|
-
const totalOperations = readOperations + writeOperations;
|
|
1798
|
-
const writeRatio = totalOperations > 0 ? writeOperations / totalOperations : 0;
|
|
1799
|
-
|
|
1800
|
-
// Check if hook primarily uses RPC functions for reads
|
|
1801
|
-
const usesReadOnlyRPCs = /\.rpc\s*\(\s*['"](get_|data_|rbac_roles_list|rbac_permissions_get|data_user_)/.test(hookContent);
|
|
1802
|
-
const onlyReadOperations = writeOperations === 0 && readOperations > 0;
|
|
1803
|
-
|
|
1804
|
-
// Check if hook name suggests data fetching (supporting data, stats, etc.)
|
|
1805
|
-
const isDataFetchingHook = /SupportingData|Stats|Form/.test(name);
|
|
1806
|
-
|
|
1807
|
-
// Calculate ratio of role/permission-related code to total code
|
|
1808
|
-
const rolePermissionPatterns = [
|
|
1809
|
-
/rbac_(organisation|event_app|global)_roles/g,
|
|
1810
|
-
/rbac_page_permissions/g,
|
|
1811
|
-
/rbac_role_grant|rbac_role_revoke/g,
|
|
1812
|
-
/grant.*role|revoke.*role/gi
|
|
1813
|
-
];
|
|
1814
|
-
const rolePermissionMatches = rolePermissionPatterns.reduce((count, pattern) => {
|
|
1815
|
-
const matches = hookContent.match(pattern);
|
|
1816
|
-
return count + (matches ? matches.length : 0);
|
|
1817
|
-
}, 0);
|
|
1818
|
-
|
|
1819
|
-
// Count total significant operations (not just RBAC, but all operations)
|
|
1820
|
-
const totalSignificantOps = (
|
|
1821
|
-
(hookContent.match(/(secureQuery|secureUpdate|secureInsert|secureDelete|\.from|\.rpc|\.select|\.insert|\.update|\.delete)\s*\(/g) || []).length
|
|
1822
|
-
);
|
|
1823
|
-
const rolePermissionRatio = totalSignificantOps > 0 ? rolePermissionMatches / totalSignificantOps : 0;
|
|
1824
|
-
|
|
1825
|
-
// Check for patterns indicating primary purpose
|
|
1826
|
-
const isEventManagement = /event.*(create|update|delete|form|manage)/gi.test(hookContent) && name.includes('Event');
|
|
1827
|
-
const isFormManagement = /useZodForm|register|handleSubmit|setValue|watch|reset/.test(hookContent);
|
|
1828
|
-
const isStatsAggregation = /count|sum|aggregate|stats|statistics/gi.test(hookContent) && name.includes('Stats');
|
|
1829
|
-
const isSupportingData = name.includes('SupportingData') || /dropdown|select|options|reference/gi.test(hookContent);
|
|
1830
|
-
|
|
1831
|
-
// Only flag if:
|
|
1832
|
-
// 1. It's useRoleMutations (always permission management)
|
|
1833
|
-
// 2. OR write operations > 20% of total operations AND operates on role/permission tables
|
|
1834
|
-
// 3. OR role/permission operations > 20% of total operations
|
|
1835
|
-
// Don't flag if:
|
|
1836
|
-
// - It's primarily data fetching (>80% reads, no mutations on role tables)
|
|
1837
|
-
// - It's event/form/stats management with <20% role/permission code
|
|
1838
|
-
// - It only uses read-only RPCs for data fetching
|
|
1839
|
-
if (name === 'useRoleMutations') {
|
|
1840
|
-
// Always flag useRoleMutations - it's explicitly for role mutations
|
|
1841
|
-
violations.customAuthCode.push({
|
|
1842
|
-
name,
|
|
1843
|
-
type,
|
|
1844
|
-
file: relativePath,
|
|
1845
|
-
line: getLineNumber(content, hookStartMatch[0]),
|
|
1846
|
-
reason: `Custom ${type} '${name}' detected that implements role/permission management logic. Even though it may use some pace-core utilities, it should use pace-core RBAC APIs instead of implementing custom logic.`,
|
|
1847
|
-
replacement,
|
|
1848
|
-
severity: 'error'
|
|
1849
|
-
});
|
|
1850
|
-
} else if (writeRatio > 0.2 && rolePermissionRatio > 0.1) {
|
|
1851
|
-
// Has significant write operations on role/permission tables
|
|
1852
|
-
violations.customAuthCode.push({
|
|
1853
|
-
name,
|
|
1854
|
-
type,
|
|
1855
|
-
file: relativePath,
|
|
1856
|
-
line: getLineNumber(content, hookStartMatch[0]),
|
|
1857
|
-
reason: `Custom ${type} '${name}' detected that implements role/permission management logic. Even though it may use some pace-core utilities, it should use pace-core RBAC APIs instead of implementing custom logic.`,
|
|
1858
|
-
replacement,
|
|
1859
|
-
severity: 'error'
|
|
1860
|
-
});
|
|
1861
|
-
} else if (rolePermissionRatio > 0.2 && !isEventManagement && !isFormManagement && !isStatsAggregation) {
|
|
1862
|
-
// High ratio of role/permission code and not primarily event/form/stats management
|
|
1863
|
-
violations.customAuthCode.push({
|
|
1864
|
-
name,
|
|
1865
|
-
type,
|
|
1866
|
-
file: relativePath,
|
|
1867
|
-
line: getLineNumber(content, hookStartMatch[0]),
|
|
1868
|
-
reason: `Custom ${type} '${name}' detected that implements role/permission management logic. Even though it may use some pace-core utilities, it should use pace-core RBAC APIs instead of implementing custom logic.`,
|
|
1869
|
-
replacement,
|
|
1870
|
-
severity: 'error'
|
|
1871
|
-
});
|
|
1872
|
-
} else if (onlyReadOperations && (usesReadOnlyRPCs || isDataFetchingHook || isSupportingData)) {
|
|
1873
|
-
// Data fetching hook using pace-core RPCs or secure queries - don't flag
|
|
1874
|
-
// These are legitimate data fetching hooks
|
|
1875
|
-
return; // Exit early to prevent flagging
|
|
1876
|
-
} else if (name === 'useUserOrganisationRoles') {
|
|
1877
|
-
// useUserOrganisationRoles only uses get_user_organisations RPC - don't flag
|
|
1878
|
-
// This is a pure data fetching hook that only uses RPC functions
|
|
1879
|
-
// Check if it only uses RPC functions and no direct table queries
|
|
1880
|
-
const hasDirectTableQueries = /\.from\s*\(\s*['"]rbac_/.test(hookContent);
|
|
1881
|
-
if (!hasDirectTableQueries && usesReadOnlyRPCs) {
|
|
1882
|
-
// Pure RPC-based data fetching - don't flag
|
|
1883
|
-
return; // Exit early to prevent flagging
|
|
1884
|
-
} else if (onlyReadOperations && usesReadOnlyRPCs) {
|
|
1885
|
-
// Read-only data fetching hook - don't flag
|
|
1886
|
-
return; // Exit early to prevent flagging
|
|
1887
|
-
}
|
|
1888
|
-
// If it has table queries, continue to check below
|
|
1889
|
-
} else if (name === 'useUserRoles') {
|
|
1890
|
-
// useUserRoles uses RPC functions and secure methods for data fetching
|
|
1891
|
-
// Check if it's primarily read-only and uses secure methods
|
|
1892
|
-
const usesSecureForApps = /secureSupabase|useSecureSupabase|secureQuery/.test(hookContent);
|
|
1893
|
-
if (usesReadOnlyRPCs && writeOperations === 0 && (usesSecureForApps || onlyReadOperations)) {
|
|
1894
|
-
// Using secure methods or read-only RPC-based data fetching - don't flag
|
|
1895
|
-
return; // Exit early to prevent flagging
|
|
1896
|
-
}
|
|
1897
|
-
// If it has write operations, continue to check below
|
|
1898
|
-
} else if (isEventManagement || isFormManagement || isStatsAggregation) {
|
|
1899
|
-
// Primary purpose is event/form/stats management - don't flag
|
|
1900
|
-
// Role cleanup during deletion is a side effect, not primary purpose
|
|
1901
|
-
}
|
|
1902
|
-
}
|
|
1903
|
-
});
|
|
1904
|
-
|
|
1905
|
-
// Check for custom permission management components
|
|
1906
|
-
// Distinguish between configuration management and permission management
|
|
1907
|
-
const customPermissionComponents = [
|
|
1908
|
-
{ name: 'UnifiedPermissionsManager', isPermissionManagement: true },
|
|
1909
|
-
{ name: 'PermissionsDataTable', isPermissionManagement: true },
|
|
1910
|
-
{ name: 'ApplicationPermissionsTable', isPermissionManagement: true },
|
|
1911
|
-
{ name: 'ApplicationPermissionsManager', isPermissionManagement: true },
|
|
1912
|
-
{ name: 'PermissionsManager', isPermissionManagement: true },
|
|
1913
|
-
// Configuration management components (manage rbac_apps, rbac_app_pages)
|
|
1914
|
-
{ name: 'ApplicationsDataTable', isPermissionManagement: false, managesConfig: true },
|
|
1915
|
-
{ name: 'PagesDataTable', isPermissionManagement: false, managesConfig: true }
|
|
1916
|
-
];
|
|
1917
|
-
|
|
1918
|
-
customPermissionComponents.forEach(({ name, isPermissionManagement, managesConfig }) => {
|
|
1919
|
-
const componentPattern = new RegExp(`export\\s+(default\\s+)?(function|const)\\s+${name}\\s*[=\\(]`, 'g');
|
|
1920
|
-
if (componentPattern.test(content)) {
|
|
1921
|
-
// Check which tables this component operates on
|
|
1922
|
-
const operatesOnPagePermissions = /rbac_page_permissions/.test(content);
|
|
1923
|
-
const operatesOnRoleTables = /rbac_(organisation|event_app|global)_roles/.test(content);
|
|
1924
|
-
const operatesOnConfigTables = /rbac_apps|rbac_app_pages/.test(content);
|
|
1925
|
-
const usesSecureMethods = /useSecureSupabase|secureQuery|secureUpdate|secureInsert|secureDelete/.test(content);
|
|
1926
|
-
|
|
1927
|
-
// Only flag as permission management if:
|
|
1928
|
-
// 1. It's explicitly a permission management component AND operates on permission/role tables
|
|
1929
|
-
// 2. OR it operates on permission/role tables (regardless of name)
|
|
1930
|
-
// Don't flag configuration management components that only manage config tables
|
|
1931
|
-
if (isPermissionManagement && (operatesOnPagePermissions || operatesOnRoleTables)) {
|
|
1932
|
-
violations.customAuthCode.push({
|
|
1933
|
-
name: `${name} component`,
|
|
1934
|
-
type: 'permission management component',
|
|
1935
|
-
file: relativePath,
|
|
1936
|
-
line: getLineNumber(content, content.match(componentPattern)[0]),
|
|
1937
|
-
reason: `Custom permission management component '${name}' detected. Even though it may use some pace-core utilities, it implements custom permission management logic that should use pace-core permission management APIs instead.`,
|
|
1938
|
-
replacement: 'pace-core permission management APIs or PagePermissionGuard',
|
|
1939
|
-
severity: 'error'
|
|
1940
|
-
});
|
|
1941
|
-
} else if (!isPermissionManagement && managesConfig && operatesOnConfigTables) {
|
|
1942
|
-
// Configuration management component - check if it only does cleanup on permission tables
|
|
1943
|
-
// If it uses secure methods and primarily manages config tables, don't flag
|
|
1944
|
-
// Permission table operations for cleanup (cascade deletes) are acceptable
|
|
1945
|
-
if (operatesOnPagePermissions && usesSecureMethods) {
|
|
1946
|
-
// Config management component that uses secure methods and does permission cleanup
|
|
1947
|
-
// This is acceptable - don't flag
|
|
1948
|
-
} else if (!usesSecureMethods) {
|
|
1949
|
-
violations.customAuthCode.push({
|
|
1950
|
-
name: `${name} component`,
|
|
1951
|
-
type: 'configuration management component',
|
|
1952
|
-
file: relativePath,
|
|
1953
|
-
line: getLineNumber(content, content.match(componentPattern)[0]),
|
|
1954
|
-
reason: `Configuration management component '${name}' detected. Ensure you're using useSecureSupabase for secure operations on configuration tables.`,
|
|
1955
|
-
replacement: 'Use useSecureSupabase from pace-core for admin operations on configuration tables',
|
|
1956
|
-
severity: 'warning'
|
|
1957
|
-
});
|
|
1958
|
-
}
|
|
1959
|
-
// If using secure methods, don't flag (acceptable for admin operations)
|
|
1960
|
-
} else if (operatesOnPagePermissions || operatesOnRoleTables) {
|
|
1961
|
-
// Component operates on permission/role tables - flag it UNLESS it's a config management component using secure methods
|
|
1962
|
-
if (!isPermissionManagement && managesConfig && usesSecureMethods) {
|
|
1963
|
-
// Config management component using secure methods - acceptable, don't flag
|
|
1964
|
-
} else {
|
|
1965
|
-
violations.customAuthCode.push({
|
|
1966
|
-
name: `${name} component`,
|
|
1967
|
-
type: 'permission management component',
|
|
1968
|
-
file: relativePath,
|
|
1969
|
-
line: getLineNumber(content, content.match(componentPattern)[0]),
|
|
1970
|
-
reason: `Component '${name}' detected that operates on permission or role tables. Use pace-core permission management APIs instead.`,
|
|
1971
|
-
replacement: 'pace-core permission management APIs or PagePermissionGuard',
|
|
1972
|
-
severity: 'error'
|
|
1973
|
-
});
|
|
1974
|
-
}
|
|
1975
|
-
}
|
|
1976
|
-
}
|
|
1977
|
-
});
|
|
1978
|
-
|
|
1979
|
-
// Check provider setup (only main.tsx/main.ts, not App.tsx since it doesn't define providers)
|
|
1980
|
-
if (relativePath.match(/^(src\/)?main\.(tsx?|jsx?)$/)) {
|
|
1981
|
-
const providerIssues = scanProviderSetup(filePath, content, relativePath);
|
|
1982
|
-
violations.providerSetupIssues.push(...providerIssues);
|
|
1983
|
-
}
|
|
1984
|
-
|
|
1985
|
-
// Check Vite configuration
|
|
1986
|
-
// Skip root-level vite.config files - these are typically for library/monorepo development, not consuming apps
|
|
1987
|
-
// The audit recommendations (exclude @jmruthers/pace-core, dedupe) are for consuming apps, not library dev setups
|
|
1988
|
-
const isRootViteConfig = /^vite\.config\.(ts|js|tsx|jsx)$/.test(relativePath);
|
|
1989
|
-
if (!isRootViteConfig && relativePath.match(/vite\.config\.(ts|js|tsx|jsx)$/)) {
|
|
1990
|
-
const viteIssues = scanViteConfig(filePath, content, relativePath);
|
|
1991
|
-
violations.viteConfigIssues.push(...viteIssues);
|
|
1992
|
-
}
|
|
1993
|
-
|
|
1994
|
-
// Check Router setup (main.tsx, main.ts, App.tsx, App.ts)
|
|
1995
|
-
if (relativePath.match(/^(src\/)?(main|App)\.(tsx?|jsx?)$/)) {
|
|
1996
|
-
const routerIssues = scanRouterSetup(filePath, content, relativePath);
|
|
1997
|
-
violations.routerSetupIssues.push(...routerIssues);
|
|
1998
|
-
}
|
|
1999
|
-
|
|
2000
|
-
// Check for custom auth type files (should use pace-core types)
|
|
2001
|
-
if (relativePath.match(/types\/auth\.(ts|tsx)$/i) || relativePath.match(/src\/types\/auth\.(ts|tsx)$/i)) {
|
|
2002
|
-
// Check if file defines auth-related types without importing from pace-core
|
|
2003
|
-
const hasAuthTypeDefinitions = /(interface|type)\s+(User|Session|AuthError|SignIn|SignUp|AuthState|AuthContext)/.test(content);
|
|
2004
|
-
const hasPaceCoreTypeImport = /from\s+['"]@jmruthers\/pace-core\/types/.test(content) ||
|
|
2005
|
-
/from\s+['"]@jmruthers\/pace-core['"]/.test(content);
|
|
2006
|
-
|
|
2007
|
-
if (hasAuthTypeDefinitions && !hasPaceCoreTypeImport) {
|
|
2008
|
-
violations.customAuthCode.push({
|
|
2009
|
-
name: 'auth types',
|
|
2010
|
-
type: 'types',
|
|
2011
|
-
file: relativePath,
|
|
2012
|
-
line: 1,
|
|
2013
|
-
reason: 'Custom auth types detected. Use types from @jmruthers/pace-core/types/auth instead.',
|
|
2014
|
-
replacement: '@jmruthers/pace-core/types/auth'
|
|
2015
|
-
});
|
|
2016
|
-
}
|
|
2017
|
-
}
|
|
2018
|
-
|
|
2019
|
-
// Check for custom auth/RBAC context providers
|
|
2020
|
-
const customProviderPatterns = [
|
|
2021
|
-
{ pattern: /export\s+(default\s+)?(function|const)\s+AuthProvider\s*[=\(]/g, name: 'AuthProvider', replacement: 'UnifiedAuthProvider' },
|
|
2022
|
-
{ pattern: /export\s+(default\s+)?(function|const)\s+AuthContext\s*[=\(]/g, name: 'AuthContext', replacement: 'useUnifiedAuth' },
|
|
2023
|
-
{ pattern: /export\s+(default\s+)?(function|const)\s+PermissionProvider\s*[=\(]/g, name: 'PermissionProvider', replacement: 'pace-core RBAC' },
|
|
2024
|
-
{ pattern: /export\s+(default\s+)?(function|const)\s+RBACProvider\s*[=\(]/g, name: 'RBACProvider', replacement: 'pace-core RBAC' },
|
|
2025
|
-
{ pattern: /export\s+(default\s+)?(function|const)\s+RoleProvider\s*[=\(]/g, name: 'RoleProvider', replacement: 'pace-core RBAC' },
|
|
2026
|
-
{ pattern: /createContext\s*<\s*(Auth|Permission|RBAC|Role)/gi, name: 'auth context', replacement: 'pace-core providers' }
|
|
2027
|
-
];
|
|
2028
|
-
|
|
2029
|
-
customProviderPatterns.forEach(({ pattern, name, replacement }) => {
|
|
2030
|
-
if (pattern.test(content) && !hasPaceCoreImport && !hasUnifiedAuthImport) {
|
|
2031
|
-
violations.customAuthCode.push({
|
|
2032
|
-
name,
|
|
2033
|
-
type: 'provider',
|
|
2034
|
-
file: relativePath,
|
|
2035
|
-
line: getLineNumber(content, content.match(pattern)[0]),
|
|
2036
|
-
reason: `Custom auth/RBAC provider '${name}' detected. Use ${replacement} from pace-core instead.`,
|
|
2037
|
-
replacement
|
|
2038
|
-
});
|
|
2039
|
-
}
|
|
2040
|
-
});
|
|
2041
|
-
|
|
2042
|
-
// Check for unused PermissionService interfaces that duplicate pace-core functionality
|
|
2043
|
-
// Flag it even if file imports from pace-core, since it's a duplicate type definition
|
|
2044
|
-
const permissionServicePattern = /(interface|type)\s+PermissionService\s*[={<]/gi;
|
|
2045
|
-
if (permissionServicePattern.test(content)) {
|
|
2046
|
-
// Check if it's actually used in the file (beyond just the definition)
|
|
2047
|
-
const permissionServiceUsage = /PermissionService[^:]/g;
|
|
2048
|
-
const allMatches = content.match(/PermissionService/g) || [];
|
|
2049
|
-
// If defined but only appears in the definition (and maybe one type annotation), it's likely unused
|
|
2050
|
-
// Count: 1 for definition, 1-2 for potential type annotations = 2-3 total
|
|
2051
|
-
if (allMatches.length <= 3) {
|
|
2052
|
-
violations.customAuthCode.push({
|
|
2053
|
-
name: 'PermissionService interface',
|
|
2054
|
-
type: 'unused type',
|
|
2055
|
-
file: relativePath,
|
|
2056
|
-
line: getLineNumber(content, content.match(permissionServicePattern)[0]),
|
|
2057
|
-
reason: 'PermissionService interface detected that duplicates pace-core permission checking APIs. This appears unused and should be removed.',
|
|
2058
|
-
replacement: 'Remove if unused, or use pace-core RBAC APIs (useRBAC, usePermissions) instead',
|
|
2059
|
-
severity: 'error'
|
|
2060
|
-
});
|
|
2061
|
-
}
|
|
2062
|
-
}
|
|
2063
|
-
|
|
2064
|
-
// Check for unnecessary wrappers around pace-core components and local components
|
|
2065
|
-
// Only check .tsx/.jsx files (component files)
|
|
2066
|
-
if (filePath.match(/\.(tsx|jsx)$/)) {
|
|
2067
|
-
const wrapperIssues = scanUnnecessaryWrappers(content, relativePath, manifest);
|
|
2068
|
-
violations.unnecessaryWrappers.push(...wrapperIssues);
|
|
2069
|
-
}
|
|
2070
|
-
|
|
2071
|
-
// ============================================
|
|
2072
|
-
// App Discovery Compliance Checks
|
|
2073
|
-
// ============================================
|
|
2074
|
-
// Check for direct queries to rbac_apps table or hardcoded app names
|
|
2075
|
-
// Should use data_rbac_apps_list RPC function instead
|
|
2076
|
-
|
|
2077
|
-
// Check for direct queries to rbac_apps table
|
|
2078
|
-
const rbacAppsQueryPatterns = [
|
|
2079
|
-
// Supabase client queries
|
|
2080
|
-
/\.from\s*\(\s*['"]rbac_apps['"]\s*\)/g,
|
|
2081
|
-
/\.from\s*\(\s*['"]rbac_apps['"]\s*\)/g,
|
|
2082
|
-
// SQL queries (less common but possible)
|
|
2083
|
-
/FROM\s+rbac_apps\b/gi,
|
|
2084
|
-
/SELECT\s+.*\s+FROM\s+rbac_apps\b/gi
|
|
2085
|
-
];
|
|
2086
|
-
|
|
2087
|
-
// Check if file uses data_rbac_apps_list RPC function
|
|
2088
|
-
const usesRpcFunction = /data_rbac_apps_list|rpc\s*\(\s*['"]data_rbac_apps_list['"]/gi.test(content);
|
|
2089
|
-
|
|
2090
|
-
rbacAppsQueryPatterns.forEach(pattern => {
|
|
2091
|
-
let match;
|
|
2092
|
-
while ((match = pattern.exec(content)) !== null) {
|
|
2093
|
-
// Skip if in a line comment
|
|
2094
|
-
const lineStart = content.lastIndexOf('\n', match.index) + 1;
|
|
2095
|
-
const lineUpToMatch = content.substring(lineStart, match.index);
|
|
2096
|
-
const isInLineComment = /\/\/[^\n]*$/.test(lineUpToMatch);
|
|
2097
|
-
|
|
2098
|
-
if (isInLineComment) {
|
|
2099
|
-
continue;
|
|
2100
|
-
}
|
|
2101
|
-
|
|
2102
|
-
// Check what comes after .from('rbac_apps') to determine if it's a SELECT query or CRUD operation
|
|
2103
|
-
const afterMatch = content.substring(match.index, Math.min(content.length, match.index + 200));
|
|
2104
|
-
const isSelectQuery = /\.(select|selectAll)\s*\(/i.test(afterMatch);
|
|
2105
|
-
const isCrudOperation = /\.(update|insert|delete|upsert)\s*\(/i.test(afterMatch);
|
|
2106
|
-
|
|
2107
|
-
// Only flag SELECT queries - UPDATE/INSERT/DELETE operations are acceptable with secureSupabase
|
|
2108
|
-
if (isSelectQuery && !isCrudOperation) {
|
|
2109
|
-
violations.appDiscoveryIssues.push({
|
|
2110
|
-
type: 'direct_table_query',
|
|
2111
|
-
file: relativePath,
|
|
2112
|
-
line: getLineNumber(content, match[0]),
|
|
2113
|
-
reason: 'Direct query to rbac_apps table detected. Use data_rbac_apps_list RPC function for dynamic app discovery.',
|
|
2114
|
-
recommendation: 'Replace with: const { data } = await supabase.rpc(\'data_rbac_apps_list\');',
|
|
2115
|
-
severity: 'warning'
|
|
2116
|
-
});
|
|
2117
|
-
}
|
|
2118
|
-
// Skip CRUD operations (UPDATE/INSERT/DELETE) - these are acceptable with secureSupabase
|
|
2119
|
-
}
|
|
2120
|
-
});
|
|
2121
|
-
|
|
2122
|
-
// Check for hardcoded app names in arrays or string literals
|
|
2123
|
-
// Known app names: BASE, CAKE, PACE, MINT, TRAC, PORTAL, MEDI
|
|
2124
|
-
// Only flag if they appear to be used for app discovery (arrays, comparisons, etc.)
|
|
2125
|
-
const hardcodedAppNamePatterns = [
|
|
2126
|
-
// Array of app names (likely used for iteration/discovery)
|
|
2127
|
-
/\[['"]\s*(BASE|CAKE|PACE|MINT|TRAC|PORTAL|MEDI)\s*['"]/gi,
|
|
2128
|
-
// String literals in comparisons or includes checks (app discovery patterns)
|
|
2129
|
-
/(app|apps|appName|app_name)\s*[=!]==?\s*['"]\s*(BASE|CAKE|PACE|MINT|TRAC|PORTAL|MEDI)\s*['"]/gi,
|
|
2130
|
-
/(app|apps|appName|app_name)\s*\.(includes|indexOf|find|filter)\s*\([^)]*['"]\s*(BASE|CAKE|PACE|MINT|TRAC|PORTAL|MEDI)\s*['"]/gi,
|
|
2131
|
-
/['"]\s*(BASE|CAKE|PACE|MINT|TRAC|PORTAL|MEDI)\s*['"]\s*\.(includes|indexOf)/gi
|
|
2132
|
-
];
|
|
2133
|
-
|
|
2134
|
-
hardcodedAppNamePatterns.forEach(pattern => {
|
|
2135
|
-
let match;
|
|
2136
|
-
while ((match = pattern.exec(content)) !== null) {
|
|
2137
|
-
// Skip if it's in a get_app_id call (acceptable usage)
|
|
2138
|
-
const beforeMatch = content.substring(Math.max(0, match.index - 50), match.index);
|
|
2139
|
-
const isInGetAppId = /get_app_id\s*\(/i.test(beforeMatch);
|
|
2140
|
-
|
|
2141
|
-
// Skip if in a line comment
|
|
2142
|
-
const lineStart = content.lastIndexOf('\n', match.index) + 1;
|
|
2143
|
-
const lineUpToMatch = content.substring(lineStart, match.index);
|
|
2144
|
-
const isInLineComment = /\/\/[^\n]*$/.test(lineUpToMatch);
|
|
2145
|
-
|
|
2146
|
-
// Skip if it's a comment about app names
|
|
2147
|
-
const isInComment = /\/\*[\s\S]*?\*\//.test(beforeMatch + match[0]);
|
|
2148
|
-
|
|
2149
|
-
// Skip if it's in a data_rbac_apps_list call (already using the function)
|
|
2150
|
-
const isInRpcCall = /data_rbac_apps_list/i.test(beforeMatch);
|
|
2151
|
-
|
|
2152
|
-
// Extract app name (could be in different capture groups depending on pattern)
|
|
2153
|
-
const appName = match[1] || match[2] || match[3];
|
|
2154
|
-
|
|
2155
|
-
if (!isInGetAppId && !isInLineComment && !isInComment && !isInRpcCall && appName) {
|
|
2156
|
-
violations.appDiscoveryIssues.push({
|
|
2157
|
-
type: 'hardcoded_app_name',
|
|
2158
|
-
file: relativePath,
|
|
2159
|
-
line: getLineNumber(content, match[0]),
|
|
2160
|
-
appName: appName,
|
|
2161
|
-
reason: `Hardcoded app name '${appName}' detected. Use data_rbac_apps_list RPC function for dynamic app discovery.`,
|
|
2162
|
-
recommendation: 'Use data_rbac_apps_list() to discover apps dynamically instead of hardcoding app names.',
|
|
2163
|
-
severity: 'warning'
|
|
2164
|
-
});
|
|
2165
|
-
}
|
|
2166
|
-
}
|
|
2167
|
-
});
|
|
2168
|
-
|
|
2169
|
-
// If file has app discovery code but doesn't use the RPC function, suggest it
|
|
2170
|
-
if (violations.appDiscoveryIssues.length > 0 && !usesRpcFunction) {
|
|
2171
|
-
// Add a general suggestion if there are multiple issues
|
|
2172
|
-
if (violations.appDiscoveryIssues.length > 1) {
|
|
2173
|
-
violations.appDiscoveryIssues.push({
|
|
2174
|
-
type: 'suggestion',
|
|
2175
|
-
file: relativePath,
|
|
2176
|
-
line: 1,
|
|
2177
|
-
reason: 'Multiple app discovery issues found. Consider using data_rbac_apps_list RPC function for all app discovery needs.',
|
|
2178
|
-
recommendation: 'Replace all hardcoded app names and direct table queries with: const { data: apps } = await supabase.rpc(\'data_rbac_apps_list\');',
|
|
2179
|
-
severity: 'info'
|
|
2180
|
-
});
|
|
2181
|
-
}
|
|
2182
|
-
}
|
|
2183
|
-
|
|
2184
|
-
// ============================================
|
|
2185
|
-
// Check for Direct Supabase Client Usage
|
|
2186
|
-
// ============================================
|
|
2187
|
-
// Detect when consuming apps use createClient from @supabase/supabase-js
|
|
2188
|
-
// and then use that client for database queries instead of useSecureSupabase
|
|
2189
|
-
// This is a critical security issue as it bypasses RLS and organisation context
|
|
2190
|
-
|
|
2191
|
-
// Skip Edge Functions - they run in Deno and must use createClient
|
|
2192
|
-
// Reuse isEdgeFunction declared at the top of the function
|
|
2193
|
-
if (!isEdgeFunction) {
|
|
2194
|
-
// Check for createClient import from @supabase/supabase-js
|
|
2195
|
-
const createClientImportPattern = /import\s+{\s*createClient\s*}\s+from\s+['"]@supabase\/supabase-js['"]/;
|
|
2196
|
-
const hasCreateClientImport = createClientImportPattern.test(content);
|
|
2197
|
-
|
|
2198
|
-
// Check for createClient usage
|
|
2199
|
-
const createClientUsagePattern = /createClient\s*\(/g;
|
|
2200
|
-
const createClientMatches = content.match(createClientUsagePattern);
|
|
2201
|
-
const hasCreateClientUsage = createClientMatches && createClientMatches.length > 0;
|
|
2202
|
-
|
|
2203
|
-
// Check if file uses useSecureSupabase (correct usage)
|
|
2204
|
-
const usesSecureSupabase = /useSecureSupabase/.test(content) ||
|
|
2205
|
-
/from\s+['"]@jmruthers\/pace-core\/rbac['"]/.test(content);
|
|
2206
|
-
|
|
2207
|
-
// Find all variables assigned from createClient
|
|
2208
|
-
const createClientVariablePattern = /(const|let|var)\s+(\w+)\s*=\s*createClient\s*\(/g;
|
|
2209
|
-
const nonSecureClients = new Set();
|
|
2210
|
-
let match;
|
|
2211
|
-
while ((match = createClientVariablePattern.exec(content)) !== null) {
|
|
2212
|
-
if (match[2]) {
|
|
2213
|
-
nonSecureClients.add(match[2]);
|
|
2214
|
-
}
|
|
2215
|
-
}
|
|
2216
|
-
|
|
2217
|
-
// Check for database queries (.from() calls) using non-secure clients
|
|
2218
|
-
// Pattern: variable.from('table_name')
|
|
2219
|
-
const fromPattern = /\.from\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
2220
|
-
let fromMatch;
|
|
2221
|
-
while ((fromMatch = fromPattern.exec(content)) !== null) {
|
|
2222
|
-
const matchIndex = fromMatch.index;
|
|
2223
|
-
const tableName = fromMatch[1];
|
|
2224
|
-
|
|
2225
|
-
// Skip RBAC tables (already checked above)
|
|
2226
|
-
if (tableName.startsWith('rbac_')) {
|
|
2227
|
-
continue;
|
|
2228
|
-
}
|
|
2229
|
-
|
|
2230
|
-
// Skip if in a line comment
|
|
2231
|
-
const lineStart = content.lastIndexOf('\n', matchIndex) + 1;
|
|
2232
|
-
const lineUpToMatch = content.substring(lineStart, matchIndex);
|
|
2233
|
-
const isInLineComment = /\/\/[^\n]*$/.test(lineUpToMatch);
|
|
2234
|
-
|
|
2235
|
-
if (isInLineComment) {
|
|
2236
|
-
continue;
|
|
2237
|
-
}
|
|
2238
|
-
|
|
2239
|
-
// Find the variable name before .from()
|
|
2240
|
-
const beforeMatch = content.substring(Math.max(0, matchIndex - 200), matchIndex);
|
|
2241
|
-
const parts = beforeMatch.split('.from');
|
|
2242
|
-
let variableName = null;
|
|
2243
|
-
if (parts.length > 0) {
|
|
2244
|
-
const beforeFrom = parts[parts.length - 1].trim();
|
|
2245
|
-
const words = beforeFrom.match(/\b\w+\b/g);
|
|
2246
|
-
if (words && words.length > 0) {
|
|
2247
|
-
variableName = words[words.length - 1];
|
|
2248
|
-
}
|
|
2249
|
-
}
|
|
2250
|
-
|
|
2251
|
-
// Check if this variable is from createClient (non-secure)
|
|
2252
|
-
if (variableName && nonSecureClients.has(variableName)) {
|
|
2253
|
-
// Check if it's in a config file (acceptable for centralized config)
|
|
2254
|
-
const isConfigFile = /config|supabase|client/i.test(relativePath) &&
|
|
2255
|
-
(relativePath.includes('supabase.ts') ||
|
|
2256
|
-
relativePath.includes('supabase.js') ||
|
|
2257
|
-
relativePath.includes('client.ts') ||
|
|
2258
|
-
relativePath.includes('client.js'));
|
|
2259
|
-
|
|
2260
|
-
if (!isConfigFile) {
|
|
2261
|
-
const lineNumber = content.substring(0, matchIndex).split('\n').length;
|
|
2262
|
-
|
|
2263
|
-
// Check if this is already reported
|
|
2264
|
-
const alreadyReported = violations.directSupabaseClient.some(v =>
|
|
2265
|
-
v.file === relativePath &&
|
|
2266
|
-
v.variable === variableName &&
|
|
2267
|
-
Math.abs(v.line - lineNumber) <= 2
|
|
2268
|
-
);
|
|
2269
|
-
|
|
2270
|
-
if (!alreadyReported) {
|
|
2271
|
-
violations.directSupabaseClient.push({
|
|
2272
|
-
file: relativePath,
|
|
2273
|
-
line: lineNumber,
|
|
2274
|
-
variable: variableName,
|
|
2275
|
-
table: tableName,
|
|
2276
|
-
reason: `Direct Supabase client usage detected. Variable '${variableName}' is created with createClient() and used for database queries. You MUST use useSecureSupabase() instead to ensure RLS policies and organisation context are enforced.`,
|
|
2277
|
-
recommendation: `Replace with: import { useSecureSupabase } from '@jmruthers/pace-core/rbac'; const ${variableName} = useSecureSupabase();`
|
|
2278
|
-
});
|
|
2279
|
-
}
|
|
2280
|
-
}
|
|
2281
|
-
}
|
|
2282
|
-
}
|
|
2283
|
-
|
|
2284
|
-
// Also check if file imports createClient but doesn't use useSecureSupabase
|
|
2285
|
-
if (hasCreateClientImport && hasCreateClientUsage && !usesSecureSupabase) {
|
|
2286
|
-
// Check if it's a config file (acceptable)
|
|
2287
|
-
const isConfigFile = /config|supabase|client/i.test(relativePath) &&
|
|
2288
|
-
(relativePath.includes('supabase.ts') ||
|
|
2289
|
-
relativePath.includes('supabase.js') ||
|
|
2290
|
-
relativePath.includes('client.ts') ||
|
|
2291
|
-
relativePath.includes('client.js'));
|
|
2292
|
-
|
|
2293
|
-
if (!isConfigFile) {
|
|
2294
|
-
// Check if createClient is used for queries (not just config)
|
|
2295
|
-
const hasDatabaseQueries = /\.from\s*\(/.test(content);
|
|
2296
|
-
|
|
2297
|
-
if (hasDatabaseQueries) {
|
|
2298
|
-
violations.directSupabaseClient.push({
|
|
2299
|
-
file: relativePath,
|
|
2300
|
-
line: 1,
|
|
2301
|
-
variable: 'unknown',
|
|
2302
|
-
table: 'multiple',
|
|
2303
|
-
reason: 'File imports createClient from @supabase/supabase-js and performs database queries. You MUST use useSecureSupabase() instead to ensure RLS policies and organisation context are enforced.',
|
|
2304
|
-
recommendation: 'Replace createClient with: import { useSecureSupabase } from \'@jmruthers/pace-core/rbac\'; const supabase = useSecureSupabase();'
|
|
2305
|
-
});
|
|
2306
|
-
}
|
|
2307
|
-
}
|
|
2308
|
-
}
|
|
2309
|
-
|
|
2310
|
-
// Check for createClient imports even without immediate query usage
|
|
2311
|
-
// This catches cases where createClient is imported but may be used later
|
|
2312
|
-
if (hasCreateClientImport && !usesSecureSupabase) {
|
|
2313
|
-
const isConfigFile = /config|supabase|client/i.test(relativePath) &&
|
|
2314
|
-
(relativePath.includes('supabase.ts') ||
|
|
2315
|
-
relativePath.includes('supabase.js') ||
|
|
2316
|
-
relativePath.includes('client.ts') ||
|
|
2317
|
-
relativePath.includes('client.js'));
|
|
2318
|
-
|
|
2319
|
-
if (!isConfigFile) {
|
|
2320
|
-
// Find the line number of the import
|
|
2321
|
-
const importMatch = content.match(/import\s+{\s*createClient\s*}\s+from\s+['"]@supabase\/supabase-js['"]/);
|
|
2322
|
-
if (importMatch) {
|
|
2323
|
-
const lineNumber = content.substring(0, importMatch.index).split('\n').length;
|
|
2324
|
-
|
|
2325
|
-
// Check if this violation is already reported
|
|
2326
|
-
const alreadyReported = violations.directSupabaseClient.some(v =>
|
|
2327
|
-
v.file === relativePath &&
|
|
2328
|
-
v.variable === 'createClient' &&
|
|
2329
|
-
Math.abs(v.line - lineNumber) <= 2
|
|
2330
|
-
);
|
|
2331
|
-
|
|
2332
|
-
if (!alreadyReported) {
|
|
2333
|
-
violations.directSupabaseClient.push({
|
|
2334
|
-
file: relativePath,
|
|
2335
|
-
line: lineNumber,
|
|
2336
|
-
variable: 'createClient',
|
|
2337
|
-
table: 'none',
|
|
2338
|
-
reason: 'Direct import of createClient from @supabase/supabase-js detected. You MUST use useSecureSupabase() from @jmruthers/pace-core/rbac instead to ensure organisation context and RLS policies are enforced.',
|
|
2339
|
-
recommendation: 'Remove this import and use: import { useSecureSupabase } from \'@jmruthers/pace-core/rbac\'; const supabase = useSecureSupabase();'
|
|
2340
|
-
});
|
|
2341
|
-
}
|
|
2342
|
-
}
|
|
2343
|
-
}
|
|
2344
|
-
}
|
|
2345
|
-
|
|
2346
|
-
// Check for createClient() calls even when variable isn't used for queries yet
|
|
2347
|
-
// This catches potential security issues early
|
|
2348
|
-
if (hasCreateClientUsage && !usesSecureSupabase) {
|
|
2349
|
-
const isConfigFile = /config|supabase|client/i.test(relativePath) &&
|
|
2350
|
-
(relativePath.includes('supabase.ts') ||
|
|
2351
|
-
relativePath.includes('supabase.js') ||
|
|
2352
|
-
relativePath.includes('client.ts') ||
|
|
2353
|
-
relativePath.includes('client.js'));
|
|
2354
|
-
|
|
2355
|
-
if (!isConfigFile) {
|
|
2356
|
-
// Find all createClient() calls
|
|
2357
|
-
const createClientCallPattern = /createClient\s*\(/g;
|
|
2358
|
-
let callMatch;
|
|
2359
|
-
while ((callMatch = createClientCallPattern.exec(content)) !== null) {
|
|
2360
|
-
const lineNumber = content.substring(0, callMatch.index).split('\n').length;
|
|
2361
|
-
|
|
2362
|
-
// Check if this violation is already reported
|
|
2363
|
-
const alreadyReported = violations.directSupabaseClient.some(v =>
|
|
2364
|
-
v.file === relativePath &&
|
|
2365
|
-
Math.abs(v.line - lineNumber) <= 2
|
|
2366
|
-
);
|
|
2367
|
-
|
|
2368
|
-
if (!alreadyReported) {
|
|
2369
|
-
violations.directSupabaseClient.push({
|
|
2370
|
-
file: relativePath,
|
|
2371
|
-
line: lineNumber,
|
|
2372
|
-
variable: 'unknown',
|
|
2373
|
-
table: 'none',
|
|
2374
|
-
reason: 'Direct createClient() call detected. You MUST use useSecureSupabase() from @jmruthers/pace-core/rbac instead to ensure organisation context and RLS policies are enforced.',
|
|
2375
|
-
recommendation: 'Replace with: import { useSecureSupabase } from \'@jmruthers/pace-core/rbac\'; const supabase = useSecureSupabase();'
|
|
2376
|
-
});
|
|
2377
|
-
}
|
|
2378
|
-
}
|
|
2379
|
-
}
|
|
2380
|
-
}
|
|
2381
|
-
}
|
|
2382
|
-
|
|
2383
|
-
// ============================================
|
|
2384
|
-
// Check for Deprecated useSecureDataAccess Usage
|
|
2385
|
-
// ============================================
|
|
2386
|
-
// Detect when consuming apps use useSecureDataAccess() with secureQuery/secureInsert/etc
|
|
2387
|
-
// This is deprecated - they should migrate to useSecureSupabase() instead
|
|
2388
|
-
// This helps identify code that needs migration before retiring the old API
|
|
2389
|
-
|
|
2390
|
-
// Skip Edge Functions - they run in Deno
|
|
2391
|
-
if (!isEdgeFunction) {
|
|
2392
|
-
// Check for useSecureDataAccess import
|
|
2393
|
-
const hasSecureDataAccessImport = /import.*useSecureDataAccess.*from\s+['"]@jmruthers\/pace-core['"]/.test(content) ||
|
|
2394
|
-
/import.*useSecureDataAccess.*from\s+['"]@jmruthers\/pace-core\/hooks['"]/.test(content);
|
|
2395
|
-
|
|
2396
|
-
// Check for useSecureDataAccess hook usage
|
|
2397
|
-
const hasSecureDataAccessHook = /useSecureDataAccess\s*\(/.test(content);
|
|
2398
|
-
|
|
2399
|
-
// Check for deprecated secure methods
|
|
2400
|
-
const deprecatedMethods = [
|
|
2401
|
-
{ name: 'secureQuery', operation: 'query' },
|
|
2402
|
-
{ name: 'secureInsert', operation: 'insert' },
|
|
2403
|
-
{ name: 'secureUpdate', operation: 'update' },
|
|
2404
|
-
{ name: 'secureDelete', operation: 'delete' },
|
|
2405
|
-
{ name: 'secureRpc', operation: 'RPC call' }
|
|
2406
|
-
];
|
|
2407
|
-
|
|
2408
|
-
// Pattern to find destructured secure methods from useSecureDataAccess
|
|
2409
|
-
const destructurePattern = /(const|let)\s*\{[^}]*\b(secureQuery|secureInsert|secureUpdate|secureDelete|secureRpc)\b[^}]*\}\s*=\s*useSecureDataAccess\s*\(/g;
|
|
2410
|
-
|
|
2411
|
-
// Pattern to find direct method calls (secureQuery(...), secureInsert(...), etc.)
|
|
2412
|
-
const methodCallPatterns = deprecatedMethods.map(method => ({
|
|
2413
|
-
name: method.name,
|
|
2414
|
-
operation: method.operation,
|
|
2415
|
-
pattern: new RegExp(`\\b${method.name}\\s*\\(`, 'g')
|
|
2416
|
-
}));
|
|
2417
|
-
|
|
2418
|
-
// Check if file uses the deprecated hook
|
|
2419
|
-
if (hasSecureDataAccessImport || hasSecureDataAccessHook) {
|
|
2420
|
-
// Find all destructured methods
|
|
2421
|
-
let destructureMatch;
|
|
2422
|
-
const foundMethods = new Set();
|
|
2423
|
-
|
|
2424
|
-
while ((destructureMatch = destructurePattern.exec(content)) !== null) {
|
|
2425
|
-
const destructureText = destructureMatch[0];
|
|
2426
|
-
deprecatedMethods.forEach(method => {
|
|
2427
|
-
if (destructureText.includes(method.name)) {
|
|
2428
|
-
foundMethods.add(method.name);
|
|
2429
|
-
}
|
|
2430
|
-
});
|
|
2431
|
-
}
|
|
2432
|
-
|
|
2433
|
-
// Find all method calls
|
|
2434
|
-
methodCallPatterns.forEach(({ name, operation, pattern }) => {
|
|
2435
|
-
let match;
|
|
2436
|
-
pattern.lastIndex = 0;
|
|
2437
|
-
|
|
2438
|
-
while ((match = pattern.exec(content)) !== null) {
|
|
2439
|
-
const matchIndex = match.index;
|
|
2440
|
-
|
|
2441
|
-
// Skip if in a line comment
|
|
2442
|
-
const lineStart = content.lastIndexOf('\n', matchIndex) + 1;
|
|
2443
|
-
const lineUpToMatch = content.substring(lineStart, matchIndex);
|
|
2444
|
-
const isInLineComment = /\/\/[^\n]*$/.test(lineUpToMatch);
|
|
2445
|
-
|
|
2446
|
-
if (isInLineComment) {
|
|
2447
|
-
continue;
|
|
2448
|
-
}
|
|
2449
|
-
|
|
2450
|
-
// Check if this is from useSecureDataAccess (not from a different source)
|
|
2451
|
-
// Look backwards to see if it's from useSecureDataAccess destructuring
|
|
2452
|
-
const beforeMatch = content.substring(Math.max(0, matchIndex - 500), matchIndex);
|
|
2453
|
-
const isFromSecureDataAccess =
|
|
2454
|
-
/useSecureDataAccess\s*\(/.test(beforeMatch) ||
|
|
2455
|
-
/(const|let)\s*\{[^}]*\bsecure(Query|Insert|Update|Delete|Rpc)\b/.test(beforeMatch);
|
|
2456
|
-
|
|
2457
|
-
if (isFromSecureDataAccess) {
|
|
2458
|
-
foundMethods.add(name);
|
|
2459
|
-
|
|
2460
|
-
const lineNumber = content.substring(0, matchIndex).split('\n').length;
|
|
2461
|
-
|
|
2462
|
-
// Check if already reported
|
|
2463
|
-
const alreadyReported = violations.deprecatedSecureDataAccess.some(v =>
|
|
2464
|
-
v.file === relativePath &&
|
|
2465
|
-
v.method === name &&
|
|
2466
|
-
Math.abs(v.line - lineNumber) <= 2
|
|
2467
|
-
);
|
|
2468
|
-
|
|
2469
|
-
if (!alreadyReported) {
|
|
2470
|
-
violations.deprecatedSecureDataAccess.push({
|
|
2471
|
-
file: relativePath,
|
|
2472
|
-
line: lineNumber,
|
|
2473
|
-
method: name,
|
|
2474
|
-
operation: operation,
|
|
2475
|
-
reason: `Deprecated method '${name}' from useSecureDataAccess() detected. This API is being retired. Migrate to useSecureSupabase() instead.`,
|
|
2476
|
-
recommendation: getMigrationRecommendation(name, operation)
|
|
2477
|
-
});
|
|
2478
|
-
}
|
|
2479
|
-
}
|
|
2480
|
-
}
|
|
2481
|
-
});
|
|
2482
|
-
|
|
2483
|
-
// If we found the hook usage but haven't reported specific methods, add a general warning
|
|
2484
|
-
if (foundMethods.size === 0 && hasSecureDataAccessHook) {
|
|
2485
|
-
// Check if it's just imported but not used, or used in a way we didn't detect
|
|
2486
|
-
const hasAnySecureMethodCall = /secure(Query|Insert|Update|Delete|Rpc)\s*\(/.test(content);
|
|
2487
|
-
|
|
2488
|
-
if (hasAnySecureMethodCall) {
|
|
2489
|
-
violations.deprecatedSecureDataAccess.push({
|
|
2490
|
-
file: relativePath,
|
|
2491
|
-
line: 1,
|
|
2492
|
-
method: 'useSecureDataAccess',
|
|
2493
|
-
operation: 'general',
|
|
2494
|
-
reason: 'useSecureDataAccess() hook detected. This API is deprecated and will be retired. Migrate to useSecureSupabase() instead.',
|
|
2495
|
-
recommendation: 'Replace useSecureDataAccess() with useSecureSupabase() and use standard Supabase query builder API (.from(), .select(), etc.)'
|
|
2496
|
-
});
|
|
2497
|
-
}
|
|
2498
|
-
}
|
|
2499
|
-
}
|
|
2500
|
-
}
|
|
2501
|
-
|
|
2502
|
-
return violations;
|
|
2503
|
-
}
|
|
2504
|
-
|
|
2505
|
-
/**
|
|
2506
|
-
* Compliance check module
|
|
2507
|
-
*/
|
|
2508
|
-
const complianceCheck = {
|
|
2509
|
-
name: 'compliance',
|
|
2510
|
-
description: 'pace-core compliance checks (restricted imports, duplicates, auth/RBAC, etc.)',
|
|
2511
|
-
severity: 'error',
|
|
2512
|
-
|
|
2513
|
-
async run(context) {
|
|
2514
|
-
const { projectRoot, files, manifest: providedManifest } = context;
|
|
2515
|
-
|
|
2516
|
-
// Load manifest if not provided
|
|
2517
|
-
const manifest = providedManifest || loadManifest();
|
|
2518
|
-
|
|
2519
|
-
if (!files || files.length === 0) {
|
|
2520
|
-
return {
|
|
2521
|
-
issues: [],
|
|
2522
|
-
warnings: [],
|
|
2523
|
-
suggestions: [],
|
|
2524
|
-
violations: {
|
|
2525
|
-
restrictedImports: [],
|
|
2526
|
-
duplicateComponents: [],
|
|
2527
|
-
duplicateHooks: [],
|
|
2528
|
-
duplicateUtils: [],
|
|
2529
|
-
suggestions: [],
|
|
2530
|
-
customAuthCode: [],
|
|
2531
|
-
duplicateConfig: [],
|
|
2532
|
-
unprotectedPages: [],
|
|
2533
|
-
directSupabaseAuth: [],
|
|
2534
|
-
directSupabaseClient: [],
|
|
2535
|
-
deprecatedSecureDataAccess: [],
|
|
2536
|
-
providerSetupIssues: [],
|
|
2537
|
-
viteConfigIssues: [],
|
|
2538
|
-
routerSetupIssues: [],
|
|
2539
|
-
unnecessaryWrappers: [],
|
|
2540
|
-
appDiscoveryIssues: []
|
|
2541
|
-
}
|
|
2542
|
-
};
|
|
2543
|
-
}
|
|
2544
|
-
|
|
2545
|
-
// Aggregate all violations
|
|
2546
|
-
const allViolations = {
|
|
2547
|
-
restrictedImports: [],
|
|
2548
|
-
duplicateComponents: [],
|
|
2549
|
-
duplicateHooks: [],
|
|
2550
|
-
duplicateUtils: [],
|
|
2551
|
-
suggestions: [],
|
|
2552
|
-
customAuthCode: [],
|
|
2553
|
-
duplicateConfig: [],
|
|
2554
|
-
unprotectedPages: [],
|
|
2555
|
-
directSupabaseAuth: [],
|
|
2556
|
-
directSupabaseClient: [],
|
|
2557
|
-
deprecatedSecureDataAccess: [],
|
|
2558
|
-
providerSetupIssues: [],
|
|
2559
|
-
viteConfigIssues: [],
|
|
2560
|
-
routerSetupIssues: [],
|
|
2561
|
-
unnecessaryWrappers: [],
|
|
2562
|
-
appDiscoveryIssues: []
|
|
2563
|
-
};
|
|
2564
|
-
|
|
2565
|
-
// Scan all files
|
|
2566
|
-
for (const filePath of files) {
|
|
2567
|
-
try {
|
|
2568
|
-
const violations = scanFile(filePath, manifest, projectRoot);
|
|
2569
|
-
|
|
2570
|
-
// Aggregate violations
|
|
2571
|
-
Object.keys(allViolations).forEach(key => {
|
|
2572
|
-
if (violations[key] && Array.isArray(violations[key])) {
|
|
2573
|
-
allViolations[key].push(...violations[key]);
|
|
2574
|
-
}
|
|
2575
|
-
});
|
|
2576
|
-
} catch (error) {
|
|
2577
|
-
// Skip files with errors
|
|
2578
|
-
console.warn(`Error scanning ${filePath}: ${error.message}`);
|
|
2579
|
-
}
|
|
2580
|
-
}
|
|
2581
|
-
|
|
2582
|
-
// Convert violations to issues/warnings/suggestions format
|
|
2583
|
-
const issues = [
|
|
2584
|
-
...allViolations.restrictedImports.map(v => ({
|
|
2585
|
-
type: 'restricted-import',
|
|
2586
|
-
file: v.file,
|
|
2587
|
-
line: v.line,
|
|
2588
|
-
message: `Restricted import: ${v.module} - ${v.reason}`,
|
|
2589
|
-
recommendation: `Use pace-core alternative instead`
|
|
2590
|
-
})),
|
|
2591
|
-
...allViolations.duplicateComponents.map(v => ({
|
|
2592
|
-
type: 'duplicate-component',
|
|
2593
|
-
file: v.file,
|
|
2594
|
-
message: `Duplicate component: ${v.component}`,
|
|
2595
|
-
recommendation: `Use ${v.component} from '@jmruthers/pace-core' instead`
|
|
2596
|
-
})),
|
|
2597
|
-
...allViolations.duplicateHooks.map(v => ({
|
|
2598
|
-
type: 'duplicate-hook',
|
|
2599
|
-
file: v.file,
|
|
2600
|
-
message: `Duplicate hook: ${v.hook}`,
|
|
2601
|
-
recommendation: `Use ${v.hook} from '@jmruthers/pace-core' instead`
|
|
2602
|
-
})),
|
|
2603
|
-
...allViolations.duplicateUtils.map(v => ({
|
|
2604
|
-
type: 'duplicate-util',
|
|
2605
|
-
file: v.file,
|
|
2606
|
-
message: `Duplicate util: ${v.util}`,
|
|
2607
|
-
recommendation: `Use ${v.util} from '@jmruthers/pace-core' instead`
|
|
2608
|
-
})),
|
|
2609
|
-
...allViolations.customAuthCode.filter(v => !v.severity || v.severity === 'error').map(v => ({
|
|
2610
|
-
type: 'custom-auth-code',
|
|
2611
|
-
file: v.file,
|
|
2612
|
-
line: v.line,
|
|
2613
|
-
message: `${v.type}: ${v.name} - ${v.reason}`,
|
|
2614
|
-
recommendation: v.replacement || 'Use pace-core APIs instead'
|
|
2615
|
-
})),
|
|
2616
|
-
...allViolations.directSupabaseClient.map(v => ({
|
|
2617
|
-
type: 'direct-supabase-client',
|
|
2618
|
-
file: v.file,
|
|
2619
|
-
line: v.line,
|
|
2620
|
-
message: `Direct Supabase client usage: ${v.reason}`,
|
|
2621
|
-
recommendation: v.recommendation || 'Use useSecureSupabase() instead'
|
|
2622
|
-
})),
|
|
2623
|
-
...allViolations.providerSetupIssues.map(v => ({
|
|
2624
|
-
type: 'provider-setup',
|
|
2625
|
-
file: v.file,
|
|
2626
|
-
line: v.line,
|
|
2627
|
-
message: v.issue || v.reason,
|
|
2628
|
-
recommendation: v.recommendation
|
|
2629
|
-
})),
|
|
2630
|
-
...allViolations.viteConfigIssues.map(v => ({
|
|
2631
|
-
type: 'vite-config',
|
|
2632
|
-
file: v.file,
|
|
2633
|
-
line: v.line,
|
|
2634
|
-
message: v.issue,
|
|
2635
|
-
recommendation: v.recommendation
|
|
2636
|
-
})),
|
|
2637
|
-
...allViolations.routerSetupIssues.map(v => ({
|
|
2638
|
-
type: 'router-setup',
|
|
2639
|
-
file: v.file,
|
|
2640
|
-
line: v.line,
|
|
2641
|
-
message: v.issue,
|
|
2642
|
-
recommendation: v.recommendation
|
|
2643
|
-
}))
|
|
2644
|
-
];
|
|
2645
|
-
|
|
2646
|
-
const warnings = [
|
|
2647
|
-
...allViolations.customAuthCode.filter(v => v.severity === 'warning').map(v => ({
|
|
2648
|
-
type: 'custom-auth-code',
|
|
2649
|
-
file: v.file,
|
|
2650
|
-
line: v.line,
|
|
2651
|
-
message: `${v.type}: ${v.name} - ${v.reason}`,
|
|
2652
|
-
recommendation: v.replacement || 'Consider using pace-core APIs'
|
|
2653
|
-
})),
|
|
2654
|
-
...allViolations.directSupabaseAuth.map(v => ({
|
|
2655
|
-
type: 'direct-supabase-auth',
|
|
2656
|
-
file: v.file,
|
|
2657
|
-
line: v.line,
|
|
2658
|
-
message: `Direct Supabase auth usage: ${v.reason}`,
|
|
2659
|
-
recommendation: v.recommendation || 'Use useUnifiedAuth() instead'
|
|
2660
|
-
})),
|
|
2661
|
-
...allViolations.deprecatedSecureDataAccess.map(v => ({
|
|
2662
|
-
type: 'deprecated-api',
|
|
2663
|
-
file: v.file,
|
|
2664
|
-
line: v.line,
|
|
2665
|
-
message: `Deprecated API: ${v.method} - ${v.reason}`,
|
|
2666
|
-
recommendation: v.recommendation || 'Migrate to useSecureSupabase()'
|
|
2667
|
-
})),
|
|
2668
|
-
...allViolations.unnecessaryWrappers.map(v => ({
|
|
2669
|
-
type: 'unnecessary-wrapper',
|
|
2670
|
-
file: v.file,
|
|
2671
|
-
line: v.line,
|
|
2672
|
-
message: `Unnecessary wrapper: ${v.component} wraps ${v.wrappedComponent}`,
|
|
2673
|
-
recommendation: v.recommendation || 'Remove wrapper and use component directly'
|
|
2674
|
-
})),
|
|
2675
|
-
...allViolations.appDiscoveryIssues.filter(v => v.severity === 'warning').map(v => ({
|
|
2676
|
-
type: 'app-discovery',
|
|
2677
|
-
file: v.file,
|
|
2678
|
-
message: v.issue || v.reason,
|
|
2679
|
-
recommendation: v.recommendation
|
|
2680
|
-
}))
|
|
2681
|
-
];
|
|
2682
|
-
|
|
2683
|
-
const suggestions = [
|
|
2684
|
-
...allViolations.suggestions.map(v => ({
|
|
2685
|
-
type: 'suggestion',
|
|
2686
|
-
file: v.file,
|
|
2687
|
-
message: v.suggestion
|
|
2688
|
-
})),
|
|
2689
|
-
...allViolations.appDiscoveryIssues.filter(v => v.severity === 'info').map(v => ({
|
|
2690
|
-
type: 'app-discovery',
|
|
2691
|
-
file: v.file,
|
|
2692
|
-
message: v.issue || v.reason,
|
|
2693
|
-
recommendation: v.recommendation
|
|
2694
|
-
}))
|
|
2695
|
-
];
|
|
2696
|
-
|
|
2697
|
-
return {
|
|
2698
|
-
issues,
|
|
2699
|
-
warnings,
|
|
2700
|
-
suggestions,
|
|
2701
|
-
violations: allViolations
|
|
2702
|
-
};
|
|
2703
|
-
}
|
|
2704
|
-
};
|
|
2705
|
-
|
|
2706
|
-
module.exports = complianceCheck;
|