@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.
Files changed (473) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/README.md +5 -403
  3. package/audit-tool/00-dependencies.cjs +394 -0
  4. package/audit-tool/audits/01-pace-core-compliance.cjs +556 -0
  5. package/audit-tool/audits/02-project-structure.cjs +255 -0
  6. package/audit-tool/audits/03-architecture.cjs +196 -0
  7. package/audit-tool/audits/04-code-quality.cjs +149 -0
  8. package/audit-tool/audits/05-styling.cjs +224 -0
  9. package/audit-tool/audits/06-security-rbac.cjs +544 -0
  10. package/audit-tool/audits/07-api-tech-stack.cjs +301 -0
  11. package/audit-tool/audits/08-testing-documentation.cjs +202 -0
  12. package/audit-tool/audits/09-operations.cjs +208 -0
  13. package/audit-tool/index.cjs +291 -0
  14. package/audit-tool/utils/code-utils.cjs +218 -0
  15. package/audit-tool/utils/file-utils.cjs +230 -0
  16. package/audit-tool/utils/report-utils.cjs +241 -0
  17. package/core-usage-manifest.json +93 -0
  18. package/cursor-rules/00-standards-overview.mdc +156 -0
  19. package/cursor-rules/01-pace-core-compliance.mdc +586 -0
  20. package/cursor-rules/02-project-structure.mdc +42 -4
  21. package/cursor-rules/{03-solid-principles.mdc → 03-architecture.mdc} +126 -10
  22. package/cursor-rules/04-code-quality.mdc +419 -0
  23. package/cursor-rules/{08-markup-quality.mdc → 05-styling.mdc} +104 -34
  24. package/cursor-rules/06-security-rbac.mdc +518 -0
  25. package/cursor-rules/07-api-tech-stack.mdc +377 -0
  26. package/cursor-rules/08-testing-documentation.mdc +324 -0
  27. package/cursor-rules/09-operations.mdc +365 -0
  28. package/dist/{AuthService-Cb34EQs3.d.ts → AuthService-DmfO5rGS.d.ts} +10 -0
  29. package/dist/DataTable-7PMH7XN7.js +15 -0
  30. package/dist/{DataTable-BMRU8a1j.d.ts → DataTable-DRUIgtUH.d.ts} +1 -1
  31. package/dist/{PublicPageProvider-QTFVrL-Z.d.ts → PublicPageProvider-DlsCaR5v.d.ts} +33 -72
  32. package/dist/UnifiedAuthProvider-ZT6TIGM7.js +7 -0
  33. package/dist/api-Y4MQWOFW.js +4 -0
  34. package/dist/audit-MYQXYZFU.js +3 -0
  35. package/dist/{chunk-DGUM43GV.js → chunk-3RG5ZIWI.js} +1 -4
  36. package/dist/{chunk-QXHPKYJV.js → chunk-4SXLQIZO.js} +1 -26
  37. package/dist/{chunk-UPPMRMYG.js → chunk-5X4QLXRG.js} +73 -151
  38. package/dist/chunk-6F3IILHI.js +62 -0
  39. package/dist/{chunk-E66EQZE6.js → chunk-6GLLNA6U.js} +3 -9
  40. package/dist/{chunk-ZSAAAMVR.js → chunk-6QYDGKQY.js} +1 -4
  41. package/dist/{chunk-FMUCXFII.js → chunk-7ILTDCL2.js} +9 -5
  42. package/dist/{chunk-M43Y4SSO.js → chunk-A3W6LW53.js} +15 -13
  43. package/dist/{chunk-63FOKYGO.js → chunk-AHU7G2R5.js} +2 -11
  44. package/dist/{chunk-HU2C6SSC.js → chunk-BM4CQ5P3.js} +606 -559
  45. package/dist/chunk-C7NSAPTL.js +1 -0
  46. package/dist/{chunk-J36DSWQK.js → chunk-FEJLJNWA.js} +7 -41
  47. package/dist/{chunk-IHB5DR3H.js → chunk-FTCRZOG2.js} +188 -387
  48. package/dist/{chunk-G37KK66H.js → chunk-FYHN4DD5.js} +60 -19
  49. package/dist/chunk-GHYHJTYV.js +994 -0
  50. package/dist/{chunk-VBXEHIUJ.js → chunk-HF6O3O37.js} +6 -88
  51. package/dist/{chunk-FFQEQTNW.js → chunk-IUBRCBSY.js} +134 -45
  52. package/dist/{chunk-6COVEUS7.js → chunk-JGWDVX64.js} +983 -1034
  53. package/dist/{chunk-RGAWHO7N.js → chunk-L4XMVJKY.js} +77 -222
  54. package/dist/chunk-MBADTM7L.js +64 -0
  55. package/dist/{chunk-M7MPQISP.js → chunk-OJ4SKRSV.js} +3 -16
  56. package/dist/{chunk-IVOFDYWT.js → chunk-Q7Q7V5NV.js} +2109 -1604
  57. package/dist/{chunk-JGRYX5UX.js → chunk-S7DKJPLT.js} +29 -58
  58. package/dist/{chunk-PWLANIRT.js → chunk-TTRFSOKR.js} +1 -7
  59. package/dist/{chunk-5DRSZLL2.js → chunk-UH3NTO3F.js} +1 -6
  60. package/dist/{chunk-NTM7ZSB6.js → chunk-VBCS3DUA.js} +261 -168
  61. package/dist/{chunk-EFN2EIMK.js → chunk-ZFYPMX46.js} +271 -87
  62. package/dist/{chunk-L4OXEN46.js → chunk-ZKAWKYT4.js} +10 -24
  63. package/dist/components.d.ts +7 -5
  64. package/dist/components.js +46 -257
  65. package/dist/{database.generated-CzIvgcPu.d.ts → database.generated-CcnC_DRc.d.ts} +4795 -3691
  66. package/dist/eslint-rules/index.cjs +35 -0
  67. package/{src/eslint-rules/pace-core-compliance.cjs → dist/eslint-rules/rules/01-pace-core-compliance.cjs} +234 -235
  68. package/dist/eslint-rules/rules/04-code-quality.cjs +290 -0
  69. package/dist/eslint-rules/rules/05-styling.cjs +61 -0
  70. package/dist/eslint-rules/rules/06-security-rbac.cjs +806 -0
  71. package/dist/eslint-rules/rules/07-api-tech-stack.cjs +263 -0
  72. package/dist/eslint-rules/rules/08-testing.cjs +94 -0
  73. package/dist/eslint-rules/utils/helpers.cjs +42 -0
  74. package/dist/eslint-rules/utils/manifest-loader.cjs +75 -0
  75. package/dist/hooks.d.ts +6 -6
  76. package/dist/hooks.js +62 -172
  77. package/dist/icons/index.d.ts +1 -0
  78. package/dist/icons/index.js +1 -0
  79. package/dist/index.d.ts +12 -11
  80. package/dist/index.js +67 -660
  81. package/dist/providers.d.ts +2 -2
  82. package/dist/providers.js +8 -35
  83. package/dist/rbac/eslint-rules.d.ts +46 -44
  84. package/dist/rbac/eslint-rules.js +7 -4
  85. package/dist/rbac/index.d.ts +109 -586
  86. package/dist/rbac/index.js +14 -207
  87. package/dist/styles/index.js +2 -12
  88. package/dist/theming/runtime.d.ts +14 -1
  89. package/dist/theming/runtime.js +3 -19
  90. package/dist/{timezone-CHhWg6b4.d.ts → timezone-BZe_eUxx.d.ts} +175 -1
  91. package/dist/{types-CkbwOr4Y.d.ts → types-DXstZpNI.d.ts} +4 -17
  92. package/dist/types-t9H8qKRw.d.ts +55 -0
  93. package/dist/types.d.ts +1 -1
  94. package/dist/types.js +7 -94
  95. package/dist/{usePublicRouteParams-ClnV4tnv.d.ts → usePublicRouteParams-MamNgwqe.d.ts} +20 -20
  96. package/dist/utils.d.ts +24 -117
  97. package/dist/utils.js +54 -392
  98. package/docs/README.md +17 -7
  99. package/docs/api/README.md +4 -402
  100. package/docs/api/modules.md +301 -871
  101. package/docs/api-reference/components.md +21 -21
  102. package/docs/api-reference/deprecated.md +31 -6
  103. package/docs/api-reference/hooks.md +80 -80
  104. package/docs/api-reference/rpc-functions.md +78 -3
  105. package/docs/api-reference/types.md +1 -1
  106. package/docs/api-reference/utilities.md +1 -1
  107. package/docs/architecture/README.md +1 -1
  108. package/docs/core-concepts/events.md +3 -3
  109. package/docs/core-concepts/organisations.md +6 -6
  110. package/docs/core-concepts/permissions.md +6 -6
  111. package/docs/documentation-index.md +12 -18
  112. package/docs/getting-started/cursor-rules.md +3 -23
  113. package/docs/getting-started/dependencies.md +650 -0
  114. package/docs/getting-started/documentation-index.md +1 -1
  115. package/docs/getting-started/examples/README.md +4 -4
  116. package/docs/getting-started/examples/full-featured-app.md +1 -1
  117. package/docs/getting-started/faq.md +2 -2
  118. package/docs/getting-started/installation-guide.md +20 -7
  119. package/docs/getting-started/quick-reference.md +4 -4
  120. package/docs/getting-started/quick-start.md +23 -12
  121. package/docs/implementation-guides/authentication.md +15 -15
  122. package/docs/implementation-guides/component-styling.md +1 -1
  123. package/docs/implementation-guides/data-tables.md +126 -33
  124. package/docs/implementation-guides/datatable-rbac-usage.md +1 -1
  125. package/docs/implementation-guides/dynamic-colors.md +3 -3
  126. package/docs/implementation-guides/file-upload-storage.md +2 -2
  127. package/docs/implementation-guides/hierarchical-datatable.md +40 -60
  128. package/docs/implementation-guides/inactivity-tracking.md +3 -3
  129. package/docs/implementation-guides/large-datasets.md +3 -2
  130. package/docs/implementation-guides/organisation-security.md +2 -2
  131. package/docs/implementation-guides/performance.md +2 -2
  132. package/docs/implementation-guides/permission-enforcement.md +5 -1
  133. package/docs/migration/V0.3.44_organisation-context-timing-fix.md +1 -1
  134. package/docs/migration/V0.4.0_rbac-migration.md +6 -6
  135. package/docs/rbac/MIGRATION_GUIDE.md +819 -0
  136. package/docs/rbac/RBAC_CONTRACT.md +724 -0
  137. package/docs/rbac/README.md +17 -8
  138. package/docs/rbac/advanced-patterns.md +6 -6
  139. package/docs/rbac/api-reference.md +20 -20
  140. package/docs/rbac/edge-functions-guide.md +376 -0
  141. package/docs/rbac/event-based-apps.md +3 -3
  142. package/docs/rbac/examples.md +41 -41
  143. package/docs/rbac/getting-started.md +37 -37
  144. package/docs/rbac/performance.md +1 -1
  145. package/docs/rbac/quick-start.md +52 -52
  146. package/docs/rbac/secure-client-protection.md +1 -35
  147. package/docs/rbac/troubleshooting.md +1 -1
  148. package/docs/security/README.md +5 -5
  149. package/docs/standards/0-standards-overview.md +220 -0
  150. package/docs/standards/1-pace-core-compliance-standards.md +986 -0
  151. package/docs/standards/2-project-structure-standards.md +949 -0
  152. package/docs/standards/3-architecture-standards.md +606 -0
  153. package/docs/standards/4-code-quality-standards.md +728 -0
  154. package/docs/standards/5-styling-standards.md +348 -0
  155. package/docs/standards/{07-rbac-and-rls-standard.md → 6-security-rbac-standards.md} +269 -66
  156. package/docs/standards/7-api-tech-stack-standards.md +662 -0
  157. package/docs/standards/8-testing-documentation-standards.md +401 -0
  158. package/docs/standards/9-operations-standards.md +1102 -0
  159. package/docs/standards/README.md +185 -57
  160. package/docs/troubleshooting/README.md +4 -4
  161. package/docs/troubleshooting/common-issues.md +2 -2
  162. package/docs/troubleshooting/debugging.md +9 -9
  163. package/docs/troubleshooting/migration.md +4 -4
  164. package/docs/troubleshooting/organisation-context-setup.md +42 -19
  165. package/eslint-config-pace-core.cjs +33 -6
  166. package/package.json +35 -23
  167. package/scripts/install-cursor-rules.cjs +25 -6
  168. package/scripts/install-eslint-config.cjs +284 -0
  169. package/src/__tests__/fixtures/supabase.ts +1 -1
  170. package/src/__tests__/helpers/__tests__/component-test-utils.test.tsx +3 -3
  171. package/src/__tests__/helpers/__tests__/optimized-test-setup.test.ts +1 -1
  172. package/src/__tests__/helpers/__tests__/supabaseMock.test.ts +1 -1
  173. package/src/__tests__/helpers/__tests__/test-providers.test.tsx +2 -2
  174. package/src/__tests__/helpers/__tests__/test-utils.test.tsx +13 -13
  175. package/src/__tests__/helpers/component-test-utils.tsx +1 -1
  176. package/src/__tests__/helpers/supabaseMock.ts +2 -2
  177. package/src/__tests__/integration/UserProfile.test.tsx +14 -14
  178. package/src/__tests__/public-recipe-view.test.ts +38 -9
  179. package/src/__tests__/rbac/PagePermissionGuard.test.tsx +6 -6
  180. package/src/__tests__/templates/accessibility.test.template.tsx +9 -9
  181. package/src/__tests__/templates/component.test.template.tsx +18 -15
  182. package/src/components/Button/Button.tsx +5 -1
  183. package/src/components/Calendar/Calendar.tsx +201 -47
  184. package/src/components/ContextSelector/ContextSelector.tsx +106 -119
  185. package/src/components/DataTable/AUDIT_REPORT.md +293 -0
  186. package/src/components/DataTable/__tests__/DataTableCore.test.tsx +10 -2
  187. package/src/components/DataTable/__tests__/a11y.basic.test.tsx +10 -4
  188. package/src/components/DataTable/__tests__/test-utils/sharedTestUtils.tsx +9 -9
  189. package/src/components/DataTable/components/ColumnFilter.tsx +63 -74
  190. package/src/components/DataTable/components/ColumnVisibilityDropdown.tsx +43 -41
  191. package/src/components/DataTable/components/DataTableCore.tsx +186 -13
  192. package/src/components/DataTable/components/DataTableErrorBoundary.tsx +9 -11
  193. package/src/components/DataTable/components/DataTableLayout.tsx +35 -21
  194. package/src/components/DataTable/components/EditFields.tsx +23 -3
  195. package/src/components/DataTable/components/EditableRow.tsx +12 -9
  196. package/src/components/DataTable/components/EmptyState.tsx +10 -9
  197. package/src/components/DataTable/components/FilterRow.tsx +2 -4
  198. package/src/components/DataTable/components/ImportModal.tsx +124 -126
  199. package/src/components/DataTable/components/LoadingState.tsx +5 -6
  200. package/src/components/DataTable/components/RowComponent.tsx +12 -0
  201. package/src/components/DataTable/components/SortIndicator.tsx +50 -0
  202. package/src/components/DataTable/components/__tests__/COVERAGE_NOTE.md +4 -4
  203. package/src/components/DataTable/components/__tests__/ColumnFilter.test.tsx +23 -82
  204. package/src/components/DataTable/components/__tests__/DataTableErrorBoundary.test.tsx +37 -9
  205. package/src/components/DataTable/components/__tests__/EmptyState.test.tsx +7 -4
  206. package/src/components/DataTable/components/__tests__/FilterRow.test.tsx +12 -4
  207. package/src/components/DataTable/components/__tests__/LoadingState.test.tsx +41 -27
  208. package/src/components/DataTable/components/hooks/usePermissionTracking.ts +0 -4
  209. package/src/components/DataTable/components/index.ts +2 -1
  210. package/src/components/DataTable/hooks/__tests__/useDataTableState.test.ts +51 -47
  211. package/src/components/DataTable/hooks/useDataTablePermissions.ts +24 -21
  212. package/src/components/DataTable/hooks/useDataTableState.ts +125 -9
  213. package/src/components/DataTable/hooks/useTableColumns.ts +40 -2
  214. package/src/components/DataTable/hooks/useTableHandlers.ts +11 -0
  215. package/src/components/DataTable/types.ts +5 -18
  216. package/src/components/DataTable/utils/a11yUtils.ts +17 -0
  217. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +2 -1
  218. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.tsx +11 -15
  219. package/src/components/DateTimeField/DateTimeField.tsx +10 -9
  220. package/src/components/Dialog/Dialog.test.tsx +128 -104
  221. package/src/components/Dialog/Dialog.tsx +742 -24
  222. package/src/components/ErrorBoundary/ErrorBoundary.tsx +77 -79
  223. package/src/components/FileDisplay/FileDisplay.test.tsx +4 -2
  224. package/src/components/FileDisplay/FileDisplay.tsx +23 -17
  225. package/src/components/FileUpload/FileUpload.test.tsx +52 -14
  226. package/src/components/FileUpload/FileUpload.tsx +112 -130
  227. package/src/components/Form/Form.test.tsx +6 -8
  228. package/src/components/Form/Form.tsx +365 -4
  229. package/src/components/NavigationMenu/NavigationMenu.test.tsx +14 -13
  230. package/src/components/NavigationMenu/useNavigationFiltering.ts +11 -21
  231. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +6 -4
  232. package/src/components/PaceAppLayout/PaceAppLayout.tsx +11 -15
  233. package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +108 -61
  234. package/src/components/PaceLoginPage/PaceLoginPage.tsx +27 -3
  235. package/src/components/Progress/Progress.tsx +2 -4
  236. package/src/components/ProtectedRoute/ProtectedRoute.tsx +8 -8
  237. package/src/components/Select/Select.tsx +109 -98
  238. package/src/components/Select/types.ts +4 -1
  239. package/src/components/UserMenu/UserMenu.tsx +9 -6
  240. package/src/hooks/__tests__/ServiceHooks.test.tsx +16 -16
  241. package/src/hooks/__tests__/hooks.integration.test.tsx +55 -57
  242. package/src/hooks/__tests__/useAppConfig.unit.test.ts +129 -67
  243. package/src/hooks/__tests__/useFocusTrap.unit.test.tsx +97 -97
  244. package/src/hooks/__tests__/usePublicEvent.simple.test.ts +149 -67
  245. package/src/hooks/__tests__/usePublicEvent.test.ts +149 -79
  246. package/src/hooks/__tests__/usePublicEvent.unit.test.ts +158 -109
  247. package/src/hooks/__tests__/useSessionDraft.test.ts +163 -0
  248. package/src/hooks/__tests__/useSessionRestoration.unit.test.tsx +10 -5
  249. package/src/hooks/public/usePublicEvent.ts +67 -195
  250. package/src/hooks/public/usePublicEventLogo.test.ts +70 -17
  251. package/src/hooks/public/usePublicEventLogo.ts +24 -14
  252. package/src/hooks/public/usePublicFileDisplay.ts +2 -2
  253. package/src/hooks/public/usePublicRouteParams.ts +5 -5
  254. package/src/hooks/useAppConfig.ts +28 -26
  255. package/src/hooks/useEventTheme.test.ts +217 -239
  256. package/src/hooks/useEventTheme.ts +16 -28
  257. package/src/hooks/useFileDisplay.ts +2 -2
  258. package/src/hooks/useOrganisationPermissions.ts +5 -7
  259. package/src/hooks/useQueryCache.ts +0 -1
  260. package/src/hooks/useSessionDraft.ts +380 -0
  261. package/src/hooks/useSessionRestoration.ts +3 -1
  262. package/src/icons/index.ts +27 -0
  263. package/src/index.ts +5 -0
  264. package/src/providers/OrganisationProvider.tsx +23 -14
  265. package/src/providers/UnifiedAuthProvider.smoke.test.tsx +21 -21
  266. package/src/providers/__tests__/AuthProvider.test.tsx +21 -21
  267. package/src/providers/__tests__/EventProvider.test.tsx +61 -61
  268. package/src/providers/__tests__/InactivityProvider.test.tsx +56 -56
  269. package/src/providers/__tests__/OrganisationProvider.test.tsx +75 -75
  270. package/src/providers/__tests__/ProviderLifecycle.test.tsx +37 -37
  271. package/src/providers/__tests__/UnifiedAuthProvider.test.tsx +103 -103
  272. package/src/providers/services/EventServiceProvider.tsx +1 -24
  273. package/src/providers/services/UnifiedAuthProvider.tsx +5 -48
  274. package/src/providers/services/__tests__/AuthServiceProvider.integration.test.tsx +7 -7
  275. package/src/providers/services/__tests__/UnifiedAuthProvider.integration.test.tsx +13 -10
  276. package/src/rbac/__tests__/adapters.comprehensive.test.tsx +7 -457
  277. package/src/rbac/__tests__/auth-rbac.e2e.test.tsx +33 -7
  278. package/src/rbac/adapters.tsx +7 -295
  279. package/src/rbac/api.test.ts +44 -56
  280. package/src/rbac/api.ts +10 -17
  281. package/src/rbac/cache-invalidation.ts +0 -1
  282. package/src/rbac/compliance/index.ts +10 -0
  283. package/src/rbac/compliance/pattern-detector.ts +553 -0
  284. package/src/rbac/compliance/runtime-compliance.ts +22 -0
  285. package/src/rbac/components/AccessDenied.tsx +150 -0
  286. package/src/rbac/components/NavigationGuard.tsx +12 -20
  287. package/src/rbac/components/PagePermissionGuard.tsx +4 -24
  288. package/src/rbac/components/__tests__/NavigationGuard.test.tsx +21 -8
  289. package/src/rbac/components/index.ts +3 -41
  290. package/src/rbac/eslint-rules.js +1 -1
  291. package/src/rbac/hooks/index.ts +0 -3
  292. package/src/rbac/hooks/permissions/index.ts +0 -3
  293. package/src/rbac/hooks/permissions/useAccessLevel.ts +4 -8
  294. package/src/rbac/hooks/usePermissions.ts +0 -3
  295. package/src/rbac/hooks/useResolvedScope.test.ts +57 -47
  296. package/src/rbac/hooks/useResolvedScope.ts +58 -140
  297. package/src/rbac/hooks/useResourcePermissions.test.ts +124 -38
  298. package/src/rbac/hooks/useResourcePermissions.ts +139 -48
  299. package/src/rbac/hooks/useRoleManagement.test.ts +65 -22
  300. package/src/rbac/hooks/useRoleManagement.ts +147 -19
  301. package/src/rbac/hooks/useSecureSupabase.ts +4 -8
  302. package/src/rbac/index.ts +7 -9
  303. package/src/rbac/utils/contextValidator.ts +9 -7
  304. package/src/services/AuthService.ts +130 -18
  305. package/src/services/EventService.ts +4 -97
  306. package/src/services/InactivityService.ts +16 -0
  307. package/src/services/OrganisationService.ts +7 -44
  308. package/src/services/__tests__/OrganisationService.test.ts +26 -8
  309. package/src/services/base/BaseService.ts +0 -3
  310. package/src/styles/core.css +7 -0
  311. package/src/theming/__tests__/parseEventColours.test.ts +9 -3
  312. package/src/theming/parseEventColours.ts +22 -10
  313. package/src/types/database.generated.ts +4733 -3809
  314. package/src/utils/__tests__/lazyLoad.unit.test.tsx +42 -39
  315. package/src/utils/__tests__/organisationContext.unit.test.ts +9 -10
  316. package/src/utils/context/organisationContext.test.ts +13 -28
  317. package/src/utils/context/organisationContext.ts +21 -52
  318. package/src/utils/dynamic/dynamicUtils.ts +1 -1
  319. package/src/utils/file-reference/index.ts +39 -15
  320. package/src/utils/formatting/formatDateTime.test.ts +3 -2
  321. package/src/utils/google-places/loadGoogleMapsScript.ts +29 -4
  322. package/src/utils/index.ts +4 -1
  323. package/src/utils/persistence/__tests__/keyDerivation.test.ts +135 -0
  324. package/src/utils/persistence/__tests__/sensitiveFieldDetection.test.ts +123 -0
  325. package/src/utils/persistence/keyDerivation.ts +304 -0
  326. package/src/utils/persistence/sensitiveFieldDetection.ts +212 -0
  327. package/src/utils/security/secureStorage.ts +5 -5
  328. package/src/utils/storage/README.md +1 -1
  329. package/src/utils/storage/helpers.ts +3 -3
  330. package/src/utils/supabase/createBaseClient.ts +147 -0
  331. package/src/utils/timezone/timezone.test.ts +1 -2
  332. package/src/utils/timezone/timezone.ts +1 -1
  333. package/src/utils/validation/csrf.ts +4 -4
  334. package/cursor-rules/00-pace-core-compliance.mdc +0 -331
  335. package/cursor-rules/01-standards-compliance.mdc +0 -244
  336. package/cursor-rules/04-testing-standards.mdc +0 -268
  337. package/cursor-rules/05-bug-reports-and-features.mdc +0 -246
  338. package/cursor-rules/06-code-quality.mdc +0 -309
  339. package/cursor-rules/07-tech-stack-compliance.mdc +0 -214
  340. package/cursor-rules/CHANGELOG.md +0 -119
  341. package/cursor-rules/README.md +0 -192
  342. package/dist/DataTable-AOVNCPTX.js +0 -175
  343. package/dist/DataTable-AOVNCPTX.js.map +0 -1
  344. package/dist/UnifiedAuthProvider-4SBX4LU5.js +0 -18
  345. package/dist/UnifiedAuthProvider-4SBX4LU5.js.map +0 -1
  346. package/dist/api-O6HTBX5Y.js +0 -52
  347. package/dist/api-O6HTBX5Y.js.map +0 -1
  348. package/dist/audit-V53FV5AG.js +0 -17
  349. package/dist/audit-V53FV5AG.js.map +0 -1
  350. package/dist/chunk-5DRSZLL2.js.map +0 -1
  351. package/dist/chunk-63FOKYGO.js.map +0 -1
  352. package/dist/chunk-6COVEUS7.js.map +0 -1
  353. package/dist/chunk-AFVQODI2.js +0 -263
  354. package/dist/chunk-AFVQODI2.js.map +0 -1
  355. package/dist/chunk-DGUM43GV.js.map +0 -1
  356. package/dist/chunk-E66EQZE6.js.map +0 -1
  357. package/dist/chunk-EFN2EIMK.js.map +0 -1
  358. package/dist/chunk-FFQEQTNW.js.map +0 -1
  359. package/dist/chunk-FMUCXFII.js.map +0 -1
  360. package/dist/chunk-G37KK66H.js.map +0 -1
  361. package/dist/chunk-G7QEZTYQ.js +0 -2053
  362. package/dist/chunk-G7QEZTYQ.js.map +0 -1
  363. package/dist/chunk-HU2C6SSC.js.map +0 -1
  364. package/dist/chunk-IHB5DR3H.js.map +0 -1
  365. package/dist/chunk-IVOFDYWT.js.map +0 -1
  366. package/dist/chunk-J36DSWQK.js.map +0 -1
  367. package/dist/chunk-JGRYX5UX.js.map +0 -1
  368. package/dist/chunk-KQCRWDSA.js +0 -1
  369. package/dist/chunk-KQCRWDSA.js.map +0 -1
  370. package/dist/chunk-L4OXEN46.js.map +0 -1
  371. package/dist/chunk-LMC26NLJ.js +0 -84
  372. package/dist/chunk-LMC26NLJ.js.map +0 -1
  373. package/dist/chunk-M43Y4SSO.js.map +0 -1
  374. package/dist/chunk-M7MPQISP.js.map +0 -1
  375. package/dist/chunk-NTM7ZSB6.js.map +0 -1
  376. package/dist/chunk-PWLANIRT.js.map +0 -1
  377. package/dist/chunk-QXHPKYJV.js.map +0 -1
  378. package/dist/chunk-RGAWHO7N.js.map +0 -1
  379. package/dist/chunk-UPPMRMYG.js.map +0 -1
  380. package/dist/chunk-VBXEHIUJ.js.map +0 -1
  381. package/dist/chunk-ZSAAAMVR.js.map +0 -1
  382. package/dist/components.js.map +0 -1
  383. package/dist/contextValidator-5OGXSPKS.js +0 -9
  384. package/dist/contextValidator-5OGXSPKS.js.map +0 -1
  385. package/dist/eslint-rules/pace-core-compliance.cjs +0 -510
  386. package/dist/hooks.js.map +0 -1
  387. package/dist/index.js.map +0 -1
  388. package/dist/providers.js.map +0 -1
  389. package/dist/rbac/eslint-rules.js.map +0 -1
  390. package/dist/rbac/index.js.map +0 -1
  391. package/dist/styles/index.js.map +0 -1
  392. package/dist/theming/runtime.js.map +0 -1
  393. package/dist/types.js.map +0 -1
  394. package/dist/utils.js.map +0 -1
  395. package/docs/best-practices/README.md +0 -472
  396. package/docs/best-practices/accessibility.md +0 -601
  397. package/docs/best-practices/common-patterns.md +0 -516
  398. package/docs/best-practices/deployment.md +0 -1103
  399. package/docs/best-practices/performance.md +0 -1328
  400. package/docs/best-practices/security.md +0 -940
  401. package/docs/best-practices/testing.md +0 -1034
  402. package/docs/rbac/compliance/compliance-guide.md +0 -544
  403. package/docs/standards/01-architecture-standard.md +0 -44
  404. package/docs/standards/02-api-and-rpc-standard.md +0 -39
  405. package/docs/standards/03-component-standard.md +0 -32
  406. package/docs/standards/04-code-style-standard.md +0 -32
  407. package/docs/standards/05-security-standard.md +0 -44
  408. package/docs/standards/06-testing-and-docs-standard.md +0 -29
  409. package/docs/standards/pace-core-compliance.md +0 -432
  410. package/scripts/audit/core/checks/accessibility.cjs +0 -197
  411. package/scripts/audit/core/checks/api-usage.cjs +0 -191
  412. package/scripts/audit/core/checks/bundle.cjs +0 -142
  413. package/scripts/audit/core/checks/compliance.cjs +0 -2706
  414. package/scripts/audit/core/checks/config.cjs +0 -54
  415. package/scripts/audit/core/checks/coverage.cjs +0 -84
  416. package/scripts/audit/core/checks/dependencies.cjs +0 -994
  417. package/scripts/audit/core/checks/documentation.cjs +0 -268
  418. package/scripts/audit/core/checks/environment.cjs +0 -116
  419. package/scripts/audit/core/checks/error-handling.cjs +0 -340
  420. package/scripts/audit/core/checks/forms.cjs +0 -172
  421. package/scripts/audit/core/checks/heuristics.cjs +0 -68
  422. package/scripts/audit/core/checks/hooks.cjs +0 -334
  423. package/scripts/audit/core/checks/imports.cjs +0 -244
  424. package/scripts/audit/core/checks/performance.cjs +0 -325
  425. package/scripts/audit/core/checks/routes.cjs +0 -117
  426. package/scripts/audit/core/checks/state.cjs +0 -130
  427. package/scripts/audit/core/checks/structure.cjs +0 -65
  428. package/scripts/audit/core/checks/style.cjs +0 -584
  429. package/scripts/audit/core/checks/testing.cjs +0 -122
  430. package/scripts/audit/core/checks/typescript.cjs +0 -61
  431. package/scripts/audit/core/scanner.cjs +0 -199
  432. package/scripts/audit/core/utils.cjs +0 -137
  433. package/scripts/audit/index.cjs +0 -223
  434. package/scripts/audit/reporters/console.cjs +0 -151
  435. package/scripts/audit/reporters/json.cjs +0 -54
  436. package/scripts/audit/reporters/markdown.cjs +0 -124
  437. package/scripts/audit-consuming-app.cjs +0 -86
  438. package/src/components/DataTable/components/DataTableBody.tsx +0 -454
  439. package/src/components/DataTable/components/DraggableColumnHeader.tsx +0 -156
  440. package/src/components/DataTable/components/ExpandButton.tsx +0 -113
  441. package/src/components/DataTable/components/GroupHeader.tsx +0 -54
  442. package/src/components/DataTable/components/ViewRowModal.tsx +0 -68
  443. package/src/components/DataTable/components/VirtualizedDataTable.tsx +0 -525
  444. package/src/components/DataTable/components/__tests__/ExpandButton.test.tsx +0 -462
  445. package/src/components/DataTable/components/__tests__/GroupHeader.test.tsx +0 -393
  446. package/src/components/DataTable/components/__tests__/ViewRowModal.test.tsx +0 -476
  447. package/src/components/DataTable/components/__tests__/VirtualizedDataTable.test.tsx +0 -128
  448. package/src/components/DataTable/core/DataTableContext.tsx +0 -216
  449. package/src/components/DataTable/core/__tests__/DataTableContext.test.tsx +0 -136
  450. package/src/components/DataTable/hooks/__tests__/useColumnReordering.test.ts +0 -570
  451. package/src/components/DataTable/hooks/useColumnReordering.ts +0 -123
  452. package/src/components/DataTable/utils/debugTools.ts +0 -514
  453. package/src/eslint-rules/pace-core-compliance.js +0 -638
  454. package/src/rbac/components/EnhancedNavigationMenu.test.tsx +0 -555
  455. package/src/rbac/components/EnhancedNavigationMenu.tsx +0 -293
  456. package/src/rbac/components/NavigationProvider.test.tsx +0 -481
  457. package/src/rbac/components/NavigationProvider.tsx +0 -345
  458. package/src/rbac/components/PagePermissionProvider.test.tsx +0 -476
  459. package/src/rbac/components/PagePermissionProvider.tsx +0 -279
  460. package/src/rbac/components/PermissionEnforcer.tsx +0 -312
  461. package/src/rbac/components/RoleBasedRouter.tsx +0 -440
  462. package/src/rbac/components/SecureDataProvider.test.tsx +0 -543
  463. package/src/rbac/components/SecureDataProvider.tsx +0 -339
  464. package/src/rbac/components/__tests__/EnhancedNavigationMenu.test.tsx +0 -620
  465. package/src/rbac/components/__tests__/NavigationProvider.test.tsx +0 -726
  466. package/src/rbac/components/__tests__/PagePermissionProvider.test.tsx +0 -661
  467. package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +0 -881
  468. package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +0 -783
  469. package/src/rbac/components/__tests__/SecureDataProvider.fixed.test.tsx +0 -645
  470. package/src/rbac/components/__tests__/SecureDataProvider.test.tsx +0 -659
  471. package/src/rbac/hooks/permissions/useCachedPermissions.ts +0 -79
  472. package/src/rbac/hooks/permissions/useHasAllPermissions.ts +0 -90
  473. 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;