@jmruthers/pace-core 0.6.1 → 0.6.3

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 (549) hide show
  1. package/CHANGELOG.md +88 -10
  2. package/cursor-rules/00-pace-core-compliance.mdc +46 -87
  3. package/cursor-rules/01-standards-compliance.mdc +16 -47
  4. package/cursor-rules/02-project-structure.mdc +4 -4
  5. package/cursor-rules/03-solid-principles.mdc +45 -164
  6. package/cursor-rules/04-testing-standards.mdc +22 -69
  7. package/cursor-rules/05-bug-reports-and-features.mdc +2 -2
  8. package/cursor-rules/06-code-quality.mdc +42 -125
  9. package/cursor-rules/07-tech-stack-compliance.mdc +33 -128
  10. package/cursor-rules/08-markup-quality.mdc +452 -0
  11. package/cursor-rules/CHANGELOG.md +18 -0
  12. package/cursor-rules/README.md +2 -1
  13. package/dist/{AuthService-DjnJHDtC.d.ts → AuthService-Cb34EQs3.d.ts} +63 -1
  14. package/dist/{DataTable-CH1U5Tpy.d.ts → DataTable-BMRU8a1j.d.ts} +33 -1
  15. package/dist/{DataTable-DQ7RSOHE.js → DataTable-THFPBKTP.js} +12 -10
  16. package/dist/{PublicPageProvider-ce4xlHYA.d.ts → PublicPageProvider-DEMpysFR.d.ts} +394 -171
  17. package/dist/{UnifiedAuthProvider-185Ih4dj.d.ts → UnifiedAuthProvider-CKvHP1MK.d.ts} +30 -8
  18. package/dist/{UnifiedAuthProvider-ATAP5UTR.js → UnifiedAuthProvider-KAGUYQ4J.js} +5 -4
  19. package/dist/{api-N774RPUA.js → api-IAGWF3ZG.js} +10 -10
  20. package/dist/{audit-B5P6FFIR.js → audit-V53FV5AG.js} +2 -2
  21. package/dist/{chunk-JBKQ3SAO.js → chunk-2T2IG7T7.js} +107 -57
  22. package/dist/chunk-2T2IG7T7.js.map +1 -0
  23. package/dist/{chunk-3QRJFVBR.js → chunk-6SOIHG6Z.js} +1 -1
  24. package/dist/chunk-6SOIHG6Z.js.map +1 -0
  25. package/dist/{chunk-3XTALGJF.js → chunk-6Z7LTB3D.js} +69 -240
  26. package/dist/chunk-6Z7LTB3D.js.map +1 -0
  27. package/dist/{chunk-4ZC4GX36.js → chunk-CNCQDFLN.js} +199 -46
  28. package/dist/chunk-CNCQDFLN.js.map +1 -0
  29. package/dist/chunk-DGUM43GV.js +11 -0
  30. package/dist/{chunk-BYFSK72L.js → chunk-DWUBLJJM.js} +361 -187
  31. package/dist/chunk-DWUBLJJM.js.map +1 -0
  32. package/dist/{chunk-LXQLPRQ2.js → chunk-FFQEQTNW.js} +6 -8
  33. package/dist/chunk-FFQEQTNW.js.map +1 -0
  34. package/dist/chunk-FMUCXFII.js +76 -0
  35. package/dist/chunk-FMUCXFII.js.map +1 -0
  36. package/dist/{chunk-4N5C5XZU.js → chunk-HFZBI76P.js} +4 -4
  37. package/dist/chunk-HFZBI76P.js.map +1 -0
  38. package/dist/{chunk-SQGMNID3.js → chunk-L4OXEN46.js} +4 -5
  39. package/dist/chunk-L4OXEN46.js.map +1 -0
  40. package/dist/{chunk-R77UEZ4E.js → chunk-M43Y4SSO.js} +1 -1
  41. package/dist/chunk-M43Y4SSO.js.map +1 -0
  42. package/dist/{chunk-I7PSE6JW.js → chunk-M7MPQISP.js} +3 -76
  43. package/dist/chunk-M7MPQISP.js.map +1 -0
  44. package/dist/chunk-PQBSKX33.js +7793 -0
  45. package/dist/chunk-PQBSKX33.js.map +1 -0
  46. package/dist/chunk-QRPVRXYT.js +226 -0
  47. package/dist/chunk-QRPVRXYT.js.map +1 -0
  48. package/dist/{chunk-KNC55RTG.js → chunk-RWEBCB47.js} +194 -416
  49. package/dist/chunk-RWEBCB47.js.map +1 -0
  50. package/dist/{chunk-XM25TVIE.js → chunk-YDQHOZNA.js} +843 -388
  51. package/dist/chunk-YDQHOZNA.js.map +1 -0
  52. package/dist/{chunk-GLK6VM3F.js → chunk-ZNIWI3UC.js} +739 -737
  53. package/dist/chunk-ZNIWI3UC.js.map +1 -0
  54. package/dist/components.d.ts +5 -5
  55. package/dist/components.js +18 -16
  56. package/dist/components.js.map +1 -1
  57. package/dist/contextValidator-3JNZKUTX.js +9 -0
  58. package/dist/contextValidator-3JNZKUTX.js.map +1 -0
  59. package/dist/eslint-rules/pace-core-compliance.cjs +106 -0
  60. package/dist/{functions-D_kgHktt.d.ts → functions-DHebl8-F.d.ts} +1 -1
  61. package/dist/hooks.d.ts +55 -122
  62. package/dist/hooks.js +10 -13
  63. package/dist/hooks.js.map +1 -1
  64. package/dist/index.d.ts +60 -13
  65. package/dist/index.js +30 -25
  66. package/dist/index.js.map +1 -1
  67. package/dist/providers.d.ts +21 -3
  68. package/dist/providers.js +4 -3
  69. package/dist/rbac/index.d.ts +210 -139
  70. package/dist/rbac/index.js +17 -13
  71. package/dist/styles/index.js +1 -1
  72. package/dist/theming/runtime.d.ts +1 -13
  73. package/dist/theming/runtime.js +2 -2
  74. package/dist/{timezone-_pgH8qrY.d.ts → timezone-CHhWg6b4.d.ts} +3 -10
  75. package/dist/{types-UU913iLA.d.ts → types-BeoeWV5I.d.ts} +8 -0
  76. package/dist/{types-CEpcvwwF.d.ts → types-CkbwOr4Y.d.ts} +6 -0
  77. package/dist/types.d.ts +2 -2
  78. package/dist/types.js +1 -1
  79. package/dist/{usePublicRouteParams-BJAlWfuJ.d.ts → usePublicRouteParams-i3qtoBgg.d.ts} +38 -17
  80. package/dist/utils.d.ts +4 -5
  81. package/dist/utils.js +17 -19
  82. package/dist/utils.js.map +1 -1
  83. package/docs/api/README.md +21 -17
  84. package/docs/api/modules.md +4191 -2967
  85. package/docs/architecture/database-schema-requirements.md +161 -0
  86. package/docs/components/context-selector.md +126 -0
  87. package/docs/core-concepts/rbac-system.md +3 -3
  88. package/docs/documentation-index.md +2 -4
  89. package/docs/getting-started/cursor-rules.md +2 -1
  90. package/docs/migration/DOCUMENTATION_STRUCTURE.md +441 -0
  91. package/docs/migration/MIGRATION_GUIDE.md +2 -24
  92. package/docs/migration/RBAC_SCOPE_MIGRATION.md +385 -0
  93. package/docs/migration/README.md +52 -6
  94. package/docs/migration/V0.5.190_TO_V0.6.1_MIGRATION.md +1153 -0
  95. package/docs/migration/database-changes-december-2025.md +3 -3
  96. package/docs/pace-mint-fix-auto-selection.md +218 -0
  97. package/docs/pace-mint-rbac-setup.md +391 -0
  98. package/docs/rbac/event-based-apps.md +1 -1
  99. package/docs/rbac/getting-started.md +1 -1
  100. package/docs/rbac/quick-start.md +1 -1
  101. package/docs/rbac/secure-client-protection.md +330 -0
  102. package/docs/standards/README.md +1 -0
  103. package/package.json +4 -3
  104. package/scripts/audit/core/checks/accessibility.cjs +197 -0
  105. package/scripts/audit/core/checks/api-usage.cjs +191 -0
  106. package/scripts/audit/core/checks/bundle.cjs +142 -0
  107. package/scripts/{check-pace-core-compliance.cjs → audit/core/checks/compliance.cjs} +784 -685
  108. package/scripts/audit/core/checks/config.cjs +54 -0
  109. package/scripts/audit/core/checks/coverage.cjs +84 -0
  110. package/scripts/audit/core/checks/dependencies.cjs +985 -0
  111. package/scripts/audit/core/checks/documentation.cjs +268 -0
  112. package/scripts/audit/core/checks/environment.cjs +116 -0
  113. package/scripts/audit/core/checks/error-handling.cjs +340 -0
  114. package/scripts/audit/core/checks/forms.cjs +172 -0
  115. package/scripts/audit/core/checks/heuristics.cjs +68 -0
  116. package/scripts/audit/core/checks/hooks.cjs +334 -0
  117. package/scripts/audit/core/checks/imports.cjs +244 -0
  118. package/scripts/audit/core/checks/performance.cjs +325 -0
  119. package/scripts/audit/core/checks/routes.cjs +117 -0
  120. package/scripts/audit/core/checks/state.cjs +130 -0
  121. package/scripts/audit/core/checks/structure.cjs +65 -0
  122. package/scripts/audit/core/checks/style.cjs +584 -0
  123. package/scripts/audit/core/checks/testing.cjs +122 -0
  124. package/scripts/audit/core/checks/typescript.cjs +61 -0
  125. package/scripts/audit/core/scanner.cjs +199 -0
  126. package/scripts/audit/core/utils.cjs +137 -0
  127. package/scripts/audit/index.cjs +223 -0
  128. package/scripts/audit/reporters/console.cjs +151 -0
  129. package/scripts/audit/reporters/json.cjs +54 -0
  130. package/scripts/audit/reporters/markdown.cjs +124 -0
  131. package/scripts/audit-consuming-app.cjs +61 -936
  132. package/scripts/build-docs/build-decision.js +240 -0
  133. package/scripts/build-docs/cache-utils.js +105 -0
  134. package/scripts/build-docs/content-normalization.js +150 -0
  135. package/scripts/build-docs/file-utils.js +105 -0
  136. package/scripts/build-docs/git-utils.js +86 -0
  137. package/scripts/build-docs/hash-utils.js +116 -0
  138. package/scripts/build-docs/typedoc-runner.js +220 -0
  139. package/scripts/build-docs-incremental.js +77 -913
  140. package/scripts/utils/command-runner.js +16 -11
  141. package/scripts/validate-formats.js +61 -56
  142. package/scripts/validate-master.js +74 -69
  143. package/scripts/validate-pre-publish.js +70 -65
  144. package/src/__tests__/hooks/usePermissions.test.ts +2 -2
  145. package/src/components/Alert/Alert.test.tsx +12 -18
  146. package/src/components/Alert/Alert.tsx +5 -7
  147. package/src/components/Avatar/Avatar.test.tsx +4 -4
  148. package/src/components/Badge/Badge.tsx +14 -0
  149. package/src/components/Button/Button.tsx +22 -0
  150. package/src/components/Calendar/Calendar.tsx +8 -2
  151. package/src/components/Card/Card.tsx +4 -0
  152. package/src/components/Checkbox/Checkbox.test.tsx +12 -12
  153. package/src/components/Checkbox/Checkbox.tsx +2 -2
  154. package/src/components/ContextSelector/ContextSelector.tsx +384 -0
  155. package/src/components/ContextSelector/index.ts +3 -0
  156. package/src/components/DataTable/DataTable.tsx +38 -4
  157. package/src/components/DataTable/__tests__/DataTableCore.test-setup.ts +5 -6
  158. package/src/components/DataTable/__tests__/pagination.modes.test.tsx +18 -4
  159. package/src/components/DataTable/__tests__/test-utils/sharedTestUtils.tsx +2 -3
  160. package/src/components/DataTable/components/AccessDeniedPage.tsx +16 -25
  161. package/src/components/DataTable/components/ActionButtons.tsx +10 -7
  162. package/src/components/DataTable/components/BulkOperationsDropdown.tsx +1 -1
  163. package/src/components/DataTable/components/ColumnFilter.tsx +10 -0
  164. package/src/components/DataTable/components/ColumnVisibilityDropdown.tsx +12 -0
  165. package/src/components/DataTable/components/DataTableBody.tsx +8 -0
  166. package/src/components/DataTable/components/DataTableCore.tsx +196 -554
  167. package/src/components/DataTable/components/DataTableErrorBoundary.tsx +11 -0
  168. package/src/components/DataTable/components/DataTableLayout.tsx +559 -0
  169. package/src/components/DataTable/components/DataTableModals.tsx +8 -0
  170. package/src/components/DataTable/components/DataTableToolbar.tsx +8 -0
  171. package/src/components/DataTable/components/DraggableColumnHeader.tsx +12 -0
  172. package/src/components/DataTable/components/EditFields.tsx +307 -0
  173. package/src/components/DataTable/components/EditableRow.tsx +8 -0
  174. package/src/components/DataTable/components/EmptyState.tsx +10 -0
  175. package/src/components/DataTable/components/FilterRow.tsx +12 -0
  176. package/src/components/DataTable/components/GroupHeader.tsx +12 -0
  177. package/src/components/DataTable/components/GroupingDropdown.tsx +12 -0
  178. package/src/components/DataTable/components/ImportModal.tsx +7 -0
  179. package/src/components/DataTable/components/LoadingState.tsx +6 -0
  180. package/src/components/DataTable/components/PaginationControls.tsx +16 -1
  181. package/src/components/DataTable/components/RowComponent.tsx +391 -0
  182. package/src/components/DataTable/components/UnifiedTableBody.tsx +63 -851
  183. package/src/components/DataTable/components/VirtualizedDataTable.tsx +16 -4
  184. package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +4 -2
  185. package/src/components/DataTable/components/cellValueUtils.ts +40 -0
  186. package/src/components/DataTable/components/hooks/useImportModalFocus.ts +53 -0
  187. package/src/components/DataTable/components/hooks/usePermissionTracking.ts +126 -0
  188. package/src/components/DataTable/context/DataTableContext.tsx +50 -0
  189. package/src/components/DataTable/core/ColumnFactory.ts +31 -0
  190. package/src/components/DataTable/core/DataTableContext.tsx +32 -1
  191. package/src/components/DataTable/hooks/useColumnOrderPersistence.ts +10 -0
  192. package/src/components/DataTable/hooks/useColumnReordering.ts +12 -0
  193. package/src/components/DataTable/hooks/useColumnVisibilityPersistence.ts +10 -0
  194. package/src/components/DataTable/hooks/useDataTableDataPipeline.ts +16 -0
  195. package/src/components/DataTable/hooks/useDataTablePermissions.ts +127 -33
  196. package/src/components/DataTable/hooks/useDataTableState.ts +35 -1
  197. package/src/components/DataTable/hooks/useEffectiveColumnOrder.ts +12 -0
  198. package/src/components/DataTable/hooks/useServerSideDataEffect.ts +11 -0
  199. package/src/components/DataTable/hooks/useTableColumns.ts +8 -0
  200. package/src/components/DataTable/hooks/useTableHandlers.ts +14 -0
  201. package/src/components/DataTable/styles.ts +6 -6
  202. package/src/components/DataTable/types.ts +6 -10
  203. package/src/components/DataTable/utils/a11yUtils.ts +7 -0
  204. package/src/components/DataTable/utils/debugTools.ts +18 -113
  205. package/src/components/DataTable/utils/errorHandling.ts +12 -0
  206. package/src/components/DataTable/utils/exportUtils.ts +9 -0
  207. package/src/components/DataTable/utils/flexibleImport.ts +12 -48
  208. package/src/components/DataTable/utils/paginationUtils.ts +8 -0
  209. package/src/components/DataTable/utils/performanceUtils.ts +5 -1
  210. package/src/components/Dialog/Dialog.tsx +31 -3
  211. package/src/components/ErrorBoundary/ErrorBoundary.test.tsx +180 -1
  212. package/src/components/ErrorBoundary/ErrorBoundary.tsx +45 -5
  213. package/src/components/ErrorBoundary/ErrorBoundaryContext.tsx +129 -0
  214. package/src/components/ErrorBoundary/index.ts +27 -2
  215. package/src/components/FileDisplay/FileDisplay.tsx +74 -28
  216. package/src/components/FileUpload/FileUpload.tsx +22 -2
  217. package/src/components/Footer/Footer.test.tsx +16 -16
  218. package/src/components/Footer/Footer.tsx +14 -11
  219. package/src/components/Form/Form.tsx +1 -0
  220. package/src/components/Header/Header.test.tsx +43 -73
  221. package/src/components/Header/Header.tsx +59 -49
  222. package/src/components/Input/Input.test.tsx +2 -2
  223. package/src/components/Input/Input.tsx +8 -4
  224. package/src/components/LoadingSpinner/LoadingSpinner.test.tsx +4 -4
  225. package/src/components/LoginForm/LoginForm.tsx +4 -0
  226. package/src/components/NavigationMenu/NavigationMenu.tsx +14 -513
  227. package/src/components/NavigationMenu/types.ts +56 -0
  228. package/src/components/NavigationMenu/useNavigationFiltering.ts +390 -0
  229. package/src/components/PaceAppLayout/PaceAppLayout.integration.test.tsx +10 -19
  230. package/src/components/PaceAppLayout/PaceAppLayout.performance.test.tsx +2 -2
  231. package/src/components/PaceAppLayout/PaceAppLayout.security.test.tsx +5 -5
  232. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +13 -11
  233. package/src/components/PaceAppLayout/PaceAppLayout.tsx +167 -44
  234. package/src/components/PaceAppLayout/README.md +14 -17
  235. package/src/components/PaceAppLayout/test-setup.tsx +3 -4
  236. package/src/components/PaceLoginPage/PaceLoginPage.tsx +3 -0
  237. package/src/components/PasswordChange/PasswordChangeForm.tsx +9 -0
  238. package/src/components/ProtectedRoute/ProtectedRoute.tsx +3 -9
  239. package/src/components/PublicLayout/PublicPageLayout.tsx +2 -5
  240. package/src/components/PublicLayout/PublicPageProvider.tsx +4 -0
  241. package/src/components/Select/Select.tsx +80 -434
  242. package/src/components/Select/context.ts +23 -0
  243. package/src/components/Select/hooks/useSelectEvents.ts +87 -0
  244. package/src/components/Select/hooks/useSelectSearch.ts +91 -0
  245. package/src/components/Select/hooks/useSelectState.ts +104 -0
  246. package/src/components/Select/index.ts +9 -1
  247. package/src/components/Select/types.ts +123 -0
  248. package/src/components/Select/utils/text.ts +26 -0
  249. package/src/components/SessionRestorationLoader/SessionRestorationLoader.tsx +4 -5
  250. package/src/components/Switch/Switch.tsx +4 -4
  251. package/src/components/Tabs/Tabs.tsx +1 -1
  252. package/src/components/Toast/Toast.tsx +4 -0
  253. package/src/components/Tooltip/Tooltip.tsx +2 -2
  254. package/src/components/UserMenu/UserMenu.test.tsx +24 -11
  255. package/src/components/UserMenu/UserMenu.tsx +21 -18
  256. package/src/components/index.ts +7 -7
  257. package/src/eslint-rules/pace-core-compliance.cjs +106 -0
  258. package/src/hooks/__tests__/index.unit.test.ts +2 -5
  259. package/src/hooks/__tests__/useAppConfig.unit.test.ts +4 -98
  260. package/src/hooks/index.ts +1 -2
  261. package/src/hooks/public/usePublicEvent.ts +4 -0
  262. package/src/hooks/public/usePublicEventLogo.ts +4 -0
  263. package/src/hooks/public/usePublicFileDisplay.ts +4 -0
  264. package/src/hooks/public/usePublicRouteParams.ts +4 -0
  265. package/src/hooks/services/useAuth.ts +32 -0
  266. package/src/hooks/services/useCurrentEvent.ts +6 -0
  267. package/src/hooks/services/useCurrentOrganisation.ts +6 -0
  268. package/src/hooks/useAppConfig.ts +15 -30
  269. package/src/hooks/useDebounce.ts +9 -0
  270. package/src/hooks/useEventTheme.ts +6 -0
  271. package/src/hooks/useFileDisplay.ts +81 -50
  272. package/src/hooks/useFileReference.ts +25 -7
  273. package/src/hooks/useFileUrl.ts +11 -1
  274. package/src/hooks/useFocusManagement.ts +14 -0
  275. package/src/hooks/useFocusTrap.ts +3 -0
  276. package/src/hooks/useInactivityTracker.ts +3 -0
  277. package/src/hooks/useKeyboardShortcuts.ts +4 -0
  278. package/src/hooks/useOrganisationPermissions.ts +4 -0
  279. package/src/hooks/useOrganisationSecurity.ts +4 -0
  280. package/src/hooks/usePerformanceMonitor.ts +4 -0
  281. package/src/hooks/usePermissionCache.ts +7 -0
  282. package/src/hooks/useQueryCache.ts +12 -1
  283. package/src/hooks/useSessionRestoration.ts +4 -0
  284. package/src/hooks/useStorage.ts +4 -0
  285. package/src/hooks/useToast.ts +1 -1
  286. package/src/index.ts +6 -6
  287. package/src/providers/__tests__/OrganisationProvider.test.tsx +92 -70
  288. package/src/providers/services/AuthServiceProvider.tsx +35 -7
  289. package/src/providers/services/EventServiceProvider.tsx +51 -5
  290. package/src/providers/services/InactivityServiceProvider.tsx +18 -0
  291. package/src/providers/services/OrganisationServiceProvider.tsx +18 -0
  292. package/src/providers/services/UnifiedAuthProvider.tsx +126 -134
  293. package/src/providers/services/__tests__/AuthServiceProvider.integration.test.tsx +29 -13
  294. package/src/rbac/README.md +1 -1
  295. package/src/rbac/__tests__/adapters.comprehensive.test.tsx +1 -1
  296. package/src/rbac/__tests__/scenarios.user-role.test.tsx +4 -5
  297. package/src/rbac/adapters.tsx +12 -3
  298. package/src/rbac/api.test.ts +59 -51
  299. package/src/rbac/api.ts +246 -167
  300. package/src/rbac/components/NavigationProvider.tsx +4 -1
  301. package/src/rbac/components/PagePermissionGuard.tsx +185 -17
  302. package/src/rbac/components/RoleBasedRouter.tsx +5 -1
  303. package/src/rbac/components/SecureDataProvider.test.tsx +84 -49
  304. package/src/rbac/components/SecureDataProvider.tsx +20 -5
  305. package/src/rbac/components/__tests__/PagePermissionGuard.race-condition.test.tsx +24 -14
  306. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +7 -0
  307. package/src/rbac/components/__tests__/PagePermissionGuard.verification.test.tsx +14 -6
  308. package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +15 -4
  309. package/src/rbac/components/__tests__/SecureDataProvider.fixed.test.tsx +148 -24
  310. package/src/rbac/components/__tests__/SecureDataProvider.test.tsx +81 -15
  311. package/src/rbac/engine.ts +38 -14
  312. package/src/rbac/hooks/__tests__/useSecureSupabase.test.ts +32 -21
  313. package/src/rbac/hooks/permissions/index.ts +7 -0
  314. package/src/rbac/hooks/permissions/useAccessLevel.ts +105 -0
  315. package/src/rbac/hooks/permissions/useCachedPermissions.ts +79 -0
  316. package/src/rbac/hooks/permissions/useCan.ts +377 -0
  317. package/src/rbac/hooks/permissions/useHasAllPermissions.ts +90 -0
  318. package/src/rbac/hooks/permissions/useHasAnyPermission.ts +90 -0
  319. package/src/rbac/hooks/permissions/useMultiplePermissions.ts +93 -0
  320. package/src/rbac/hooks/permissions/usePermissions.ts +253 -0
  321. package/src/rbac/hooks/useCan.test.ts +64 -66
  322. package/src/rbac/hooks/usePermissions.ts +14 -995
  323. package/src/rbac/hooks/useRBAC.test.ts +1 -5
  324. package/src/rbac/hooks/useRBAC.ts +36 -37
  325. package/src/rbac/hooks/useResolvedScope.test.ts +120 -35
  326. package/src/rbac/hooks/useResolvedScope.ts +35 -40
  327. package/src/rbac/hooks/useResourcePermissions.test.ts +54 -18
  328. package/src/rbac/hooks/useResourcePermissions.ts +14 -4
  329. package/src/rbac/hooks/useSecureSupabase.ts +27 -7
  330. package/src/rbac/index.ts +7 -0
  331. package/src/rbac/permissions.ts +0 -30
  332. package/src/rbac/secureClient.test.ts +22 -18
  333. package/src/rbac/secureClient.ts +294 -68
  334. package/src/rbac/security.ts +0 -17
  335. package/src/rbac/types.ts +9 -0
  336. package/src/rbac/utils/__tests__/contextValidator.test.ts +64 -86
  337. package/src/rbac/utils/clientSecurity.ts +93 -0
  338. package/src/rbac/utils/contextValidator.ts +77 -168
  339. package/src/services/AuthService.ts +39 -7
  340. package/src/services/EventService.ts +186 -54
  341. package/src/services/OrganisationService.ts +81 -14
  342. package/src/services/__tests__/EventService.test.ts +1 -2
  343. package/src/services/base/BaseService.ts +3 -0
  344. package/src/theming/__tests__/parseEventColours.test.ts +6 -9
  345. package/src/theming/parseEventColours.ts +5 -19
  346. package/src/types/vitest-globals.d.ts +51 -26
  347. package/src/utils/__mocks__/supabaseMock.ts +1 -3
  348. package/src/utils/__tests__/formatting.unit.test.ts +4 -4
  349. package/src/utils/__tests__/index.unit.test.ts +2 -2
  350. package/src/utils/audit/audit.ts +0 -3
  351. package/src/utils/core/cn.ts +1 -1
  352. package/src/utils/dynamic/dynamicUtils.ts +7 -4
  353. package/src/utils/file-reference/index.ts +53 -1
  354. package/src/utils/formatting/formatting.ts +8 -18
  355. package/src/utils/index.ts +0 -1
  356. package/dist/chunk-3QRJFVBR.js.map +0 -1
  357. package/dist/chunk-3XTALGJF.js.map +0 -1
  358. package/dist/chunk-4N5C5XZU.js.map +0 -1
  359. package/dist/chunk-4ZC4GX36.js.map +0 -1
  360. package/dist/chunk-7D4SUZUM.js +0 -38
  361. package/dist/chunk-BYFSK72L.js.map +0 -1
  362. package/dist/chunk-EXUD6RNJ.js +0 -451
  363. package/dist/chunk-EXUD6RNJ.js.map +0 -1
  364. package/dist/chunk-GLK6VM3F.js.map +0 -1
  365. package/dist/chunk-I7PSE6JW.js.map +0 -1
  366. package/dist/chunk-JBKQ3SAO.js.map +0 -1
  367. package/dist/chunk-KNC55RTG.js.map +0 -1
  368. package/dist/chunk-LXQLPRQ2.js.map +0 -1
  369. package/dist/chunk-R77UEZ4E.js.map +0 -1
  370. package/dist/chunk-SQGMNID3.js.map +0 -1
  371. package/dist/chunk-T33XF5ZC.js +0 -12922
  372. package/dist/chunk-T33XF5ZC.js.map +0 -1
  373. package/dist/chunk-XM25TVIE.js.map +0 -1
  374. package/docs/api/classes/ColumnFactory.md +0 -243
  375. package/docs/api/classes/ErrorBoundary.md +0 -144
  376. package/docs/api/classes/InvalidScopeError.md +0 -73
  377. package/docs/api/classes/Logger.md +0 -178
  378. package/docs/api/classes/MissingUserContextError.md +0 -66
  379. package/docs/api/classes/OrganisationContextRequiredError.md +0 -66
  380. package/docs/api/classes/PermissionDeniedError.md +0 -73
  381. package/docs/api/classes/RBACAuditManager.md +0 -297
  382. package/docs/api/classes/RBACCache.md +0 -322
  383. package/docs/api/classes/RBACEngine.md +0 -171
  384. package/docs/api/classes/RBACError.md +0 -76
  385. package/docs/api/classes/RBACNotInitializedError.md +0 -66
  386. package/docs/api/classes/SecureSupabaseClient.md +0 -160
  387. package/docs/api/classes/StorageUtils.md +0 -328
  388. package/docs/api/enums/FileCategory.md +0 -184
  389. package/docs/api/enums/LogLevel.md +0 -54
  390. package/docs/api/enums/RBACErrorCode.md +0 -228
  391. package/docs/api/enums/RPCFunction.md +0 -118
  392. package/docs/api/interfaces/AddressFieldProps.md +0 -241
  393. package/docs/api/interfaces/AddressFieldRef.md +0 -94
  394. package/docs/api/interfaces/AggregateConfig.md +0 -43
  395. package/docs/api/interfaces/AutocompleteOptions.md +0 -75
  396. package/docs/api/interfaces/AvatarProps.md +0 -128
  397. package/docs/api/interfaces/BadgeProps.md +0 -27
  398. package/docs/api/interfaces/ButtonProps.md +0 -53
  399. package/docs/api/interfaces/CalendarProps.md +0 -70
  400. package/docs/api/interfaces/CardProps.md +0 -66
  401. package/docs/api/interfaces/ColorPalette.md +0 -7
  402. package/docs/api/interfaces/ColorShade.md +0 -66
  403. package/docs/api/interfaces/ComplianceResult.md +0 -30
  404. package/docs/api/interfaces/DataAccessRecord.md +0 -96
  405. package/docs/api/interfaces/DataRecord.md +0 -11
  406. package/docs/api/interfaces/DataTableAction.md +0 -249
  407. package/docs/api/interfaces/DataTableColumn.md +0 -504
  408. package/docs/api/interfaces/DataTableProps.md +0 -625
  409. package/docs/api/interfaces/DataTableToolbarButton.md +0 -96
  410. package/docs/api/interfaces/DatabaseComplianceResult.md +0 -85
  411. package/docs/api/interfaces/DatabaseIssue.md +0 -41
  412. package/docs/api/interfaces/EmptyStateConfig.md +0 -61
  413. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +0 -235
  414. package/docs/api/interfaces/EventAppRoleData.md +0 -71
  415. package/docs/api/interfaces/ExportColumn.md +0 -90
  416. package/docs/api/interfaces/ExportOptions.md +0 -126
  417. package/docs/api/interfaces/FileDisplayProps.md +0 -249
  418. package/docs/api/interfaces/FileMetadata.md +0 -129
  419. package/docs/api/interfaces/FileReference.md +0 -118
  420. package/docs/api/interfaces/FileSizeLimits.md +0 -7
  421. package/docs/api/interfaces/FileUploadOptions.md +0 -139
  422. package/docs/api/interfaces/FileUploadProps.md +0 -293
  423. package/docs/api/interfaces/FooterProps.md +0 -105
  424. package/docs/api/interfaces/FormFieldProps.md +0 -166
  425. package/docs/api/interfaces/FormProps.md +0 -113
  426. package/docs/api/interfaces/GrantEventAppRoleParams.md +0 -122
  427. package/docs/api/interfaces/InactivityWarningModalProps.md +0 -115
  428. package/docs/api/interfaces/InputProps.md +0 -53
  429. package/docs/api/interfaces/LabelProps.md +0 -107
  430. package/docs/api/interfaces/LoggerConfig.md +0 -62
  431. package/docs/api/interfaces/LoginFormProps.md +0 -184
  432. package/docs/api/interfaces/NavigationAccessRecord.md +0 -107
  433. package/docs/api/interfaces/NavigationContextType.md +0 -164
  434. package/docs/api/interfaces/NavigationGuardProps.md +0 -139
  435. package/docs/api/interfaces/NavigationItem.md +0 -120
  436. package/docs/api/interfaces/NavigationMenuProps.md +0 -221
  437. package/docs/api/interfaces/NavigationProviderProps.md +0 -117
  438. package/docs/api/interfaces/Organisation.md +0 -140
  439. package/docs/api/interfaces/OrganisationContextType.md +0 -388
  440. package/docs/api/interfaces/OrganisationMembership.md +0 -140
  441. package/docs/api/interfaces/OrganisationProviderProps.md +0 -76
  442. package/docs/api/interfaces/OrganisationSecurityError.md +0 -62
  443. package/docs/api/interfaces/PaceAppLayoutProps.md +0 -406
  444. package/docs/api/interfaces/PaceLoginPageProps.md +0 -47
  445. package/docs/api/interfaces/PageAccessRecord.md +0 -85
  446. package/docs/api/interfaces/PagePermissionContextType.md +0 -140
  447. package/docs/api/interfaces/PagePermissionGuardProps.md +0 -153
  448. package/docs/api/interfaces/PagePermissionProviderProps.md +0 -119
  449. package/docs/api/interfaces/PaletteData.md +0 -41
  450. package/docs/api/interfaces/ParsedAddress.md +0 -120
  451. package/docs/api/interfaces/PermissionEnforcerProps.md +0 -153
  452. package/docs/api/interfaces/ProgressProps.md +0 -42
  453. package/docs/api/interfaces/ProtectedRouteProps.md +0 -97
  454. package/docs/api/interfaces/PublicPageFooterProps.md +0 -112
  455. package/docs/api/interfaces/PublicPageHeaderProps.md +0 -125
  456. package/docs/api/interfaces/PublicPageLayoutProps.md +0 -198
  457. package/docs/api/interfaces/QuickFix.md +0 -52
  458. package/docs/api/interfaces/RBACAccessValidateParams.md +0 -52
  459. package/docs/api/interfaces/RBACAccessValidateResult.md +0 -41
  460. package/docs/api/interfaces/RBACAuditLogParams.md +0 -85
  461. package/docs/api/interfaces/RBACAuditLogResult.md +0 -52
  462. package/docs/api/interfaces/RBACConfig.md +0 -133
  463. package/docs/api/interfaces/RBACContext.md +0 -52
  464. package/docs/api/interfaces/RBACLogger.md +0 -112
  465. package/docs/api/interfaces/RBACPageAccessCheckParams.md +0 -74
  466. package/docs/api/interfaces/RBACPerformanceMetrics.md +0 -138
  467. package/docs/api/interfaces/RBACPermissionCheckParams.md +0 -74
  468. package/docs/api/interfaces/RBACPermissionCheckResult.md +0 -52
  469. package/docs/api/interfaces/RBACPermissionsGetParams.md +0 -63
  470. package/docs/api/interfaces/RBACPermissionsGetResult.md +0 -63
  471. package/docs/api/interfaces/RBACResult.md +0 -58
  472. package/docs/api/interfaces/RBACRoleGrantParams.md +0 -63
  473. package/docs/api/interfaces/RBACRoleGrantResult.md +0 -52
  474. package/docs/api/interfaces/RBACRoleRevokeParams.md +0 -63
  475. package/docs/api/interfaces/RBACRoleRevokeResult.md +0 -52
  476. package/docs/api/interfaces/RBACRoleValidateParams.md +0 -52
  477. package/docs/api/interfaces/RBACRoleValidateResult.md +0 -63
  478. package/docs/api/interfaces/RBACRolesListParams.md +0 -52
  479. package/docs/api/interfaces/RBACRolesListResult.md +0 -74
  480. package/docs/api/interfaces/RBACSessionTrackParams.md +0 -74
  481. package/docs/api/interfaces/RBACSessionTrackResult.md +0 -52
  482. package/docs/api/interfaces/ResourcePermissions.md +0 -155
  483. package/docs/api/interfaces/RevokeEventAppRoleParams.md +0 -100
  484. package/docs/api/interfaces/RoleBasedRouterContextType.md +0 -151
  485. package/docs/api/interfaces/RoleBasedRouterProps.md +0 -156
  486. package/docs/api/interfaces/RoleManagementResult.md +0 -52
  487. package/docs/api/interfaces/RouteAccessRecord.md +0 -107
  488. package/docs/api/interfaces/RouteConfig.md +0 -134
  489. package/docs/api/interfaces/RuntimeComplianceResult.md +0 -55
  490. package/docs/api/interfaces/SecureDataContextType.md +0 -168
  491. package/docs/api/interfaces/SecureDataProviderProps.md +0 -132
  492. package/docs/api/interfaces/SessionRestorationLoaderProps.md +0 -34
  493. package/docs/api/interfaces/SetupIssue.md +0 -41
  494. package/docs/api/interfaces/StorageConfig.md +0 -41
  495. package/docs/api/interfaces/StorageFileInfo.md +0 -74
  496. package/docs/api/interfaces/StorageFileMetadata.md +0 -151
  497. package/docs/api/interfaces/StorageListOptions.md +0 -99
  498. package/docs/api/interfaces/StorageListResult.md +0 -41
  499. package/docs/api/interfaces/StorageUploadOptions.md +0 -101
  500. package/docs/api/interfaces/StorageUploadResult.md +0 -63
  501. package/docs/api/interfaces/StorageUrlOptions.md +0 -60
  502. package/docs/api/interfaces/StyleImport.md +0 -19
  503. package/docs/api/interfaces/SwitchProps.md +0 -34
  504. package/docs/api/interfaces/TabsContentProps.md +0 -9
  505. package/docs/api/interfaces/TabsListProps.md +0 -9
  506. package/docs/api/interfaces/TabsProps.md +0 -9
  507. package/docs/api/interfaces/TabsTriggerProps.md +0 -50
  508. package/docs/api/interfaces/TextareaProps.md +0 -53
  509. package/docs/api/interfaces/ToastActionElement.md +0 -9
  510. package/docs/api/interfaces/ToastProps.md +0 -9
  511. package/docs/api/interfaces/UnifiedAuthContextType.md +0 -820
  512. package/docs/api/interfaces/UnifiedAuthProviderProps.md +0 -171
  513. package/docs/api/interfaces/UseFormDialogOptions.md +0 -62
  514. package/docs/api/interfaces/UseFormDialogReturn.md +0 -117
  515. package/docs/api/interfaces/UseInactivityTrackerOptions.md +0 -136
  516. package/docs/api/interfaces/UseInactivityTrackerReturn.md +0 -123
  517. package/docs/api/interfaces/UsePublicEventLogoOptions.md +0 -87
  518. package/docs/api/interfaces/UsePublicEventLogoReturn.md +0 -81
  519. package/docs/api/interfaces/UsePublicEventOptions.md +0 -34
  520. package/docs/api/interfaces/UsePublicEventReturn.md +0 -68
  521. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +0 -47
  522. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +0 -120
  523. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +0 -94
  524. package/docs/api/interfaces/UseResolvedScopeOptions.md +0 -47
  525. package/docs/api/interfaces/UseResolvedScopeReturn.md +0 -47
  526. package/docs/api/interfaces/UseResourcePermissionsOptions.md +0 -34
  527. package/docs/api/interfaces/UserEventAccess.md +0 -118
  528. package/docs/api/interfaces/UserMenuProps.md +0 -86
  529. package/docs/api/interfaces/UserProfile.md +0 -63
  530. package/docs/migration/quick-migration-guide.md +0 -356
  531. package/docs/migration/service-architecture.md +0 -281
  532. package/src/components/EventSelector/EventSelector.test.tsx +0 -720
  533. package/src/components/EventSelector/EventSelector.tsx +0 -420
  534. package/src/components/EventSelector/index.ts +0 -3
  535. package/src/components/OrganisationSelector/OrganisationSelector.test.tsx +0 -784
  536. package/src/components/OrganisationSelector/OrganisationSelector.tsx +0 -324
  537. package/src/components/OrganisationSelector/index.ts +0 -9
  538. package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +0 -680
  539. package/src/hooks/useSecureDataAccess.test.ts +0 -559
  540. package/src/hooks/useSecureDataAccess.ts +0 -681
  541. /package/dist/{DataTable-DQ7RSOHE.js.map → DataTable-THFPBKTP.js.map} +0 -0
  542. /package/dist/{UnifiedAuthProvider-ATAP5UTR.js.map → UnifiedAuthProvider-KAGUYQ4J.js.map} +0 -0
  543. /package/dist/{api-N774RPUA.js.map → api-IAGWF3ZG.js.map} +0 -0
  544. /package/dist/{audit-B5P6FFIR.js.map → audit-V53FV5AG.js.map} +0 -0
  545. /package/dist/{chunk-7D4SUZUM.js.map → chunk-DGUM43GV.js.map} +0 -0
  546. /package/docs/migration/{organisation-context-timing-fix.md → V0.3.44_organisation-context-timing-fix.md} +0 -0
  547. /package/docs/migration/{rbac-migration.md → V0.4.0_rbac-migration.md} +0 -0
  548. /package/docs/migration/{person-scoped-profiles-migration-guide.md → V0.5.190_person-scoped-profiles-migration-guide.md} +0 -0
  549. /package/docs/migration/{REACT_19_MIGRATION.md → V0.6.0_REACT_19_MIGRATION.md} +0 -0
