@jmruthers/pace-core 0.6.4 → 0.6.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +104 -0
- package/README.md +5 -403
- package/core-usage-manifest.json +93 -0
- package/cursor-rules/00-pace-core-compliance.mdc +128 -26
- package/cursor-rules/01-standards-compliance.mdc +49 -8
- package/cursor-rules/02-project-structure.mdc +6 -0
- package/cursor-rules/03-solid-principles.mdc +2 -0
- package/cursor-rules/04-testing-standards.mdc +2 -0
- package/cursor-rules/05-bug-reports-and-features.mdc +2 -0
- package/cursor-rules/06-code-quality.mdc +2 -0
- package/cursor-rules/07-tech-stack-compliance.mdc +2 -0
- package/cursor-rules/08-markup-quality.mdc +52 -27
- package/cursor-rules/09-rbac-compliance.mdc +462 -0
- package/cursor-rules/10-error-handling-patterns.mdc +179 -0
- package/cursor-rules/11-performance-optimization.mdc +169 -0
- package/cursor-rules/12-ci-cd-integration.mdc +150 -0
- package/dist/{AuthService-Cb34EQs3.d.ts → AuthService-DmfO5rGS.d.ts} +10 -0
- package/dist/{DataTable-BMRU8a1j.d.ts → DataTable-2N_tqbfq.d.ts} +1 -1
- package/dist/DataTable-LRJL4IRV.js +15 -0
- package/dist/{PublicPageProvider-DEMpysFR.d.ts → PublicPageProvider-BBH6Vqg7.d.ts} +72 -139
- package/dist/UnifiedAuthProvider-ZT6TIGM7.js +7 -0
- package/dist/api-Y4MQWOFW.js +4 -0
- package/dist/audit-MYQXYZFU.js +3 -0
- package/dist/{chunk-J36DSWQK.js → chunk-2HGJFNAH.js} +8 -28
- package/dist/{chunk-OEWDTMG7.js → chunk-3O3WHILE.js} +38 -121
- package/dist/{chunk-M43Y4SSO.js → chunk-3QC3KRHK.js} +1 -14
- package/dist/{chunk-DGUM43GV.js → chunk-3RG5ZIWI.js} +1 -4
- package/dist/{chunk-QXHPKYJV.js → chunk-4SXLQIZO.js} +1 -26
- package/dist/chunk-4T7OBVTU.js +62 -0
- package/dist/{chunk-E66EQZE6.js → chunk-6GLLNA6U.js} +3 -9
- package/dist/{chunk-ZSAAAMVR.js → chunk-6QYDGKQY.js} +1 -4
- package/dist/{chunk-NN6WWZ5U.js → chunk-7TYHROIV.js} +579 -563
- package/dist/{chunk-M7MPQISP.js → chunk-A55DK444.js} +9 -16
- package/dist/{chunk-63FOKYGO.js → chunk-AHU7G2R5.js} +2 -11
- package/dist/{chunk-L4OXEN46.js → chunk-BVP2BCJF.js} +2 -16
- package/dist/chunk-C7NSAPTL.js +1 -0
- package/dist/{chunk-YKRAFF5K.js → chunk-FENMYN2U.js} +73 -149
- package/dist/{chunk-AVMLPIM7.js → chunk-FTCRZOG2.js} +284 -432
- package/dist/{chunk-G37KK66H.js → chunk-FYHN4DD5.js} +60 -19
- package/dist/{chunk-VBXEHIUJ.js → chunk-HF6O3O37.js} +6 -88
- package/dist/{chunk-I6DAQMWX.js → chunk-LAZMKTTF.js} +930 -891
- package/dist/{chunk-5EC5MEWX.js → chunk-MAGBIDNS.js} +77 -222
- package/dist/chunk-MBADTM7L.js +64 -0
- package/dist/chunk-OHIK3MIO.js +994 -0
- package/dist/{chunk-6SOIHG6Z.js → chunk-S7DKJPLT.js} +115 -44
- package/dist/{chunk-FMUCXFII.js → chunk-SD6WQY43.js} +1 -5
- package/dist/{chunk-PWLANIRT.js → chunk-TTRFSOKR.js} +1 -7
- package/dist/{chunk-5DRSZLL2.js → chunk-UH3NTO3F.js} +1 -6
- package/dist/{chunk-FFQEQTNW.js → chunk-UIYSCEV7.js} +134 -45
- package/dist/{chunk-3LPHPB62.js → chunk-ZFYPMX46.js} +271 -87
- package/dist/{chunk-7JPAB3T5.js → chunk-ZS5VO5JB.js} +1989 -1283
- package/dist/components.d.ts +6 -6
- package/dist/components.js +57 -267
- package/dist/{database.generated-CzIvgcPu.d.ts → database.generated-CcnC_DRc.d.ts} +4795 -3691
- package/dist/eslint-rules/index.cjs +22 -0
- package/dist/eslint-rules/rules/compliance.cjs +348 -0
- package/dist/eslint-rules/rules/components.cjs +113 -0
- package/dist/eslint-rules/rules/imports.cjs +102 -0
- package/dist/eslint-rules/rules/rbac.cjs +790 -0
- package/dist/eslint-rules/utils/helpers.cjs +42 -0
- package/dist/eslint-rules/utils/manifest-loader.cjs +75 -0
- package/dist/hooks.d.ts +5 -5
- package/dist/hooks.js +62 -270
- package/dist/icons/index.d.ts +1 -0
- package/dist/icons/index.js +1 -0
- package/dist/index.d.ts +36 -26
- package/dist/index.js +87 -690
- package/dist/providers.d.ts +2 -2
- package/dist/providers.js +8 -35
- package/dist/rbac/eslint-rules.d.ts +46 -44
- package/dist/rbac/eslint-rules.js +7 -4
- package/dist/rbac/index.d.ts +124 -594
- package/dist/rbac/index.js +14 -207
- package/dist/styles/index.js +2 -12
- package/dist/theming/runtime.js +3 -19
- package/dist/{timezone-CHhWg6b4.d.ts → timezone-BZe_eUxx.d.ts} +175 -1
- package/dist/{types-CkbwOr4Y.d.ts → types-B-K_5VnO.d.ts} +4 -0
- package/dist/types-t9H8qKRw.d.ts +55 -0
- package/dist/types.d.ts +1 -1
- package/dist/types.js +7 -94
- package/dist/{usePublicRouteParams-i3qtoBgg.d.ts → usePublicRouteParams-COZ28Mvq.d.ts} +9 -9
- package/dist/utils.d.ts +24 -117
- package/dist/utils.js +54 -392
- package/docs/README.md +16 -6
- package/docs/api/README.md +4 -402
- package/docs/api/modules.md +454 -930
- package/docs/api-reference/components.md +3 -1
- package/docs/api-reference/deprecated.md +31 -6
- package/docs/api-reference/rpc-functions.md +78 -3
- package/docs/best-practices/accessibility.md +6 -3
- package/docs/getting-started/cursor-rules.md +3 -23
- package/docs/getting-started/dependencies.md +650 -0
- package/docs/getting-started/installation-guide.md +20 -7
- package/docs/getting-started/quick-start.md +23 -12
- package/docs/implementation-guides/permission-enforcement.md +4 -0
- package/docs/rbac/MIGRATION_GUIDE.md +819 -0
- package/docs/rbac/RBAC_CONTRACT.md +724 -0
- package/docs/rbac/README.md +12 -3
- package/docs/rbac/edge-functions-guide.md +376 -0
- package/docs/rbac/secure-client-protection.md +0 -34
- package/docs/standards/00-pace-core-compliance.md +967 -0
- package/docs/standards/01-standards-compliance.md +188 -0
- package/docs/standards/02-project-structure.md +985 -0
- package/docs/standards/03-solid-principles.md +39 -0
- package/docs/standards/04-testing-standards.md +36 -0
- package/docs/standards/05-bug-reports-and-features.md +27 -0
- package/docs/standards/{04-code-style-standard.md → 06-code-quality.md} +2 -0
- package/docs/standards/07-tech-stack-compliance.md +30 -0
- package/docs/standards/08-markup-quality.md +345 -0
- package/docs/standards/{07-rbac-and-rls-standard.md → 09-rbac-compliance.md} +149 -54
- package/docs/standards/10-error-handling-patterns.md +401 -0
- package/docs/standards/11-performance-optimization.md +348 -0
- package/docs/standards/12-ci-cd-integration.md +370 -0
- package/docs/standards/ALIGNMENT_REVIEW_SUMMARY.md +192 -0
- package/docs/standards/README.md +62 -33
- package/docs/troubleshooting/organisation-context-setup.md +42 -19
- package/eslint-config-pace-core.cjs +20 -4
- package/package.json +31 -21
- package/scripts/audit/audit-compliance.cjs +1295 -0
- package/scripts/audit/audit-components.cjs +260 -0
- package/scripts/audit/audit-dependencies.cjs +395 -0
- package/scripts/audit/audit-rbac.cjs +954 -0
- package/scripts/audit/audit-standards.cjs +1268 -0
- package/scripts/audit/index.cjs +1898 -194
- package/scripts/install-cursor-rules.cjs +259 -8
- package/scripts/validate-master.js +1 -1
- package/src/__tests__/fixtures/supabase.ts +1 -1
- package/src/__tests__/helpers/__tests__/component-test-utils.test.tsx +1 -1
- package/src/__tests__/helpers/__tests__/optimized-test-setup.test.ts +1 -1
- package/src/__tests__/helpers/__tests__/supabaseMock.test.ts +1 -1
- package/src/__tests__/helpers/__tests__/test-utils.test.tsx +3 -3
- package/src/__tests__/helpers/component-test-utils.tsx +1 -1
- package/src/__tests__/helpers/supabaseMock.ts +2 -2
- package/src/__tests__/public-recipe-view.test.ts +38 -9
- package/src/components/Button/Button.tsx +5 -1
- package/src/components/ContextSelector/ContextSelector.tsx +42 -39
- package/src/components/DataTable/__tests__/keyboard.test.tsx +15 -2
- package/src/components/DataTable/components/DataTableBody.tsx +55 -31
- package/src/components/DataTable/components/DataTableCore.tsx +186 -13
- package/src/components/DataTable/components/DataTableLayout.tsx +30 -5
- package/src/components/DataTable/components/EditFields.tsx +23 -3
- package/src/components/DataTable/components/EditableRow.tsx +7 -2
- package/src/components/DataTable/components/ImportModal.tsx +4 -6
- package/src/components/DataTable/components/RowComponent.tsx +12 -0
- package/src/components/DataTable/components/ViewRowModal.tsx +4 -4
- package/src/components/DataTable/components/__tests__/ImportModal.test.tsx +455 -96
- package/src/components/DataTable/components/__tests__/ViewRowModal.test.tsx +122 -58
- package/src/components/DataTable/components/hooks/usePermissionTracking.ts +0 -4
- package/src/components/DataTable/core/DataTableContext.tsx +1 -1
- package/src/components/DataTable/hooks/__tests__/useDataTableState.test.ts +51 -47
- package/src/components/DataTable/hooks/useDataTablePermissions.ts +24 -21
- package/src/components/DataTable/hooks/useDataTableState.ts +125 -9
- package/src/components/DataTable/hooks/useTableColumns.ts +40 -2
- package/src/components/DataTable/hooks/useTableHandlers.ts +11 -0
- package/src/components/DataTable/types.ts +5 -0
- package/src/components/DateTimeField/DateTimeField.tsx +20 -20
- package/src/components/DateTimeField/README.md +5 -2
- package/src/components/Dialog/Dialog.test.tsx +361 -318
- package/src/components/Dialog/Dialog.tsx +1154 -323
- package/src/components/Dialog/index.ts +3 -3
- package/src/components/FileDisplay/FileDisplay.test.tsx +45 -2
- package/src/components/FileDisplay/FileDisplay.tsx +28 -22
- package/src/components/Form/Form.test.tsx +9 -10
- package/src/components/Form/Form.tsx +369 -9
- package/src/components/InactivityWarningModal/InactivityWarningModal.test.tsx +28 -28
- package/src/components/InactivityWarningModal/InactivityWarningModal.tsx +40 -54
- package/src/components/LoginForm/LoginForm.tsx +2 -2
- package/src/components/NavigationMenu/NavigationMenu.test.tsx +14 -13
- package/src/components/NavigationMenu/NavigationMenu.tsx +2 -2
- package/src/components/NavigationMenu/useNavigationFiltering.ts +11 -21
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +6 -4
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +30 -41
- package/src/components/PaceAppLayout/README.md +10 -9
- package/src/components/PaceAppLayout/test-setup.tsx +40 -31
- package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +108 -61
- package/src/components/PaceLoginPage/PaceLoginPage.tsx +27 -3
- package/src/components/PasswordChange/PasswordChangeForm.test.tsx +61 -0
- package/src/components/PasswordChange/PasswordChangeForm.tsx +20 -13
- package/src/components/PublicLayout/PublicLayout.test.tsx +7 -3
- package/src/components/PublicLayout/PublicPageLayout.tsx +5 -8
- package/src/components/Select/Select.tsx +23 -21
- package/src/components/Select/types.ts +1 -1
- package/src/components/UserMenu/UserMenu.test.tsx +38 -6
- package/src/components/UserMenu/UserMenu.tsx +39 -34
- package/src/components/index.ts +3 -4
- package/src/eslint-rules/index.cjs +22 -0
- package/src/eslint-rules/rules/compliance.cjs +348 -0
- package/src/eslint-rules/rules/components.cjs +113 -0
- package/src/eslint-rules/rules/imports.cjs +102 -0
- package/src/eslint-rules/rules/rbac.cjs +790 -0
- package/src/eslint-rules/utils/helpers.cjs +42 -0
- package/src/eslint-rules/utils/manifest-loader.cjs +75 -0
- package/src/hooks/__tests__/hooks.integration.test.tsx +6 -8
- package/src/hooks/__tests__/useAppConfig.unit.test.ts +129 -67
- package/src/hooks/__tests__/usePublicEvent.simple.test.ts +149 -67
- package/src/hooks/__tests__/usePublicEvent.test.ts +149 -79
- package/src/hooks/__tests__/usePublicEvent.unit.test.ts +158 -109
- package/src/hooks/__tests__/useSessionDraft.test.ts +163 -0
- package/src/hooks/__tests__/useSessionRestoration.unit.test.tsx +10 -5
- package/src/hooks/public/usePublicEvent.ts +62 -190
- package/src/hooks/public/usePublicEventLogo.test.ts +70 -17
- package/src/hooks/public/usePublicEventLogo.ts +19 -9
- package/src/hooks/useAppConfig.ts +26 -24
- package/src/hooks/useEventTheme.test.ts +211 -233
- package/src/hooks/useEventTheme.ts +19 -28
- package/src/hooks/useEvents.ts +11 -7
- package/src/hooks/useKeyboardShortcuts.ts +1 -1
- package/src/hooks/useOrganisationPermissions.ts +9 -11
- package/src/hooks/useOrganisations.ts +13 -7
- package/src/hooks/useQueryCache.ts +0 -1
- package/src/hooks/useSessionDraft.ts +380 -0
- package/src/hooks/useSessionRestoration.ts +3 -1
- package/src/icons/index.ts +27 -0
- package/src/index.ts +16 -1
- package/src/providers/OrganisationProvider.tsx +23 -14
- package/src/providers/services/EventServiceProvider.tsx +1 -24
- package/src/providers/services/UnifiedAuthProvider.tsx +5 -48
- package/src/providers/services/__tests__/UnifiedAuthProvider.integration.test.tsx +3 -0
- package/src/rbac/README.md +20 -20
- package/src/rbac/__tests__/adapters.comprehensive.test.tsx +7 -457
- package/src/rbac/__tests__/auth-rbac.e2e.test.tsx +33 -7
- package/src/rbac/adapters.tsx +7 -295
- package/src/rbac/api.test.ts +44 -56
- package/src/rbac/api.ts +10 -17
- package/src/rbac/cache-invalidation.ts +0 -1
- package/src/rbac/compliance/index.ts +10 -0
- package/src/rbac/compliance/pattern-detector.ts +553 -0
- package/src/rbac/compliance/runtime-compliance.ts +22 -0
- package/src/rbac/components/AccessDenied.tsx +150 -0
- package/src/rbac/components/NavigationGuard.tsx +12 -20
- package/src/rbac/components/PagePermissionGuard.tsx +4 -24
- package/src/rbac/components/__tests__/NavigationGuard.test.tsx +21 -8
- package/src/rbac/components/index.ts +3 -41
- package/src/rbac/eslint-rules.js +1 -1
- package/src/rbac/hooks/index.ts +0 -3
- package/src/rbac/hooks/permissions/index.ts +0 -3
- package/src/rbac/hooks/permissions/useAccessLevel.ts +4 -8
- package/src/rbac/hooks/usePermissions.ts +0 -3
- package/src/rbac/hooks/useRBAC.test.ts +21 -3
- package/src/rbac/hooks/useRBAC.ts +4 -3
- package/src/rbac/hooks/useResolvedScope.test.ts +57 -47
- package/src/rbac/hooks/useResolvedScope.ts +58 -140
- package/src/rbac/hooks/useResourcePermissions.test.ts +241 -60
- package/src/rbac/hooks/useResourcePermissions.ts +182 -63
- package/src/rbac/hooks/useRoleManagement.test.ts +65 -22
- package/src/rbac/hooks/useRoleManagement.ts +147 -19
- package/src/rbac/hooks/useSecureSupabase.ts +4 -8
- package/src/rbac/index.ts +7 -9
- package/src/rbac/permissions.ts +17 -17
- package/src/rbac/utils/contextValidator.ts +45 -7
- package/src/services/AuthService.ts +132 -23
- package/src/services/EventService.ts +4 -97
- package/src/services/InactivityService.ts +155 -58
- package/src/services/OrganisationService.ts +7 -44
- package/src/services/__tests__/OrganisationService.test.ts +26 -8
- package/src/services/base/BaseService.ts +0 -3
- package/src/styles/core.css +4 -0
- package/src/types/database.generated.ts +4733 -3809
- package/src/utils/__tests__/organisationContext.unit.test.ts +9 -10
- package/src/utils/context/organisationContext.test.ts +13 -28
- package/src/utils/context/organisationContext.ts +21 -52
- package/src/utils/dynamic/dynamicUtils.ts +1 -1
- package/src/utils/file-reference/index.ts +39 -15
- package/src/utils/formatting/formatDateTime.test.ts +3 -2
- package/src/utils/formatting/formatTime.test.ts +3 -2
- package/src/utils/google-places/loadGoogleMapsScript.ts +29 -4
- package/src/utils/index.ts +4 -1
- package/src/utils/persistence/__tests__/keyDerivation.test.ts +135 -0
- package/src/utils/persistence/__tests__/sensitiveFieldDetection.test.ts +123 -0
- package/src/utils/persistence/keyDerivation.ts +304 -0
- package/src/utils/persistence/sensitiveFieldDetection.ts +212 -0
- package/src/utils/security/secureStorage.ts +5 -5
- package/src/utils/storage/helpers.ts +3 -3
- package/src/utils/supabase/createBaseClient.ts +147 -0
- package/src/utils/timezone/timezone.test.ts +1 -2
- package/src/utils/timezone/timezone.ts +1 -1
- package/src/utils/validation/csrf.ts +4 -4
- package/cursor-rules/CHANGELOG.md +0 -119
- package/cursor-rules/README.md +0 -192
- package/dist/DataTable-E7YQZD7D.js +0 -175
- package/dist/DataTable-E7YQZD7D.js.map +0 -1
- package/dist/UnifiedAuthProvider-QPXO24B4.js +0 -18
- package/dist/UnifiedAuthProvider-QPXO24B4.js.map +0 -1
- package/dist/api-6LVZTHDS.js +0 -52
- package/dist/api-6LVZTHDS.js.map +0 -1
- package/dist/audit-V53FV5AG.js +0 -17
- package/dist/audit-V53FV5AG.js.map +0 -1
- package/dist/chunk-36LVWXB2.js +0 -227
- package/dist/chunk-36LVWXB2.js.map +0 -1
- package/dist/chunk-3LPHPB62.js.map +0 -1
- package/dist/chunk-5DRSZLL2.js.map +0 -1
- package/dist/chunk-5EC5MEWX.js.map +0 -1
- package/dist/chunk-63FOKYGO.js.map +0 -1
- package/dist/chunk-6SOIHG6Z.js.map +0 -1
- package/dist/chunk-7JPAB3T5.js.map +0 -1
- package/dist/chunk-ATKZM7RX.js +0 -2053
- package/dist/chunk-ATKZM7RX.js.map +0 -1
- package/dist/chunk-AVMLPIM7.js.map +0 -1
- package/dist/chunk-DGUM43GV.js.map +0 -1
- package/dist/chunk-E66EQZE6.js.map +0 -1
- package/dist/chunk-FFQEQTNW.js.map +0 -1
- package/dist/chunk-FMUCXFII.js.map +0 -1
- package/dist/chunk-G37KK66H.js.map +0 -1
- package/dist/chunk-I6DAQMWX.js.map +0 -1
- package/dist/chunk-J36DSWQK.js.map +0 -1
- package/dist/chunk-KQCRWDSA.js +0 -1
- package/dist/chunk-KQCRWDSA.js.map +0 -1
- package/dist/chunk-L4OXEN46.js.map +0 -1
- package/dist/chunk-LMC26NLJ.js +0 -84
- package/dist/chunk-LMC26NLJ.js.map +0 -1
- package/dist/chunk-M43Y4SSO.js.map +0 -1
- package/dist/chunk-M7MPQISP.js.map +0 -1
- package/dist/chunk-NN6WWZ5U.js.map +0 -1
- package/dist/chunk-OEWDTMG7.js.map +0 -1
- package/dist/chunk-PWLANIRT.js.map +0 -1
- package/dist/chunk-QXHPKYJV.js.map +0 -1
- package/dist/chunk-VBXEHIUJ.js.map +0 -1
- package/dist/chunk-YKRAFF5K.js.map +0 -1
- package/dist/chunk-ZSAAAMVR.js.map +0 -1
- package/dist/components.js.map +0 -1
- package/dist/contextValidator-OOPCLPZW.js +0 -9
- package/dist/contextValidator-OOPCLPZW.js.map +0 -1
- package/dist/eslint-rules/pace-core-compliance.cjs +0 -510
- package/dist/hooks.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/providers.js.map +0 -1
- package/dist/rbac/eslint-rules.js.map +0 -1
- package/dist/rbac/index.js.map +0 -1
- package/dist/styles/index.js.map +0 -1
- package/dist/theming/runtime.js.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/utils.js.map +0 -1
- package/docs/standards/01-architecture-standard.md +0 -44
- package/docs/standards/02-api-and-rpc-standard.md +0 -39
- package/docs/standards/03-component-standard.md +0 -32
- package/docs/standards/05-security-standard.md +0 -44
- package/docs/standards/06-testing-and-docs-standard.md +0 -29
- package/docs/standards/pace-core-compliance.md +0 -432
- package/scripts/audit/core/checks/accessibility.cjs +0 -197
- package/scripts/audit/core/checks/api-usage.cjs +0 -191
- package/scripts/audit/core/checks/bundle.cjs +0 -142
- package/scripts/audit/core/checks/compliance.cjs +0 -2706
- package/scripts/audit/core/checks/config.cjs +0 -54
- package/scripts/audit/core/checks/coverage.cjs +0 -84
- package/scripts/audit/core/checks/dependencies.cjs +0 -994
- package/scripts/audit/core/checks/documentation.cjs +0 -268
- package/scripts/audit/core/checks/environment.cjs +0 -116
- package/scripts/audit/core/checks/error-handling.cjs +0 -340
- package/scripts/audit/core/checks/forms.cjs +0 -172
- package/scripts/audit/core/checks/heuristics.cjs +0 -68
- package/scripts/audit/core/checks/hooks.cjs +0 -334
- package/scripts/audit/core/checks/imports.cjs +0 -244
- package/scripts/audit/core/checks/performance.cjs +0 -325
- package/scripts/audit/core/checks/routes.cjs +0 -117
- package/scripts/audit/core/checks/state.cjs +0 -130
- package/scripts/audit/core/checks/structure.cjs +0 -65
- package/scripts/audit/core/checks/style.cjs +0 -584
- package/scripts/audit/core/checks/testing.cjs +0 -122
- package/scripts/audit/core/checks/typescript.cjs +0 -61
- package/scripts/audit/core/scanner.cjs +0 -199
- package/scripts/audit/core/utils.cjs +0 -137
- package/scripts/audit/reporters/console.cjs +0 -151
- package/scripts/audit/reporters/json.cjs +0 -54
- package/scripts/audit/reporters/markdown.cjs +0 -124
- package/scripts/audit-consuming-app.cjs +0 -86
- package/src/eslint-rules/pace-core-compliance.cjs +0 -510
- package/src/eslint-rules/pace-core-compliance.js +0 -638
- package/src/rbac/components/EnhancedNavigationMenu.test.tsx +0 -555
- package/src/rbac/components/EnhancedNavigationMenu.tsx +0 -293
- package/src/rbac/components/NavigationProvider.test.tsx +0 -481
- package/src/rbac/components/NavigationProvider.tsx +0 -345
- package/src/rbac/components/PagePermissionProvider.test.tsx +0 -476
- package/src/rbac/components/PagePermissionProvider.tsx +0 -279
- package/src/rbac/components/PermissionEnforcer.tsx +0 -312
- package/src/rbac/components/RoleBasedRouter.tsx +0 -440
- package/src/rbac/components/SecureDataProvider.test.tsx +0 -543
- package/src/rbac/components/SecureDataProvider.tsx +0 -339
- package/src/rbac/components/__tests__/EnhancedNavigationMenu.test.tsx +0 -620
- package/src/rbac/components/__tests__/NavigationProvider.test.tsx +0 -726
- package/src/rbac/components/__tests__/PagePermissionProvider.test.tsx +0 -661
- package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +0 -881
- package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +0 -783
- package/src/rbac/components/__tests__/SecureDataProvider.fixed.test.tsx +0 -645
- package/src/rbac/components/__tests__/SecureDataProvider.test.tsx +0 -659
- package/src/rbac/hooks/permissions/useCachedPermissions.ts +0 -79
- package/src/rbac/hooks/permissions/useHasAllPermissions.ts +0 -90
- package/src/rbac/hooks/permissions/useHasAnyPermission.ts +0 -90
|
@@ -0,0 +1,1268 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Standards Compliance Audit Script
|
|
5
|
+
*
|
|
6
|
+
* Audits consuming apps for compliance with pace-core standards from 01-standards-compliance.mdc.
|
|
7
|
+
* Checks for:
|
|
8
|
+
* - Cursor ruleset compliance
|
|
9
|
+
* - TypeScript configuration
|
|
10
|
+
* - Naming conventions
|
|
11
|
+
* - RLS policy compliance (SQL migrations)
|
|
12
|
+
* - RPC naming conventions (SQL migrations)
|
|
13
|
+
* - Testing configuration
|
|
14
|
+
* - Input validation (heuristic)
|
|
15
|
+
* - Logging security (heuristic)
|
|
16
|
+
* - Error message safety (heuristic)
|
|
17
|
+
*
|
|
18
|
+
* Usage:
|
|
19
|
+
* const { runStandardsAudit } = require('./audit-standards.cjs');
|
|
20
|
+
* const issues = runStandardsAudit(consumingAppPath);
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const path = require('path');
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Recursively find all TypeScript/JavaScript files in a directory
|
|
28
|
+
*/
|
|
29
|
+
function findSourceFiles(dir, fileList = []) {
|
|
30
|
+
if (!fs.existsSync(dir)) {
|
|
31
|
+
return fileList;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const files = fs.readdirSync(dir);
|
|
35
|
+
|
|
36
|
+
files.forEach(file => {
|
|
37
|
+
const filePath = path.join(dir, file);
|
|
38
|
+
const stat = fs.statSync(filePath);
|
|
39
|
+
|
|
40
|
+
if (stat.isDirectory()) {
|
|
41
|
+
// Skip node_modules, dist, build, .git, etc.
|
|
42
|
+
if (!['node_modules', 'dist', 'build', '.git', '.next', '.vite', 'coverage', '.turbo'].includes(file)) {
|
|
43
|
+
findSourceFiles(filePath, fileList);
|
|
44
|
+
}
|
|
45
|
+
} else if (/\.(ts|tsx|js|jsx)$/.test(file)) {
|
|
46
|
+
fileList.push(filePath);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return fileList;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Recursively find all SQL files in a directory
|
|
55
|
+
*/
|
|
56
|
+
function findSQLFiles(dir, fileList = []) {
|
|
57
|
+
if (!fs.existsSync(dir)) {
|
|
58
|
+
return fileList;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const files = fs.readdirSync(dir);
|
|
62
|
+
|
|
63
|
+
files.forEach(file => {
|
|
64
|
+
const filePath = path.join(dir, file);
|
|
65
|
+
const stat = fs.statSync(filePath);
|
|
66
|
+
|
|
67
|
+
if (stat.isDirectory()) {
|
|
68
|
+
// Skip node_modules, dist, build, .git, etc.
|
|
69
|
+
if (!['node_modules', 'dist', 'build', '.git', '.next', '.vite', 'coverage', '.turbo'].includes(file)) {
|
|
70
|
+
findSQLFiles(filePath, fileList);
|
|
71
|
+
}
|
|
72
|
+
} else if (/\.sql$/.test(file)) {
|
|
73
|
+
fileList.push(filePath);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return fileList;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get line number from index in content
|
|
82
|
+
*/
|
|
83
|
+
function getLineNumber(content, index) {
|
|
84
|
+
return content.substring(0, index).split('\n').length;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get code snippet around a match for context
|
|
89
|
+
*/
|
|
90
|
+
function getCodeSnippet(content, index, before = 30, after = 50) {
|
|
91
|
+
const start = Math.max(0, index - before);
|
|
92
|
+
const end = Math.min(content.length, index + after);
|
|
93
|
+
return content.substring(start, end).trim();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Check if content is in a comment or string
|
|
98
|
+
*/
|
|
99
|
+
function isInCommentOrString(content, index) {
|
|
100
|
+
const before = content.substring(0, index);
|
|
101
|
+
|
|
102
|
+
// Check for line comments
|
|
103
|
+
const lastLineComment = before.lastIndexOf('--');
|
|
104
|
+
const lastNewline = before.lastIndexOf('\n');
|
|
105
|
+
if (lastLineComment > lastNewline && !before.substring(lastLineComment, index).includes('\n')) {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Check for block comments
|
|
110
|
+
const lastBlockCommentStart = before.lastIndexOf('/*');
|
|
111
|
+
const lastBlockCommentEnd = before.lastIndexOf('*/');
|
|
112
|
+
if (lastBlockCommentStart > lastBlockCommentEnd) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Check for string literals (simple check)
|
|
117
|
+
const singleQuoteMatches = [...before.matchAll(/'/g)];
|
|
118
|
+
const doubleQuoteMatches = [...before.matchAll(/"/g)];
|
|
119
|
+
|
|
120
|
+
const inSingleQuote = singleQuoteMatches.length % 2 === 1;
|
|
121
|
+
const inDoubleQuote = doubleQuoteMatches.length % 2 === 1;
|
|
122
|
+
|
|
123
|
+
return inSingleQuote || inDoubleQuote;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Check for cursor ruleset compliance
|
|
128
|
+
*/
|
|
129
|
+
function checkCursorRuleset(consumingAppPath) {
|
|
130
|
+
const issues = [];
|
|
131
|
+
|
|
132
|
+
const requiredRules = [
|
|
133
|
+
'00-pace-core-compliance.mdc',
|
|
134
|
+
'01-standards-compliance.mdc',
|
|
135
|
+
'02-project-structure.mdc',
|
|
136
|
+
'03-solid-principles.mdc',
|
|
137
|
+
'06-code-quality.mdc',
|
|
138
|
+
'07-tech-stack-compliance.mdc',
|
|
139
|
+
'08-markup-quality.mdc',
|
|
140
|
+
'09-rbac-compliance.mdc',
|
|
141
|
+
'10-error-handling-patterns.mdc',
|
|
142
|
+
'11-performance-optimization.mdc',
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
const rulesDir = path.join(consumingAppPath, '.cursor', 'rules');
|
|
146
|
+
|
|
147
|
+
if (!fs.existsSync(rulesDir)) {
|
|
148
|
+
issues.push({
|
|
149
|
+
type: 'cursorRuleset',
|
|
150
|
+
file: '.cursor/rules (not found)',
|
|
151
|
+
line: 0,
|
|
152
|
+
message: '.cursor/rules directory not found. Must include complete pace-core ruleset.',
|
|
153
|
+
code: '',
|
|
154
|
+
severity: 'error',
|
|
155
|
+
fix: 'Create .cursor/rules directory and copy required rule files from pace-core.',
|
|
156
|
+
});
|
|
157
|
+
return issues;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
requiredRules.forEach(ruleFile => {
|
|
161
|
+
const rulePath = path.join(rulesDir, ruleFile);
|
|
162
|
+
if (!fs.existsSync(rulePath)) {
|
|
163
|
+
issues.push({
|
|
164
|
+
type: 'cursorRuleset',
|
|
165
|
+
file: `.cursor/rules/${ruleFile}`,
|
|
166
|
+
line: 0,
|
|
167
|
+
message: `Required rule file '${ruleFile}' not found. Must include complete pace-core ruleset.`,
|
|
168
|
+
code: '',
|
|
169
|
+
severity: 'error',
|
|
170
|
+
fix: `Copy ${ruleFile} from pace-core to .cursor/rules/ directory.`,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
return issues;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Check TypeScript configuration
|
|
180
|
+
*/
|
|
181
|
+
function checkTypeScriptConfig(consumingAppPath) {
|
|
182
|
+
const issues = [];
|
|
183
|
+
|
|
184
|
+
const tsconfigPaths = [
|
|
185
|
+
path.join(consumingAppPath, 'tsconfig.json'),
|
|
186
|
+
path.join(consumingAppPath, 'tsconfig.app.json'),
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
let tsconfigPath = null;
|
|
190
|
+
let tsconfig = null;
|
|
191
|
+
|
|
192
|
+
for (const configPath of tsconfigPaths) {
|
|
193
|
+
if (fs.existsSync(configPath)) {
|
|
194
|
+
tsconfigPath = configPath;
|
|
195
|
+
try {
|
|
196
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
197
|
+
tsconfig = JSON.parse(content);
|
|
198
|
+
break;
|
|
199
|
+
} catch (e) {
|
|
200
|
+
// Skip if can't parse
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (!tsconfigPath || !tsconfig) {
|
|
206
|
+
issues.push({
|
|
207
|
+
type: 'typescriptConfig',
|
|
208
|
+
file: 'tsconfig.json (not found)',
|
|
209
|
+
line: 0,
|
|
210
|
+
message: 'tsconfig.json not found or invalid. TypeScript strict mode must be enabled.',
|
|
211
|
+
code: '',
|
|
212
|
+
severity: 'error',
|
|
213
|
+
fix: 'Create tsconfig.json with "strict": true in compilerOptions.',
|
|
214
|
+
});
|
|
215
|
+
return issues;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const relativePath = path.relative(consumingAppPath || process.cwd(), tsconfigPath);
|
|
219
|
+
const compilerOptions = tsconfig.compilerOptions || {};
|
|
220
|
+
|
|
221
|
+
// Check for strict mode
|
|
222
|
+
if (compilerOptions.strict !== true) {
|
|
223
|
+
issues.push({
|
|
224
|
+
type: 'typescriptConfig',
|
|
225
|
+
file: relativePath,
|
|
226
|
+
line: 1,
|
|
227
|
+
message: 'TypeScript strict mode not enabled. Must set "strict": true in compilerOptions.',
|
|
228
|
+
code: JSON.stringify(compilerOptions, null, 2).substring(0, 100),
|
|
229
|
+
severity: 'error',
|
|
230
|
+
fix: 'Set "strict": true in tsconfig.json compilerOptions.',
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Check for noImplicitAny
|
|
235
|
+
if (compilerOptions.noImplicitAny !== true && compilerOptions.strict !== true) {
|
|
236
|
+
issues.push({
|
|
237
|
+
type: 'typescriptConfig',
|
|
238
|
+
file: relativePath,
|
|
239
|
+
line: 1,
|
|
240
|
+
message: 'noImplicitAny not enabled. Should set "noImplicitAny": true in compilerOptions (or enable strict mode).',
|
|
241
|
+
code: JSON.stringify(compilerOptions, null, 2).substring(0, 100),
|
|
242
|
+
severity: 'error',
|
|
243
|
+
fix: 'Set "noImplicitAny": true in tsconfig.json compilerOptions (or enable strict mode).',
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return issues;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Check for any types in code (with cached file list)
|
|
252
|
+
*/
|
|
253
|
+
function checkAnyTypesWithCache(consumingAppPath, cachedSourceFiles) {
|
|
254
|
+
const issues = [];
|
|
255
|
+
|
|
256
|
+
cachedSourceFiles.forEach(filePath => {
|
|
257
|
+
try {
|
|
258
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
259
|
+
|
|
260
|
+
// Patterns to match any usage
|
|
261
|
+
const anyPatterns = [
|
|
262
|
+
/:\s*any\b/g, // : any
|
|
263
|
+
/\bas\s+any\b/g, // as any
|
|
264
|
+
/<\s*any\s*>/g, // <any>
|
|
265
|
+
/:\s*any\[\]/g, // : any[]
|
|
266
|
+
/Array\s*<\s*any\s*>/g, // Array<any>
|
|
267
|
+
];
|
|
268
|
+
|
|
269
|
+
anyPatterns.forEach(pattern => {
|
|
270
|
+
let match;
|
|
271
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
272
|
+
if (isInCommentOrString(content, match.index)) {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const relativePath = path.relative(consumingAppPath || process.cwd(), filePath);
|
|
277
|
+
issues.push({
|
|
278
|
+
type: 'anyTypes',
|
|
279
|
+
file: relativePath,
|
|
280
|
+
line: getLineNumber(content, match.index),
|
|
281
|
+
message: 'any type detected. Use unknown or proper types instead.',
|
|
282
|
+
code: getCodeSnippet(content, match.index),
|
|
283
|
+
severity: 'warning',
|
|
284
|
+
fix: 'Replace any with unknown or define proper types.',
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
} catch (error) {
|
|
289
|
+
// Skip files that can't be read
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
return issues;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Check naming conventions (with cached file list)
|
|
298
|
+
*/
|
|
299
|
+
function checkNamingConventionsWithCache(consumingAppPath, cachedSourceFiles) {
|
|
300
|
+
const issues = [];
|
|
301
|
+
|
|
302
|
+
cachedSourceFiles.forEach(filePath => {
|
|
303
|
+
try {
|
|
304
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
305
|
+
const relativePath = path.relative(consumingAppPath || process.cwd(), filePath);
|
|
306
|
+
|
|
307
|
+
// Check hook naming: use[A-Z][a-zA-Z]*
|
|
308
|
+
const hookPattern = /(?:function|const|export\s+(?:function|const))\s+(use[a-z][a-zA-Z]*)\s*[=(]/g;
|
|
309
|
+
let match;
|
|
310
|
+
while ((match = hookPattern.exec(content)) !== null) {
|
|
311
|
+
if (isInCommentOrString(content, match.index)) {
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const hookName = match[1];
|
|
316
|
+
if (!/^use[A-Z]/.test(hookName)) {
|
|
317
|
+
issues.push({
|
|
318
|
+
type: 'namingConvention',
|
|
319
|
+
file: relativePath,
|
|
320
|
+
line: getLineNumber(content, match.index),
|
|
321
|
+
message: `Hook '${hookName}' should follow PascalCase after 'use' prefix (e.g., useAuth, useSomething).`,
|
|
322
|
+
code: getCodeSnippet(content, match.index),
|
|
323
|
+
severity: 'warning',
|
|
324
|
+
fix: `Rename ${hookName} to follow use[A-Z][a-zA-Z]* pattern.`,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Check provider naming: *Provider
|
|
330
|
+
const providerPattern = /(?:function|const|export\s+(?:function|const))\s+([a-zA-Z]*Provider)\s*[=(]/g;
|
|
331
|
+
while ((match = providerPattern.exec(content)) !== null) {
|
|
332
|
+
if (isInCommentOrString(content, match.index)) {
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const providerName = match[1];
|
|
337
|
+
if (!/^[A-Z]/.test(providerName)) {
|
|
338
|
+
issues.push({
|
|
339
|
+
type: 'namingConvention',
|
|
340
|
+
file: relativePath,
|
|
341
|
+
line: getLineNumber(content, match.index),
|
|
342
|
+
message: `Provider '${providerName}' should start with uppercase letter (PascalCase).`,
|
|
343
|
+
code: getCodeSnippet(content, match.index),
|
|
344
|
+
severity: 'warning',
|
|
345
|
+
fix: `Rename ${providerName} to start with uppercase letter.`,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Check component exports: export (function|const) [A-Z]
|
|
351
|
+
const componentPattern = /export\s+(?:function|const)\s+([a-z][a-zA-Z]*)\s*[=(]/g;
|
|
352
|
+
while ((match = componentPattern.exec(content)) !== null) {
|
|
353
|
+
if (isInCommentOrString(content, match.index)) {
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const componentName = match[1];
|
|
358
|
+
// Skip if it's a hook (starts with use)
|
|
359
|
+
if (componentName.startsWith('use')) {
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Check if it's likely a component (exported, might be used in JSX)
|
|
364
|
+
// This is heuristic - components are usually PascalCase
|
|
365
|
+
issues.push({
|
|
366
|
+
type: 'namingConvention',
|
|
367
|
+
file: relativePath,
|
|
368
|
+
line: getLineNumber(content, match.index),
|
|
369
|
+
message: `Exported component '${componentName}' should use PascalCase (starts with uppercase).`,
|
|
370
|
+
code: getCodeSnippet(content, match.index),
|
|
371
|
+
severity: 'warning',
|
|
372
|
+
fix: `Rename ${componentName} to PascalCase (e.g., ${componentName.charAt(0).toUpperCase() + componentName.slice(1)}).`,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
} catch (error) {
|
|
377
|
+
// Skip files that can't be read
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
return issues;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Check RLS policy compliance in SQL migrations
|
|
386
|
+
*/
|
|
387
|
+
function checkRLSPolicies(consumingAppPath) {
|
|
388
|
+
const issues = [];
|
|
389
|
+
|
|
390
|
+
// Find SQL migration files
|
|
391
|
+
const migrationsPath = path.join(consumingAppPath, 'supabase', 'migrations');
|
|
392
|
+
if (!fs.existsSync(migrationsPath)) {
|
|
393
|
+
// Try alternative location
|
|
394
|
+
const altPath = path.join(consumingAppPath, 'migrations');
|
|
395
|
+
if (!fs.existsSync(altPath)) {
|
|
396
|
+
return issues; // No migrations directory, skip check
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const sqlFiles = findSQLFiles(fs.existsSync(migrationsPath) ? migrationsPath : path.join(consumingAppPath, 'migrations'));
|
|
401
|
+
|
|
402
|
+
sqlFiles.forEach(filePath => {
|
|
403
|
+
try {
|
|
404
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
405
|
+
const relativePath = path.relative(consumingAppPath || process.cwd(), filePath);
|
|
406
|
+
|
|
407
|
+
// Find CREATE POLICY statements
|
|
408
|
+
const policyPattern = /CREATE\s+POLICY\s+["']?([^"'\s]+)["']?\s+ON\s+(\w+)\s+FOR\s+(\w+)/gi;
|
|
409
|
+
let match;
|
|
410
|
+
|
|
411
|
+
while ((match = policyPattern.exec(content)) !== null) {
|
|
412
|
+
const policyName = match[1];
|
|
413
|
+
const tableName = match[2];
|
|
414
|
+
const operation = match[3].toLowerCase();
|
|
415
|
+
|
|
416
|
+
// Check policy naming: rbac_{operation}_{table_name}_{scope}
|
|
417
|
+
const expectedPattern = new RegExp(`^rbac_${operation}_${tableName}(?:_\\w+)?$`, 'i');
|
|
418
|
+
if (!expectedPattern.test(policyName)) {
|
|
419
|
+
issues.push({
|
|
420
|
+
type: 'rlsPolicy',
|
|
421
|
+
file: relativePath,
|
|
422
|
+
line: getLineNumber(content, match.index),
|
|
423
|
+
message: `RLS policy '${policyName}' does not follow naming convention. Should be 'rbac_${operation}_${tableName}' or 'rbac_${operation}_${tableName}_scope'.`,
|
|
424
|
+
code: getCodeSnippet(content, match.index),
|
|
425
|
+
severity: 'error',
|
|
426
|
+
fix: `Rename policy to follow pattern: rbac_${operation}_${tableName}`,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Find the USING/WITH CHECK clause
|
|
431
|
+
const policyStart = match.index;
|
|
432
|
+
const policyEnd = content.indexOf(';', policyStart);
|
|
433
|
+
if (policyEnd === -1) continue;
|
|
434
|
+
|
|
435
|
+
const policyBody = content.substring(policyStart, policyEnd);
|
|
436
|
+
|
|
437
|
+
// Check for subqueries in USING/WITH CHECK
|
|
438
|
+
const subqueryPattern = /(?:USING|WITH\s+CHECK)\s*\([^)]*(?:SELECT\s+[^)]+FROM[^)]+WHERE[^)]*)\)/gi;
|
|
439
|
+
if (subqueryPattern.test(policyBody)) {
|
|
440
|
+
issues.push({
|
|
441
|
+
type: 'rlsPolicy',
|
|
442
|
+
file: relativePath,
|
|
443
|
+
line: getLineNumber(content, policyStart),
|
|
444
|
+
message: `RLS policy '${policyName}' contains subquery. Must use helper functions instead for performance.`,
|
|
445
|
+
code: getCodeSnippet(content, policyStart, 0, 200),
|
|
446
|
+
severity: 'error',
|
|
447
|
+
fix: 'Replace subquery with helper function. Helper functions must be STABLE SECURITY DEFINER SET search_path TO public.',
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Check for inline auth.uid() calls
|
|
452
|
+
const authUidPattern = /\bauth\.uid\s*\(/gi;
|
|
453
|
+
if (authUidPattern.test(policyBody)) {
|
|
454
|
+
issues.push({
|
|
455
|
+
type: 'rlsPolicy',
|
|
456
|
+
file: relativePath,
|
|
457
|
+
line: getLineNumber(content, policyStart),
|
|
458
|
+
message: `RLS policy '${policyName}' contains inline auth.uid() call. Must use helper function instead for performance.`,
|
|
459
|
+
code: getCodeSnippet(content, policyStart, 0, 200),
|
|
460
|
+
severity: 'error',
|
|
461
|
+
fix: 'Replace auth.uid() with helper function like get_effective_user_id(). Helper functions must be STABLE SECURITY DEFINER.',
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Check for inline current_setting calls
|
|
466
|
+
const currentSettingPattern = /\bcurrent_setting\s*\(/gi;
|
|
467
|
+
if (currentSettingPattern.test(policyBody)) {
|
|
468
|
+
issues.push({
|
|
469
|
+
type: 'rlsPolicy',
|
|
470
|
+
file: relativePath,
|
|
471
|
+
line: getLineNumber(content, policyStart),
|
|
472
|
+
message: `RLS policy '${policyName}' contains inline current_setting() call. Must use helper function instead for performance.`,
|
|
473
|
+
code: getCodeSnippet(content, policyStart, 0, 200),
|
|
474
|
+
severity: 'error',
|
|
475
|
+
fix: 'Replace current_setting() with helper function. Helper functions must be STABLE SECURITY DEFINER.',
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Check for is_super_admin() without parameter (security risk - uses fallback strategies)
|
|
480
|
+
const isSuperAdminWithoutParamPattern = /\bis_super_admin\s*\(\s*\)/gi;
|
|
481
|
+
if (isSuperAdminWithoutParamPattern.test(policyBody)) {
|
|
482
|
+
issues.push({
|
|
483
|
+
type: 'rlsPolicy',
|
|
484
|
+
file: relativePath,
|
|
485
|
+
line: getLineNumber(content, policyStart),
|
|
486
|
+
message: `RLS policy '${policyName}' uses is_super_admin() without parameter. Must use is_super_admin(safe_get_user_id_for_rls()) to avoid fallback strategy vulnerabilities.`,
|
|
487
|
+
code: getCodeSnippet(content, policyStart, 0, 200),
|
|
488
|
+
severity: 'error',
|
|
489
|
+
fix: 'Replace is_super_admin() with is_super_admin(safe_get_user_id_for_rls()) to require explicit parameter passing.',
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Check for missing super-admin checks in authenticated policies
|
|
494
|
+
// Only check if this is an authenticated policy (not public/anonymous)
|
|
495
|
+
const isAuthenticatedPolicy = /TO\s+authenticated/i.test(policyBody);
|
|
496
|
+
const hasSuperAdminCheck = /\bis_super_admin\s*\(/i.test(policyBody);
|
|
497
|
+
const isPublicPolicy = /TO\s+(?:public|anon)/i.test(policyBody) || policyName.toLowerCase().includes('public') || policyName.toLowerCase().includes('anon');
|
|
498
|
+
|
|
499
|
+
// Exception: User-scoped policies that only check user_id don't need super-admin
|
|
500
|
+
const isUserScopedOnly = /organisation_id\s+IS\s+NULL/i.test(policyBody) &&
|
|
501
|
+
/get_effective_user_id\s*\(\s*\)\s*=\s*user_id/i.test(policyBody) &&
|
|
502
|
+
!/\bOR\b/i.test(policyBody);
|
|
503
|
+
|
|
504
|
+
// Skip check for public/anonymous policies or service role policies
|
|
505
|
+
if (isAuthenticatedPolicy && !hasSuperAdminCheck && !isPublicPolicy && !policyName.toLowerCase().includes('service')) {
|
|
506
|
+
if (!isUserScopedOnly) {
|
|
507
|
+
issues.push({
|
|
508
|
+
type: 'rlsPolicy',
|
|
509
|
+
file: relativePath,
|
|
510
|
+
line: getLineNumber(content, policyStart),
|
|
511
|
+
message: `RLS policy '${policyName}' for authenticated users is missing super-admin check. All authenticated policies must include is_super_admin(safe_get_user_id_for_rls()) as a bypass.`,
|
|
512
|
+
code: getCodeSnippet(content, policyStart, 0, 200),
|
|
513
|
+
severity: 'error',
|
|
514
|
+
fix: 'Add is_super_admin(safe_get_user_id_for_rls()) check. Pattern: (is_super_admin(safe_get_user_id_for_rls()) OR ...other checks...)',
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Check for use of deprecated event access functions when RBAC permissions should be used
|
|
520
|
+
// These functions should only be used for event-scoped data WITHOUT page permissions
|
|
521
|
+
const usesEventAccess = /\bcheck_user_event_access\s*\(/i.test(policyBody);
|
|
522
|
+
const usesEventCreator = /\bcheck_user_is_event_creator\s*\(/i.test(policyBody);
|
|
523
|
+
const usesRBACPermission = /\bcheck_rbac_permission_with_context\s*\(/i.test(policyBody);
|
|
524
|
+
|
|
525
|
+
// Tables that have page permissions and should use RBAC permission checks
|
|
526
|
+
// This list should be maintained as pages are added to rbac_app_pages
|
|
527
|
+
const tablesWithPagePermissions = [
|
|
528
|
+
'trac_contacts', 'trac_risks', 'trac_journal_posts', 'trac_currency_rates',
|
|
529
|
+
'trac_accommodation', 'trac_activity', 'trac_transport',
|
|
530
|
+
'mint_budgets', 'mint_budget_variables',
|
|
531
|
+
'cake_dish', 'cake_meal', 'cake_item', 'cake_recipe',
|
|
532
|
+
'medi_profile', 'medi_action_plan'
|
|
533
|
+
];
|
|
534
|
+
|
|
535
|
+
const hasPagePermissions = tablesWithPagePermissions.some(t =>
|
|
536
|
+
tableName.toLowerCase().includes(t.toLowerCase())
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
if (hasPagePermissions && (usesEventAccess || usesEventCreator) && !usesRBACPermission) {
|
|
540
|
+
issues.push({
|
|
541
|
+
type: 'rlsPolicy',
|
|
542
|
+
file: relativePath,
|
|
543
|
+
line: getLineNumber(content, policyStart),
|
|
544
|
+
message: `RLS policy '${policyName}' uses ${usesEventAccess ? 'check_user_event_access()' : 'check_user_is_event_creator()'} but table '${tableName}' has page permissions. Should use check_rbac_permission_with_context() with page-level permissions instead.`,
|
|
545
|
+
code: getCodeSnippet(content, policyStart, 0, 200),
|
|
546
|
+
severity: 'error',
|
|
547
|
+
fix: `Replace ${usesEventAccess ? 'check_user_event_access(event_id)' : 'check_user_is_event_creator(event_id)'} with check_rbac_permission_with_context('${operation}:page.{page_name}', '{page_name}', organisation_id, event_id::text, get_app_id('{app_name}')). See RBAC compliance standard for page name mapping.`,
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Check for missing required field checks in event-scoped tables
|
|
552
|
+
// Event-scoped tables should check event_id IS NOT NULL
|
|
553
|
+
// Tables with organisation_id should check organisation_id IS NOT NULL
|
|
554
|
+
const hasEventIdColumn = /event_id/i.test(policyBody) || tableName.toLowerCase().includes('trac_') ||
|
|
555
|
+
tableName.toLowerCase().includes('mint_') ||
|
|
556
|
+
tableName.toLowerCase().includes('cake_') ||
|
|
557
|
+
tableName.toLowerCase().includes('medi_');
|
|
558
|
+
const hasOrganisationIdColumn = /organisation_id/i.test(policyBody) ||
|
|
559
|
+
!tableName.toLowerCase().includes('rbac_') &&
|
|
560
|
+
!tableName.toLowerCase().includes('core_organisations');
|
|
561
|
+
|
|
562
|
+
const checksEventIdNotNull = /event_id\s+IS\s+NOT\s+NULL/i.test(policyBody);
|
|
563
|
+
const checksOrganisationIdNotNull = /organisation_id\s+IS\s+NOT\s+NULL/i.test(policyBody);
|
|
564
|
+
|
|
565
|
+
// For INSERT policies on event-scoped tables, event_id should be required
|
|
566
|
+
if (operation === 'insert' && hasEventIdColumn && !checksEventIdNotNull) {
|
|
567
|
+
issues.push({
|
|
568
|
+
type: 'rlsPolicy',
|
|
569
|
+
file: relativePath,
|
|
570
|
+
line: getLineNumber(content, policyStart),
|
|
571
|
+
message: `RLS INSERT policy '${policyName}' for event-scoped table '${tableName}' should require event_id IS NOT NULL. Permission checks need event_id to verify event-level roles.`,
|
|
572
|
+
code: getCodeSnippet(content, policyStart, 0, 200),
|
|
573
|
+
severity: 'error',
|
|
574
|
+
fix: 'Add event_id IS NOT NULL check to WITH CHECK clause. Pattern: event_id IS NOT NULL AND ...',
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// For authenticated policies, organisation_id should typically be checked
|
|
579
|
+
if (isAuthenticatedPolicy && hasOrganisationIdColumn && !checksOrganisationIdNotNull &&
|
|
580
|
+
!isUserScopedOnly && !tableName.toLowerCase().includes('rbac_')) {
|
|
581
|
+
// Exception: Some tables legitimately allow NULL organisation_id (e.g., user profiles)
|
|
582
|
+
const allowsNullOrg = /organisation_id\s+IS\s+NULL/i.test(policyBody) &&
|
|
583
|
+
/get_effective_user_id\s*\(\s*\)\s*=\s*user_id/i.test(policyBody);
|
|
584
|
+
|
|
585
|
+
if (!allowsNullOrg) {
|
|
586
|
+
issues.push({
|
|
587
|
+
type: 'rlsPolicy',
|
|
588
|
+
file: relativePath,
|
|
589
|
+
line: getLineNumber(content, policyStart),
|
|
590
|
+
message: `RLS policy '${policyName}' for table '${tableName}' should check organisation_id IS NOT NULL. Permission checks need organisation_id for proper RBAC context.`,
|
|
591
|
+
code: getCodeSnippet(content, policyStart, 0, 200),
|
|
592
|
+
severity: 'warning',
|
|
593
|
+
fix: 'Add organisation_id IS NOT NULL check. Pattern: organisation_id IS NOT NULL AND ...',
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Check helper function definitions for required attributes
|
|
600
|
+
const functionPattern = /CREATE\s+(?:OR\s+REPLACE\s+)?FUNCTION\s+(\w+)\s*\([^)]*\)\s*RETURNS[^;]+LANGUAGE\s+plpgsql[^;]*AS/gi;
|
|
601
|
+
let funcMatch;
|
|
602
|
+
while ((funcMatch = functionPattern.exec(content)) !== null) {
|
|
603
|
+
const funcName = funcMatch[1];
|
|
604
|
+
const funcStart = funcMatch.index;
|
|
605
|
+
const funcEnd = content.indexOf('AS $$', funcStart);
|
|
606
|
+
if (funcEnd === -1) continue;
|
|
607
|
+
|
|
608
|
+
const funcDef = content.substring(funcStart, funcEnd);
|
|
609
|
+
|
|
610
|
+
// Check if this function is used in policies (heuristic: check if function name appears in policy context)
|
|
611
|
+
// For now, check all helper functions for required attributes
|
|
612
|
+
const hasStable = /\bSTABLE\b/i.test(funcDef);
|
|
613
|
+
const hasSecurityDefiner = /\bSECURITY\s+DEFINER\b/i.test(funcDef);
|
|
614
|
+
const hasSearchPath = /SET\s+search_path\s+TO\s+['"]?public['"]?/i.test(funcDef);
|
|
615
|
+
|
|
616
|
+
// Only flag if function looks like it might be used in RLS (has common helper function patterns)
|
|
617
|
+
const isLikelyRLSHelper = /check_|get_|is_/.test(funcName.toLowerCase());
|
|
618
|
+
|
|
619
|
+
// Check for security-critical functions with fallback strategies
|
|
620
|
+
// This is especially important for is_super_admin and similar functions
|
|
621
|
+
if (funcName.toLowerCase() === 'is_super_admin' || funcName.toLowerCase().includes('super_admin')) {
|
|
622
|
+
const funcBodyStart = content.indexOf('AS $$', funcStart);
|
|
623
|
+
if (funcBodyStart !== -1) {
|
|
624
|
+
const funcBodyEnd = content.indexOf('$$;', funcBodyStart);
|
|
625
|
+
if (funcBodyEnd !== -1) {
|
|
626
|
+
const funcBody = content.substring(funcBodyStart, funcBodyEnd);
|
|
627
|
+
|
|
628
|
+
// Check for DEFAULT NULL parameter (allows fallback strategies)
|
|
629
|
+
const hasDefaultNull = /p_\w+\s+UUID\s+DEFAULT\s+NULL/i.test(funcDef);
|
|
630
|
+
|
|
631
|
+
// Check for multiple fallback patterns in function body
|
|
632
|
+
const hasFallbackPatterns = (
|
|
633
|
+
/IF\s+\w+\s+IS\s+NULL\s+THEN/i.test(funcBody) &&
|
|
634
|
+
/ELSE/i.test(funcBody) &&
|
|
635
|
+
(/\bauth\.uid\s*\(/i.test(funcBody) || /\bcurrent_setting\s*\(/i.test(funcBody))
|
|
636
|
+
);
|
|
637
|
+
|
|
638
|
+
if (hasDefaultNull || hasFallbackPatterns) {
|
|
639
|
+
issues.push({
|
|
640
|
+
type: 'rlsPolicy',
|
|
641
|
+
file: relativePath,
|
|
642
|
+
line: getLineNumber(content, funcStart),
|
|
643
|
+
message: `Security-critical function '${funcName}' uses fallback strategies (DEFAULT NULL parameter or multiple fallback patterns). This is a security risk - functions should require explicit parameters and fail secure.`,
|
|
644
|
+
code: getCodeSnippet(content, funcStart, 0, 300),
|
|
645
|
+
severity: 'error',
|
|
646
|
+
fix: 'Remove DEFAULT NULL parameter and all fallback logic. Function should require explicit parameter and return false if parameter is NULL (fail secure).',
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
if (isLikelyRLSHelper) {
|
|
654
|
+
if (!hasStable) {
|
|
655
|
+
issues.push({
|
|
656
|
+
type: 'rlsPolicy',
|
|
657
|
+
file: relativePath,
|
|
658
|
+
line: getLineNumber(content, funcStart),
|
|
659
|
+
message: `Helper function '${funcName}' missing STABLE attribute. RLS helper functions must be STABLE for performance.`,
|
|
660
|
+
code: getCodeSnippet(content, funcStart, 0, 150),
|
|
661
|
+
severity: 'error',
|
|
662
|
+
fix: 'Add STABLE attribute to function definition.',
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (!hasSecurityDefiner) {
|
|
667
|
+
issues.push({
|
|
668
|
+
type: 'rlsPolicy',
|
|
669
|
+
file: relativePath,
|
|
670
|
+
line: getLineNumber(content, funcStart),
|
|
671
|
+
message: `Helper function '${funcName}' missing SECURITY DEFINER attribute. RLS helper functions must be SECURITY DEFINER to bypass RLS recursion.`,
|
|
672
|
+
code: getCodeSnippet(content, funcStart, 0, 150),
|
|
673
|
+
severity: 'error',
|
|
674
|
+
fix: 'Add SECURITY DEFINER attribute to function definition.',
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
if (!hasSearchPath) {
|
|
679
|
+
issues.push({
|
|
680
|
+
type: 'rlsPolicy',
|
|
681
|
+
file: relativePath,
|
|
682
|
+
line: getLineNumber(content, funcStart),
|
|
683
|
+
message: `Helper function '${funcName}' missing SET search_path TO public. Required to prevent search path injection.`,
|
|
684
|
+
code: getCodeSnippet(content, funcStart, 0, 150),
|
|
685
|
+
severity: 'error',
|
|
686
|
+
fix: 'Add SET search_path TO public to function definition.',
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
} catch (error) {
|
|
693
|
+
// Skip files that can't be read
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
return issues;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Check RPC naming conventions
|
|
702
|
+
*/
|
|
703
|
+
function checkRPCNaming(consumingAppPath) {
|
|
704
|
+
const issues = [];
|
|
705
|
+
|
|
706
|
+
// Find SQL migration files
|
|
707
|
+
const migrationsPath = path.join(consumingAppPath, 'supabase', 'migrations');
|
|
708
|
+
if (!fs.existsSync(migrationsPath)) {
|
|
709
|
+
const altPath = path.join(consumingAppPath, 'migrations');
|
|
710
|
+
if (!fs.existsSync(altPath)) {
|
|
711
|
+
return issues; // No migrations directory, skip check
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const sqlFiles = findSQLFiles(fs.existsSync(migrationsPath) ? migrationsPath : path.join(consumingAppPath, 'migrations'));
|
|
716
|
+
|
|
717
|
+
sqlFiles.forEach(filePath => {
|
|
718
|
+
try {
|
|
719
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
720
|
+
const relativePath = path.relative(consumingAppPath || process.cwd(), filePath);
|
|
721
|
+
|
|
722
|
+
// Find CREATE FUNCTION statements
|
|
723
|
+
const functionPattern = /CREATE\s+(?:OR\s+REPLACE\s+)?FUNCTION\s+(?:public\.)?(\w+)\s*\(/gi;
|
|
724
|
+
let match;
|
|
725
|
+
|
|
726
|
+
while ((match = functionPattern.exec(content)) !== null) {
|
|
727
|
+
const funcName = match[1];
|
|
728
|
+
|
|
729
|
+
// Skip if it's a helper function (starts with check_, get_, is_)
|
|
730
|
+
if (/^(check_|get_|is_)/i.test(funcName)) {
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Check if it follows RPC naming pattern: data_* or app_* prefix
|
|
735
|
+
const hasDataPrefix = /^data_/.test(funcName);
|
|
736
|
+
const hasAppPrefix = /^app_/.test(funcName);
|
|
737
|
+
|
|
738
|
+
if (!hasDataPrefix && !hasAppPrefix) {
|
|
739
|
+
// Check if it's a system function (starts with pg_ or other system prefixes)
|
|
740
|
+
if (/^(pg_|information_schema|current_|session_|set_|reset_|show_)/i.test(funcName)) {
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
issues.push({
|
|
745
|
+
type: 'rpcNaming',
|
|
746
|
+
file: relativePath,
|
|
747
|
+
line: getLineNumber(content, match.index),
|
|
748
|
+
message: `RPC function '${funcName}' does not follow naming convention. Should start with 'data_' (read) or 'app_' (write) prefix.`,
|
|
749
|
+
code: getCodeSnippet(content, match.index),
|
|
750
|
+
severity: 'error',
|
|
751
|
+
fix: `Rename function to follow pattern: data_${funcName} (for read) or app_${funcName} (for write)`,
|
|
752
|
+
});
|
|
753
|
+
} else {
|
|
754
|
+
// Check verb pattern: should end with create, read, update, delete, list, get, or _bulk
|
|
755
|
+
const validVerbs = ['create', 'read', 'update', 'delete', 'list', 'get'];
|
|
756
|
+
const hasValidVerb = validVerbs.some(verb => funcName.toLowerCase().endsWith(`_${verb}`) || funcName.toLowerCase().endsWith(`_${verb}_bulk`));
|
|
757
|
+
const hasBulkSuffix = funcName.toLowerCase().endsWith('_bulk');
|
|
758
|
+
|
|
759
|
+
if (!hasValidVerb && !hasBulkSuffix) {
|
|
760
|
+
issues.push({
|
|
761
|
+
type: 'rpcNaming',
|
|
762
|
+
file: relativePath,
|
|
763
|
+
line: getLineNumber(content, match.index),
|
|
764
|
+
message: `RPC function '${funcName}' should end with a CRUD verb (create, read, update, delete, list, get) or _bulk suffix.`,
|
|
765
|
+
code: getCodeSnippet(content, match.index),
|
|
766
|
+
severity: 'error',
|
|
767
|
+
fix: `Rename function to include CRUD verb: ${funcName}_list, ${funcName}_get, ${funcName}_create, etc.`,
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
} catch (error) {
|
|
774
|
+
// Skip files that can't be read
|
|
775
|
+
}
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
return issues;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* Check testing configuration
|
|
783
|
+
*/
|
|
784
|
+
function checkTestingConfig(consumingAppPath) {
|
|
785
|
+
const issues = [];
|
|
786
|
+
|
|
787
|
+
const packageJsonPath = path.join(consumingAppPath, 'package.json');
|
|
788
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
789
|
+
issues.push({
|
|
790
|
+
type: 'testingConfig',
|
|
791
|
+
file: 'package.json (not found)',
|
|
792
|
+
line: 0,
|
|
793
|
+
message: 'package.json not found. test:coverage script must be configured.',
|
|
794
|
+
code: '',
|
|
795
|
+
severity: 'error',
|
|
796
|
+
fix: 'Create package.json with test:coverage script.',
|
|
797
|
+
});
|
|
798
|
+
return issues;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
try {
|
|
802
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
803
|
+
const scripts = packageJson.scripts || {};
|
|
804
|
+
|
|
805
|
+
// Check for test:coverage script
|
|
806
|
+
if (!scripts['test:coverage']) {
|
|
807
|
+
issues.push({
|
|
808
|
+
type: 'testingConfig',
|
|
809
|
+
file: 'package.json',
|
|
810
|
+
line: 1,
|
|
811
|
+
message: 'test:coverage script not found. Must provide npm run test:coverage script.',
|
|
812
|
+
code: '',
|
|
813
|
+
severity: 'error',
|
|
814
|
+
fix: 'Add "test:coverage" script to package.json scripts section.',
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// Check for vitest or jest config
|
|
819
|
+
const vitestConfigPaths = [
|
|
820
|
+
path.join(consumingAppPath, 'vitest.config.ts'),
|
|
821
|
+
path.join(consumingAppPath, 'vitest.config.js'),
|
|
822
|
+
path.join(consumingAppPath, 'vitest.config.mjs'),
|
|
823
|
+
path.join(consumingAppPath, 'vitest.config.cjs'),
|
|
824
|
+
];
|
|
825
|
+
|
|
826
|
+
const jestConfigPaths = [
|
|
827
|
+
path.join(consumingAppPath, 'jest.config.js'),
|
|
828
|
+
path.join(consumingAppPath, 'jest.config.ts'),
|
|
829
|
+
path.join(consumingAppPath, 'jest.config.json'),
|
|
830
|
+
];
|
|
831
|
+
|
|
832
|
+
let configPath = null;
|
|
833
|
+
let configContent = null;
|
|
834
|
+
|
|
835
|
+
for (const configFile of [...vitestConfigPaths, ...jestConfigPaths]) {
|
|
836
|
+
if (fs.existsSync(configFile)) {
|
|
837
|
+
configPath = configFile;
|
|
838
|
+
try {
|
|
839
|
+
configContent = fs.readFileSync(configFile, 'utf8');
|
|
840
|
+
break;
|
|
841
|
+
} catch (e) {
|
|
842
|
+
// Continue to next
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
if (!configPath) {
|
|
848
|
+
issues.push({
|
|
849
|
+
type: 'testingConfig',
|
|
850
|
+
file: 'vitest.config.* or jest.config.* (not found)',
|
|
851
|
+
line: 0,
|
|
852
|
+
message: 'Test runner config not found. Must configure coverage thresholds.',
|
|
853
|
+
code: '',
|
|
854
|
+
severity: 'error',
|
|
855
|
+
fix: 'Create vitest.config.ts or jest.config.js with coverage thresholds configured.',
|
|
856
|
+
});
|
|
857
|
+
} else {
|
|
858
|
+
// Check for coverage thresholds
|
|
859
|
+
const hasCoverageThresholds = /coverage\s*:\s*\{[^}]*thresholds/i.test(configContent) ||
|
|
860
|
+
/coverageThreshold/i.test(configContent);
|
|
861
|
+
|
|
862
|
+
if (!hasCoverageThresholds) {
|
|
863
|
+
const relativePath = path.relative(consumingAppPath || process.cwd(), configPath);
|
|
864
|
+
issues.push({
|
|
865
|
+
type: 'testingConfig',
|
|
866
|
+
file: relativePath,
|
|
867
|
+
line: 1,
|
|
868
|
+
message: 'Coverage thresholds not configured. Must set ≥90% for utils/hooks, ≥70% for components.',
|
|
869
|
+
code: '',
|
|
870
|
+
severity: 'error',
|
|
871
|
+
fix: 'Add coverage thresholds to test config: { coverage: { thresholds: { lines: 90, functions: 90, branches: 90, statements: 90 } } }',
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
} catch (error) {
|
|
877
|
+
issues.push({
|
|
878
|
+
type: 'testingConfig',
|
|
879
|
+
file: 'package.json',
|
|
880
|
+
line: 0,
|
|
881
|
+
message: `Error reading package.json: ${error.message}`,
|
|
882
|
+
code: '',
|
|
883
|
+
severity: 'error',
|
|
884
|
+
fix: 'Fix package.json syntax errors.',
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
return issues;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* Check input validation (heuristic, with cached file list)
|
|
893
|
+
*/
|
|
894
|
+
function checkInputValidationWithCache(consumingAppPath, cachedSourceFiles) {
|
|
895
|
+
const issues = [];
|
|
896
|
+
|
|
897
|
+
cachedSourceFiles.forEach(filePath => {
|
|
898
|
+
try {
|
|
899
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
900
|
+
const relativePath = path.relative(consumingAppPath || process.cwd(), filePath);
|
|
901
|
+
|
|
902
|
+
// Check for form components without Zod schemas
|
|
903
|
+
// Look for form-related patterns
|
|
904
|
+
const hasFormComponent = /<Form|useForm|FormField/.test(content);
|
|
905
|
+
const hasZodImport = /from\s+['"]@jmruthers\/pace-core['"].*z\b|from\s+['"]zod['"]/.test(content);
|
|
906
|
+
const hasZodSchema = /z\.(object|string|number|boolean|array)/.test(content);
|
|
907
|
+
|
|
908
|
+
if (hasFormComponent && !hasZodImport && !hasZodSchema) {
|
|
909
|
+
// Might be using pace-core Form which handles validation, but flag for review
|
|
910
|
+
// This is heuristic - pace-core Form might be imported differently
|
|
911
|
+
const hasPaceCoreForm = /from\s+['"]@jmruthers\/pace-core['"].*Form/.test(content);
|
|
912
|
+
if (!hasPaceCoreForm) {
|
|
913
|
+
issues.push({
|
|
914
|
+
type: 'inputValidation',
|
|
915
|
+
file: relativePath,
|
|
916
|
+
line: 1,
|
|
917
|
+
message: 'Form component detected but no Zod schema found. Forms should use Zod validation.',
|
|
918
|
+
code: '',
|
|
919
|
+
severity: 'warning',
|
|
920
|
+
fix: 'Add Zod schema validation. Import z from @jmruthers/pace-core and define schema.',
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
} catch (error) {
|
|
926
|
+
// Skip files that can't be read
|
|
927
|
+
}
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
return issues;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/**
|
|
934
|
+
* Check logging security (heuristic, with cached file list)
|
|
935
|
+
*/
|
|
936
|
+
function checkLoggingSecurityWithCache(consumingAppPath, cachedSourceFiles) {
|
|
937
|
+
const issues = [];
|
|
938
|
+
|
|
939
|
+
// Use simpler, more efficient patterns that avoid catastrophic backtracking
|
|
940
|
+
// Instead of [^)]* which can hang, use a limited lookahead
|
|
941
|
+
const sensitivePatterns = [
|
|
942
|
+
{
|
|
943
|
+
// Match console.log( or logger.log( followed by password-related keywords within reasonable distance
|
|
944
|
+
pattern: /(?:console\.(log|warn|error|info)|logger\.(log|warn|error|info|debug))\s*\([^)]{0,200}?(?:password|pwd|passwd)/i,
|
|
945
|
+
keyword: 'password',
|
|
946
|
+
},
|
|
947
|
+
{
|
|
948
|
+
pattern: /(?:console\.(log|warn|error|info)|logger\.(log|warn|error|info|debug))\s*\([^)]{0,200}?(?:token|access_token|refresh_token|api_key|apikey)/i,
|
|
949
|
+
keyword: 'token',
|
|
950
|
+
},
|
|
951
|
+
{
|
|
952
|
+
pattern: /(?:console\.(log|warn|error|info)|logger\.(log|warn|error|info|debug))\s*\([^)]{0,200}?(?:secret|secret_key|private_key)/i,
|
|
953
|
+
keyword: 'secret',
|
|
954
|
+
},
|
|
955
|
+
];
|
|
956
|
+
|
|
957
|
+
let processedCount = 0;
|
|
958
|
+
const totalFiles = cachedSourceFiles.length;
|
|
959
|
+
|
|
960
|
+
cachedSourceFiles.forEach(filePath => {
|
|
961
|
+
try {
|
|
962
|
+
processedCount++;
|
|
963
|
+
// Show progress every 20 files
|
|
964
|
+
if (processedCount % 20 === 0 || processedCount === totalFiles) {
|
|
965
|
+
process.stdout.write(`\r Checking logging security... ${processedCount}/${totalFiles} files`);
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
969
|
+
const relativePath = path.relative(consumingAppPath || process.cwd(), filePath);
|
|
970
|
+
|
|
971
|
+
sensitivePatterns.forEach(({ pattern, keyword }) => {
|
|
972
|
+
// Reset regex lastIndex to avoid issues with global regex
|
|
973
|
+
pattern.lastIndex = 0;
|
|
974
|
+
let match;
|
|
975
|
+
let matchCount = 0;
|
|
976
|
+
// Limit matches per file to prevent infinite loops
|
|
977
|
+
const maxMatchesPerFile = 10;
|
|
978
|
+
|
|
979
|
+
while ((match = pattern.exec(content)) !== null && matchCount < maxMatchesPerFile) {
|
|
980
|
+
matchCount++;
|
|
981
|
+
if (isInCommentOrString(content, match.index)) {
|
|
982
|
+
continue;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
issues.push({
|
|
986
|
+
type: 'loggingSecurity',
|
|
987
|
+
file: relativePath,
|
|
988
|
+
line: getLineNumber(content, match.index),
|
|
989
|
+
message: `Potential sensitive data (${keyword}) in logging statement. Must NOT log passwords, tokens, or secrets.`,
|
|
990
|
+
code: getCodeSnippet(content, match.index),
|
|
991
|
+
severity: 'warning',
|
|
992
|
+
fix: 'Remove sensitive data from log statements. Log only IDs and non-PII metadata.',
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
} catch (error) {
|
|
998
|
+
// Skip files that can't be read
|
|
999
|
+
}
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
// Clear progress line
|
|
1003
|
+
if (totalFiles > 20) {
|
|
1004
|
+
process.stdout.write('\r Checking logging security... ');
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
return issues;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
/**
|
|
1011
|
+
* Check error message safety (heuristic, with cached file list)
|
|
1012
|
+
*/
|
|
1013
|
+
function checkErrorMessagesWithCache(consumingAppPath, cachedSourceFiles) {
|
|
1014
|
+
const issues = [];
|
|
1015
|
+
|
|
1016
|
+
// Use simpler, more efficient patterns that avoid catastrophic backtracking
|
|
1017
|
+
const errorPatterns = [
|
|
1018
|
+
{
|
|
1019
|
+
pattern: /throw\s+new\s+Error\s*\([^)]{0,200}?(?:stack|trace|at\s+\w+)/i,
|
|
1020
|
+
message: 'Error message may expose stack trace. Error messages should not expose internal details.',
|
|
1021
|
+
},
|
|
1022
|
+
{
|
|
1023
|
+
pattern: /throw\s+new\s+Error\s*\([^)]{0,200}?(?:\/[a-z]+\/|\\[a-z]+\\|\.tsx?|\.jsx?)/i,
|
|
1024
|
+
message: 'Error message may expose file paths. Error messages should not expose internal details.',
|
|
1025
|
+
},
|
|
1026
|
+
{
|
|
1027
|
+
pattern: /(?:console\.error|logger\.error)\s*\([^)]{0,200}?(?:stack|trace|at\s+\w+)/i,
|
|
1028
|
+
message: 'Error logging may expose stack trace. Log stack traces only in development, not in user-facing errors.',
|
|
1029
|
+
},
|
|
1030
|
+
];
|
|
1031
|
+
|
|
1032
|
+
let processedCount = 0;
|
|
1033
|
+
const totalFiles = cachedSourceFiles.length;
|
|
1034
|
+
|
|
1035
|
+
cachedSourceFiles.forEach(filePath => {
|
|
1036
|
+
try {
|
|
1037
|
+
processedCount++;
|
|
1038
|
+
// Show progress every 20 files
|
|
1039
|
+
if (processedCount % 20 === 0 || processedCount === totalFiles) {
|
|
1040
|
+
process.stdout.write(`\r Checking error messages... ${processedCount}/${totalFiles} files`);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
1044
|
+
const relativePath = path.relative(consumingAppPath || process.cwd(), filePath);
|
|
1045
|
+
|
|
1046
|
+
errorPatterns.forEach(({ pattern, message }) => {
|
|
1047
|
+
// Reset regex lastIndex to avoid issues with global regex
|
|
1048
|
+
pattern.lastIndex = 0;
|
|
1049
|
+
let match;
|
|
1050
|
+
let matchCount = 0;
|
|
1051
|
+
// Limit matches per file to prevent infinite loops
|
|
1052
|
+
const maxMatchesPerFile = 10;
|
|
1053
|
+
|
|
1054
|
+
while ((match = pattern.exec(content)) !== null && matchCount < maxMatchesPerFile) {
|
|
1055
|
+
matchCount++;
|
|
1056
|
+
if (isInCommentOrString(content, match.index)) {
|
|
1057
|
+
continue;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
issues.push({
|
|
1061
|
+
type: 'errorMessages',
|
|
1062
|
+
file: relativePath,
|
|
1063
|
+
line: getLineNumber(content, match.index),
|
|
1064
|
+
message: message,
|
|
1065
|
+
code: getCodeSnippet(content, match.index),
|
|
1066
|
+
severity: 'warning',
|
|
1067
|
+
fix: 'Use safe error messages that do not expose internal details. Log detailed errors server-side only.',
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
} catch (error) {
|
|
1073
|
+
// Skip files that can't be read
|
|
1074
|
+
}
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
// Clear progress line
|
|
1078
|
+
if (totalFiles > 20) {
|
|
1079
|
+
process.stdout.write('\r Checking error messages... ');
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
return issues;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
/**
|
|
1086
|
+
* Main audit function
|
|
1087
|
+
*/
|
|
1088
|
+
function runStandardsAudit(consumingAppPath = process.cwd(), showProgress = false) {
|
|
1089
|
+
const issues = {
|
|
1090
|
+
cursorRuleset: [],
|
|
1091
|
+
typescriptConfig: [],
|
|
1092
|
+
anyTypes: [],
|
|
1093
|
+
namingConvention: [],
|
|
1094
|
+
rlsPolicy: [],
|
|
1095
|
+
rpcNaming: [],
|
|
1096
|
+
testingConfig: [],
|
|
1097
|
+
inputValidation: [],
|
|
1098
|
+
loggingSecurity: [],
|
|
1099
|
+
errorMessages: [],
|
|
1100
|
+
};
|
|
1101
|
+
|
|
1102
|
+
try {
|
|
1103
|
+
// Check cursor ruleset
|
|
1104
|
+
if (showProgress) {
|
|
1105
|
+
process.stdout.write(' Checking cursor ruleset... ');
|
|
1106
|
+
}
|
|
1107
|
+
const cursorIssues = checkCursorRuleset(consumingAppPath);
|
|
1108
|
+
issues.cursorRuleset.push(...cursorIssues);
|
|
1109
|
+
if (showProgress) {
|
|
1110
|
+
process.stdout.write(`✓ (${cursorIssues.length} issues)\n`);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// Check TypeScript config
|
|
1114
|
+
if (showProgress) {
|
|
1115
|
+
process.stdout.write(' Checking TypeScript config... ');
|
|
1116
|
+
}
|
|
1117
|
+
const tsConfigIssues = checkTypeScriptConfig(consumingAppPath);
|
|
1118
|
+
issues.typescriptConfig.push(...tsConfigIssues);
|
|
1119
|
+
if (showProgress) {
|
|
1120
|
+
process.stdout.write(`✓ (${tsConfigIssues.length} issues)\n`);
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// Cache source files to avoid multiple scans
|
|
1124
|
+
if (showProgress) {
|
|
1125
|
+
process.stdout.write(' Scanning source files... ');
|
|
1126
|
+
}
|
|
1127
|
+
const srcPath = path.join(consumingAppPath, 'src');
|
|
1128
|
+
const searchPath = fs.existsSync(srcPath) ? srcPath : consumingAppPath;
|
|
1129
|
+
let cachedSourceFiles = [];
|
|
1130
|
+
if (fs.existsSync(searchPath)) {
|
|
1131
|
+
cachedSourceFiles = findSourceFiles(searchPath);
|
|
1132
|
+
}
|
|
1133
|
+
if (showProgress) {
|
|
1134
|
+
process.stdout.write(`✓ (${cachedSourceFiles.length} files)\n`);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// Check for any types
|
|
1138
|
+
if (showProgress) {
|
|
1139
|
+
process.stdout.write(' Checking for any types... ');
|
|
1140
|
+
}
|
|
1141
|
+
const anyTypeIssues = checkAnyTypesWithCache(consumingAppPath, cachedSourceFiles);
|
|
1142
|
+
issues.anyTypes.push(...anyTypeIssues);
|
|
1143
|
+
if (showProgress) {
|
|
1144
|
+
process.stdout.write(`✓ (${anyTypeIssues.length} issues)\n`);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
// Check naming conventions
|
|
1148
|
+
if (showProgress) {
|
|
1149
|
+
process.stdout.write(' Checking naming conventions... ');
|
|
1150
|
+
}
|
|
1151
|
+
const namingIssues = checkNamingConventionsWithCache(consumingAppPath, cachedSourceFiles);
|
|
1152
|
+
issues.namingConvention.push(...namingIssues);
|
|
1153
|
+
if (showProgress) {
|
|
1154
|
+
process.stdout.write(`✓ (${namingIssues.length} issues)\n`);
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// Check RLS policies
|
|
1158
|
+
if (showProgress) {
|
|
1159
|
+
process.stdout.write(' Checking RLS policies... ');
|
|
1160
|
+
}
|
|
1161
|
+
const rlsIssues = checkRLSPolicies(consumingAppPath);
|
|
1162
|
+
issues.rlsPolicy.push(...rlsIssues);
|
|
1163
|
+
if (showProgress) {
|
|
1164
|
+
process.stdout.write(`✓ (${rlsIssues.length} issues)\n`);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// Check RPC naming
|
|
1168
|
+
if (showProgress) {
|
|
1169
|
+
process.stdout.write(' Checking RPC naming... ');
|
|
1170
|
+
}
|
|
1171
|
+
const rpcIssues = checkRPCNaming(consumingAppPath);
|
|
1172
|
+
issues.rpcNaming.push(...rpcIssues);
|
|
1173
|
+
if (showProgress) {
|
|
1174
|
+
process.stdout.write(`✓ (${rpcIssues.length} issues)\n`);
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
// Check testing config
|
|
1178
|
+
if (showProgress) {
|
|
1179
|
+
process.stdout.write(' Checking testing config... ');
|
|
1180
|
+
}
|
|
1181
|
+
const testingIssues = checkTestingConfig(consumingAppPath);
|
|
1182
|
+
issues.testingConfig.push(...testingIssues);
|
|
1183
|
+
if (showProgress) {
|
|
1184
|
+
process.stdout.write(`✓ (${testingIssues.length} issues)\n`);
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// Check input validation (heuristic)
|
|
1188
|
+
if (showProgress) {
|
|
1189
|
+
process.stdout.write(' Checking input validation... ');
|
|
1190
|
+
}
|
|
1191
|
+
const validationIssues = checkInputValidationWithCache(consumingAppPath, cachedSourceFiles);
|
|
1192
|
+
issues.inputValidation.push(...validationIssues);
|
|
1193
|
+
if (showProgress) {
|
|
1194
|
+
process.stdout.write(`✓ (${validationIssues.length} issues)\n`);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// Check logging security (heuristic)
|
|
1198
|
+
if (showProgress) {
|
|
1199
|
+
process.stdout.write(' Checking logging security... ');
|
|
1200
|
+
}
|
|
1201
|
+
const loggingIssues = checkLoggingSecurityWithCache(consumingAppPath, cachedSourceFiles);
|
|
1202
|
+
issues.loggingSecurity.push(...loggingIssues);
|
|
1203
|
+
if (showProgress) {
|
|
1204
|
+
process.stdout.write(`✓ (${loggingIssues.length} issues)\n`);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
// Check error messages (heuristic)
|
|
1208
|
+
if (showProgress) {
|
|
1209
|
+
process.stdout.write(' Checking error messages... ');
|
|
1210
|
+
}
|
|
1211
|
+
const errorIssues = checkErrorMessagesWithCache(consumingAppPath, cachedSourceFiles);
|
|
1212
|
+
issues.errorMessages.push(...errorIssues);
|
|
1213
|
+
if (showProgress) {
|
|
1214
|
+
process.stdout.write(`✓ (${errorIssues.length} issues)\n`);
|
|
1215
|
+
}
|
|
1216
|
+
} catch (error) {
|
|
1217
|
+
return {
|
|
1218
|
+
error: `Standards audit failed: ${error.message}`,
|
|
1219
|
+
issues,
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
return {
|
|
1224
|
+
issues,
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
// Export for use by other scripts
|
|
1229
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
1230
|
+
module.exports = { runStandardsAudit };
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// If run directly, output results
|
|
1234
|
+
if (require.main === module) {
|
|
1235
|
+
const consumingAppPath = process.argv[2] || process.cwd();
|
|
1236
|
+
const result = runStandardsAudit(consumingAppPath, true);
|
|
1237
|
+
|
|
1238
|
+
if (result.error) {
|
|
1239
|
+
console.error(`Error: ${result.error}`);
|
|
1240
|
+
process.exit(1);
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
const { issues } = result;
|
|
1244
|
+
|
|
1245
|
+
// Count total issues
|
|
1246
|
+
const totalIssues = Object.values(issues).reduce((sum, arr) => sum + arr.length, 0);
|
|
1247
|
+
|
|
1248
|
+
if (totalIssues === 0) {
|
|
1249
|
+
console.log('\n✅ No standards compliance issues found!');
|
|
1250
|
+
process.exit(0);
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
console.log(`\n❌ Found ${totalIssues} standards compliance issue(s):\n`);
|
|
1254
|
+
|
|
1255
|
+
// Group by type
|
|
1256
|
+
Object.entries(issues).forEach(([type, typeIssues]) => {
|
|
1257
|
+
if (typeIssues.length > 0) {
|
|
1258
|
+
console.log(`\n${type}: ${typeIssues.length} issue(s)`);
|
|
1259
|
+
typeIssues.forEach(issue => {
|
|
1260
|
+
console.log(` ${issue.file}:${issue.line}`);
|
|
1261
|
+
console.log(` ${issue.message}`);
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
});
|
|
1265
|
+
|
|
1266
|
+
process.exit(1);
|
|
1267
|
+
}
|
|
1268
|
+
|