@@ -1,51 +1,33 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * Static Analysis Script for pace-core Compliance
4
+ * pace-core Compliance Check Module
5
5
  * @package @jmruthers/pace-core
6
- * @module Scripts/check-pace-core-compliance
6
+ * @module Audit/Checks/Compliance
7
7
  *
8
- * Scans a consuming app's codebase to check compliance with pace-core usage.
9
- * Generates a report of violations and suggestions.
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
10
16
  */
11
17
 
12
18
  const fs = require('fs');
13
19
  const path = require('path');
14
-
15
- // ANSI color codes for terminal output
16
- const colors = {
17
- reset: '\x1b[0m',
18
- red: '\x1b[31m',
19
- green: '\x1b[32m',
20
- yellow: '\x1b[33m',
21
- blue: '\x1b[34m',
22
- cyan: '\x1b[36m',
23
- bold: '\x1b[1m'
24
- };
20
+ const { getLineNumber, getRelativePath } = require('../utils.cjs');
25
21
 
26
22
  // Load manifest
27
23
  function loadManifest() {
28
- const manifestPath = path.join(__dirname, '../core-usage-manifest.json');
24
+ const manifestPath = path.join(__dirname, '../../../../core-usage-manifest.json');
29
25
  if (!fs.existsSync(manifestPath)) {
30
- console.error(`${colors.red}Error: core-usage-manifest.json not found at ${manifestPath}${colors.reset}`);
31
- process.exit(1);
26
+ throw new Error(`core-usage-manifest.json not found at ${manifestPath}`);
32
27
  }
33
28
  return JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
34
29
  }
35
30
 
36
- // Find project root (look for package.json, going up from current dir or script location)
37
- function findProjectRoot(startDir = process.cwd()) {
38
- let current = path.resolve(startDir);
39
- while (current !== path.dirname(current)) {
40
- if (fs.existsSync(path.join(current, 'package.json'))) {
41
- return current;
42
- }
43
- current = path.dirname(current);
44
- }
45
- return startDir;
46
- }
47
-
48
- // Scan provider setup in main entry files
49
31
  function scanProviderSetup(filePath, content, relativePath) {
50
32
  const issues = [];
51
33
 
@@ -363,8 +345,114 @@ function scanRouterSetup(filePath, content, relativePath) {
363
345
  return issues;
364
346
  }
365
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
+
366
454
  // Scan file for violations
367
- function scanFile(filePath, manifest) {
455
+ function scanFile(filePath, manifest, projectRoot) {
368
456
  const violations = {
369
457
  restrictedImports: [],
370
458
  duplicateComponents: [],
@@ -375,6 +463,8 @@ function scanFile(filePath, manifest) {
375
463
  duplicateConfig: [],
376
464
  unprotectedPages: [],
377
465
  directSupabaseAuth: [],
466
+ directSupabaseClient: [], // Direct Supabase client usage instead of useSecureSupabase
467
+ deprecatedSecureDataAccess: [], // Deprecated useSecureDataAccess with secureQuery/secureInsert/etc
378
468
  providerSetupIssues: [],
379
469
  viteConfigIssues: [],
380
470
  routerSetupIssues: [],
@@ -383,7 +473,7 @@ function scanFile(filePath, manifest) {
383
473
  };
384
474
 
385
475
  const content = fs.readFileSync(filePath, 'utf8');
386
- const relativePath = path.relative(process.cwd(), filePath);
476
+ const relativePath = getRelativePath(filePath, projectRoot);
387
477
 
388
478
  // Normalize path for cross-platform compatibility (handle both forward and backslash paths)
389
479
  const normalizedPath = relativePath.replace(/\\/g, '/');
@@ -392,6 +482,28 @@ function scanFile(filePath, manifest) {
392
482
  // Direct Supabase auth calls are the correct approach in Edge Functions
393
483
  const isEdgeFunction = normalizedPath.includes('supabase/functions/');
394
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
+
395
507
  // Check for restricted imports
396
508
  manifest.restrictedImports.forEach(({ module, reason }) => {
397
509
  const importPattern = new RegExp(`from\\s+['"]${module.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}['"]`, 'g');
@@ -485,6 +597,11 @@ function scanFile(filePath, manifest) {
485
597
  // RBAC/Auth Compliance Checks
486
598
  // ============================================
487
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
+
488
605
  // Check for custom auth/rbac/permission code that doesn't import from pace-core
489
606
  const authRbacPatterns = [
490
607
  // Custom auth hooks
@@ -515,11 +632,6 @@ function scanFile(filePath, manifest) {
515
632
  { pattern: /export\s+(default\s+)?(function|const)\s+isPermitted\s*[=\(]/g, name: 'isPermitted', type: 'util' }
516
633
  ];
517
634
 
518
- // Check if file imports from pace-core for auth/rbac
519
- const hasPaceCoreImport = /from\s+['"]@jmruthers\/pace-core/.test(content) ||
520
- /from\s+['"]@jmruthers\/pace-core\/rbac/.test(content) ||
521
- /from\s+['"]@jmruthers\/pace-core\/providers/.test(content);
522
-
523
635
  authRbacPatterns.forEach(({ pattern, name, type, replacement }) => {
524
636
  // Create a new regex instance to avoid state issues
525
637
  const testPattern = new RegExp(pattern.source, pattern.flags);
@@ -537,7 +649,7 @@ function scanFile(filePath, manifest) {
537
649
  ].includes(name);
538
650
 
539
651
  // Check if the hook uses pace-core RBAC APIs (use a fresh regex to avoid state issues)
540
- const rbacApiPattern = /useRoleManagement|useSecureSupabase|useSecureDataAccess|useRBAC|usePermissions|useCan|rbac_role_grant|rbac_role_revoke|rbac_roles_list|data_rbac_apps_list/;
652
+ const rbacApiPattern = /useRoleManagement|useSecureSupabase|useRBAC|usePermissions|useCan|rbac_role_grant|rbac_role_revoke|rbac_roles_list|data_rbac_apps_list/;
541
653
  const usesPaceCoreRBAC = isCustomRBACHook && rbacApiPattern.test(content);
542
654
 
543
655
  // Check for @pace-core-compliant comment (use a fresh regex)
@@ -677,6 +789,13 @@ function scanFile(filePath, manifest) {
677
789
  { pattern: /\.auth\.onAuthStateChange\s*\(/g, method: 'onAuthStateChange' }
678
790
  ];
679
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
+
680
799
  // Skip all auth checks for Edge Functions - they cannot use React hooks
681
800
  if (isEdgeFunction) {
682
801
  // Edge Functions use service role client or direct auth calls - correct pattern
@@ -696,12 +815,6 @@ function scanFile(filePath, manifest) {
696
815
  { pattern: /\w+\.auth\.getUser\s*\(/g, method: 'getUser', specific: false },
697
816
  { pattern: /\w+\.auth\.getSession\s*\(/g, method: 'getSession', specific: false }
698
817
  ];
699
-
700
- // Check if file actually uses useUnifiedAuth hook (not just imports it)
701
- const usesUnifiedAuthHook = /useUnifiedAuth\s*\(/.test(content);
702
- const hasUnifiedAuthImport = /UnifiedAuthProvider/.test(content) ||
703
- /useUnifiedAuth/.test(content) ||
704
- /from\s+['"]@jmruthers\/pace-core\/providers/.test(content);
705
818
 
706
819
  // Check for usage of useCurrentUser hook (even if imported from local file)
707
820
  // This catches both local imports and direct usage
@@ -885,11 +998,11 @@ function scanFile(filePath, manifest) {
885
998
  { name: 'rbac_apps', type: 'config', recommendation: 'For admin operations, use useSecureSupabase. For application use, use pace-core RBAC APIs' },
886
999
  { name: 'rbac_app_pages', type: 'config', recommendation: 'For admin operations, use useSecureSupabase. For application use, use pace-core permission management APIs or PagePermissionGuard' },
887
1000
  { name: 'rbac_page_permissions', type: 'config', recommendation: 'For admin operations, use useSecureSupabase. For application use, use pace-core permission management APIs or PagePermissionGuard' },
888
- { name: 'rbac_user_units', type: 'user_data', recommendation: 'Use useSecureSupabase or useSecureDataAccess. For reading, consider data_user_unit_get RPC function' },
1001
+ { name: 'rbac_user_units', type: 'user_data', recommendation: 'Use useSecureSupabase. For reading, consider data_user_unit_get RPC function' },
889
1002
  // User data tables - acceptable to query but must use secure methods
890
- { name: 'rbac_user_profiles', type: 'user_data', recommendation: 'Use useSecureSupabase or useSecureDataAccess from pace-core to ensure organisation context is enforced' },
891
- { name: 'rbac_user_login_history', type: 'audit', recommendation: 'Use useSecureSupabase or useSecureDataAccess from pace-core. Login history is automatically tracked by UnifiedAuthProvider, but queries should use secure methods' },
892
- { name: 'rbac_user_sessions', type: 'session', recommendation: 'Use useSecureSupabase or useSecureDataAccess from pace-core to ensure organisation context is enforced' }
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' }
893
1006
  ];
894
1007
 
895
1008
  // Detect admin/management context
@@ -914,26 +1027,19 @@ function scanFile(filePath, manifest) {
914
1027
  /useRBAC/.test(content) ||
915
1028
  /usePermissions/.test(content) ||
916
1029
  /useSecureSupabase/.test(content) ||
917
- /useSecureDataAccess/.test(content) ||
918
1030
  /PagePermissionGuard/.test(content);
919
1031
 
920
- // Check if file uses useSecureDataAccess hook
921
- const usesSecureDataAccess = /useSecureDataAccess/.test(content);
922
-
923
- // Check if file destructures secure methods from useSecureDataAccess
1032
+ // Check if file destructures secure methods (deprecated secureQuery/secureInsert/etc from useSecureDataAccess)
924
1033
  const hasSecureMethods = /(const|let)\s*\{[^}]*secure(Query|Update|Insert|Delete)/.test(content) ||
925
1034
  /secure(Query|Update|Insert|Delete)\s*\(/.test(content);
926
1035
 
927
1036
  // First, identify all variables assigned from secure hooks
928
- // Match patterns like: const supabase = useSecureSupabase(); or let client = useSecureDataAccess();
1037
+ // Match patterns like: const supabase = useSecureSupabase();
929
1038
  // Also detect fromSupabaseClient and wrapper patterns
930
1039
  const secureVariablePatterns = [
931
1040
  /const\s+(\w+)\s*=\s*useSecureSupabase\s*\(/g,
932
- /const\s+(\w+)\s*=\s*useSecureDataAccess\s*\(/g,
933
1041
  /let\s+(\w+)\s*=\s*useSecureSupabase\s*\(/g,
934
- /let\s+(\w+)\s*=\s*useSecureDataAccess\s*\(/g,
935
1042
  /(\w+)\s*=\s*useSecureSupabase\s*\(/g,
936
- /(\w+)\s*=\s*useSecureDataAccess\s*\(/g,
937
1043
  // Detect fromSupabaseClient usage
938
1044
  /const\s+(\w+)\s*=\s*fromSupabaseClient\s*\(/g,
939
1045
  /let\s+(\w+)\s*=\s*fromSupabaseClient\s*\(/g,
@@ -1059,12 +1165,55 @@ function scanFile(filePath, manifest) {
1059
1165
  const isConfigTable = type === 'config';
1060
1166
 
1061
1167
  // Check if the variable comes from a secure hook
1062
- // Check both the secureVariables set and also check if the variable is assigned from useSecureSupabase/useSecureDataAccess
1168
+ // Check both the secureVariables set and also check if the variable is assigned from useSecureSupabase
1063
1169
  // Escape special regex characters in variable name and use multiline flag to handle newlines
1064
1170
  const escapedVarName = variableName ? variableName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') : '';
1065
- const isUsingSecureVariable = secureVariables.has(variableName) ||
1066
- (variableName && new RegExp(`(const|let)\\s+${escapedVarName}\\s*=\\s*useSecureSupabase\\s*\\(`, 'm').test(content)) ||
1067
- (variableName && new RegExp(`(const|let)\\s+${escapedVarName}\\s*=\\s*useSecureDataAccess\\s*\\(`, 'm').test(content));
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);
1068
1217
 
1069
1218
  // Determine severity based on context
1070
1219
  let severity = 'error';
@@ -1076,7 +1225,7 @@ function scanFile(filePath, manifest) {
1076
1225
  continue;
1077
1226
  } else {
1078
1227
  severity = 'error';
1079
- reason = `Direct query to RBAC table '${tableName}' detected. This table can be queried, but you MUST use useSecureSupabase or useSecureDataAccess to ensure organisation context is enforced and RLS policies are respected.`;
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.`;
1080
1229
  }
1081
1230
  } else if (isConfigTable) {
1082
1231
  if (isUsingSecureVariable) {
@@ -1086,7 +1235,7 @@ function scanFile(filePath, manifest) {
1086
1235
  } else if (isAdminContext) {
1087
1236
  // Admin operations without secure methods - warning
1088
1237
  severity = 'warning';
1089
- reason = `Admin operation on configuration table '${tableName}' detected. Ensure you're using useSecureSupabase or useSecureDataAccess for security.`;
1238
+ reason = `Admin operation on configuration table '${tableName}' detected. Ensure you're using useSecureSupabase for security.`;
1090
1239
  } else {
1091
1240
  // Not admin context and not using secure methods - error
1092
1241
  severity = 'error';
@@ -1148,8 +1297,8 @@ function scanFile(filePath, manifest) {
1148
1297
  } else if (isConfigTable) {
1149
1298
  // Config tables using secure methods - acceptable for admin operations
1150
1299
  // If using secureQuery/secureUpdate/etc., it's already using secure methods
1151
- if (isAdminContext || usesSecureDataAccess) {
1152
- // Config tables in admin context or using secure methods - acceptable
1300
+ if (isAdminContext) {
1301
+ // Config tables in admin context - acceptable
1153
1302
  continue; // Skip - this is correct usage for admin operations
1154
1303
  } else {
1155
1304
  severity = 'error';
@@ -1225,10 +1374,9 @@ function scanFile(filePath, manifest) {
1225
1374
  // Check if using secure variable (check both set and direct pattern match)
1226
1375
  // Escape special regex characters in variable name and use multiline flag to handle newlines
1227
1376
  const escapedVarName = variableName ? variableName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') : '';
1228
- // Check if variable is declared with useSecureSupabase or useSecureDataAccess
1377
+ // Check if variable is declared with useSecureSupabase
1229
1378
  const isDeclaredSecure = (variableName && secureVariables.has(variableName)) ||
1230
- (variableName && new RegExp(`(const|let)\\s+${escapedVarName}\\s*=\\s*useSecureSupabase\\s*\\(`, 'm').test(content)) ||
1231
- (variableName && new RegExp(`(const|let)\\s+${escapedVarName}\\s*=\\s*useSecureDataAccess\\s*\\(`, 'm').test(content));
1379
+ (variableName && new RegExp(`(const|let)\\s+${escapedVarName}\\s*=\\s*useSecureSupabase\\s*\\(`, 'm').test(content));
1232
1380
  // Check if variable is passed as parameter with useSecureSupabase type annotation
1233
1381
  // Look for the parameter in function signatures (check both beforeMatch and full content)
1234
1382
  const isParameterSecure = variableName && (
@@ -1270,12 +1418,12 @@ function scanFile(filePath, manifest) {
1270
1418
  let severity = 'error';
1271
1419
  let reason = '';
1272
1420
 
1273
- if (isConfigTable && (isAdminContext || usesSecureDataAccess) && (isUsingSecureVariable || hasSecureMethods)) {
1421
+ if (isConfigTable && isAdminContext && (isUsingSecureVariable || hasSecureMethods)) {
1274
1422
  // Admin operations with secure methods - acceptable, skip
1275
1423
  continue;
1276
1424
  } else if (isConfigTable && isAdminContext && !isUsingSecureVariable && !hasSecureMethods) {
1277
1425
  severity = 'warning';
1278
- reason = `Admin operation on configuration table '${tableName}' detected. Ensure you're using useSecureSupabase or useSecureDataAccess for security.`;
1426
+ reason = `Admin operation on configuration table '${tableName}' detected. Ensure you're using useSecureSupabase for security.`;
1279
1427
  } else if (isConfigTable && !isAdminContext) {
1280
1428
  severity = 'error';
1281
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.`;
@@ -1285,7 +1433,7 @@ function scanFile(filePath, manifest) {
1285
1433
  continue;
1286
1434
  }
1287
1435
  severity = 'error';
1288
- reason = `Direct query to RBAC table '${tableName}' detected. This table can be queried, but you MUST use useSecureSupabase or useSecureDataAccess to ensure organisation context is enforced and RLS policies are respected.`;
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.`;
1289
1437
  } else {
1290
1438
  severity = 'error';
1291
1439
  reason = `Direct query to RBAC table '${tableName}' detected. Use pace-core RBAC hooks, RPC functions, or useSecureSupabase instead.`;
@@ -1470,6 +1618,61 @@ function scanFile(filePath, manifest) {
1470
1618
  // For .from() patterns, match[1] is the CRUD method; for secure* patterns, operation is already set
1471
1619
  const crudMethod = method === 'from' ? match[1] : operation;
1472
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
+
1473
1676
  // Skip if in a line comment
1474
1677
  const lineStart = content.lastIndexOf('\n', matchIndex) + 1;
1475
1678
  const lineUpToMatch = content.substring(lineStart, matchIndex);
@@ -1481,6 +1684,16 @@ function scanFile(filePath, manifest) {
1481
1684
  let severity = 'error';
1482
1685
  let example = '';
1483
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
+
1484
1697
  if (isRole) {
1485
1698
  // Role mutations should use RPC functions
1486
1699
  const isGrant = crudMethod === 'insert' || crudMethod === 'upsert';
@@ -1503,24 +1716,25 @@ function scanFile(filePath, manifest) {
1503
1716
  if (method && method.startsWith('secure')) {
1504
1717
  // Using secureInsert/secureUpdate/secureDelete - this is correct, don't flag
1505
1718
  continue;
1506
- } else if (isAdminContext && usesSecureDataAccess) {
1507
- // Admin context and using useSecureDataAccess - check if using secure methods
1719
+ } else if (isAdminContext) {
1720
+ // Admin context - check if using secure methods
1508
1721
  // If the operation uses secureQuery/secureUpdate/etc, it's already handled above
1509
1722
  // This case is for direct .from() calls in admin context
1510
1723
  severity = 'warning';
1511
- reason = `Admin operation (${crudMethod}) on configuration table '${table}' detected. Ensure you're using useSecureSupabase or useSecureDataAccess for security.`;
1512
- replacement = 'Use useSecureSupabase or useSecureDataAccess from pace-core for admin operations on configuration tables';
1513
- } else if (isAdminContext) {
1514
- severity = 'warning';
1515
- reason = `Admin operation (${crudMethod}) on configuration table '${table}' detected. Ensure you're using useSecureSupabase or useSecureDataAccess for security.`;
1516
- replacement = 'Use useSecureSupabase or useSecureDataAccess from pace-core for admin operations on configuration tables';
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';
1517
1726
  } else {
1518
1727
  reason = `Direct ${crudMethod} operation on configuration table '${table}' detected. These are system configuration tables. For admin operations, use useSecureSupabase.`;
1519
- replacement = 'Use useSecureSupabase or useSecureDataAccess from pace-core for admin operations';
1728
+ replacement = 'Use useSecureSupabase from pace-core for admin operations';
1520
1729
  }
1521
1730
  } else if (isUserData) {
1522
- reason = `Direct ${crudMethod} operation on '${table}' detected. Use useSecureSupabase or useSecureDataAccess to ensure organisation context is enforced.`;
1523
- replacement = 'Use useSecureSupabase or useSecureDataAccess from pace-core';
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';
1524
1738
  } else {
1525
1739
  reason = `Direct ${crudMethod} operation on '${table}' detected. Use pace-core permission management APIs or documented RPC functions instead.`;
1526
1740
  replacement = 'pace-core permission management APIs or RPC functions';
@@ -1629,7 +1843,7 @@ function scanFile(filePath, manifest) {
1629
1843
  type,
1630
1844
  file: relativePath,
1631
1845
  line: getLineNumber(content, hookStartMatch[0]),
1632
- reason: `Custom ${type} '${name}' detected that implements role/permission management logic. Even though it may use some pace-core utilities (like useSecureDataAccess), it should use pace-core RBAC APIs instead of implementing custom logic.`,
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.`,
1633
1847
  replacement,
1634
1848
  severity: 'error'
1635
1849
  });
@@ -1640,7 +1854,7 @@ function scanFile(filePath, manifest) {
1640
1854
  type,
1641
1855
  file: relativePath,
1642
1856
  line: getLineNumber(content, hookStartMatch[0]),
1643
- reason: `Custom ${type} '${name}' detected that implements role/permission management logic. Even though it may use some pace-core utilities (like useSecureDataAccess), it should use pace-core RBAC APIs instead of implementing custom logic.`,
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.`,
1644
1858
  replacement,
1645
1859
  severity: 'error'
1646
1860
  });
@@ -1651,7 +1865,7 @@ function scanFile(filePath, manifest) {
1651
1865
  type,
1652
1866
  file: relativePath,
1653
1867
  line: getLineNumber(content, hookStartMatch[0]),
1654
- reason: `Custom ${type} '${name}' detected that implements role/permission management logic. Even though it may use some pace-core utilities (like useSecureDataAccess), it should use pace-core RBAC APIs instead of implementing custom logic.`,
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.`,
1655
1869
  replacement,
1656
1870
  severity: 'error'
1657
1871
  });
@@ -1708,7 +1922,7 @@ function scanFile(filePath, manifest) {
1708
1922
  const operatesOnPagePermissions = /rbac_page_permissions/.test(content);
1709
1923
  const operatesOnRoleTables = /rbac_(organisation|event_app|global)_roles/.test(content);
1710
1924
  const operatesOnConfigTables = /rbac_apps|rbac_app_pages/.test(content);
1711
- const usesSecureMethods = /useSecureDataAccess|useSecureSupabase|secureQuery|secureUpdate|secureInsert|secureDelete/.test(content);
1925
+ const usesSecureMethods = /useSecureSupabase|secureQuery|secureUpdate|secureInsert|secureDelete/.test(content);
1712
1926
 
1713
1927
  // Only flag as permission management if:
1714
1928
  // 1. It's explicitly a permission management component AND operates on permission/role tables
@@ -1737,8 +1951,8 @@ function scanFile(filePath, manifest) {
1737
1951
  type: 'configuration management component',
1738
1952
  file: relativePath,
1739
1953
  line: getLineNumber(content, content.match(componentPattern)[0]),
1740
- reason: `Configuration management component '${name}' detected. Ensure you're using useSecureDataAccess or useSecureSupabase for secure operations on configuration tables.`,
1741
- replacement: 'Use useSecureDataAccess or useSecureSupabase from pace-core for admin operations on configuration tables',
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',
1742
1956
  severity: 'warning'
1743
1957
  });
1744
1958
  }
@@ -1769,7 +1983,10 @@ function scanFile(filePath, manifest) {
1769
1983
  }
1770
1984
 
1771
1985
  // Check Vite configuration
1772
- if (relativePath.match(/vite\.config\.(ts|js|tsx|jsx)$/)) {
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)$/)) {
1773
1990
  const viteIssues = scanViteConfig(filePath, content, relativePath);
1774
1991
  violations.viteConfigIssues.push(...viteIssues);
1775
1992
  }
@@ -1964,644 +2181,526 @@ function scanFile(filePath, manifest) {
1964
2181
  }
1965
2182
  }
1966
2183
 
1967
- return violations;
1968
- }
1969
-
1970
- // Scan for unnecessary wrappers around pace-core components and local components
1971
- function scanUnnecessaryWrappers(content, relativePath, manifest) {
1972
- const issues = [];
1973
-
1974
- // Check if file imports from pace-core
1975
- const paceCoreImportPattern = /import\s+{([^}]+)}\s+from\s+['"]@jmruthers\/pace-core['"]/;
1976
- const paceCoreImportMatch = content.match(paceCoreImportPattern);
1977
-
1978
- // Extract imported pace-core component names
1979
- let importedPaceCoreComponents = [];
1980
- if (paceCoreImportMatch) {
1981
- importedPaceCoreComponents = (paceCoreImportMatch[1] || '')
1982
- .split(',')
1983
- .map(name => name.trim().replace(/\s+as\s+\w+/, '')) // Remove aliases
1984
- .filter(name => manifest.components.includes(name));
1985
- }
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
1986
2190
 
1987
- // Also find all imported components (local and pace-core) to check for wrappers
1988
- const allImportPattern = /import\s+(?:(?:{([^}]+)})|(\w+))\s+from\s+['"]([^'"]+)['"]/g;
1989
- const allImports = [];
1990
- let importMatch;
1991
- while ((importMatch = allImportPattern.exec(content)) !== null) {
1992
- const namedImports = importMatch[1]; // { Component1, Component2 }
1993
- const defaultImport = importMatch[2]; // Component
1994
- const modulePath = importMatch[3];
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);
1995
2197
 
1996
- if (namedImports) {
1997
- namedImports.split(',').forEach(name => {
1998
- const cleanName = name.trim().replace(/\s+as\s+(\w+)/, ''); // Remove aliases but keep original
1999
- allImports.push({ name: cleanName, module: modulePath });
2000
- });
2001
- }
2002
- if (defaultImport) {
2003
- allImports.push({ name: defaultImport, module: modulePath });
2004
- }
2005
- }
2006
-
2007
- // Find exported component definitions
2008
- // Match: export (default)? (function|const) ComponentName = ...
2009
- const componentPattern = /export\s+(default\s+)?(function|const)\s+(\w+)\s*[=\(]/g;
2010
- const componentMatches = [...content.matchAll(componentPattern)];
2011
-
2012
- componentMatches.forEach(match => {
2013
- const componentName = match[3];
2014
- const matchIndex = match.index;
2198
+ // Check for createClient usage
2199
+ const createClientUsagePattern = /createClient\s*\(/g;
2200
+ const createClientMatches = content.match(createClientUsagePattern);
2201
+ const hasCreateClientUsage = createClientMatches && createClientMatches.length > 0;
2015
2202
 
2016
- // Skip if it's a test file or example file
2017
- if (relativePath.includes('.test.') || relativePath.includes('.spec.') ||
2018
- relativePath.includes('example') || relativePath.includes('Example')) {
2019
- return;
2020
- }
2203
+ // Check if file uses useSecureSupabase (correct usage)
2204
+ const usesSecureSupabase = /useSecureSupabase/.test(content) ||
2205
+ /from\s+['"]@jmruthers\/pace-core\/rbac['"]/.test(content);
2021
2206
 
2022
- // Find the component body
2023
- // Look for the opening brace after the function/const declaration
2024
- let braceCount = 0;
2025
- let bodyStart = -1;
2026
- let bodyEnd = -1;
2027
- let inBody = false;
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
+ }
2028
2216
 
2029
- for (let i = matchIndex + match[0].length; i < content.length; i++) {
2030
- const char = content[i];
2031
- if (char === '{' && !inBody) {
2032
- bodyStart = i;
2033
- inBody = true;
2034
- braceCount = 1;
2035
- } else if (char === '{') {
2036
- braceCount++;
2037
- } else if (char === '}') {
2038
- braceCount--;
2039
- if (braceCount === 0 && inBody) {
2040
- bodyEnd = i;
2041
- break;
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
+ }
2042
2280
  }
2043
2281
  }
2044
2282
  }
2045
2283
 
2046
- if (bodyStart === -1 || bodyEnd === -1) {
2047
- return; // Couldn't find component body
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
+ }
2048
2308
  }
2049
2309
 
2050
- const componentBody = content.substring(bodyStart + 1, bodyEnd).trim();
2051
-
2052
- // Check if body has significant logic (hooks, state, conditionals, etc.)
2053
- const hasHooks = /use[A-Z]\w+/.test(componentBody);
2054
- const hasState = /useState|useReducer|useRef/.test(componentBody);
2055
- const hasConditionals = /if\s*\(|&&|\?|switch/.test(componentBody);
2056
- const hasMultipleReturns = (componentBody.match(/return/g) || []).length > 1;
2057
- const hasLoops = /for\s*\(|while\s*\(|\.map\s*\(/.test(componentBody);
2058
-
2059
- // Find all JSX components used in the body
2060
- // Match JSX opening tags: <ComponentName or <ComponentName>
2061
- const jsxComponentPattern = /<([A-Z][a-zA-Z0-9]*)[\s>\/]/g;
2062
- const jsxComponents = [];
2063
- let jsxMatch;
2064
- while ((jsxMatch = jsxComponentPattern.exec(componentBody)) !== null) {
2065
- const jsxComponentName = jsxMatch[1];
2066
- // Skip React fragments and the component itself
2067
- if (jsxComponentName !== 'Fragment' &&
2068
- jsxComponentName !== componentName &&
2069
- !jsxComponents.includes(jsxComponentName)) {
2070
- jsxComponents.push(jsxComponentName);
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
+ }
2071
2343
  }
2072
2344
  }
2073
2345
 
2074
- // Check if component is a simple wrapper around another component
2075
- // Pattern 1: Just returns a single component (pace-core or local)
2076
- // Pattern 2: Only does auth/permission checks and returns a component
2077
- // Pattern 3: Minimal logic that just forwards to another component
2078
-
2079
- // Check for auth/permission check patterns (common in page wrappers)
2080
- const hasAuthCheck = /useUnifiedAuth|usePermissions|useCan|PagePermissionGuard|PermissionGuard|useRBAC/.test(componentBody);
2081
- const hasEarlyReturn = /if\s*\([^)]*\)\s*return/.test(componentBody);
2082
-
2083
- // Count conditional returns (early returns for auth checks)
2084
- const conditionalReturns = (componentBody.match(/if\s*\([^)]*\)\s*return/g) || []).length;
2085
-
2086
- // If there's only one JSX component used and minimal logic, it's likely a wrapper
2087
- if (jsxComponents.length === 1) {
2088
- const wrappedComponent = jsxComponents[0];
2089
- const wrappedComponentCount = (componentBody.match(new RegExp(`<${wrappedComponent}`, 'gi')) || []).length;
2090
-
2091
- // Check if it's a simple wrapper:
2092
- // 1. Uses only one other component
2093
- // 2. No state management
2094
- // 3. No loops
2095
- // 4. Component appears only once or twice (opening and closing tag)
2096
- // 5. Either no logic at all, OR only auth/permission checks with early returns
2097
-
2098
- // Allow auth/permission checks but still flag as wrapper if that's all it does
2099
- // Pattern: uses auth hook, has early return(s) for auth checks, then returns the wrapped component
2100
- const hasOnlyAuthLogic = hasAuthCheck &&
2101
- !hasState &&
2102
- !hasLoops &&
2103
- (conditionalReturns === 0 || (conditionalReturns <= 2 && hasEarlyReturn)) &&
2104
- (!hasConditionals || conditionalReturns <= 2);
2105
-
2106
- // Check if hooks are only auth-related
2107
- const authHookPattern = /use(UnifiedAuth|Permissions|Can|RBAC)/;
2108
- const allHooks = componentBody.match(/use[A-Z]\w+/g) || [];
2109
- const onlyAuthHooks = allHooks.length === 0 || allHooks.every(hook => authHookPattern.test(hook));
2110
-
2111
- const isSimpleWrapper =
2112
- wrappedComponentCount <= 2 && // Opening and closing tag, or self-closing
2113
- !hasState &&
2114
- !hasLoops &&
2115
- (hasMultipleReturns === false || (hasMultipleReturns && hasOnlyAuthLogic && conditionalReturns <= 2)) &&
2116
- (!hasConditionals || hasOnlyAuthLogic) &&
2117
- (!hasHooks || (onlyAuthHooks && hasOnlyAuthLogic));
2118
-
2119
- if (isSimpleWrapper) {
2120
- // Check if the wrapped component is from pace-core or local
2121
- const wrappedComponentImport = allImports.find(imp => imp.name === wrappedComponent);
2122
- const isPaceCoreComponent = wrappedComponentImport &&
2123
- wrappedComponentImport.module === '@jmruthers/pace-core';
2124
-
2125
- let reason, recommendation;
2126
- if (isPaceCoreComponent) {
2127
- reason = `Component '${componentName}' appears to be an unnecessary wrapper around pace-core's '${wrappedComponent}'. It only forwards props without adding functionality.`;
2128
- recommendation = `Use '${wrappedComponent}' directly from '@jmruthers/pace-core' instead of wrapping it in '${componentName}'.`;
2129
- } else if (hasOnlyAuthLogic) {
2130
- reason = `Component '${componentName}' is an unnecessary wrapper around '${wrappedComponent}'. It only performs auth/permission checks and returns the wrapped component.`;
2131
- recommendation = `Merge the auth/permission logic into '${wrappedComponent}' and rename it to '${componentName}', or use PagePermissionGuard from pace-core to protect the route instead.`;
2132
- } else {
2133
- reason = `Component '${componentName}' appears to be an unnecessary wrapper around '${wrappedComponent}'. It only forwards props without adding functionality.`;
2134
- recommendation = `Remove the wrapper and use '${wrappedComponent}' directly, or merge the logic into '${wrappedComponent}' and rename it to '${componentName}'.`;
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
+ }
2135
2378
  }
2136
-
2137
- issues.push({
2138
- component: componentName,
2139
- wrappedComponent: wrappedComponent,
2140
- file: relativePath,
2141
- line: getLineNumber(content, match[0]),
2142
- reason: reason,
2143
- recommendation: recommendation
2144
- });
2145
2379
  }
2146
2380
  }
2147
- });
2148
-
2149
- return issues;
2150
- }
2151
-
2152
- // Get line number for a match
2153
- function getLineNumber(content, match) {
2154
- const lines = content.substring(0, content.indexOf(match)).split('\n');
2155
- return lines.length;
2156
- }
2157
-
2158
- // Generate report
2159
- function generateReport(allViolations, manifest) {
2160
- const totalRestricted = allViolations.restrictedImports.length;
2161
- const totalDuplicates =
2162
- allViolations.duplicateComponents.length +
2163
- allViolations.duplicateHooks.length +
2164
- allViolations.duplicateUtils.length;
2165
- const totalSuggestions = allViolations.suggestions.length;
2166
- const totalRbacAuth =
2167
- allViolations.customAuthCode.length +
2168
- allViolations.duplicateConfig.length +
2169
- allViolations.unprotectedPages.length +
2170
- allViolations.directSupabaseAuth.length;
2171
- const totalSetupIssues =
2172
- allViolations.providerSetupIssues.length +
2173
- allViolations.viteConfigIssues.length +
2174
- allViolations.routerSetupIssues.length;
2175
- const totalUnnecessaryWrappers = allViolations.unnecessaryWrappers.length;
2176
- const totalAppDiscovery = allViolations.appDiscoveryIssues.length;
2177
- const totalIssues = totalRestricted + totalDuplicates + totalSuggestions + totalRbacAuth + totalSetupIssues + totalUnnecessaryWrappers + totalAppDiscovery;
2178
-
2179
- console.log(`\n${colors.bold}${colors.cyan}═══════════════════════════════════════════════════════════${colors.reset}`);
2180
- console.log(`${colors.bold}${colors.cyan} pace-core Compliance Report${colors.reset}`);
2181
- console.log(`${colors.bold}${colors.cyan}═══════════════════════════════════════════════════════════${colors.reset}\n`);
2182
-
2183
- // Restricted Imports
2184
- if (totalRestricted > 0) {
2185
- console.log(`${colors.red}${colors.bold}❌ Restricted Imports Found: ${totalRestricted}${colors.reset}\n`);
2186
- allViolations.restrictedImports.forEach(({ module, reason, file, line }) => {
2187
- console.log(` ${colors.red}•${colors.reset} ${colors.yellow}${file}:${line}${colors.reset}`);
2188
- console.log(` Import: ${colors.cyan}${module}${colors.reset}`);
2189
- console.log(` ${colors.yellow}Reason:${colors.reset} ${reason}\n`);
2190
- });
2191
- } else {
2192
- console.log(`${colors.green}✅ No restricted imports found${colors.reset}\n`);
2193
- }
2194
-
2195
- // Duplicate Components
2196
- if (allViolations.duplicateComponents.length > 0) {
2197
- console.log(`${colors.red}${colors.bold}❌ Duplicate Components Found: ${allViolations.duplicateComponents.length}${colors.reset}\n`);
2198
- allViolations.duplicateComponents.forEach(({ component, file }) => {
2199
- console.log(` ${colors.red}•${colors.reset} ${colors.yellow}${file}${colors.reset}`);
2200
- console.log(` Component '${colors.cyan}${component}${colors.reset}' conflicts with pace-core component`);
2201
- console.log(` ${colors.yellow}Suggestion:${colors.reset} Use '${component}' from '@jmruthers/pace-core' instead\n`);
2202
- });
2203
- }
2204
-
2205
- // Duplicate Hooks
2206
- if (allViolations.duplicateHooks.length > 0) {
2207
- console.log(`${colors.red}${colors.bold}❌ Duplicate Hooks Found: ${allViolations.duplicateHooks.length}${colors.reset}\n`);
2208
- allViolations.duplicateHooks.forEach(({ hook, file }) => {
2209
- console.log(` ${colors.red}•${colors.reset} ${colors.yellow}${file}${colors.reset}`);
2210
- console.log(` Hook '${colors.cyan}${hook}${colors.reset}' conflicts with pace-core hook`);
2211
- console.log(` ${colors.yellow}Suggestion:${colors.reset} Use '${hook}' from '@jmruthers/pace-core' instead\n`);
2212
- });
2213
- }
2214
-
2215
- // Duplicate Utils
2216
- if (allViolations.duplicateUtils.length > 0) {
2217
- console.log(`${colors.red}${colors.bold}❌ Duplicate Utils Found: ${allViolations.duplicateUtils.length}${colors.reset}\n`);
2218
- allViolations.duplicateUtils.forEach(({ util, file }) => {
2219
- console.log(` ${colors.red}•${colors.reset} ${colors.yellow}${file}${colors.reset}`);
2220
- console.log(` Util '${colors.cyan}${util}${colors.reset}' conflicts with pace-core util`);
2221
- console.log(` ${colors.yellow}Suggestion:${colors.reset} Use '${util}' from '@jmruthers/pace-core' instead\n`);
2222
- });
2223
- }
2224
-
2225
- // Suggestions
2226
- if (totalSuggestions > 0) {
2227
- console.log(`${colors.yellow}${colors.bold}💡 Suggestions: ${totalSuggestions}${colors.reset}\n`);
2228
- const grouped = {};
2229
- allViolations.suggestions.forEach(s => {
2230
- if (!grouped[s.file]) grouped[s.file] = [];
2231
- grouped[s.file].push(s);
2232
- });
2233
- Object.entries(grouped).forEach(([file, suggestions]) => {
2234
- console.log(` ${colors.yellow}•${colors.reset} ${colors.yellow}${file}${colors.reset}`);
2235
- suggestions.forEach(s => {
2236
- console.log(` ${s.suggestion}\n`);
2237
- });
2238
- });
2239
2381
  }
2240
2382
 
2241
- // Unnecessary Wrappers
2242
- if (totalUnnecessaryWrappers > 0) {
2243
- console.log(`${colors.yellow}${colors.bold}⚠️ Unnecessary Wrappers: ${totalUnnecessaryWrappers}${colors.reset}\n`);
2244
- allViolations.unnecessaryWrappers.forEach(({ component, wrappedComponent, file, line, reason, recommendation }) => {
2245
- console.log(` ${colors.yellow}•${colors.reset} ${colors.yellow}${file}:${line}${colors.reset}`);
2246
- console.log(` Component: ${colors.cyan}${component}${colors.reset}`);
2247
- console.log(` Wraps: ${colors.cyan}${wrappedComponent}${colors.reset}`);
2248
- console.log(` ${colors.yellow}Issue:${colors.reset} ${reason}`);
2249
- if (recommendation) {
2250
- console.log(` ${colors.green}Recommendation:${colors.reset} ${recommendation}\n`);
2251
- } else {
2252
- console.log(` ${colors.green}Recommendation:${colors.reset} Remove the wrapper and use the component directly.\n`);
2253
- }
2254
- });
2255
- }
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
2256
2389
 
2257
- // App Discovery Issues
2258
- if (totalAppDiscovery > 0) {
2259
- console.log(`\n${colors.bold}${colors.cyan}═══════════════════════════════════════════════════════════${colors.reset}`);
2260
- console.log(`${colors.bold}${colors.cyan} App Discovery Compliance${colors.reset}`);
2261
- console.log(`${colors.bold}${colors.cyan}═══════════════════════════════════════════════════════════${colors.reset}\n`);
2262
-
2263
- // Separate by type
2264
- const directQueries = allViolations.appDiscoveryIssues.filter(v => v.type === 'direct_table_query');
2265
- const hardcodedNames = allViolations.appDiscoveryIssues.filter(v => v.type === 'hardcoded_app_name');
2266
- const suggestions = allViolations.appDiscoveryIssues.filter(v => v.type === 'suggestion');
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);
2267
2395
 
2268
- if (directQueries.length > 0) {
2269
- console.log(`${colors.yellow}${colors.bold}⚠️ Direct rbac_apps Queries: ${directQueries.length}${colors.reset}\n`);
2270
- directQueries.forEach(({ file, line, reason, recommendation }) => {
2271
- console.log(` ${colors.yellow}•${colors.reset} ${colors.yellow}${file}:${line}${colors.reset}`);
2272
- console.log(` ${colors.yellow}Issue:${colors.reset} ${reason}`);
2273
- console.log(` ${colors.green}Fix:${colors.reset} ${recommendation}\n`);
2274
- });
2275
- }
2396
+ // Check for useSecureDataAccess hook usage
2397
+ const hasSecureDataAccessHook = /useSecureDataAccess\s*\(/.test(content);
2276
2398
 
2277
- if (hardcodedNames.length > 0) {
2278
- console.log(`${colors.yellow}${colors.bold}⚠️ Hardcoded App Names: ${hardcodedNames.length}${colors.reset}\n`);
2279
- hardcodedNames.forEach(({ file, line, appName, reason, recommendation }) => {
2280
- console.log(` ${colors.yellow}•${colors.reset} ${colors.yellow}${file}:${line}${colors.reset}`);
2281
- console.log(` App Name: ${colors.cyan}${appName}${colors.reset}`);
2282
- console.log(` ${colors.yellow}Issue:${colors.reset} ${reason}`);
2283
- console.log(` ${colors.green}Fix:${colors.reset} ${recommendation}\n`);
2284
- });
2285
- }
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
+ ];
2286
2407
 
2287
- if (suggestions.length > 0) {
2288
- console.log(`${colors.cyan}${colors.bold}💡 Suggestions: ${suggestions.length}${colors.reset}\n`);
2289
- suggestions.forEach(({ file, reason, recommendation }) => {
2290
- console.log(` ${colors.cyan}•${colors.reset} ${colors.yellow}${file}${colors.reset}`);
2291
- console.log(` ${reason}`);
2292
- console.log(` ${colors.green}Recommendation:${colors.reset} ${recommendation}\n`);
2293
- });
2294
- }
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;
2295
2410
 
2296
- console.log(`${colors.cyan}Example Usage:${colors.reset}`);
2297
- console.log(` ${colors.green}const { data: apps } = await supabase.rpc('data_rbac_apps_list');${colors.reset}`);
2298
- console.log(` ${colors.green}const appNames = apps?.map(app => app.name) || [];${colors.reset}\n`);
2299
- } else {
2300
- console.log(`${colors.green}✅ App discovery compliance: Using data_rbac_apps_list RPC function${colors.reset}\n`);
2301
- }
2302
-
2303
- // RBAC/Auth Compliance Section
2304
- if (totalRbacAuth > 0) {
2305
- console.log(`\n${colors.bold}${colors.cyan}═══════════════════════════════════════════════════════════${colors.reset}`);
2306
- console.log(`${colors.bold}${colors.cyan} RBAC/Auth Compliance${colors.reset}`);
2307
- console.log(`${colors.bold}${colors.cyan}═══════════════════════════════════════════════════════════${colors.reset}\n`);
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
+ }));
2308
2417
 
2309
- // Custom Auth/RBAC Code
2310
- if (allViolations.customAuthCode.length > 0) {
2311
- // Separate by severity
2312
- const errors = allViolations.customAuthCode.filter(v => !v.severity || v.severity === 'error');
2313
- const warnings = allViolations.customAuthCode.filter(v => v.severity === 'warning');
2314
- const info = allViolations.customAuthCode.filter(v => v.severity === 'info');
2315
-
2316
- if (errors.length > 0) {
2317
- console.log(`${colors.red}${colors.bold}❌ Custom Auth/RBAC Code Found: ${errors.length}${colors.reset}\n`);
2318
- errors.forEach(({ name, type, file, line, reason, replacement, example }) => {
2319
- console.log(` ${colors.red}•${colors.reset} ${colors.yellow}${file}:${line}${colors.reset}`);
2320
- console.log(` ${type}: ${colors.cyan}${name}${colors.reset}`);
2321
- console.log(` ${colors.yellow}Reason:${colors.reset} ${reason}`);
2322
- if (replacement) {
2323
- console.log(` ${colors.green}Fix:${colors.reset} ${replacement}`);
2324
- // Add example code if provided
2325
- if (example) {
2326
- console.log(` ${colors.cyan}Example:${colors.reset}`);
2327
- example.split('\n').forEach(line => {
2328
- console.log(` ${colors.green}${line}${colors.reset}`);
2329
- });
2330
- } else {
2331
- // Add example code for common cases
2332
- if (type === 'rbac query' && name.includes('rbac_user_profiles')) {
2333
- console.log(` ${colors.cyan}Example:${colors.reset}`);
2334
- console.log(` ${colors.green}import { useSecureSupabase } from '@jmruthers/pace-core/rbac';${colors.reset}`);
2335
- console.log(` ${colors.green}const supabase = useSecureSupabase();${colors.reset}`);
2336
- console.log(` ${colors.green}const { data } = await supabase.from('rbac_user_profiles').select('*');${colors.reset}`);
2337
- } else if (type === 'rbac query' && name.includes('rbac_user_login_history')) {
2338
- console.log(` ${colors.cyan}Example:${colors.reset}`);
2339
- console.log(` ${colors.green}import { useSecureSupabase } from '@jmruthers/pace-core/rbac';${colors.reset}`);
2340
- console.log(` ${colors.green}const supabase = useSecureSupabase();${colors.reset}`);
2341
- console.log(` ${colors.green}const { data } = await supabase.from('rbac_user_login_history').select('*').eq('user_id', userId);${colors.reset}`);
2342
- console.log(` ${colors.yellow}Note:${colors.reset} Login history is automatically tracked by UnifiedAuthProvider.`);
2343
- console.log(` ${colors.yellow} ${colors.reset} Queries should use useSecureSupabase to ensure organisation context is enforced.`);
2344
- } else if (type === 'rbac query' && name.includes('rbac_user_units')) {
2345
- console.log(` ${colors.cyan}Example:${colors.reset}`);
2346
- console.log(` ${colors.green}import { useSecureSupabase } from '@jmruthers/pace-core/rbac';${colors.reset}`);
2347
- console.log(` ${colors.green}const supabase = useSecureSupabase();${colors.reset}`);
2348
- console.log(` ${colors.green}// For reading, use RPC:${colors.reset}`);
2349
- console.log(` ${colors.green}const { data } = await supabase.rpc('data_user_unit_get', {${colors.reset}`);
2350
- console.log(` ${colors.green} p_user_id: userId,${colors.reset}`);
2351
- console.log(` ${colors.green} p_event_id: eventId${colors.reset}`);
2352
- console.log(` ${colors.green}});${colors.reset}`);
2353
- } else if (type === 'rbac query' && name.includes('rbac_apps')) {
2354
- console.log(` ${colors.cyan}Example (app discovery):${colors.reset}`);
2355
- console.log(` ${colors.green}const { data: apps } = await supabase.rpc('data_rbac_apps_list');${colors.reset}`);
2356
- console.log(` ${colors.yellow}Note:${colors.reset} Use data_rbac_apps_list RPC function for dynamic app discovery instead of querying rbac_apps directly.`);
2357
- } else if (type === 'rbac query' && (name.includes('rbac_app_pages') || name.includes('rbac_page_permissions'))) {
2358
- console.log(` ${colors.cyan}Example (admin operations):${colors.reset}`);
2359
- console.log(` ${colors.green}import { useSecureSupabase } from '@jmruthers/pace-core/rbac';${colors.reset}`);
2360
- console.log(` ${colors.green}const supabase = useSecureSupabase();${colors.reset}`);
2361
- console.log(` ${colors.green}const { data } = await supabase.from('${name.match(/rbac_\w+/)?.[0] || 'rbac_table'}').select('*');${colors.reset}`);
2362
- }
2363
- }
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);
2364
2429
  }
2365
- console.log('');
2366
2430
  });
2367
2431
  }
2368
2432
 
2369
- if (warnings.length > 0) {
2370
- console.log(`${colors.yellow}${colors.bold}⚠️ Warnings (acceptable but should use secure methods): ${warnings.length}${colors.reset}\n`);
2371
- warnings.forEach(({ name, type, file, line, reason, replacement, example }) => {
2372
- console.log(` ${colors.yellow}•${colors.reset} ${colors.yellow}${file}:${line}${colors.reset}`);
2373
- console.log(` ${type}: ${colors.cyan}${name}${colors.reset}`);
2374
- console.log(` ${colors.yellow}Note:${colors.reset} ${reason}`);
2375
- if (replacement) {
2376
- console.log(` ${colors.green}Recommendation:${colors.reset} ${replacement}`);
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;
2377
2448
  }
2378
- if (example) {
2379
- console.log(` ${colors.cyan}Example:${colors.reset}`);
2380
- example.split('\n').forEach(line => {
2381
- console.log(` ${colors.green}${line}${colors.reset}`);
2382
- });
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
+ }
2383
2479
  }
2384
- console.log('');
2385
- });
2386
- }
2387
-
2388
- // Don't show info-level items (these are correct usage)
2389
- // They're tracked but not displayed to avoid noise
2390
- }
2391
-
2392
- // Duplicate Configurations
2393
- if (allViolations.duplicateConfig.length > 0) {
2394
- console.log(`${colors.red}${colors.bold}❌ Duplicate Configurations Found: ${allViolations.duplicateConfig.length}${colors.reset}\n`);
2395
- allViolations.duplicateConfig.forEach(({ type, file, count, reason }) => {
2396
- console.log(` ${colors.red}•${colors.reset} ${colors.yellow}${file}${colors.reset}`);
2397
- console.log(` Type: ${colors.cyan}${type}${colors.reset}${count ? ` (${count} instances)` : ''}`);
2398
- console.log(` ${colors.yellow}Reason:${colors.reset} ${reason}\n`);
2399
- });
2400
- }
2401
-
2402
- // Unprotected Pages
2403
- if (allViolations.unprotectedPages.length > 0) {
2404
- console.log(`${colors.red}${colors.bold}❌ Unprotected Pages Found: ${allViolations.unprotectedPages.length}${colors.reset}\n`);
2405
- allViolations.unprotectedPages.forEach(({ file, reason }) => {
2406
- console.log(` ${colors.red}•${colors.reset} ${colors.yellow}${file}${colors.reset}`);
2407
- console.log(` ${colors.yellow}Reason:${colors.reset} ${reason}\n`);
2408
- });
2409
- }
2410
-
2411
- // Direct Supabase Auth Usage
2412
- if (allViolations.directSupabaseAuth.length > 0) {
2413
- console.log(`${colors.red}${colors.bold}❌ Direct Supabase Auth Usage Found: ${allViolations.directSupabaseAuth.length}${colors.reset}\n`);
2414
- allViolations.directSupabaseAuth.forEach(({ file, line, reason, method, recommendation }) => {
2415
- console.log(` ${colors.red}•${colors.reset} ${colors.yellow}${file}:${line}${colors.reset}`);
2416
- console.log(` ${colors.yellow}Reason:${colors.reset} ${reason}`);
2417
- if (recommendation) {
2418
- console.log(` ${colors.green}Fix:${colors.reset} ${recommendation}\n`);
2419
- } else {
2420
- console.log(` ${colors.green}Fix:${colors.reset} Use ${colors.cyan}useUnifiedAuth${colors.reset} hook from @jmruthers/pace-core instead of direct ${method ? `supabase.auth.${method}()` : 'Supabase auth'} calls\n`);
2421
2480
  }
2422
2481
  });
2423
- }
2424
- } else {
2425
- console.log(`\n${colors.green}✅ RBAC/Auth compliance: All checks passed${colors.reset}\n`);
2426
- }
2427
-
2428
- // Setup/Configuration Issues Section
2429
- if (totalSetupIssues > 0) {
2430
- console.log(`\n${colors.bold}${colors.cyan}═══════════════════════════════════════════════════════════${colors.reset}`);
2431
- console.log(`${colors.bold}${colors.cyan} Setup & Configuration Issues${colors.reset}`);
2432
- console.log(`${colors.bold}${colors.cyan}═══════════════════════════════════════════════════════════${colors.reset}\n`);
2433
-
2434
- // Provider Setup Issues
2435
- if (allViolations.providerSetupIssues.length > 0) {
2436
- console.log(`${colors.red}${colors.bold}❌ Provider Setup Issues Found: ${allViolations.providerSetupIssues.length}${colors.reset}\n`);
2437
- allViolations.providerSetupIssues.forEach(({ file, line, type, provider, reason, recommendation }) => {
2438
- console.log(` ${colors.red}•${colors.reset} ${colors.yellow}${file}:${line}${colors.reset}`);
2439
- if (provider) {
2440
- console.log(` Missing Provider: ${colors.cyan}${provider}${colors.reset}`);
2441
- }
2442
- if (type) {
2443
- console.log(` Issue Type: ${colors.cyan}${type}${colors.reset}`);
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
+ });
2444
2497
  }
2445
- console.log(` ${colors.yellow}Problem:${colors.reset} ${reason}`);
2446
- console.log(` ${colors.green}Fix:${colors.reset} ${recommendation}\n`);
2447
- });
2448
- }
2449
-
2450
- // Vite Configuration Issues
2451
- if (allViolations.viteConfigIssues.length > 0) {
2452
- console.log(`${colors.red}${colors.bold}❌ Vite Configuration Issues Found: ${allViolations.viteConfigIssues.length}${colors.reset}\n`);
2453
- allViolations.viteConfigIssues.forEach(({ file, line, type, reason, recommendation }) => {
2454
- const severity = type === 'recommendation' ? colors.yellow : colors.red;
2455
- const icon = type === 'recommendation' ? '💡' : '❌';
2456
- console.log(` ${severity}${icon}${colors.reset} ${colors.yellow}${file}:${line}${colors.reset}`);
2457
- console.log(` ${colors.yellow}Issue:${colors.reset} ${reason}`);
2458
- console.log(` ${colors.green}Fix:${colors.reset} ${recommendation}\n`);
2459
- });
2460
- }
2461
-
2462
- // Router Setup Issues
2463
- if (allViolations.routerSetupIssues.length > 0) {
2464
- console.log(`${colors.red}${colors.bold}❌ Router Setup Issues Found: ${allViolations.routerSetupIssues.length}${colors.reset}\n`);
2465
- allViolations.routerSetupIssues.forEach(({ file, line, type, reason, recommendation }) => {
2466
- console.log(` ${colors.red}•${colors.reset} ${colors.yellow}${file}:${line}${colors.reset}`);
2467
- console.log(` Issue Type: ${colors.cyan}${type}${colors.reset}`);
2468
- console.log(` ${colors.yellow}Problem:${colors.reset} ${reason}`);
2469
- console.log(` ${colors.green}Fix:${colors.reset} ${recommendation}\n`);
2470
- });
2498
+ }
2471
2499
  }
2472
- } else {
2473
- console.log(`\n${colors.green}✅ Setup & Configuration: All checks passed${colors.reset}\n`);
2474
2500
  }
2475
2501
 
2476
- // Summary
2477
- console.log(`${colors.bold}${colors.cyan}═══════════════════════════════════════════════════════════${colors.reset}`);
2478
- console.log(`${colors.bold}Summary:${colors.reset}`);
2479
- console.log(` Total Issues: ${totalIssues > 0 ? colors.red : colors.green}${totalIssues}${colors.reset}`);
2480
- console.log(` - Restricted Imports: ${totalRestricted > 0 ? colors.red : colors.green}${totalRestricted}${colors.reset}`);
2481
- console.log(` - Duplicate Components/Hooks/Utils: ${totalDuplicates > 0 ? colors.red : colors.green}${totalDuplicates}${colors.reset}`);
2482
- console.log(` - Suggestions: ${colors.yellow}${totalSuggestions}${colors.reset}`);
2483
- console.log(` - RBAC/Auth Issues: ${totalRbacAuth > 0 ? colors.red : colors.green}${totalRbacAuth}${colors.reset}`);
2484
- console.log(` - Setup/Configuration Issues: ${totalSetupIssues > 0 ? colors.red : colors.green}${totalSetupIssues}${colors.reset}`);
2485
- console.log(` - Unnecessary Wrappers: ${totalUnnecessaryWrappers > 0 ? colors.yellow : colors.green}${totalUnnecessaryWrappers}${colors.reset}`);
2486
- console.log(` - App Discovery Issues: ${totalAppDiscovery > 0 ? colors.yellow : colors.green}${totalAppDiscovery}${colors.reset}`);
2487
-
2488
- if (totalIssues === 0) {
2489
- console.log(`\n${colors.green}${colors.bold}✅ Excellent! Your codebase is fully compliant with pace-core standards.${colors.reset}\n`);
2490
- return 0;
2491
- } else {
2492
- console.log(`\n${colors.yellow}${colors.bold}⚠️ Please review the issues above and migrate to pace-core components/hooks/utils.${colors.reset}\n`);
2493
- return 1;
2494
- }
2502
+ return violations;
2495
2503
  }
2496
2504
 
2497
- // Recursively find source files
2498
- function findSourceFiles(dir, fileList = []) {
2499
- const ignoreDirs = ['node_modules', 'dist', 'build', '.next', 'coverage', '__tests__', '__mocks__'];
2500
- const ignoreFiles = /\.(test|spec)\.(ts|tsx|js|jsx)$/;
2501
- const sourceExtensions = /\.(ts|tsx|js|jsx)$/;
2502
-
2503
- try {
2504
- const items = fs.readdirSync(dir);
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;
2505
2515
 
2506
- items.forEach(item => {
2507
- const fullPath = path.join(dir, item);
2508
- const stat = fs.statSync(fullPath);
2509
-
2510
- if (stat.isDirectory()) {
2511
- // Skip Edge Functions directory - they run in Deno, not React, so React hooks aren't available
2512
- if (item === 'functions' && dir.includes('supabase')) {
2513
- return; // Skip supabase/functions directory
2514
- }
2515
- if (!ignoreDirs.includes(item) && !item.startsWith('.')) {
2516
- findSourceFiles(fullPath, fileList);
2517
- }
2518
- } else if (stat.isFile()) {
2519
- if (sourceExtensions.test(item) && !ignoreFiles.test(item)) {
2520
- fileList.push(fullPath);
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: []
2521
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}`);
2522
2579
  }
2523
- });
2524
- } catch (error) {
2525
- // Skip directories we can't read
2526
- }
2527
-
2528
- return fileList;
2529
- }
2530
-
2531
- // Main function
2532
- function main() {
2533
- const manifest = loadManifest();
2534
- const projectRoot = findProjectRoot();
2535
-
2536
- console.log(`${colors.cyan}Scanning project at: ${projectRoot}${colors.reset}`);
2537
-
2538
- // Find all TypeScript/JavaScript files (excluding node_modules, dist, etc.)
2539
- const files = findSourceFiles(projectRoot);
2540
-
2541
- console.log(`Found ${files.length} files to scan...\n`);
2542
-
2543
- // Scan all files
2544
- const allViolations = {
2545
- restrictedImports: [],
2546
- duplicateComponents: [],
2547
- duplicateHooks: [],
2548
- duplicateUtils: [],
2549
- suggestions: [],
2550
- customAuthCode: [],
2551
- duplicateConfig: [],
2552
- unprotectedPages: [],
2553
- directSupabaseAuth: [],
2554
- providerSetupIssues: [],
2555
- viteConfigIssues: [],
2556
- routerSetupIssues: [],
2557
- unnecessaryWrappers: [],
2558
- appDiscoveryIssues: []
2559
- };
2560
-
2561
- files.forEach(file => {
2562
- try {
2563
- const violations = scanFile(file, manifest);
2564
- allViolations.restrictedImports.push(...violations.restrictedImports);
2565
- allViolations.duplicateComponents.push(...violations.duplicateComponents);
2566
- allViolations.duplicateHooks.push(...violations.duplicateHooks);
2567
- allViolations.duplicateUtils.push(...violations.duplicateUtils);
2568
- allViolations.suggestions.push(...violations.suggestions);
2569
- allViolations.customAuthCode.push(...violations.customAuthCode);
2570
- allViolations.duplicateConfig.push(...violations.duplicateConfig);
2571
- allViolations.unprotectedPages.push(...violations.unprotectedPages);
2572
- allViolations.directSupabaseAuth.push(...violations.directSupabaseAuth);
2573
- allViolations.providerSetupIssues.push(...violations.providerSetupIssues);
2574
- allViolations.viteConfigIssues.push(...violations.viteConfigIssues);
2575
- allViolations.routerSetupIssues.push(...violations.routerSetupIssues);
2576
- allViolations.unnecessaryWrappers.push(...violations.unnecessaryWrappers);
2577
- allViolations.appDiscoveryIssues.push(...violations.appDiscoveryIssues);
2578
- } catch (error) {
2579
- console.error(`${colors.red}Error scanning ${file}: ${error.message}${colors.reset}`);
2580
2580
  }
2581
- });
2582
-
2583
- // Generate and display report
2584
- const exitCode = generateReport(allViolations, manifest);
2585
- process.exit(exitCode);
2586
- }
2587
-
2588
- // Run if called directly
2589
- if (require.main === module) {
2590
- try {
2591
- main();
2592
- } catch (error) {
2593
- console.error(`${colors.red}Error: ${error.message}${colors.reset}`);
2594
- console.error(error.stack);
2595
- process.exit(1);
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
+ };
2596
2703
  }
2597
- }
2598
-
2599
- module.exports = {
2600
- main,
2601
- scanFile,
2602
- generateReport,
2603
- loadManifest,
2604
- findProjectRoot,
2605
- findSourceFiles
2606
2704
  };
2607
2705
 
2706
+ module.exports = complianceCheck;