@jmruthers/pace-core 0.5.183 → 0.5.185

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 (307) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +60 -1
  3. package/core-usage-manifest.json +312 -0
  4. package/dist/{DataTable-QAB34V6K.js → DataTable-IX2NBUTP.js} +6 -6
  5. package/dist/{DataTable-Bz8ffqyA.d.ts → DataTable-Z9NLVJh0.d.ts} +1 -1
  6. package/dist/{index-Bl--n7-T.d.ts → PublicPageProvider-BABf6JCh.d.ts} +21 -10
  7. package/dist/{UnifiedAuthProvider-7F6T4B6K.js → UnifiedAuthProvider-A4BCQRJY.js} +4 -2
  8. package/dist/{UnifiedAuthProvider-F86d7dSi.d.ts → UnifiedAuthProvider-BG0AL5eE.d.ts} +2 -1
  9. package/dist/{api-ROMBCNKU.js → api-BMFCXVQX.js} +2 -2
  10. package/dist/{chunk-RA3JUFMW.js → chunk-445GEP27.js} +154 -4
  11. package/dist/{chunk-RA3JUFMW.js.map → chunk-445GEP27.js.map} +1 -1
  12. package/dist/{chunk-CSOFYHAG.js → chunk-AISXLWGZ.js} +374 -60
  13. package/dist/chunk-AISXLWGZ.js.map +1 -0
  14. package/dist/{chunk-FUEYYMX5.js → chunk-FXFJRTKI.js} +24 -3
  15. package/dist/chunk-FXFJRTKI.js.map +1 -0
  16. package/dist/{chunk-QETLRQI6.js → chunk-HC67NW5K.js} +380 -360
  17. package/dist/chunk-HC67NW5K.js.map +1 -0
  18. package/dist/chunk-HESYZWZW.js +388 -0
  19. package/dist/chunk-HESYZWZW.js.map +1 -0
  20. package/dist/{chunk-QUVSNGIP.js → chunk-HGPQUCBC.js} +34 -9
  21. package/dist/{chunk-QUVSNGIP.js.map → chunk-HGPQUCBC.js.map} +1 -1
  22. package/dist/{chunk-UHNYIBXL.js → chunk-IXSNYUCT.js} +1 -1
  23. package/dist/chunk-IXSNYUCT.js.map +1 -0
  24. package/dist/{chunk-MI7HBHN3.js → chunk-MX3EIJGQ.js} +4 -3
  25. package/dist/{chunk-MI7HBHN3.js.map → chunk-MX3EIJGQ.js.map} +1 -1
  26. package/dist/{chunk-PWAHJW4G.js → chunk-OKI34GZD.js} +86 -33
  27. package/dist/chunk-OKI34GZD.js.map +1 -0
  28. package/dist/{chunk-W22JP75J.js → chunk-STTZQK2I.js} +3 -3
  29. package/dist/chunk-THRPYOFK.js +215 -0
  30. package/dist/chunk-THRPYOFK.js.map +1 -0
  31. package/dist/{chunk-M7W4CP3M.js → chunk-U6WNSFX5.js} +2 -1
  32. package/dist/chunk-U6WNSFX5.js.map +1 -0
  33. package/dist/{chunk-QCDXODCA.js → chunk-XAUHJD3L.js} +2 -2
  34. package/dist/components.d.ts +182 -6
  35. package/dist/components.js +157 -11
  36. package/dist/components.js.map +1 -1
  37. package/dist/eslint-rules/pace-core-compliance.cjs +406 -0
  38. package/dist/{file-reference-D06mEEWW.d.ts → file-reference-BjR39ktt.d.ts} +7 -1
  39. package/dist/hooks.d.ts +7 -14
  40. package/dist/hooks.js +10 -22
  41. package/dist/hooks.js.map +1 -1
  42. package/dist/index.d.ts +11 -11
  43. package/dist/index.js +79 -16
  44. package/dist/index.js.map +1 -1
  45. package/dist/providers.d.ts +1 -1
  46. package/dist/providers.js +3 -1
  47. package/dist/rbac/index.d.ts +205 -14
  48. package/dist/rbac/index.js +28 -6
  49. package/dist/timezone-_pgH8qrY.d.ts +530 -0
  50. package/dist/{types-_x1f4QBF.d.ts → types-DUyCRSTj.d.ts} +1 -1
  51. package/dist/types.d.ts +1 -1
  52. package/dist/types.js +1 -1
  53. package/dist/{usePublicRouteParams-JJczomYq.d.ts → usePublicRouteParams-CvnC3d-e.d.ts} +113 -2
  54. package/dist/utils.d.ts +109 -151
  55. package/dist/utils.js +128 -138
  56. package/dist/utils.js.map +1 -1
  57. package/docs/api/README.md +60 -1
  58. package/docs/api/classes/ColumnFactory.md +1 -1
  59. package/docs/api/classes/ErrorBoundary.md +1 -1
  60. package/docs/api/classes/InvalidScopeError.md +1 -1
  61. package/docs/api/classes/Logger.md +178 -0
  62. package/docs/api/classes/MissingUserContextError.md +1 -1
  63. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  64. package/docs/api/classes/PermissionDeniedError.md +1 -1
  65. package/docs/api/classes/RBACAuditManager.md +2 -2
  66. package/docs/api/classes/RBACCache.md +1 -1
  67. package/docs/api/classes/RBACEngine.md +2 -2
  68. package/docs/api/classes/RBACError.md +1 -1
  69. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  70. package/docs/api/classes/SecureSupabaseClient.md +5 -5
  71. package/docs/api/classes/StorageUtils.md +1 -1
  72. package/docs/api/enums/FileCategory.md +1 -1
  73. package/docs/api/enums/LogLevel.md +54 -0
  74. package/docs/api/enums/RBACErrorCode.md +1 -1
  75. package/docs/api/enums/RPCFunction.md +1 -1
  76. package/docs/api/interfaces/AggregateConfig.md +1 -1
  77. package/docs/api/interfaces/BadgeProps.md +1 -1
  78. package/docs/api/interfaces/ButtonProps.md +1 -1
  79. package/docs/api/interfaces/CalendarProps.md +18 -2
  80. package/docs/api/interfaces/CardProps.md +1 -1
  81. package/docs/api/interfaces/ColorPalette.md +1 -1
  82. package/docs/api/interfaces/ColorShade.md +1 -1
  83. package/docs/api/interfaces/ComplianceResult.md +30 -0
  84. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  85. package/docs/api/interfaces/DataRecord.md +1 -1
  86. package/docs/api/interfaces/DataTableAction.md +1 -1
  87. package/docs/api/interfaces/DataTableColumn.md +1 -1
  88. package/docs/api/interfaces/DataTableProps.md +1 -1
  89. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  90. package/docs/api/interfaces/DatabaseComplianceResult.md +85 -0
  91. package/docs/api/interfaces/DatabaseIssue.md +41 -0
  92. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  93. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  94. package/docs/api/interfaces/EventAppRoleData.md +6 -6
  95. package/docs/api/interfaces/ExportColumn.md +1 -1
  96. package/docs/api/interfaces/ExportOptions.md +1 -1
  97. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  98. package/docs/api/interfaces/FileMetadata.md +1 -1
  99. package/docs/api/interfaces/FileReference.md +1 -1
  100. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  101. package/docs/api/interfaces/FileUploadOptions.md +24 -8
  102. package/docs/api/interfaces/FileUploadProps.md +24 -13
  103. package/docs/api/interfaces/FooterProps.md +1 -1
  104. package/docs/api/interfaces/FormFieldProps.md +1 -1
  105. package/docs/api/interfaces/FormProps.md +1 -1
  106. package/docs/api/interfaces/GrantEventAppRoleParams.md +9 -9
  107. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  108. package/docs/api/interfaces/InputProps.md +1 -1
  109. package/docs/api/interfaces/LabelProps.md +1 -1
  110. package/docs/api/interfaces/LoggerConfig.md +62 -0
  111. package/docs/api/interfaces/LoginFormProps.md +1 -1
  112. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  113. package/docs/api/interfaces/NavigationContextType.md +1 -1
  114. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  115. package/docs/api/interfaces/NavigationItem.md +1 -1
  116. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  117. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  118. package/docs/api/interfaces/Organisation.md +1 -1
  119. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  120. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  121. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  122. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  123. package/docs/api/interfaces/PaceAppLayoutProps.md +36 -23
  124. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  125. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  126. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  127. package/docs/api/interfaces/PagePermissionGuardProps.md +11 -11
  128. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  129. package/docs/api/interfaces/PaletteData.md +1 -1
  130. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  131. package/docs/api/interfaces/ProgressProps.md +1 -1
  132. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  133. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  134. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  135. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  136. package/docs/api/interfaces/QuickFix.md +52 -0
  137. package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
  138. package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
  139. package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
  140. package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
  141. package/docs/api/interfaces/RBACConfig.md +4 -4
  142. package/docs/api/interfaces/RBACContext.md +1 -1
  143. package/docs/api/interfaces/RBACLogger.md +1 -1
  144. package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
  145. package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
  146. package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
  147. package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
  148. package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
  149. package/docs/api/interfaces/RBACResult.md +1 -1
  150. package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
  151. package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
  152. package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
  153. package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
  154. package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
  155. package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
  156. package/docs/api/interfaces/RBACRolesListParams.md +1 -1
  157. package/docs/api/interfaces/RBACRolesListResult.md +1 -1
  158. package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
  159. package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
  160. package/docs/api/interfaces/ResourcePermissions.md +1 -1
  161. package/docs/api/interfaces/RevokeEventAppRoleParams.md +7 -7
  162. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  163. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  164. package/docs/api/interfaces/RoleManagementResult.md +5 -5
  165. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  166. package/docs/api/interfaces/RouteConfig.md +1 -1
  167. package/docs/api/interfaces/RuntimeComplianceResult.md +55 -0
  168. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  169. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  170. package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
  171. package/docs/api/interfaces/SetupIssue.md +41 -0
  172. package/docs/api/interfaces/StorageConfig.md +1 -1
  173. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  174. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  175. package/docs/api/interfaces/StorageListOptions.md +1 -1
  176. package/docs/api/interfaces/StorageListResult.md +1 -1
  177. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  178. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  179. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  180. package/docs/api/interfaces/StyleImport.md +1 -1
  181. package/docs/api/interfaces/SwitchProps.md +1 -1
  182. package/docs/api/interfaces/TabsContentProps.md +1 -1
  183. package/docs/api/interfaces/TabsListProps.md +1 -1
  184. package/docs/api/interfaces/TabsProps.md +1 -1
  185. package/docs/api/interfaces/TabsTriggerProps.md +1 -1
  186. package/docs/api/interfaces/TextareaProps.md +1 -1
  187. package/docs/api/interfaces/ToastActionElement.md +1 -1
  188. package/docs/api/interfaces/ToastProps.md +1 -1
  189. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  190. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  191. package/docs/api/interfaces/UseFormDialogOptions.md +62 -0
  192. package/docs/api/interfaces/UseFormDialogReturn.md +117 -0
  193. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  194. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  195. package/docs/api/interfaces/UsePublicEventLogoOptions.md +2 -2
  196. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  197. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  198. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  199. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +2 -2
  200. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  201. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  202. package/docs/api/interfaces/UseResolvedScopeOptions.md +2 -2
  203. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  204. package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
  205. package/docs/api/interfaces/UserEventAccess.md +1 -1
  206. package/docs/api/interfaces/UserMenuProps.md +1 -1
  207. package/docs/api/interfaces/UserProfile.md +1 -1
  208. package/docs/api/modules.md +738 -42
  209. package/docs/api-reference/hooks.md +111 -0
  210. package/docs/api-reference/rpc-functions.md +1 -1
  211. package/docs/api-reference/utilities.md +184 -0
  212. package/docs/getting-started/installation-guide.md +75 -16
  213. package/docs/getting-started/quick-start.md +61 -11
  214. package/docs/implementation-guides/authentication.md +88 -12
  215. package/docs/implementation-guides/file-reference-system.md +2 -1
  216. package/docs/implementation-guides/file-upload-storage.md +21 -0
  217. package/docs/rbac/README.md +1 -0
  218. package/docs/rbac/compliance/compliance-guide.md +544 -0
  219. package/docs/rbac/getting-started.md +158 -33
  220. package/docs/standards/pace-core-compliance.md +432 -0
  221. package/eslint-config-pace-core.cjs +93 -0
  222. package/package.json +15 -3
  223. package/scripts/analyze-bundle.js +232 -0
  224. package/scripts/build-css.js +56 -0
  225. package/scripts/build-docs-incremental.js +1015 -0
  226. package/scripts/check-pace-core-compliance.cjs +2353 -0
  227. package/scripts/generate-docs.js +157 -0
  228. package/scripts/setup-build-cache.js +73 -0
  229. package/scripts/utils/command-runner.js +131 -0
  230. package/scripts/utils/env.js +33 -0
  231. package/scripts/utils/index.js +10 -0
  232. package/scripts/utils/logger.js +88 -0
  233. package/scripts/utils/path-helpers.js +37 -0
  234. package/scripts/validate-formats.js +133 -0
  235. package/scripts/validate-master.js +155 -0
  236. package/scripts/validate-pre-publish.js +140 -0
  237. package/scripts/validate-theme.js +142 -0
  238. package/src/components/Calendar/Calendar.tsx +8 -1
  239. package/src/components/Card/Card.tsx +47 -8
  240. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +314 -0
  241. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.tsx +126 -0
  242. package/src/components/DatePickerWithTimezone/README.md +135 -0
  243. package/src/components/DatePickerWithTimezone/index.ts +10 -0
  244. package/src/components/DateTimeField/DateTimeField.test.tsx +358 -0
  245. package/src/components/DateTimeField/DateTimeField.tsx +232 -0
  246. package/src/components/DateTimeField/README.md +148 -0
  247. package/src/components/DateTimeField/index.ts +10 -0
  248. package/src/components/FileUpload/FileUpload.tsx +3 -0
  249. package/src/components/Header/Header.test.tsx +47 -18
  250. package/src/components/Header/Header.tsx +24 -6
  251. package/src/components/PaceAppLayout/PaceAppLayout.tsx +29 -20
  252. package/src/components/PaceAppLayout/README.md +9 -0
  253. package/src/components/PaceLoginPage/PaceLoginPage.tsx +1 -1
  254. package/src/components/ProtectedRoute/ProtectedRoute.test.tsx +37 -8
  255. package/src/components/ProtectedRoute/ProtectedRoute.tsx +12 -4
  256. package/src/components/index.ts +8 -0
  257. package/src/eslint-rules/pace-core-compliance.cjs +406 -0
  258. package/src/eslint-rules/pace-core-compliance.js +640 -0
  259. package/src/hooks/__tests__/useFormDialog.test.ts +478 -0
  260. package/src/hooks/index.ts +2 -0
  261. package/src/hooks/useFileReference.test.ts +1 -0
  262. package/src/hooks/useFormDialog.ts +147 -0
  263. package/src/index.ts +27 -0
  264. package/src/providers/services/OrganisationServiceProvider.tsx +6 -5
  265. package/src/providers/services/UnifiedAuthProvider.tsx +24 -3
  266. package/src/rbac/__tests__/scenarios.user-role.test.tsx +3 -0
  267. package/src/rbac/compliance/database-validator.ts +165 -0
  268. package/src/rbac/compliance/index.ts +38 -0
  269. package/src/rbac/compliance/quick-fix-suggestions.ts +209 -0
  270. package/src/rbac/compliance/runtime-compliance.ts +77 -0
  271. package/src/rbac/compliance/setup-validator.ts +131 -0
  272. package/src/rbac/components/PagePermissionGuard.tsx +8 -64
  273. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +35 -21
  274. package/src/rbac/docs/event-based-apps.md +285 -0
  275. package/src/rbac/errors.ts +11 -0
  276. package/src/rbac/hooks/useRoleManagement.ts +292 -12
  277. package/src/rbac/index.ts +30 -0
  278. package/src/services/OrganisationService.ts +4 -0
  279. package/src/types/file-reference.ts +6 -0
  280. package/src/utils/__tests__/timezone.test.ts +345 -0
  281. package/src/utils/file-reference/__tests__/file-reference.test.ts +2 -0
  282. package/src/utils/file-reference/index.ts +1 -0
  283. package/src/utils/formatting/formatDateTimeTimezone.test.ts +167 -0
  284. package/src/utils/formatting/formatting.ts +179 -0
  285. package/src/utils/index.ts +27 -1
  286. package/src/utils/location/index.ts +16 -0
  287. package/src/utils/location/location.test.ts +286 -0
  288. package/src/utils/location/location.ts +175 -0
  289. package/src/utils/timezone/index.ts +17 -0
  290. package/src/utils/timezone/timezone.test.ts +349 -0
  291. package/src/utils/timezone/timezone.ts +281 -0
  292. package/dist/chunk-CSOFYHAG.js.map +0 -1
  293. package/dist/chunk-FUEYYMX5.js.map +0 -1
  294. package/dist/chunk-HKIT6O7W.js +0 -198
  295. package/dist/chunk-HKIT6O7W.js.map +0 -1
  296. package/dist/chunk-KUEN3HFB.js +0 -94
  297. package/dist/chunk-KUEN3HFB.js.map +0 -1
  298. package/dist/chunk-M7W4CP3M.js.map +0 -1
  299. package/dist/chunk-PWAHJW4G.js.map +0 -1
  300. package/dist/chunk-QETLRQI6.js.map +0 -1
  301. package/dist/chunk-UHNYIBXL.js.map +0 -1
  302. package/dist/formatting-5wETwiGF.d.ts +0 -162
  303. /package/dist/{DataTable-QAB34V6K.js.map → DataTable-IX2NBUTP.js.map} +0 -0
  304. /package/dist/{UnifiedAuthProvider-7F6T4B6K.js.map → UnifiedAuthProvider-A4BCQRJY.js.map} +0 -0
  305. /package/dist/{api-ROMBCNKU.js.map → api-BMFCXVQX.js.map} +0 -0
  306. /package/dist/{chunk-W22JP75J.js.map → chunk-STTZQK2I.js.map} +0 -0
  307. /package/dist/{chunk-QCDXODCA.js.map → chunk-XAUHJD3L.js.map} +0 -0
@@ -0,0 +1,2353 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Static Analysis Script for pace-core Compliance
5
+ * @package @jmruthers/pace-core
6
+ * @module Scripts/check-pace-core-compliance
7
+ *
8
+ * Scans a consuming app's codebase to check compliance with pace-core usage.
9
+ * Generates a report of violations and suggestions.
10
+ */
11
+
12
+ const fs = require('fs');
13
+ 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
+ };
25
+
26
+ // Load manifest
27
+ function loadManifest() {
28
+ const manifestPath = path.join(__dirname, '../core-usage-manifest.json');
29
+ if (!fs.existsSync(manifestPath)) {
30
+ console.error(`${colors.red}Error: core-usage-manifest.json not found at ${manifestPath}${colors.reset}`);
31
+ process.exit(1);
32
+ }
33
+ return JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
34
+ }
35
+
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
+ function scanProviderSetup(filePath, content, relativePath) {
50
+ const issues = [];
51
+
52
+ // Check for UnifiedAuthProvider
53
+ const hasUnifiedAuthProvider = /UnifiedAuthProvider/.test(content);
54
+ if (!hasUnifiedAuthProvider) {
55
+ issues.push({
56
+ file: relativePath,
57
+ line: 1,
58
+ type: 'missing-provider',
59
+ provider: 'UnifiedAuthProvider',
60
+ reason: 'UnifiedAuthProvider is required. Wrap your app with UnifiedAuthProvider from @jmruthers/pace-core.',
61
+ recommendation: 'Import UnifiedAuthProvider from @jmruthers/pace-core and wrap your app component with it.'
62
+ });
63
+ }
64
+
65
+ // Check for OrganisationProvider
66
+ const hasOrganisationProvider = /OrganisationProvider/.test(content);
67
+ if (hasUnifiedAuthProvider && !hasOrganisationProvider) {
68
+ issues.push({
69
+ file: relativePath,
70
+ line: 1,
71
+ type: 'missing-provider',
72
+ provider: 'OrganisationProvider',
73
+ reason: 'OrganisationProvider is recommended when using UnifiedAuthProvider. It provides organisation context.',
74
+ recommendation: 'Import OrganisationProvider from @jmruthers/pace-core and wrap your app inside UnifiedAuthProvider.'
75
+ });
76
+ }
77
+
78
+ // Check provider nesting order
79
+ if (hasUnifiedAuthProvider) {
80
+ // Normalize content for better pattern matching (handle multiline JSX)
81
+ const normalizedContent = content.replace(/\s+/g, ' ');
82
+
83
+ const hasBrowserRouter = /BrowserRouter/.test(content);
84
+ const hasQueryClientProvider = /QueryClientProvider/.test(content);
85
+
86
+ // Only check nesting if we have both BrowserRouter and UnifiedAuthProvider
87
+ if (hasBrowserRouter) {
88
+ // Check if BrowserRouter is inside UnifiedAuthProvider (wrong order)
89
+ // Pattern: <UnifiedAuthProvider ...> ... <BrowserRouter
90
+ // This is a clear wrong pattern - only report if we see this directly
91
+ // BUT first check if there's a wrapper component that uses Router hooks
92
+ const wrapperFunctionMatch = content.match(/(?:function|const)\s+(\w*UnifiedAuthProvider\w*Wrapper)\s*[=\(]/);
93
+ let wrapperUsesRouterHooks = false;
94
+ if (wrapperFunctionMatch) {
95
+ const wrapperName = wrapperFunctionMatch[1];
96
+ const wrapperStart = content.indexOf(wrapperFunctionMatch[0]);
97
+ const afterWrapper = content.substring(wrapperStart);
98
+ let braceCount = 0;
99
+ let bodyStart = -1;
100
+ let bodyEnd = -1;
101
+ for (let i = 0; i < afterWrapper.length; i++) {
102
+ if (afterWrapper[i] === '{') {
103
+ if (braceCount === 0) bodyStart = i;
104
+ braceCount++;
105
+ } else if (afterWrapper[i] === '}') {
106
+ braceCount--;
107
+ if (braceCount === 0 && bodyStart !== -1) {
108
+ bodyEnd = i;
109
+ break;
110
+ }
111
+ }
112
+ }
113
+ if (bodyStart !== -1 && bodyEnd !== -1) {
114
+ const functionBody = afterWrapper.substring(bodyStart, bodyEnd + 1);
115
+ wrapperUsesRouterHooks = /useNavigate|useLocation|useParams|useSearchParams/.test(functionBody);
116
+ }
117
+ }
118
+
119
+ const wrongNestingPattern = /<UnifiedAuthProvider[^>]*>[\s\S]*?<BrowserRouter/g;
120
+ // Only flag wrong nesting if there's no wrapper using Router hooks
121
+ if (wrongNestingPattern.test(content) && !wrapperUsesRouterHooks) {
122
+ const match = content.match(wrongNestingPattern);
123
+ issues.push({
124
+ file: relativePath,
125
+ line: getLineNumber(content, match[0]),
126
+ type: 'wrong-nesting',
127
+ reason: 'BrowserRouter should wrap UnifiedAuthProvider, not the other way around. This causes Router context errors.',
128
+ recommendation: 'Correct nesting order: QueryClientProvider → BrowserRouter → UnifiedAuthProvider → OrganisationProvider → App'
129
+ });
130
+ } else {
131
+ // Check if BrowserRouter wraps UnifiedAuthProvider (correct pattern)
132
+ // Look for BrowserRouter wrapping UnifiedAuthProvider or any component that might contain it
133
+ // Pattern: <BrowserRouter ...> ... <UnifiedAuthProvider (or wrapper component)
134
+ const correctNestingPattern = /<BrowserRouter[^>]*>[\s\S]*?<UnifiedAuthProvider/g;
135
+
136
+ // Also check for wrapper components (e.g., UnifiedAuthProviderWrapper)
137
+ // Pattern: <BrowserRouter ...> ... <UnifiedAuthProviderWrapper ...> ... <UnifiedAuthProvider
138
+ const wrapperPattern = /<BrowserRouter[^>]*>[\s\S]*?(?:<UnifiedAuthProviderWrapper|<UnifiedAuthProvider)/g;
139
+
140
+ // wrapperUsesRouterHooks is already set above, reuse it
141
+ const browserRouterIndex = content.indexOf('<BrowserRouter');
142
+ const unifiedAuthIndex = content.indexOf('UnifiedAuthProvider');
143
+ const wrapperIndex = content.indexOf('UnifiedAuthProviderWrapper');
144
+
145
+ // Only report if we can clearly see a wrong pattern
146
+ // If BrowserRouter wraps UnifiedAuthProvider (directly or via wrapper), that's correct
147
+ if (browserRouterIndex !== -1 && unifiedAuthIndex !== -1) {
148
+ // If wrapper uses Router hooks, it must be inside BrowserRouter (correct pattern)
149
+ // Check if BrowserRouter comes before UnifiedAuthProviderWrapper in the JSX
150
+ if (wrapperUsesRouterHooks && wrapperPattern.test(content) &&
151
+ (browserRouterIndex < wrapperIndex || browserRouterIndex < unifiedAuthIndex || wrapperIndex === -1)) {
152
+ // This is correct - wrapper uses Router hooks and is inside BrowserRouter
153
+ // Don't report - skip to next check
154
+ } else if (unifiedAuthIndex < browserRouterIndex && !correctNestingPattern.test(content) && !wrapperPattern.test(content)) {
155
+ // Only report if we see UnifiedAuthProvider directly wrapping BrowserRouter (clear wrong pattern)
156
+ // AND wrapper doesn't use Router hooks (which would require BrowserRouter)
157
+ const unifiedWrappingRouter = /<UnifiedAuthProvider[^>]*>[\s\S]{0,200}<BrowserRouter/g;
158
+ if (unifiedWrappingRouter.test(content) && !wrapperUsesRouterHooks) {
159
+ issues.push({
160
+ file: relativePath,
161
+ line: getLineNumber(content, /BrowserRouter/.exec(content)?.index || 0),
162
+ type: 'wrong-nesting',
163
+ reason: 'UnifiedAuthProvider should be inside BrowserRouter to provide Router context.',
164
+ recommendation: 'Correct nesting order: QueryClientProvider → BrowserRouter → UnifiedAuthProvider → OrganisationProvider → App'
165
+ });
166
+ }
167
+ // If it's a wrapper component that uses Router hooks, don't report - that's acceptable
168
+ }
169
+ }
170
+ }
171
+ }
172
+
173
+ // Check if QueryClientProvider wraps BrowserRouter
174
+ if (hasQueryClientProvider && hasBrowserRouter) {
175
+ // Check if QueryClientProvider comes before BrowserRouter in JSX structure
176
+ const queryBeforeRouter = /<QueryClientProvider[^>]*>[\s\S]*?<BrowserRouter/g;
177
+ if (!queryBeforeRouter.test(content)) {
178
+ // Only report if we can clearly see BrowserRouter comes before QueryClientProvider
179
+ const routerBeforeQuery = /<BrowserRouter[^>]*>[\s\S]*?<QueryClientProvider/g;
180
+ if (routerBeforeQuery.test(content)) {
181
+ const queryMatch = /QueryClientProvider/.exec(content);
182
+ issues.push({
183
+ file: relativePath,
184
+ line: getLineNumber(content, queryMatch?.index || 0),
185
+ type: 'wrong-nesting',
186
+ reason: 'QueryClientProvider should wrap BrowserRouter.',
187
+ recommendation: 'Correct nesting order: QueryClientProvider → BrowserRouter → UnifiedAuthProvider → OrganisationProvider → App'
188
+ });
189
+ }
190
+ // If we can't find either pattern, don't report - might be in wrapper components
191
+ }
192
+ }
193
+ }
194
+
195
+ return issues;
196
+ }
197
+
198
+ // Scan Vite configuration for required settings
199
+ function scanViteConfig(filePath, content, relativePath) {
200
+ const issues = [];
201
+
202
+ // Check if @jmruthers/pace-core is in optimizeDeps.exclude
203
+ const hasOptimizeDepsExclude = /optimizeDeps\s*:\s*\{[\s\S]*?exclude/.test(content);
204
+ if (hasOptimizeDepsExclude) {
205
+ const excludeArrayMatch = content.match(/exclude\s*:\s*\[([^\]]*)\]/);
206
+ if (excludeArrayMatch) {
207
+ const excludeContent = excludeArrayMatch[1];
208
+ if (!excludeContent.includes('@jmruthers/pace-core')) {
209
+ issues.push({
210
+ file: relativePath,
211
+ line: getLineNumber(content, excludeArrayMatch[0]),
212
+ type: 'missing-exclude',
213
+ reason: '@jmruthers/pace-core should be excluded from Vite pre-bundling to prevent React context mismatches.',
214
+ recommendation: "Add '@jmruthers/pace-core' to optimizeDeps.exclude array."
215
+ });
216
+ }
217
+ } else {
218
+ // Check if it's a single string
219
+ const excludeStringMatch = content.match(/exclude\s*:\s*['"]@jmruthers\/pace-core['"]/);
220
+ if (!excludeStringMatch) {
221
+ issues.push({
222
+ file: relativePath,
223
+ line: getLineNumber(content, /exclude\s*:/.exec(content)?.index || 0),
224
+ type: 'missing-exclude',
225
+ reason: '@jmruthers/pace-core should be excluded from Vite pre-bundling.',
226
+ recommendation: "Add '@jmruthers/pace-core' to optimizeDeps.exclude."
227
+ });
228
+ }
229
+ }
230
+ } else {
231
+ issues.push({
232
+ file: relativePath,
233
+ line: getLineNumber(content, /optimizeDeps/.exec(content)?.index || 1),
234
+ type: 'missing-optimize-deps',
235
+ reason: 'optimizeDeps.exclude is missing. This can cause React context mismatch errors.',
236
+ recommendation: "Add optimizeDeps: { exclude: ['@jmruthers/pace-core'], include: ['react', 'react-dom', 'react-router-dom'] } to your Vite config."
237
+ });
238
+ }
239
+
240
+ // Check if @jmruthers/pace-core is in optimizeDeps.include (should NOT be)
241
+ const includeArrayMatch = content.match(/include\s*:\s*\[([^\]]*)\]/);
242
+ if (includeArrayMatch) {
243
+ const includeContent = includeArrayMatch[1];
244
+ if (includeContent.includes('@jmruthers/pace-core')) {
245
+ issues.push({
246
+ file: relativePath,
247
+ line: getLineNumber(content, includeArrayMatch[0]),
248
+ type: 'should-exclude',
249
+ reason: '@jmruthers/pace-core should NOT be in optimizeDeps.include. It should be in exclude instead.',
250
+ recommendation: "Remove '@jmruthers/pace-core' from optimizeDeps.include and add it to optimizeDeps.exclude."
251
+ });
252
+ }
253
+ }
254
+
255
+ // Check for react-router-dom in dedupe (recommended)
256
+ const hasDedupe = /dedupe\s*:\s*\[/.test(content);
257
+ if (hasDedupe) {
258
+ const dedupeArrayMatch = content.match(/dedupe\s*:\s*\[([^\]]*)\]/);
259
+ if (dedupeArrayMatch) {
260
+ const dedupeContent = dedupeArrayMatch[1];
261
+ if (!dedupeContent.includes('react-router-dom')) {
262
+ issues.push({
263
+ file: relativePath,
264
+ line: getLineNumber(content, dedupeArrayMatch[0]),
265
+ type: 'recommendation',
266
+ reason: 'Adding react-router-dom to resolve.dedupe is recommended to prevent Router context issues.',
267
+ recommendation: "Add 'react-router-dom' to resolve.dedupe array: dedupe: ['react', 'react-dom', 'react-router-dom']"
268
+ });
269
+ }
270
+ }
271
+ } else {
272
+ // Check if resolve exists
273
+ if (/resolve\s*:\s*\{/.test(content)) {
274
+ issues.push({
275
+ file: relativePath,
276
+ line: getLineNumber(content, /resolve\s*:/.exec(content)?.index || 1),
277
+ type: 'recommendation',
278
+ reason: 'Adding resolve.dedupe is recommended to prevent React context issues.',
279
+ recommendation: "Add dedupe: ['react', 'react-dom', 'react-router-dom'] to resolve config."
280
+ });
281
+ }
282
+ }
283
+
284
+ // Check for react-router-dom in optimizeDeps.include (should be included, not excluded)
285
+ const hasOptimizeDepsInclude = /optimizeDeps\s*:\s*\{[\s\S]*?include/.test(content);
286
+ if (hasOptimizeDepsInclude) {
287
+ const includeArrayMatch = content.match(/include\s*:\s*\[([^\]]*)\]/);
288
+ if (includeArrayMatch) {
289
+ const includeContent = includeArrayMatch[1];
290
+ if (!includeContent.includes('react-router-dom')) {
291
+ issues.push({
292
+ file: relativePath,
293
+ line: getLineNumber(content, includeArrayMatch[0]),
294
+ type: 'recommendation',
295
+ reason: 'Including react-router-dom in pre-bundling is recommended to prevent module resolution issues.',
296
+ recommendation: "Add 'react-router-dom' to optimizeDeps.include array."
297
+ });
298
+ }
299
+ }
300
+ }
301
+
302
+ // Warn if react-router-dom is in exclude (this causes "module is not defined" errors)
303
+ if (hasOptimizeDepsExclude) {
304
+ const excludeArrayMatch = content.match(/exclude\s*:\s*\[([^\]]*)\]/);
305
+ if (excludeArrayMatch) {
306
+ const excludeContent = excludeArrayMatch[1];
307
+ if (excludeContent.includes('react-router-dom')) {
308
+ issues.push({
309
+ file: relativePath,
310
+ line: getLineNumber(content, excludeArrayMatch[0]),
311
+ type: 'should-exclude',
312
+ reason: 'react-router-dom should NOT be excluded from pre-bundling. This causes "module is not defined" errors.',
313
+ recommendation: "Remove 'react-router-dom' from optimizeDeps.exclude array and add it to optimizeDeps.include instead."
314
+ });
315
+ }
316
+ }
317
+ }
318
+
319
+ return issues;
320
+ }
321
+
322
+ // Scan Router setup in main entry files
323
+ function scanRouterSetup(filePath, content, relativePath) {
324
+ const issues = [];
325
+
326
+ // Only check main.tsx for BrowserRouter setup
327
+ // App.tsx might use Routes but doesn't define BrowserRouter
328
+ const isMainFile = relativePath.match(/^(src\/)?main\.(tsx?|jsx?)$/);
329
+
330
+ if (isMainFile) {
331
+ // Check for BrowserRouter
332
+ const hasBrowserRouter = /BrowserRouter/.test(content);
333
+ if (!hasBrowserRouter) {
334
+ issues.push({
335
+ file: relativePath,
336
+ line: 1,
337
+ type: 'missing-router',
338
+ reason: 'BrowserRouter is required for pace-core components that use React Router hooks.',
339
+ recommendation: 'Import BrowserRouter from react-router-dom and wrap your app with it.'
340
+ });
341
+ }
342
+
343
+ // Don't check nesting order here - that's handled in scanProviderSetup
344
+ // This function just checks if BrowserRouter exists
345
+ }
346
+
347
+ // Check for Routes usage in any file (indicates Router context is needed)
348
+ // But only warn if it's in main.tsx and BrowserRouter is missing
349
+ const hasRoutes = /<Routes/.test(content) || /Routes/.test(content);
350
+ if (hasRoutes && isMainFile) {
351
+ const hasBrowserRouter = /BrowserRouter/.test(content);
352
+ if (!hasBrowserRouter) {
353
+ issues.push({
354
+ file: relativePath,
355
+ line: getLineNumber(content, /Routes/.exec(content)?.index || 0),
356
+ type: 'missing-router',
357
+ reason: 'Routes component requires BrowserRouter to provide Router context.',
358
+ recommendation: 'Wrap your app with BrowserRouter from react-router-dom.'
359
+ });
360
+ }
361
+ }
362
+
363
+ return issues;
364
+ }
365
+
366
+ // Scan file for violations
367
+ function scanFile(filePath, manifest) {
368
+ const violations = {
369
+ restrictedImports: [],
370
+ duplicateComponents: [],
371
+ duplicateHooks: [],
372
+ duplicateUtils: [],
373
+ suggestions: [],
374
+ customAuthCode: [],
375
+ duplicateConfig: [],
376
+ unprotectedPages: [],
377
+ directSupabaseAuth: [],
378
+ providerSetupIssues: [],
379
+ viteConfigIssues: [],
380
+ routerSetupIssues: [],
381
+ unnecessaryWrappers: []
382
+ };
383
+
384
+ const content = fs.readFileSync(filePath, 'utf8');
385
+ const relativePath = path.relative(process.cwd(), filePath);
386
+
387
+ // Check for restricted imports
388
+ manifest.restrictedImports.forEach(({ module, reason }) => {
389
+ const importPattern = new RegExp(`from\\s+['"]${module.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}['"]`, 'g');
390
+ if (importPattern.test(content)) {
391
+ violations.restrictedImports.push({
392
+ module,
393
+ reason,
394
+ file: relativePath,
395
+ line: getLineNumber(content, content.match(importPattern)[0])
396
+ });
397
+ }
398
+
399
+ // Also check for @radix-ui/* pattern
400
+ if (module.startsWith('@radix-ui/')) {
401
+ const radixPattern = /from\s+['"]@radix-ui\/[^'"]+['"]/g;
402
+ const matches = content.match(radixPattern);
403
+ if (matches) {
404
+ matches.forEach(match => {
405
+ const matchedModule = match.match(/['"]([^'"]+)['"]/)[1];
406
+ if (!manifest.restrictedImports.find(ri => ri.module === matchedModule)) {
407
+ violations.restrictedImports.push({
408
+ module: matchedModule,
409
+ reason: 'Use pace-core component instead of direct Radix UI import',
410
+ file: relativePath,
411
+ line: getLineNumber(content, match)
412
+ });
413
+ }
414
+ });
415
+ }
416
+ }
417
+ });
418
+
419
+ // Check for duplicate component names
420
+ const filename = path.basename(filePath);
421
+ const componentName = filename.replace(/\.(tsx?|jsx?)$/, '').replace(/\.(test|spec)$/, '');
422
+
423
+ if (manifest.components.includes(componentName)) {
424
+ // Check if this file exports a component
425
+ if (content.match(/export\s+(default\s+)?(function|const|class)\s+(\w+)?/)) {
426
+ violations.duplicateComponents.push({
427
+ component: componentName,
428
+ file: relativePath
429
+ });
430
+ }
431
+ }
432
+
433
+ // Check for duplicate hook names
434
+ if (filename.startsWith('use') && filename.endsWith('.ts') || filename.endsWith('.tsx')) {
435
+ const hookName = filename.replace(/\.(tsx?|jsx?)$/, '').replace(/\.(test|spec)$/, '');
436
+ if (manifest.hooks.includes(hookName)) {
437
+ if (content.match(/export\s+(default\s+)?(function|const)\s+(\w+)?/)) {
438
+ violations.duplicateHooks.push({
439
+ hook: hookName,
440
+ file: relativePath
441
+ });
442
+ }
443
+ }
444
+ }
445
+
446
+ // Check for duplicate util names
447
+ const utilName = filename.replace(/\.(ts|js)$/, '').replace(/\.(test|spec)$/, '');
448
+ if (manifest.utils.includes(utilName)) {
449
+ if (content.match(/export\s+(default\s+)?(function|const)\s+(\w+)?/)) {
450
+ violations.duplicateUtils.push({
451
+ util: utilName,
452
+ file: relativePath
453
+ });
454
+ }
455
+ }
456
+
457
+ // Check for native HTML elements that should use pace-core components
458
+ const nativeElementPatterns = {
459
+ '<button': { suggestion: 'Use Button from @jmruthers/pace-core' },
460
+ '<input': { suggestion: 'Use Input from @jmruthers/pace-core' },
461
+ '<textarea': { suggestion: 'Use Textarea from @jmruthers/pace-core' },
462
+ '<label': { suggestion: 'Use Label from @jmruthers/pace-core' }
463
+ };
464
+
465
+ Object.entries(nativeElementPatterns).forEach(([pattern, { suggestion }]) => {
466
+ if (content.includes(pattern) && !content.includes('from \'@jmruthers/pace-core\'')) {
467
+ violations.suggestions.push({
468
+ type: 'native-element',
469
+ suggestion,
470
+ file: relativePath,
471
+ pattern
472
+ });
473
+ }
474
+ });
475
+
476
+ // ============================================
477
+ // RBAC/Auth Compliance Checks
478
+ // ============================================
479
+
480
+ // Check for custom auth/rbac/permission code that doesn't import from pace-core
481
+ const authRbacPatterns = [
482
+ // Custom auth hooks
483
+ { pattern: /export\s+(default\s+)?(function|const)\s+useAuth\s*[=\(]/g, name: 'useAuth', type: 'hook' },
484
+ { pattern: /export\s+(default\s+)?(function|const)\s+useCurrentUser\s*[=\(]/g, name: 'useCurrentUser', type: 'hook', replacement: 'useUnifiedAuth' },
485
+ { pattern: /export\s+(default\s+)?(function|const)\s+useLogin\s*[=\(]/g, name: 'useLogin', type: 'hook' },
486
+ { pattern: /export\s+(default\s+)?(function|const)\s+useLogout\s*[=\(]/g, name: 'useLogout', type: 'hook' },
487
+ { pattern: /export\s+(default\s+)?(function|const)\s+useSession\s*[=\(]/g, name: 'useSession', type: 'hook' },
488
+ { pattern: /export\s+(default\s+)?(function|const)\s+useUser\s*[=\(]/g, name: 'useUser', type: 'hook' },
489
+ { pattern: /export\s+(default\s+)?(function|const)\s+useAuthentication\s*[=\(]/g, name: 'useAuthentication', type: 'hook' },
490
+ // Custom RBAC hooks (that query RBAC tables directly)
491
+ { pattern: /export\s+(default\s+)?(function|const)\s+useUserOrganisationRoles\s*[=\(]/g, name: 'useUserOrganisationRoles', type: 'hook', replacement: 'useOrganisations or pace-core RBAC hooks' },
492
+ { pattern: /export\s+(default\s+)?(function|const)\s+useUserRoles\s*[=\(]/g, name: 'useUserRoles', type: 'hook', replacement: 'pace-core RBAC hooks' },
493
+ { pattern: /export\s+(default\s+)?(function|const)\s+usePermissions\s*[=\(]/g, name: 'usePermissions', type: 'hook' },
494
+ { pattern: /export\s+(default\s+)?(function|const)\s+useCan\s*[=\(]/g, name: 'useCan', type: 'hook' },
495
+ { pattern: /export\s+(default\s+)?(function|const)\s+useAccessLevel\s*[=\(]/g, name: 'useAccessLevel', type: 'hook' },
496
+ { pattern: /export\s+(default\s+)?(function|const)\s+useRole\s*[=\(]/g, name: 'useRole', type: 'hook' },
497
+ // Custom RBAC components
498
+ { pattern: /export\s+(default\s+)?(function|const)\s+PermissionGuard\s*[=\(]/g, name: 'PermissionGuard', type: 'component' },
499
+ { pattern: /export\s+(default\s+)?(function|const)\s+AuthGuard\s*[=\(]/g, name: 'AuthGuard', type: 'component' },
500
+ { pattern: /export\s+(default\s+)?(function|const)\s+RoleGuard\s*[=\(]/g, name: 'RoleGuard', type: 'component' },
501
+ { pattern: /export\s+(default\s+)?(function|const)\s+AccessGuard\s*[=\(]/g, name: 'AccessGuard', type: 'component' },
502
+ // Custom permission utilities
503
+ { pattern: /export\s+(default\s+)?(function|const)\s+checkPermission\s*[=\(]/g, name: 'checkPermission', type: 'util' },
504
+ { pattern: /export\s+(default\s+)?(function|const)\s+hasPermission\s*[=\(]/g, name: 'hasPermission', type: 'util' },
505
+ { pattern: /export\s+(default\s+)?(function|const)\s+hasAccess\s*[=\(]/g, name: 'hasAccess', type: 'util' },
506
+ { pattern: /export\s+(default\s+)?(function|const)\s+canAccess\s*[=\(]/g, name: 'canAccess', type: 'util' },
507
+ { pattern: /export\s+(default\s+)?(function|const)\s+isPermitted\s*[=\(]/g, name: 'isPermitted', type: 'util' }
508
+ ];
509
+
510
+ // Check if file imports from pace-core for auth/rbac
511
+ const hasPaceCoreImport = /from\s+['"]@jmruthers\/pace-core/.test(content) ||
512
+ /from\s+['"]@jmruthers\/pace-core\/rbac/.test(content) ||
513
+ /from\s+['"]@jmruthers\/pace-core\/providers/.test(content);
514
+
515
+ authRbacPatterns.forEach(({ pattern, name, type, replacement }) => {
516
+ if (pattern.test(content)) {
517
+ // For custom RBAC hooks, always flag them even if they import from pace-core
518
+ // because they're duplicating functionality that should come from pace-core
519
+ const isCustomRBACHook = [
520
+ 'useUserRoles',
521
+ 'useUserOrganisationRoles',
522
+ 'useRoleMutations',
523
+ 'usePermissions',
524
+ 'useCan',
525
+ 'useAccessLevel',
526
+ 'useRole'
527
+ ].includes(name);
528
+
529
+ // Flag custom RBAC hooks even if they import from pace-core (they're still duplicating functionality)
530
+ // For other hooks, only flag if they don't import from pace-core
531
+ if (isCustomRBACHook || !hasPaceCoreImport) {
532
+ const replacementName = replacement || name;
533
+ const reason = isCustomRBACHook
534
+ ? `Custom ${type} '${name}' detected that duplicates pace-core functionality. Even though it may use some pace-core utilities, it implements custom role/permission management logic that should use pace-core APIs instead.`
535
+ : `Custom ${type} '${name}' detected. Use pace-core's ${replacementName} instead.`;
536
+
537
+ violations.customAuthCode.push({
538
+ name,
539
+ type,
540
+ file: relativePath,
541
+ line: getLineNumber(content, content.match(pattern)[0]),
542
+ reason: reason,
543
+ replacement: replacementName,
544
+ severity: 'error'
545
+ });
546
+ }
547
+ }
548
+ });
549
+
550
+ // Check for duplicate Supabase client configurations
551
+ const supabaseCreateClientPattern = /createClient\s*\(/g;
552
+ const supabaseCreateClientMatches = content.match(supabaseCreateClientPattern);
553
+ if (supabaseCreateClientMatches && supabaseCreateClientMatches.length > 1) {
554
+ violations.duplicateConfig.push({
555
+ type: 'supabase-client',
556
+ file: relativePath,
557
+ count: supabaseCreateClientMatches.length,
558
+ reason: `Multiple Supabase client instantiations found (${supabaseCreateClientMatches.length}). Consolidate to a single client configuration.`
559
+ });
560
+ }
561
+
562
+ // Check for Supabase URL/key configuration in multiple places
563
+ // This check is designed to catch actual duplication, not centralized config files.
564
+ // A centralized config file may legitimately reference env vars multiple times:
565
+ // - Reading from import.meta.env or process.env
566
+ // - Validation/error messages
567
+ // - Using in createClient
568
+ // - Comments/documentation
569
+ const supabaseUrlPattern = /(SUPABASE_URL|VITE_SUPABASE_URL|NEXT_PUBLIC_SUPABASE_URL|REACT_APP_SUPABASE_URL)/g;
570
+ const supabaseKeyPattern = /(SUPABASE_ANON_KEY|VITE_SUPABASE_ANON_KEY|NEXT_PUBLIC_SUPABASE_ANON_KEY|REACT_APP_SUPABASE_ANON_KEY)/g;
571
+ const urlMatches = content.match(supabaseUrlPattern);
572
+ const keyMatches = content.match(supabaseKeyPattern);
573
+
574
+ // Check if this looks like a centralized config file
575
+ // Config files typically:
576
+ // - Have "config" or "supabase" or "client" in the path
577
+ // - Have a single createClient call (or none, if it's just exporting env vars)
578
+ // - Export the env vars or the client (indicating it's the centralized location)
579
+ const isConfigFile = /config|supabase|client/i.test(relativePath);
580
+ const hasSingleCreateClient = supabaseCreateClientMatches && supabaseCreateClientMatches.length === 1;
581
+ const hasNoCreateClient = !supabaseCreateClientMatches || supabaseCreateClientMatches.length === 0;
582
+
583
+ // Check if file exports env vars or client (strong indicator of centralized config)
584
+ // Matches: export const SUPABASE_URL = ...; export { SUPABASE_URL }; export const supabase = ...
585
+ const exportsEnvVars = /export\s+(const|let|var|{)\s*(SUPABASE_URL|SUPABASE_ANON_KEY|supabase)/.test(content) ||
586
+ /export\s*{\s*[^}]*\b(SUPABASE_URL|SUPABASE_ANON_KEY|supabase)\b/.test(content);
587
+ const exportsClient = /export\s+(const|let|var)\s+supabase\s*=/.test(content) ||
588
+ /export\s*{\s*[^}]*\bsupabase\b/.test(content);
589
+
590
+ // A file is considered a centralized config if:
591
+ // 1. It's in a config/supabase/client path, AND
592
+ // 2. Has a single createClient call (typical centralized config), OR
593
+ // 3. Has no createClient but references env vars (env-only config file), OR
594
+ // 4. Exports env vars or the client (indicating it's the centralized location)
595
+ // The path name is the primary indicator - if it's in a config path, trust it
596
+ const looksLikeCentralizedConfig = isConfigFile && (
597
+ hasSingleCreateClient ||
598
+ (hasNoCreateClient && (urlMatches || keyMatches)) ||
599
+ exportsEnvVars ||
600
+ exportsClient
601
+ );
602
+
603
+ // Only flag if:
604
+ // 1. Multiple createClient calls (already handled above), OR
605
+ // 2. Many references (10+) in a file that doesn't look like a centralized config
606
+ // Config files are allowed to have many references (read, validate, use, errors, comments, etc.)
607
+ const threshold = looksLikeCentralizedConfig ? 20 : 5;
608
+
609
+ if (!looksLikeCentralizedConfig &&
610
+ ((urlMatches && urlMatches.length > threshold) ||
611
+ (keyMatches && keyMatches.length > threshold))) {
612
+ violations.duplicateConfig.push({
613
+ type: 'supabase-env',
614
+ file: relativePath,
615
+ reason: `Supabase environment variables referenced many times (${urlMatches?.length || keyMatches?.length || 0}). If this is a centralized config file, consider moving it to a file with 'config', 'supabase', or 'client' in the path, or export the values to indicate it's the centralized location.`
616
+ });
617
+ }
618
+
619
+ // Check for unprotected pages/routes
620
+ // Look for route definitions without PagePermissionGuard
621
+ const routePatterns = [
622
+ /<Route\s+path=["'][^"']+["']/g,
623
+ /<Route\s+element\s*=/g,
624
+ /createBrowserRouter\s*\(/g,
625
+ /createRoutesFromElements/g
626
+ ];
627
+
628
+ const isRouteFile = routePatterns.some(pattern => pattern.test(content));
629
+ const hasPagePermissionGuard = /PagePermissionGuard/.test(content) ||
630
+ /from\s+['"]@jmruthers\/pace-core\/rbac['"]/.test(content);
631
+
632
+ if (isRouteFile && !hasPagePermissionGuard && !relativePath.includes('test') && !relativePath.includes('spec')) {
633
+ violations.unprotectedPages.push({
634
+ file: relativePath,
635
+ reason: 'Route file found without PagePermissionGuard. All routes should be protected with PagePermissionGuard from pace-core.'
636
+ });
637
+ }
638
+
639
+ // Check for direct Supabase auth usage (should use UnifiedAuthProvider)
640
+ // This includes all variations: supabase.auth.getUser(), client.auth.getUser(), etc.
641
+ // Priority patterns - these are the most common violations
642
+ // Using multiple patterns to catch all variations
643
+ const priorityAuthPatterns = [
644
+ // Most specific patterns first
645
+ { pattern: /supabase\.auth\.getUser\s*\(/g, method: 'getUser', specific: true },
646
+ { pattern: /supabase\.auth\.getSession\s*\(/g, method: 'getSession', specific: true },
647
+ // Also catch with await or const destructuring
648
+ { pattern: /await\s+supabase\.auth\.getUser\s*\(/g, method: 'getUser', specific: true },
649
+ { pattern: /await\s+supabase\.auth\.getSession\s*\(/g, method: 'getSession', specific: true },
650
+ { pattern: /const\s+[^=]*=\s*supabase\.auth\.getUser\s*\(/g, method: 'getUser', specific: true },
651
+ { pattern: /const\s+[^=]*=\s*supabase\.auth\.getSession\s*\(/g, method: 'getSession', specific: true },
652
+ // Generic patterns for other variable names
653
+ { pattern: /\w+\.auth\.getUser\s*\(/g, method: 'getUser', specific: false },
654
+ { pattern: /\w+\.auth\.getSession\s*\(/g, method: 'getSession', specific: false }
655
+ ];
656
+
657
+ // Other auth patterns
658
+ const otherAuthPatterns = [
659
+ { pattern: /\.auth\.signIn\s*\(/g, method: 'signIn' },
660
+ { pattern: /\.auth\.signUp\s*\(/g, method: 'signUp' },
661
+ { pattern: /\.auth\.signOut\s*\(/g, method: 'signOut' },
662
+ { pattern: /\.auth\.onAuthStateChange\s*\(/g, method: 'onAuthStateChange' }
663
+ ];
664
+
665
+ // Check if file actually uses useUnifiedAuth hook (not just imports it)
666
+ const usesUnifiedAuthHook = /useUnifiedAuth\s*\(/.test(content);
667
+ const hasUnifiedAuthImport = /UnifiedAuthProvider/.test(content) ||
668
+ /useUnifiedAuth/.test(content) ||
669
+ /from\s+['"]@jmruthers\/pace-core\/providers/.test(content);
670
+
671
+ // Check for usage of useCurrentUser hook (even if imported from local file)
672
+ // This catches both local imports and direct usage
673
+ const useCurrentUserImportPattern = /import\s+.*useCurrentUser.*from\s+['"][^'"]*['"]/g;
674
+ const useCurrentUserUsagePattern = /useCurrentUser\s*\(/g;
675
+
676
+ // Check for local import (not from pace-core)
677
+ const useCurrentUserImportMatch = content.match(useCurrentUserImportPattern);
678
+ if (useCurrentUserImportMatch) {
679
+ const isFromPaceCore = useCurrentUserImportMatch.some(match => match.includes('@jmruthers/pace-core'));
680
+ if (!isFromPaceCore) {
681
+ useCurrentUserImportMatch.forEach(match => {
682
+ violations.customAuthCode.push({
683
+ name: 'useCurrentUser import',
684
+ type: 'hook import',
685
+ file: relativePath,
686
+ line: getLineNumber(content, match),
687
+ reason: 'useCurrentUser imported from local file. Replace with useUnifiedAuth from pace-core.',
688
+ replacement: 'useUnifiedAuth from @jmruthers/pace-core'
689
+ });
690
+ });
691
+ }
692
+ }
693
+
694
+ // Check for usage (even if imported)
695
+ const useCurrentUserUsageMatches = content.match(useCurrentUserUsagePattern);
696
+ if (useCurrentUserUsageMatches && !usesUnifiedAuthHook) {
697
+ useCurrentUserUsageMatches.forEach(match => {
698
+ violations.customAuthCode.push({
699
+ name: 'useCurrentUser',
700
+ type: 'hook usage',
701
+ file: relativePath,
702
+ line: getLineNumber(content, match),
703
+ reason: 'useCurrentUser hook usage detected. Replace with useUnifiedAuth from pace-core.',
704
+ replacement: 'useUnifiedAuth'
705
+ });
706
+ });
707
+ }
708
+
709
+ // Check priority patterns first (getUser, getSession) - these should always be flagged
710
+ // Use exec in a loop to get all matches with their correct positions
711
+ priorityAuthPatterns.forEach(({ pattern, method, specific }) => {
712
+ // Reset regex for each pattern
713
+ const regex = new RegExp(pattern.source, pattern.flags);
714
+ regex.lastIndex = 0; // Reset to start
715
+
716
+ let match;
717
+ while ((match = regex.exec(content)) !== null) {
718
+ // Prevent infinite loops on zero-length matches
719
+ if (match.index === regex.lastIndex) {
720
+ regex.lastIndex++;
721
+ continue;
722
+ }
723
+
724
+ const matchIndex = match.index;
725
+ const matchText = match[0];
726
+
727
+ // Simple comment check - only skip if clearly in a line comment
728
+ const lineStart = content.lastIndexOf('\n', matchIndex) + 1;
729
+ const lineUpToMatch = content.substring(lineStart, matchIndex);
730
+ const isInLineComment = /\/\/[^\n]*$/.test(lineUpToMatch);
731
+
732
+ // Only skip if clearly in a comment - be conservative
733
+ if (!isInLineComment) {
734
+ violations.directSupabaseAuth.push({
735
+ file: relativePath,
736
+ line: getLineNumber(content, matchText),
737
+ reason: `Direct Supabase auth usage detected (${method}). Use useUnifiedAuth hook from pace-core instead.`,
738
+ method,
739
+ recommendation: specific
740
+ ? method === 'getUser'
741
+ ? `Replace with: const { user } = useUnifiedAuth(); then use user?.id instead of calling supabase.auth.getUser()`
742
+ : `Replace with: const { session } = useUnifiedAuth(); then use session?.access_token instead of calling supabase.auth.getSession()`
743
+ : `Use useUnifiedAuth hook from @jmruthers/pace-core instead of direct auth calls`
744
+ });
745
+ }
746
+ }
747
+ });
748
+
749
+ // Additional simple pattern check as fallback - look for literal strings
750
+ // This catches cases where the regex might miss due to formatting
751
+ const simpleAuthPatterns = [
752
+ { search: 'supabase.auth.getUser(', method: 'getUser' },
753
+ { search: 'supabase.auth.getSession(', method: 'getSession' }
754
+ ];
755
+
756
+ simpleAuthPatterns.forEach(({ search, method }) => {
757
+ let searchIndex = 0;
758
+ while ((searchIndex = content.indexOf(search, searchIndex)) !== -1) {
759
+ // Skip if in a line comment
760
+ const lineStart = content.lastIndexOf('\n', searchIndex) + 1;
761
+ const lineUpToMatch = content.substring(lineStart, searchIndex);
762
+ const isInLineComment = /\/\/[^\n]*$/.test(lineUpToMatch);
763
+
764
+ if (!isInLineComment) {
765
+ // Calculate line number from index
766
+ const lineNum = content.substring(0, searchIndex).split('\n').length;
767
+
768
+ // Check if we already reported this (might overlap with regex matches)
769
+ const alreadyReported = violations.directSupabaseAuth.some(v =>
770
+ v.file === relativePath &&
771
+ Math.abs(v.line - lineNum) <= 2
772
+ );
773
+
774
+ if (!alreadyReported) {
775
+ violations.directSupabaseAuth.push({
776
+ file: relativePath,
777
+ line: lineNum,
778
+ reason: `Direct Supabase auth usage detected (${method}). Use useUnifiedAuth hook from pace-core instead.`,
779
+ method,
780
+ recommendation: method === 'getUser'
781
+ ? `Replace with: const { user } = useUnifiedAuth(); then use user?.id instead of calling supabase.auth.getUser()`
782
+ : `Replace with: const { session } = useUnifiedAuth(); then use session?.access_token instead of calling supabase.auth.getSession()`
783
+ });
784
+ }
785
+ }
786
+
787
+ searchIndex += search.length; // Move past this match
788
+ }
789
+ });
790
+
791
+ // Check other auth patterns - flag if not using useUnifiedAuth
792
+ otherAuthPatterns.forEach(({ pattern, method }) => {
793
+ let match;
794
+ const regex = new RegExp(pattern.source, pattern.flags);
795
+
796
+ while ((match = regex.exec(content)) !== null) {
797
+ // Prevent infinite loops on zero-length matches
798
+ if (match.index === regex.lastIndex) {
799
+ regex.lastIndex++;
800
+ }
801
+
802
+ const matchIndex = match.index;
803
+ const matchText = match[0];
804
+
805
+ // Skip if this is in a comment or string literal
806
+ const lineStart = content.lastIndexOf('\n', matchIndex) + 1;
807
+ const lineBeforeMatch = content.substring(lineStart, matchIndex);
808
+ const isInLineComment = /\/\/[^\n]*$/.test(lineBeforeMatch);
809
+ const beforeMatch = content.substring(Math.max(0, matchIndex - 100), matchIndex);
810
+ const afterMatch = content.substring(matchIndex, Math.min(content.length, matchIndex + matchText.length + 50));
811
+ const isInBlockComment = /\/\*[\s\S]*?\*\//.test(beforeMatch + matchText + afterMatch);
812
+
813
+ // Check if it's in a string literal
814
+ const beforeQuotes = content.substring(0, matchIndex);
815
+ const singleQuotes = (beforeQuotes.match(/'/g) || []).length;
816
+ const doubleQuotes = (beforeQuotes.match(/"/g) || []).length;
817
+ const backticks = (beforeQuotes.match(/`/g) || []).length;
818
+ const isInString = (singleQuotes % 2 === 1 && beforeMatch.endsWith("'")) ||
819
+ (doubleQuotes % 2 === 1 && beforeMatch.endsWith('"')) ||
820
+ (backticks % 2 === 1 && beforeMatch.endsWith('`'));
821
+
822
+ if (!isInLineComment && !isInBlockComment && !isInString && !usesUnifiedAuthHook) {
823
+ violations.directSupabaseAuth.push({
824
+ file: relativePath,
825
+ line: getLineNumber(content, matchText),
826
+ reason: `Direct Supabase auth usage detected (${method}). Use UnifiedAuthProvider and useUnifiedAuth from pace-core instead.`,
827
+ method
828
+ });
829
+ }
830
+ }
831
+ });
832
+
833
+ // Check for direct RBAC table queries (should use pace-core RBAC APIs/RPC functions)
834
+ // List of all RBAC tables with specific recommendations
835
+ const rbacTables = [
836
+ // Core RBAC tables - should use pace-core APIs
837
+ { name: 'rbac_organisation_roles', type: 'role', recommendation: 'Use rbac_role_grant/rbac_role_revoke RPC functions for mutations, or useOrganisations hook for queries' },
838
+ { name: 'rbac_event_app_roles', type: 'role', recommendation: 'Use rbac_role_grant/rbac_role_revoke RPC functions for mutations, or pace-core RBAC APIs (useRBAC, usePermissions) for queries' },
839
+ { name: 'rbac_global_roles', type: 'role', recommendation: 'Use rbac_role_grant/rbac_role_revoke RPC functions for mutations, or pace-core RBAC APIs (useRBAC) for queries' },
840
+ { name: 'rbac_apps', type: 'config', recommendation: 'For admin operations, use useSecureSupabase. For application use, use pace-core RBAC APIs' },
841
+ { name: 'rbac_app_pages', type: 'config', recommendation: 'For admin operations, use useSecureSupabase. For application use, use pace-core permission management APIs or PagePermissionGuard' },
842
+ { name: 'rbac_page_permissions', type: 'config', recommendation: 'For admin operations, use useSecureSupabase. For application use, use pace-core permission management APIs or PagePermissionGuard' },
843
+ { name: 'rbac_user_units', type: 'user_data', recommendation: 'Use useSecureSupabase or useSecureDataAccess. For reading, consider data_user_unit_get RPC function' },
844
+ // User data tables - acceptable to query but must use secure methods
845
+ { name: 'rbac_user_profiles', type: 'user_data', recommendation: 'Use useSecureSupabase or useSecureDataAccess from pace-core to ensure organisation context is enforced' },
846
+ { 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' },
847
+ { name: 'rbac_user_sessions', type: 'session', recommendation: 'Use useSecureSupabase or useSecureDataAccess from pace-core to ensure organisation context is enforced' }
848
+ ];
849
+
850
+ // Detect admin/management context
851
+ const isAdminContext =
852
+ relativePath.includes('/admin/') ||
853
+ relativePath.includes('/superadmin/') ||
854
+ relativePath.includes('/permissions/') ||
855
+ relativePath.includes('/management/') ||
856
+ /(Admin|Manager|Management|Permissions)[^/]*\.(tsx?|jsx?)$/.test(relativePath);
857
+
858
+ // Multiple patterns to catch all variations
859
+ const rbacTablePatterns = [
860
+ /\.from\s*\(\s*['"]rbac_/g, // .from('rbac_ or .from("rbac_
861
+ /from\s*\(\s*['"]rbac_/g, // from('rbac_ (without dot)
862
+ /\.select\s*\([^)]*from\s+['"]rbac_/g, // .select(...).from('rbac_
863
+ /supabase\.from\s*\(\s*['"]rbac_/g // supabase.from('rbac_
864
+ ];
865
+
866
+ // Check if file uses pace-core RBAC APIs (if it does, direct queries might be acceptable in some cases)
867
+ // But we still want to flag them as they should use the APIs
868
+ const hasRBACImport = /from\s+['"]@jmruthers\/pace-core\/rbac/.test(content) ||
869
+ /useRBAC/.test(content) ||
870
+ /usePermissions/.test(content) ||
871
+ /useSecureSupabase/.test(content) ||
872
+ /useSecureDataAccess/.test(content) ||
873
+ /PagePermissionGuard/.test(content);
874
+
875
+ // Check if file uses useSecureDataAccess hook
876
+ const usesSecureDataAccess = /useSecureDataAccess/.test(content);
877
+
878
+ // Check if file destructures secure methods from useSecureDataAccess
879
+ const hasSecureMethods = /(const|let)\s*\{[^}]*secure(Query|Update|Insert|Delete)/.test(content) ||
880
+ /secure(Query|Update|Insert|Delete)\s*\(/.test(content);
881
+
882
+ // First, identify all variables assigned from secure hooks
883
+ // Match patterns like: const supabase = useSecureSupabase(); or let client = useSecureDataAccess();
884
+ // Also detect fromSupabaseClient and wrapper patterns
885
+ const secureVariablePatterns = [
886
+ /const\s+(\w+)\s*=\s*useSecureSupabase\s*\(/g,
887
+ /const\s+(\w+)\s*=\s*useSecureDataAccess\s*\(/g,
888
+ /let\s+(\w+)\s*=\s*useSecureSupabase\s*\(/g,
889
+ /let\s+(\w+)\s*=\s*useSecureDataAccess\s*\(/g,
890
+ /(\w+)\s*=\s*useSecureSupabase\s*\(/g,
891
+ /(\w+)\s*=\s*useSecureDataAccess\s*\(/g,
892
+ // Detect fromSupabaseClient usage
893
+ /const\s+(\w+)\s*=\s*fromSupabaseClient\s*\(/g,
894
+ /let\s+(\w+)\s*=\s*fromSupabaseClient\s*\(/g,
895
+ /(\w+)\s*=\s*fromSupabaseClient\s*\(/g
896
+ ];
897
+
898
+ // Check for fromSupabaseClient import
899
+ const hasFromSupabaseClientImport = /import.*fromSupabaseClient.*from\s+['"]@jmruthers\/pace-core\/rbac/.test(content);
900
+
901
+ // Check for wrapper functions that use fromSupabaseClient
902
+ // Pattern: function/hook that imports fromSupabaseClient and returns a client
903
+ const wrapperFunctionPattern = /(export\s+)?(function|const)\s+(\w+)\s*[=\(][\s\S]{0,500}fromSupabaseClient/g;
904
+ const wrapperMatches = content.match(wrapperFunctionPattern);
905
+ const wrapperFunctionNames = new Set();
906
+ if (wrapperMatches) {
907
+ wrapperMatches.forEach(match => {
908
+ const funcMatch = match.match(/(?:function|const)\s+(\w+)\s*[=\(]/);
909
+ if (funcMatch && funcMatch[1]) {
910
+ wrapperFunctionNames.add(funcMatch[1]);
911
+ }
912
+ });
913
+ }
914
+
915
+ // Also check for useSecureClient or similar wrapper patterns
916
+ const useSecureClientPattern = /(const|let)\s+(\w+)\s*=\s*useSecureClient\s*\(/g;
917
+ let useSecureClientMatch;
918
+ while ((useSecureClientMatch = useSecureClientPattern.exec(content)) !== null) {
919
+ if (useSecureClientMatch[2] && hasFromSupabaseClientImport) {
920
+ // If useSecureClient is used and file imports fromSupabaseClient, it's likely a wrapper
921
+ wrapperFunctionNames.add(useSecureClientMatch[2]);
922
+ }
923
+ }
924
+
925
+ const secureVariables = new Set();
926
+ secureVariablePatterns.forEach(pattern => {
927
+ let match;
928
+ const regex = new RegExp(pattern.source, pattern.flags);
929
+ regex.lastIndex = 0;
930
+ while ((match = regex.exec(content)) !== null) {
931
+ if (match[1]) {
932
+ secureVariables.add(match[1]);
933
+ // Debug: log detected secure variables (can be removed later)
934
+ // console.log(`Detected secure variable: ${match[1]} from pattern ${pattern.source}`);
935
+ }
936
+ }
937
+ });
938
+
939
+ // Also check for variables assigned from useSecureSupabase with different names
940
+ // Pattern: const <anyName> = useSecureSupabase();
941
+ const anySecureSupabasePattern = /(const|let)\s+(\w+)\s*=\s*useSecureSupabase\s*\(/g;
942
+ let anySecureMatch;
943
+ while ((anySecureMatch = anySecureSupabasePattern.exec(content)) !== null) {
944
+ if (anySecureMatch[2]) {
945
+ secureVariables.add(anySecureMatch[2]);
946
+ }
947
+ }
948
+
949
+ // Add wrapper function return values to secure variables
950
+ wrapperFunctionNames.forEach(wrapperName => {
951
+ // Find variables assigned from wrapper functions
952
+ const wrapperUsagePattern = new RegExp(`(const|let)\\s+(\\w+)\\s*=\\s*${wrapperName}\\s*\\(`, 'g');
953
+ let wrapperUsageMatch;
954
+ while ((wrapperUsageMatch = wrapperUsagePattern.exec(content)) !== null) {
955
+ if (wrapperUsageMatch[2]) {
956
+ secureVariables.add(wrapperUsageMatch[2]);
957
+ }
958
+ }
959
+ });
960
+
961
+ // Debug: Log detected secure variables (only in verbose mode if needed)
962
+ // For now, we'll trust the detection works
963
+
964
+ // Check each RBAC table specifically
965
+ rbacTables.forEach(({ name: tableName, type, recommendation }) => {
966
+ // Pattern to match the table name in a .from() call
967
+ // Match: variable.from('table_name') or variable\n.from('table_name') (handles newlines)
968
+ // First, find all .from('table_name') calls
969
+ const fromPattern = new RegExp(`\\.from\\s*\\(\\s*['"]${tableName.replace(/_/g, '\\_')}['"]`, 'g');
970
+ let match;
971
+ const regex = new RegExp(fromPattern.source, fromPattern.flags);
972
+ regex.lastIndex = 0;
973
+
974
+ // Also check for secureQuery/secureUpdate/secureInsert/secureDelete calls
975
+ // Pattern: secureQuery('table_name', ...) or secureUpdate('table_name', ...)
976
+ const secureQueryPattern = new RegExp(`(secureQuery|secureUpdate|secureInsert|secureDelete)\\s*\\(\\s*['"]${tableName.replace(/_/g, '\\_')}['"]`, 'g');
977
+ let secureMatch;
978
+ const secureRegex = new RegExp(secureQueryPattern.source, secureQueryPattern.flags);
979
+ secureRegex.lastIndex = 0;
980
+
981
+ while ((match = regex.exec(content)) !== null) {
982
+ if (match.index === regex.lastIndex) {
983
+ regex.lastIndex++;
984
+ continue;
985
+ }
986
+
987
+ const matchIndex = match.index;
988
+
989
+ // Look backwards to find the variable name (handle newlines and whitespace)
990
+ // Look back up to 200 characters to find the variable
991
+ const beforeMatch = content.substring(Math.max(0, matchIndex - 200), matchIndex);
992
+ // Find the last word/identifier before .from
993
+ // Split by .from and get the last part, then extract the last word
994
+ const parts = beforeMatch.split('.from');
995
+ let variableName = null;
996
+ if (parts.length > 0) {
997
+ const beforeFrom = parts[parts.length - 1].trim();
998
+ // Extract the last word (identifier) from the string before .from
999
+ // Handle cases like: "supabase\n " or "await supabase\n " or "supabase"
1000
+ const words = beforeFrom.match(/\b\w+\b/g);
1001
+ if (words && words.length > 0) {
1002
+ variableName = words[words.length - 1];
1003
+ }
1004
+ }
1005
+
1006
+ // Skip if in a line comment
1007
+ const lineStart = content.lastIndexOf('\n', matchIndex) + 1;
1008
+ const lineUpToMatch = content.substring(lineStart, matchIndex);
1009
+ const isInLineComment = /\/\/[^\n]*$/.test(lineUpToMatch);
1010
+
1011
+ if (!isInLineComment && variableName) {
1012
+ const lineNumber = content.substring(0, matchIndex).split('\n').length;
1013
+ const isUserDataOrAudit = type === 'user_data' || type === 'audit';
1014
+ const isConfigTable = type === 'config';
1015
+
1016
+ // Check if the variable comes from a secure hook
1017
+ // Check both the secureVariables set and also check if the variable is assigned from useSecureSupabase/useSecureDataAccess
1018
+ // Escape special regex characters in variable name and use multiline flag to handle newlines
1019
+ const escapedVarName = variableName ? variableName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') : '';
1020
+ const isUsingSecureVariable = secureVariables.has(variableName) ||
1021
+ (variableName && new RegExp(`(const|let)\\s+${escapedVarName}\\s*=\\s*useSecureSupabase\\s*\\(`, 'm').test(content)) ||
1022
+ (variableName && new RegExp(`(const|let)\\s+${escapedVarName}\\s*=\\s*useSecureDataAccess\\s*\\(`, 'm').test(content));
1023
+
1024
+ // Determine severity based on context
1025
+ let severity = 'error';
1026
+ let reason = '';
1027
+
1028
+ if (isUserDataOrAudit) {
1029
+ if (isUsingSecureVariable) {
1030
+ // Correct usage - skip it
1031
+ continue;
1032
+ } else {
1033
+ severity = 'error';
1034
+ 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.`;
1035
+ }
1036
+ } else if (isConfigTable) {
1037
+ if (isUsingSecureVariable) {
1038
+ // Using secure methods for config tables - acceptable (admin operations or hooks using secure methods)
1039
+ // Don't flag if using secure methods, regardless of admin context
1040
+ continue;
1041
+ } else if (isAdminContext) {
1042
+ // Admin operations without secure methods - warning
1043
+ severity = 'warning';
1044
+ reason = `Admin operation on configuration table '${tableName}' detected. Ensure you're using useSecureSupabase or useSecureDataAccess for security.`;
1045
+ } else {
1046
+ // Not admin context and not using secure methods - error
1047
+ severity = 'error';
1048
+ 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.`;
1049
+ }
1050
+ } else {
1051
+ // Role/permission tables - always error, even in admin context
1052
+ // These should use pace-core APIs or RPC functions, not direct queries
1053
+ severity = 'error';
1054
+ reason = `Direct query to RBAC role/permission table '${tableName}' detected. Use pace-core RBAC APIs (useRBAC, usePermissions) or RPC functions (rbac_role_grant, rbac_role_revoke, rbac_roles_list) instead of direct queries, even in admin contexts.`;
1055
+ }
1056
+
1057
+ violations.customAuthCode.push({
1058
+ name: `Direct RBAC table query (${tableName})`,
1059
+ type: 'rbac query',
1060
+ file: relativePath,
1061
+ line: lineNumber,
1062
+ reason: reason,
1063
+ replacement: recommendation,
1064
+ severity: severity
1065
+ });
1066
+ }
1067
+ }
1068
+
1069
+ // Check for secureQuery/secureUpdate/secureInsert/secureDelete calls
1070
+ while ((secureMatch = secureRegex.exec(content)) !== null) {
1071
+ if (secureMatch.index === secureRegex.lastIndex) {
1072
+ secureRegex.lastIndex++;
1073
+ continue;
1074
+ }
1075
+
1076
+ const secureMatchIndex = secureMatch.index;
1077
+ const secureMethod = secureMatch[1]; // secureQuery, secureUpdate, etc.
1078
+
1079
+ // Skip if in a line comment
1080
+ const secureLineStart = content.lastIndexOf('\n', secureMatchIndex) + 1;
1081
+ const secureLineUpToMatch = content.substring(secureLineStart, secureMatchIndex);
1082
+ const isSecureInLineComment = /\/\/[^\n]*$/.test(secureLineUpToMatch);
1083
+
1084
+ if (!isSecureInLineComment) {
1085
+ const secureLineNumber = content.substring(0, secureMatchIndex).split('\n').length;
1086
+ const isUserDataOrAudit = type === 'user_data' || type === 'audit';
1087
+ const isConfigTable = type === 'config';
1088
+ const isRoleTable = type === 'role';
1089
+
1090
+ // Determine severity - role tables should always be errors (should use pace-core APIs)
1091
+ // Config tables in admin context with secure methods are acceptable
1092
+ // User data/audit tables with secure methods are acceptable
1093
+ let severity = 'error';
1094
+ let reason = '';
1095
+
1096
+ if (isRoleTable) {
1097
+ // Role tables should use pace-core APIs, not secureQuery
1098
+ severity = 'error';
1099
+ reason = `Direct query to RBAC role table '${tableName}' using ${secureMethod} detected. Role tables should use pace-core RBAC APIs (useRBAC, usePermissions) or RPC functions (rbac_role_grant, rbac_role_revoke, rbac_roles_list) instead of direct queries.`;
1100
+ } else if (isUserDataOrAudit) {
1101
+ // User data/audit tables with secure methods are acceptable
1102
+ continue; // Skip - this is correct usage
1103
+ } else if (isConfigTable) {
1104
+ // Config tables using secure methods - acceptable for admin operations
1105
+ // If using secureQuery/secureUpdate/etc., it's already using secure methods
1106
+ if (isAdminContext || usesSecureDataAccess) {
1107
+ // Config tables in admin context or using secure methods - acceptable
1108
+ continue; // Skip - this is correct usage for admin operations
1109
+ } else {
1110
+ severity = 'error';
1111
+ reason = `Direct query to RBAC configuration table '${tableName}' using ${secureMethod} detected. These are system configuration tables. For admin operations, use useSecureSupabase. For application use, use pace-core RBAC APIs.`;
1112
+ }
1113
+ } else {
1114
+ severity = 'error';
1115
+ reason = `Direct query to RBAC table '${tableName}' using ${secureMethod} detected. Use pace-core RBAC APIs instead.`;
1116
+ }
1117
+
1118
+ violations.customAuthCode.push({
1119
+ name: `Direct RBAC table query via ${secureMethod} (${tableName})`,
1120
+ type: 'rbac query',
1121
+ file: relativePath,
1122
+ line: secureLineNumber,
1123
+ reason: reason,
1124
+ replacement: recommendation,
1125
+ severity: severity
1126
+ });
1127
+ }
1128
+ }
1129
+ });
1130
+
1131
+ // Also check generic pattern as fallback (for patterns that might not match the specific table patterns)
1132
+ rbacTablePatterns.forEach(pattern => {
1133
+ let match;
1134
+ const regex = new RegExp(pattern.source, pattern.flags);
1135
+ regex.lastIndex = 0;
1136
+
1137
+ while ((match = regex.exec(content)) !== null) {
1138
+ if (match.index === regex.lastIndex) {
1139
+ regex.lastIndex++;
1140
+ continue;
1141
+ }
1142
+
1143
+ const matchIndex = match.index;
1144
+ const matchText = match[0];
1145
+
1146
+ // Extract table name
1147
+ const afterMatch = content.substring(matchIndex, Math.min(content.length, matchIndex + 100));
1148
+ const tableMatch = afterMatch.match(/['"]rbac_([^'"]+)['"]/);
1149
+ const tableName = tableMatch ? `rbac_${tableMatch[1]}` : 'rbac_*';
1150
+
1151
+ // Find the table config to check if it's user_data or audit
1152
+ const tableConfig = rbacTables.find(t => t.name === tableName);
1153
+ const isUserDataOrAudit = tableConfig && (tableConfig.type === 'user_data' || tableConfig.type === 'audit');
1154
+ const isConfigTable = tableConfig && tableConfig.type === 'config';
1155
+
1156
+ // Extract variable name if pattern matches variable.from() (handle newlines)
1157
+ let variableName = null;
1158
+ const beforeMatch = content.substring(Math.max(0, matchIndex - 200), matchIndex);
1159
+ // Find the last word/identifier before .from (same logic as main pattern)
1160
+ const parts = beforeMatch.split('.from');
1161
+ if (parts.length > 0) {
1162
+ const beforeFrom = parts[parts.length - 1].trim();
1163
+ const words = beforeFrom.match(/\b\w+\b/g);
1164
+ if (words && words.length > 0) {
1165
+ variableName = words[words.length - 1];
1166
+ }
1167
+ }
1168
+
1169
+ // Check if using secure variable (check both set and direct pattern match)
1170
+ // Escape special regex characters in variable name and use multiline flag to handle newlines
1171
+ const escapedVarName = variableName ? variableName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') : '';
1172
+ const isUsingSecureVariable = (variableName && secureVariables.has(variableName)) ||
1173
+ (variableName && new RegExp(`(const|let)\\s+${escapedVarName}\\s*=\\s*useSecureSupabase\\s*\\(`, 'm').test(content)) ||
1174
+ (variableName && new RegExp(`(const|let)\\s+${escapedVarName}\\s*=\\s*useSecureDataAccess\\s*\\(`, 'm').test(content));
1175
+
1176
+ // Skip if we already reported this specific table
1177
+ const alreadyReported = violations.customAuthCode.some(v =>
1178
+ v.file === relativePath &&
1179
+ v.name && v.name.includes(tableName) &&
1180
+ Math.abs(v.line - content.substring(0, matchIndex).split('\n').length) <= 2
1181
+ );
1182
+
1183
+ // Skip if in a line comment
1184
+ const lineStart = content.lastIndexOf('\n', matchIndex) + 1;
1185
+ const lineUpToMatch = content.substring(lineStart, matchIndex);
1186
+ const isInLineComment = /\/\/[^\n]*$/.test(lineUpToMatch);
1187
+
1188
+ // Skip if using secure variable for user_data/audit tables (correct usage)
1189
+ // isUsingSecureVariable is already declared above
1190
+ if (isUsingSecureVariable && isUserDataOrAudit) {
1191
+ continue; // This is correct usage, don't flag it
1192
+ }
1193
+
1194
+ // Determine severity for config tables in admin context
1195
+ let severity = 'error';
1196
+ let reason = '';
1197
+
1198
+ if (isConfigTable && (isAdminContext || usesSecureDataAccess) && (isUsingSecureVariable || hasSecureMethods)) {
1199
+ // Admin operations with secure methods - acceptable, skip
1200
+ continue;
1201
+ } else if (isConfigTable && isAdminContext && !isUsingSecureVariable && !hasSecureMethods) {
1202
+ severity = 'warning';
1203
+ reason = `Admin operation on configuration table '${tableName}' detected. Ensure you're using useSecureSupabase or useSecureDataAccess for security.`;
1204
+ } else if (isConfigTable && !isAdminContext) {
1205
+ severity = 'error';
1206
+ 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.`;
1207
+ } else if (isUserDataOrAudit) {
1208
+ if (isUsingSecureVariable || hasSecureMethods) {
1209
+ // User data/audit tables with secure methods - acceptable, skip
1210
+ continue;
1211
+ }
1212
+ severity = 'error';
1213
+ 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.`;
1214
+ } else {
1215
+ severity = 'error';
1216
+ reason = `Direct query to RBAC table '${tableName}' detected. Use pace-core RBAC hooks, RPC functions, or useSecureSupabase instead.`;
1217
+ }
1218
+
1219
+ if (!alreadyReported && !isInLineComment) {
1220
+ violations.customAuthCode.push({
1221
+ name: `Direct RBAC table query (${tableName})`,
1222
+ type: 'rbac query',
1223
+ file: relativePath,
1224
+ line: content.substring(0, matchIndex).split('\n').length,
1225
+ reason: reason,
1226
+ replacement: tableConfig ? tableConfig.recommendation : 'pace-core RBAC APIs (useRBAC, usePermissions, RPC functions)',
1227
+ severity: severity
1228
+ });
1229
+ }
1230
+ }
1231
+ });
1232
+
1233
+ // Check for direct permission table CRUD operations
1234
+ // This includes both .from().insert/update/delete patterns AND secureUpdate/secureInsert/secureDelete calls
1235
+ const permissionCrudPatterns = [
1236
+ // .from() patterns
1237
+ {
1238
+ pattern: /\.from\s*\(\s*['"]rbac_page_permissions['"]\s*\)\s*\.(insert|update|delete|upsert)\s*\(/g,
1239
+ table: 'rbac_page_permissions',
1240
+ operation: 'CRUD',
1241
+ isConfig: true,
1242
+ method: 'from'
1243
+ },
1244
+ {
1245
+ pattern: /\.from\s*\(\s*['"]rbac_app_pages['"]\s*\)\s*\.(insert|update|delete|upsert)\s*\(/g,
1246
+ table: 'rbac_app_pages',
1247
+ operation: 'CRUD',
1248
+ isConfig: true,
1249
+ method: 'from'
1250
+ },
1251
+ {
1252
+ pattern: /\.from\s*\(\s*['"]rbac_apps['"]\s*\)\s*\.(insert|update|delete|upsert)\s*\(/g,
1253
+ table: 'rbac_apps',
1254
+ operation: 'CRUD',
1255
+ isConfig: true,
1256
+ method: 'from'
1257
+ },
1258
+ {
1259
+ pattern: /\.from\s*\(\s*['"]rbac_organisation_roles['"]\s*\)\s*\.(insert|update|delete|upsert)\s*\(/g,
1260
+ table: 'rbac_organisation_roles',
1261
+ operation: 'CRUD',
1262
+ isRole: true,
1263
+ roleType: 'organisation',
1264
+ method: 'from'
1265
+ },
1266
+ {
1267
+ pattern: /\.from\s*\(\s*['"]rbac_event_app_roles['"]\s*\)\s*\.(insert|update|delete|upsert)\s*\(/g,
1268
+ table: 'rbac_event_app_roles',
1269
+ operation: 'CRUD',
1270
+ isRole: true,
1271
+ roleType: 'event_app',
1272
+ method: 'from'
1273
+ },
1274
+ {
1275
+ pattern: /\.from\s*\(\s*['"]rbac_global_roles['"]\s*\)\s*\.(insert|update|delete|upsert)\s*\(/g,
1276
+ table: 'rbac_global_roles',
1277
+ operation: 'CRUD',
1278
+ isRole: true,
1279
+ roleType: 'global',
1280
+ method: 'from'
1281
+ },
1282
+ {
1283
+ pattern: /\.from\s*\(\s*['"]rbac_user_units['"]\s*\)\s*\.(insert|update|delete|upsert)\s*\(/g,
1284
+ table: 'rbac_user_units',
1285
+ operation: 'CRUD',
1286
+ isUserData: true,
1287
+ method: 'from'
1288
+ },
1289
+ // secureUpdate/secureInsert/secureDelete patterns
1290
+ {
1291
+ pattern: /secureUpdate\s*\(\s*['"]rbac_page_permissions['"]/g,
1292
+ table: 'rbac_page_permissions',
1293
+ operation: 'update',
1294
+ isConfig: true,
1295
+ method: 'secureUpdate'
1296
+ },
1297
+ {
1298
+ pattern: /secureInsert\s*\(\s*['"]rbac_page_permissions['"]/g,
1299
+ table: 'rbac_page_permissions',
1300
+ operation: 'insert',
1301
+ isConfig: true,
1302
+ method: 'secureInsert'
1303
+ },
1304
+ {
1305
+ pattern: /secureUpdate\s*\(\s*['"]rbac_app_pages['"]/g,
1306
+ table: 'rbac_app_pages',
1307
+ operation: 'update',
1308
+ isConfig: true,
1309
+ method: 'secureUpdate'
1310
+ },
1311
+ {
1312
+ pattern: /secureInsert\s*\(\s*['"]rbac_app_pages['"]/g,
1313
+ table: 'rbac_app_pages',
1314
+ operation: 'insert',
1315
+ isConfig: true,
1316
+ method: 'secureInsert'
1317
+ },
1318
+ {
1319
+ pattern: /secureUpdate\s*\(\s*['"]rbac_apps['"]/g,
1320
+ table: 'rbac_apps',
1321
+ operation: 'update',
1322
+ isConfig: true,
1323
+ method: 'secureUpdate'
1324
+ },
1325
+ {
1326
+ pattern: /secureInsert\s*\(\s*['"]rbac_apps['"]/g,
1327
+ table: 'rbac_apps',
1328
+ operation: 'insert',
1329
+ isConfig: true,
1330
+ method: 'secureInsert'
1331
+ },
1332
+ {
1333
+ pattern: /secureUpdate\s*\(\s*['"]rbac_organisation_roles['"]/g,
1334
+ table: 'rbac_organisation_roles',
1335
+ operation: 'update',
1336
+ isRole: true,
1337
+ roleType: 'organisation',
1338
+ method: 'secureUpdate'
1339
+ },
1340
+ {
1341
+ pattern: /secureInsert\s*\(\s*['"]rbac_organisation_roles['"]/g,
1342
+ table: 'rbac_organisation_roles',
1343
+ operation: 'insert',
1344
+ isRole: true,
1345
+ roleType: 'organisation',
1346
+ method: 'secureInsert'
1347
+ },
1348
+ {
1349
+ pattern: /secureUpdate\s*\(\s*['"]rbac_event_app_roles['"]/g,
1350
+ table: 'rbac_event_app_roles',
1351
+ operation: 'update',
1352
+ isRole: true,
1353
+ roleType: 'event_app',
1354
+ method: 'secureUpdate'
1355
+ },
1356
+ {
1357
+ pattern: /secureInsert\s*\(\s*['"]rbac_event_app_roles['"]/g,
1358
+ table: 'rbac_event_app_roles',
1359
+ operation: 'insert',
1360
+ isRole: true,
1361
+ roleType: 'event_app',
1362
+ method: 'secureInsert'
1363
+ },
1364
+ {
1365
+ pattern: /secureUpdate\s*\(\s*['"]rbac_global_roles['"]/g,
1366
+ table: 'rbac_global_roles',
1367
+ operation: 'update',
1368
+ isRole: true,
1369
+ roleType: 'global',
1370
+ method: 'secureUpdate'
1371
+ },
1372
+ {
1373
+ pattern: /secureInsert\s*\(\s*['"]rbac_global_roles['"]/g,
1374
+ table: 'rbac_global_roles',
1375
+ operation: 'insert',
1376
+ isRole: true,
1377
+ roleType: 'global',
1378
+ method: 'secureInsert'
1379
+ }
1380
+ ];
1381
+
1382
+ permissionCrudPatterns.forEach(({ pattern, table, operation, isConfig, isRole, roleType, isUserData, method }) => {
1383
+ let match;
1384
+ const regex = new RegExp(pattern.source, pattern.flags);
1385
+ regex.lastIndex = 0;
1386
+
1387
+ while ((match = regex.exec(content)) !== null) {
1388
+ if (match.index === regex.lastIndex) {
1389
+ regex.lastIndex++;
1390
+ continue;
1391
+ }
1392
+
1393
+ const matchIndex = match.index;
1394
+ const matchText = match[0];
1395
+ // For .from() patterns, match[1] is the CRUD method; for secure* patterns, operation is already set
1396
+ const crudMethod = method === 'from' ? match[1] : operation;
1397
+
1398
+ // Skip if in a line comment
1399
+ const lineStart = content.lastIndexOf('\n', matchIndex) + 1;
1400
+ const lineUpToMatch = content.substring(lineStart, matchIndex);
1401
+ const isInLineComment = /\/\/[^\n]*$/.test(lineUpToMatch);
1402
+
1403
+ if (!isInLineComment) {
1404
+ let reason = '';
1405
+ let replacement = '';
1406
+ let severity = 'error';
1407
+ let example = '';
1408
+
1409
+ if (isRole) {
1410
+ // Role mutations should use RPC functions
1411
+ const isGrant = crudMethod === 'insert' || crudMethod === 'upsert';
1412
+ const isRevoke = crudMethod === 'delete' || crudMethod === 'update';
1413
+
1414
+ if (isGrant) {
1415
+ reason = `Direct ${crudMethod} operation on '${table}' detected. Use rbac_role_grant RPC function instead.`;
1416
+ replacement = `Use rbac_role_grant RPC function with p_role_type='${roleType}'`;
1417
+ example = `const { data, error } = await supabase.rpc('rbac_role_grant', {\n p_user_id: userId,\n p_role_type: '${roleType}',\n p_role_name: 'role_name',\n p_context_id: contextId, // org_id for organisation, 'event_id:app_id' for event_app\n p_granted_by: currentUserId\n});`;
1418
+ } else if (isRevoke) {
1419
+ reason = `Direct ${crudMethod} operation on '${table}' detected. Use rbac_role_revoke RPC function instead.`;
1420
+ replacement = `Use rbac_role_revoke RPC function with p_role_type='${roleType}'`;
1421
+ example = `const { data, error } = await supabase.rpc('rbac_role_revoke', {\n p_user_id: userId,\n p_role_type: '${roleType}',\n p_role_name: 'role_name',\n p_context_id: contextId, // org_id for organisation, 'event_id:app_id' for event_app\n p_revoked_by: currentUserId\n});`;
1422
+ } else {
1423
+ reason = `Direct ${crudMethod} operation on '${table}' detected. Use rbac_role_grant or rbac_role_revoke RPC functions instead.`;
1424
+ replacement = `Use rbac_role_grant/rbac_role_revoke RPC functions with p_role_type='${roleType}'`;
1425
+ }
1426
+ } else if (isConfig) {
1427
+ // Config table mutations - check if using secure methods
1428
+ if (method && method.startsWith('secure')) {
1429
+ // Using secureInsert/secureUpdate/secureDelete - this is correct, don't flag
1430
+ continue;
1431
+ } else if (isAdminContext && usesSecureDataAccess) {
1432
+ // Admin context and using useSecureDataAccess - check if using secure methods
1433
+ // If the operation uses secureQuery/secureUpdate/etc, it's already handled above
1434
+ // This case is for direct .from() calls in admin context
1435
+ severity = 'warning';
1436
+ reason = `Admin operation (${crudMethod}) on configuration table '${table}' detected. Ensure you're using useSecureSupabase or useSecureDataAccess for security.`;
1437
+ replacement = 'Use useSecureSupabase or useSecureDataAccess from pace-core for admin operations on configuration tables';
1438
+ } else if (isAdminContext) {
1439
+ severity = 'warning';
1440
+ reason = `Admin operation (${crudMethod}) on configuration table '${table}' detected. Ensure you're using useSecureSupabase or useSecureDataAccess for security.`;
1441
+ replacement = 'Use useSecureSupabase or useSecureDataAccess from pace-core for admin operations on configuration tables';
1442
+ } else {
1443
+ reason = `Direct ${crudMethod} operation on configuration table '${table}' detected. These are system configuration tables. For admin operations, use useSecureSupabase.`;
1444
+ replacement = 'Use useSecureSupabase or useSecureDataAccess from pace-core for admin operations';
1445
+ }
1446
+ } else if (isUserData) {
1447
+ reason = `Direct ${crudMethod} operation on '${table}' detected. Use useSecureSupabase or useSecureDataAccess to ensure organisation context is enforced.`;
1448
+ replacement = 'Use useSecureSupabase or useSecureDataAccess from pace-core';
1449
+ } else {
1450
+ reason = `Direct ${crudMethod} operation on '${table}' detected. Use pace-core permission management APIs or documented RPC functions instead.`;
1451
+ replacement = 'pace-core permission management APIs or RPC functions';
1452
+ }
1453
+
1454
+ violations.customAuthCode.push({
1455
+ name: `Direct ${operation} (${table})`,
1456
+ type: 'permission management',
1457
+ file: relativePath,
1458
+ line: content.substring(0, matchIndex).split('\n').length,
1459
+ reason: reason,
1460
+ replacement: replacement,
1461
+ severity: severity,
1462
+ example: example
1463
+ });
1464
+ }
1465
+ }
1466
+ });
1467
+
1468
+ // Check for custom role management hooks/components
1469
+ // Analyze hook operations to distinguish data fetching from permission management
1470
+ const customRoleManagementPatterns = [
1471
+ { pattern: /export\s+(default\s+)?(function|const)\s+useRoleMutations\s*[=\(]/g, name: 'useRoleMutations', type: 'hook', replacement: 'pace-core RBAC APIs (rbac_role_grant, rbac_role_revoke RPC functions or useRoleManagement hook)' },
1472
+ { pattern: /export\s+(default\s+)?(function|const)\s+useUserRoles\s*[=\(]/g, name: 'useUserRoles', type: 'hook', replacement: 'pace-core RBAC hooks (useRBAC, usePermissions) or rbac_roles_list RPC function' },
1473
+ { pattern: /export\s+(default\s+)?(function|const)\s+useUserOrganisationRoles\s*[=\(]/g, name: 'useUserOrganisationRoles', type: 'hook', replacement: 'useOrganisations hook from pace-core or data_user_organisation_roles_get RPC function' },
1474
+ { pattern: /export\s+(default\s+)?(function|const)\s+useRoleSupportingData\s*[=\(]/g, name: 'useRoleSupportingData', type: 'hook', replacement: 'pace-core RBAC APIs' },
1475
+ { pattern: /export\s+(default\s+)?(function|const)\s+useAppAccess\s*[=\(]/g, name: 'useAppAccess', type: 'hook', replacement: 'pace-core RBAC APIs' },
1476
+ { pattern: /export\s+(default\s+)?(function|const)\s+useEventForm\s*[=\(]/g, name: 'useEventForm', type: 'hook', replacement: 'pace-core RBAC APIs' },
1477
+ { pattern: /export\s+(default\s+)?(function|const)\s+useUnifiedStats\s*[=\(]/g, name: 'useUnifiedStats', type: 'hook', replacement: 'pace-core RBAC APIs' }
1478
+ ];
1479
+
1480
+ customRoleManagementPatterns.forEach(({ pattern, name, type, replacement }) => {
1481
+ if (pattern.test(content)) {
1482
+ // Analyze hook operations to determine if it's data fetching or permission management
1483
+ const hookStartMatch = content.match(pattern);
1484
+ if (!hookStartMatch) {
1485
+ return; // Exit early if no match
1486
+ }
1487
+
1488
+ const hookStartIndex = content.indexOf(hookStartMatch[0]);
1489
+ // Find the end of the hook (next export or end of file, or closing brace at same level)
1490
+ const afterHookStart = content.substring(hookStartIndex);
1491
+ const hookContent = afterHookStart.split(/\n\s*export\s+/)[0]; // Get content until next export
1492
+
1493
+ // Count operations
1494
+ const readOperations = (
1495
+ (hookContent.match(/secureQuery\s*\(/g) || []).length +
1496
+ (hookContent.match(/\.select\s*\(/g) || []).length +
1497
+ (hookContent.match(/\.rpc\s*\(\s*['"](get_|data_|rbac_roles_list|rbac_permissions_get|data_user_)/g) || []).length
1498
+ );
1499
+
1500
+ const writeOperations = (
1501
+ (hookContent.match(/secureUpdate\s*\(/g) || []).length +
1502
+ (hookContent.match(/secureInsert\s*\(/g) || []).length +
1503
+ (hookContent.match(/secureDelete\s*\(/g) || []).length +
1504
+ (hookContent.match(/\.(insert|update|delete|upsert)\s*\(/g) || []).length +
1505
+ (hookContent.match(/\.rpc\s*\(\s*['"](rbac_role_grant|rbac_role_revoke|app_)/g) || []).length
1506
+ );
1507
+
1508
+ const totalOperations = readOperations + writeOperations;
1509
+ const writeRatio = totalOperations > 0 ? writeOperations / totalOperations : 0;
1510
+
1511
+ // Check if hook primarily uses RPC functions for reads
1512
+ const usesReadOnlyRPCs = /\.rpc\s*\(\s*['"](get_|data_|rbac_roles_list|rbac_permissions_get|data_user_)/.test(hookContent);
1513
+ const onlyReadOperations = writeOperations === 0 && readOperations > 0;
1514
+
1515
+ // Check if hook name suggests data fetching (supporting data, stats, etc.)
1516
+ const isDataFetchingHook = /SupportingData|Stats|Form/.test(name);
1517
+
1518
+ // Calculate ratio of role/permission-related code to total code
1519
+ const rolePermissionPatterns = [
1520
+ /rbac_(organisation|event_app|global)_roles/g,
1521
+ /rbac_page_permissions/g,
1522
+ /rbac_role_grant|rbac_role_revoke/g,
1523
+ /grant.*role|revoke.*role/gi
1524
+ ];
1525
+ const rolePermissionMatches = rolePermissionPatterns.reduce((count, pattern) => {
1526
+ const matches = hookContent.match(pattern);
1527
+ return count + (matches ? matches.length : 0);
1528
+ }, 0);
1529
+
1530
+ // Count total significant operations (not just RBAC, but all operations)
1531
+ const totalSignificantOps = (
1532
+ (hookContent.match(/(secureQuery|secureUpdate|secureInsert|secureDelete|\.from|\.rpc|\.select|\.insert|\.update|\.delete)\s*\(/g) || []).length
1533
+ );
1534
+ const rolePermissionRatio = totalSignificantOps > 0 ? rolePermissionMatches / totalSignificantOps : 0;
1535
+
1536
+ // Check for patterns indicating primary purpose
1537
+ const isEventManagement = /event.*(create|update|delete|form|manage)/gi.test(hookContent) && name.includes('Event');
1538
+ const isFormManagement = /useZodForm|register|handleSubmit|setValue|watch|reset/.test(hookContent);
1539
+ const isStatsAggregation = /count|sum|aggregate|stats|statistics/gi.test(hookContent) && name.includes('Stats');
1540
+ const isSupportingData = name.includes('SupportingData') || /dropdown|select|options|reference/gi.test(hookContent);
1541
+
1542
+ // Only flag if:
1543
+ // 1. It's useRoleMutations (always permission management)
1544
+ // 2. OR write operations > 20% of total operations AND operates on role/permission tables
1545
+ // 3. OR role/permission operations > 20% of total operations
1546
+ // Don't flag if:
1547
+ // - It's primarily data fetching (>80% reads, no mutations on role tables)
1548
+ // - It's event/form/stats management with <20% role/permission code
1549
+ // - It only uses read-only RPCs for data fetching
1550
+ if (name === 'useRoleMutations') {
1551
+ // Always flag useRoleMutations - it's explicitly for role mutations
1552
+ violations.customAuthCode.push({
1553
+ name,
1554
+ type,
1555
+ file: relativePath,
1556
+ line: getLineNumber(content, hookStartMatch[0]),
1557
+ 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.`,
1558
+ replacement,
1559
+ severity: 'error'
1560
+ });
1561
+ } else if (writeRatio > 0.2 && rolePermissionRatio > 0.1) {
1562
+ // Has significant write operations on role/permission tables
1563
+ violations.customAuthCode.push({
1564
+ name,
1565
+ type,
1566
+ file: relativePath,
1567
+ line: getLineNumber(content, hookStartMatch[0]),
1568
+ 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.`,
1569
+ replacement,
1570
+ severity: 'error'
1571
+ });
1572
+ } else if (rolePermissionRatio > 0.2 && !isEventManagement && !isFormManagement && !isStatsAggregation) {
1573
+ // High ratio of role/permission code and not primarily event/form/stats management
1574
+ violations.customAuthCode.push({
1575
+ name,
1576
+ type,
1577
+ file: relativePath,
1578
+ line: getLineNumber(content, hookStartMatch[0]),
1579
+ 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.`,
1580
+ replacement,
1581
+ severity: 'error'
1582
+ });
1583
+ } else if (onlyReadOperations && (usesReadOnlyRPCs || isDataFetchingHook || isSupportingData)) {
1584
+ // Data fetching hook using pace-core RPCs or secure queries - don't flag
1585
+ // These are legitimate data fetching hooks
1586
+ return; // Exit early to prevent flagging
1587
+ } else if (name === 'useUserOrganisationRoles') {
1588
+ // useUserOrganisationRoles only uses get_user_organisations RPC - don't flag
1589
+ // This is a pure data fetching hook that only uses RPC functions
1590
+ // Check if it only uses RPC functions and no direct table queries
1591
+ const hasDirectTableQueries = /\.from\s*\(\s*['"]rbac_/.test(hookContent);
1592
+ if (!hasDirectTableQueries && usesReadOnlyRPCs) {
1593
+ // Pure RPC-based data fetching - don't flag
1594
+ return; // Exit early to prevent flagging
1595
+ } else if (onlyReadOperations && usesReadOnlyRPCs) {
1596
+ // Read-only data fetching hook - don't flag
1597
+ return; // Exit early to prevent flagging
1598
+ }
1599
+ // If it has table queries, continue to check below
1600
+ } else if (name === 'useUserRoles') {
1601
+ // useUserRoles uses RPC functions and secure methods for data fetching
1602
+ // Check if it's primarily read-only and uses secure methods
1603
+ const usesSecureForApps = /secureSupabase|useSecureSupabase|secureQuery/.test(hookContent);
1604
+ if (usesReadOnlyRPCs && writeOperations === 0 && (usesSecureForApps || onlyReadOperations)) {
1605
+ // Using secure methods or read-only RPC-based data fetching - don't flag
1606
+ return; // Exit early to prevent flagging
1607
+ }
1608
+ // If it has write operations, continue to check below
1609
+ } else if (isEventManagement || isFormManagement || isStatsAggregation) {
1610
+ // Primary purpose is event/form/stats management - don't flag
1611
+ // Role cleanup during deletion is a side effect, not primary purpose
1612
+ }
1613
+ }
1614
+ });
1615
+
1616
+ // Check for custom permission management components
1617
+ // Distinguish between configuration management and permission management
1618
+ const customPermissionComponents = [
1619
+ { name: 'UnifiedPermissionsManager', isPermissionManagement: true },
1620
+ { name: 'PermissionsDataTable', isPermissionManagement: true },
1621
+ { name: 'ApplicationPermissionsTable', isPermissionManagement: true },
1622
+ { name: 'ApplicationPermissionsManager', isPermissionManagement: true },
1623
+ { name: 'PermissionsManager', isPermissionManagement: true },
1624
+ // Configuration management components (manage rbac_apps, rbac_app_pages)
1625
+ { name: 'ApplicationsDataTable', isPermissionManagement: false, managesConfig: true },
1626
+ { name: 'PagesDataTable', isPermissionManagement: false, managesConfig: true }
1627
+ ];
1628
+
1629
+ customPermissionComponents.forEach(({ name, isPermissionManagement, managesConfig }) => {
1630
+ const componentPattern = new RegExp(`export\\s+(default\\s+)?(function|const)\\s+${name}\\s*[=\\(]`, 'g');
1631
+ if (componentPattern.test(content)) {
1632
+ // Check which tables this component operates on
1633
+ const operatesOnPagePermissions = /rbac_page_permissions/.test(content);
1634
+ const operatesOnRoleTables = /rbac_(organisation|event_app|global)_roles/.test(content);
1635
+ const operatesOnConfigTables = /rbac_apps|rbac_app_pages/.test(content);
1636
+ const usesSecureMethods = /useSecureDataAccess|useSecureSupabase|secureQuery|secureUpdate|secureInsert|secureDelete/.test(content);
1637
+
1638
+ // Only flag as permission management if:
1639
+ // 1. It's explicitly a permission management component AND operates on permission/role tables
1640
+ // 2. OR it operates on permission/role tables (regardless of name)
1641
+ // Don't flag configuration management components that only manage config tables
1642
+ if (isPermissionManagement && (operatesOnPagePermissions || operatesOnRoleTables)) {
1643
+ violations.customAuthCode.push({
1644
+ name: `${name} component`,
1645
+ type: 'permission management component',
1646
+ file: relativePath,
1647
+ line: getLineNumber(content, content.match(componentPattern)[0]),
1648
+ reason: `Custom permission management component '${name}' detected. Even though it may use some pace-core utilities, it implements custom permission management logic that should use pace-core permission management APIs instead.`,
1649
+ replacement: 'pace-core permission management APIs or PagePermissionGuard',
1650
+ severity: 'error'
1651
+ });
1652
+ } else if (!isPermissionManagement && managesConfig && operatesOnConfigTables) {
1653
+ // Configuration management component - check if it only does cleanup on permission tables
1654
+ // If it uses secure methods and primarily manages config tables, don't flag
1655
+ // Permission table operations for cleanup (cascade deletes) are acceptable
1656
+ if (operatesOnPagePermissions && usesSecureMethods) {
1657
+ // Config management component that uses secure methods and does permission cleanup
1658
+ // This is acceptable - don't flag
1659
+ } else if (!usesSecureMethods) {
1660
+ violations.customAuthCode.push({
1661
+ name: `${name} component`,
1662
+ type: 'configuration management component',
1663
+ file: relativePath,
1664
+ line: getLineNumber(content, content.match(componentPattern)[0]),
1665
+ reason: `Configuration management component '${name}' detected. Ensure you're using useSecureDataAccess or useSecureSupabase for secure operations on configuration tables.`,
1666
+ replacement: 'Use useSecureDataAccess or useSecureSupabase from pace-core for admin operations on configuration tables',
1667
+ severity: 'warning'
1668
+ });
1669
+ }
1670
+ // If using secure methods, don't flag (acceptable for admin operations)
1671
+ } else if (operatesOnPagePermissions || operatesOnRoleTables) {
1672
+ // Component operates on permission/role tables - flag it UNLESS it's a config management component using secure methods
1673
+ if (!isPermissionManagement && managesConfig && usesSecureMethods) {
1674
+ // Config management component using secure methods - acceptable, don't flag
1675
+ } else {
1676
+ violations.customAuthCode.push({
1677
+ name: `${name} component`,
1678
+ type: 'permission management component',
1679
+ file: relativePath,
1680
+ line: getLineNumber(content, content.match(componentPattern)[0]),
1681
+ reason: `Component '${name}' detected that operates on permission or role tables. Use pace-core permission management APIs instead.`,
1682
+ replacement: 'pace-core permission management APIs or PagePermissionGuard',
1683
+ severity: 'error'
1684
+ });
1685
+ }
1686
+ }
1687
+ }
1688
+ });
1689
+
1690
+ // Check provider setup (only main.tsx/main.ts, not App.tsx since it doesn't define providers)
1691
+ if (relativePath.match(/^(src\/)?main\.(tsx?|jsx?)$/)) {
1692
+ const providerIssues = scanProviderSetup(filePath, content, relativePath);
1693
+ violations.providerSetupIssues.push(...providerIssues);
1694
+ }
1695
+
1696
+ // Check Vite configuration
1697
+ if (relativePath.match(/vite\.config\.(ts|js|tsx|jsx)$/)) {
1698
+ const viteIssues = scanViteConfig(filePath, content, relativePath);
1699
+ violations.viteConfigIssues.push(...viteIssues);
1700
+ }
1701
+
1702
+ // Check Router setup (main.tsx, main.ts, App.tsx, App.ts)
1703
+ if (relativePath.match(/^(src\/)?(main|App)\.(tsx?|jsx?)$/)) {
1704
+ const routerIssues = scanRouterSetup(filePath, content, relativePath);
1705
+ violations.routerSetupIssues.push(...routerIssues);
1706
+ }
1707
+
1708
+ // Check for custom auth type files (should use pace-core types)
1709
+ if (relativePath.match(/types\/auth\.(ts|tsx)$/i) || relativePath.match(/src\/types\/auth\.(ts|tsx)$/i)) {
1710
+ // Check if file defines auth-related types without importing from pace-core
1711
+ const hasAuthTypeDefinitions = /(interface|type)\s+(User|Session|AuthError|SignIn|SignUp|AuthState|AuthContext)/.test(content);
1712
+ const hasPaceCoreTypeImport = /from\s+['"]@jmruthers\/pace-core\/types/.test(content) ||
1713
+ /from\s+['"]@jmruthers\/pace-core['"]/.test(content);
1714
+
1715
+ if (hasAuthTypeDefinitions && !hasPaceCoreTypeImport) {
1716
+ violations.customAuthCode.push({
1717
+ name: 'auth types',
1718
+ type: 'types',
1719
+ file: relativePath,
1720
+ line: 1,
1721
+ reason: 'Custom auth types detected. Use types from @jmruthers/pace-core/types/auth instead.',
1722
+ replacement: '@jmruthers/pace-core/types/auth'
1723
+ });
1724
+ }
1725
+ }
1726
+
1727
+ // Check for custom auth/RBAC context providers
1728
+ const customProviderPatterns = [
1729
+ { pattern: /export\s+(default\s+)?(function|const)\s+AuthProvider\s*[=\(]/g, name: 'AuthProvider', replacement: 'UnifiedAuthProvider' },
1730
+ { pattern: /export\s+(default\s+)?(function|const)\s+AuthContext\s*[=\(]/g, name: 'AuthContext', replacement: 'useUnifiedAuth' },
1731
+ { pattern: /export\s+(default\s+)?(function|const)\s+PermissionProvider\s*[=\(]/g, name: 'PermissionProvider', replacement: 'pace-core RBAC' },
1732
+ { pattern: /export\s+(default\s+)?(function|const)\s+RBACProvider\s*[=\(]/g, name: 'RBACProvider', replacement: 'pace-core RBAC' },
1733
+ { pattern: /export\s+(default\s+)?(function|const)\s+RoleProvider\s*[=\(]/g, name: 'RoleProvider', replacement: 'pace-core RBAC' },
1734
+ { pattern: /createContext\s*<\s*(Auth|Permission|RBAC|Role)/gi, name: 'auth context', replacement: 'pace-core providers' }
1735
+ ];
1736
+
1737
+ customProviderPatterns.forEach(({ pattern, name, replacement }) => {
1738
+ if (pattern.test(content) && !hasPaceCoreImport && !hasUnifiedAuthImport) {
1739
+ violations.customAuthCode.push({
1740
+ name,
1741
+ type: 'provider',
1742
+ file: relativePath,
1743
+ line: getLineNumber(content, content.match(pattern)[0]),
1744
+ reason: `Custom auth/RBAC provider '${name}' detected. Use ${replacement} from pace-core instead.`,
1745
+ replacement
1746
+ });
1747
+ }
1748
+ });
1749
+
1750
+ // Check for unused PermissionService interfaces that duplicate pace-core functionality
1751
+ // Flag it even if file imports from pace-core, since it's a duplicate type definition
1752
+ const permissionServicePattern = /(interface|type)\s+PermissionService\s*[={<]/gi;
1753
+ if (permissionServicePattern.test(content)) {
1754
+ // Check if it's actually used in the file (beyond just the definition)
1755
+ const permissionServiceUsage = /PermissionService[^:]/g;
1756
+ const allMatches = content.match(/PermissionService/g) || [];
1757
+ // If defined but only appears in the definition (and maybe one type annotation), it's likely unused
1758
+ // Count: 1 for definition, 1-2 for potential type annotations = 2-3 total
1759
+ if (allMatches.length <= 3) {
1760
+ violations.customAuthCode.push({
1761
+ name: 'PermissionService interface',
1762
+ type: 'unused type',
1763
+ file: relativePath,
1764
+ line: getLineNumber(content, content.match(permissionServicePattern)[0]),
1765
+ reason: 'PermissionService interface detected that duplicates pace-core permission checking APIs. This appears unused and should be removed.',
1766
+ replacement: 'Remove if unused, or use pace-core RBAC APIs (useRBAC, usePermissions) instead',
1767
+ severity: 'error'
1768
+ });
1769
+ }
1770
+ }
1771
+
1772
+ // Check for unnecessary wrappers around pace-core components and local components
1773
+ // Only check .tsx/.jsx files (component files)
1774
+ if (filePath.match(/\.(tsx|jsx)$/)) {
1775
+ const wrapperIssues = scanUnnecessaryWrappers(content, relativePath, manifest);
1776
+ violations.unnecessaryWrappers.push(...wrapperIssues);
1777
+ }
1778
+
1779
+ return violations;
1780
+ }
1781
+
1782
+ // Scan for unnecessary wrappers around pace-core components and local components
1783
+ function scanUnnecessaryWrappers(content, relativePath, manifest) {
1784
+ const issues = [];
1785
+
1786
+ // Check if file imports from pace-core
1787
+ const paceCoreImportPattern = /import\s+{([^}]+)}\s+from\s+['"]@jmruthers\/pace-core['"]/;
1788
+ const paceCoreImportMatch = content.match(paceCoreImportPattern);
1789
+
1790
+ // Extract imported pace-core component names
1791
+ let importedPaceCoreComponents = [];
1792
+ if (paceCoreImportMatch) {
1793
+ importedPaceCoreComponents = (paceCoreImportMatch[1] || '')
1794
+ .split(',')
1795
+ .map(name => name.trim().replace(/\s+as\s+\w+/, '')) // Remove aliases
1796
+ .filter(name => manifest.components.includes(name));
1797
+ }
1798
+
1799
+ // Also find all imported components (local and pace-core) to check for wrappers
1800
+ const allImportPattern = /import\s+(?:(?:{([^}]+)})|(\w+))\s+from\s+['"]([^'"]+)['"]/g;
1801
+ const allImports = [];
1802
+ let importMatch;
1803
+ while ((importMatch = allImportPattern.exec(content)) !== null) {
1804
+ const namedImports = importMatch[1]; // { Component1, Component2 }
1805
+ const defaultImport = importMatch[2]; // Component
1806
+ const modulePath = importMatch[3];
1807
+
1808
+ if (namedImports) {
1809
+ namedImports.split(',').forEach(name => {
1810
+ const cleanName = name.trim().replace(/\s+as\s+(\w+)/, ''); // Remove aliases but keep original
1811
+ allImports.push({ name: cleanName, module: modulePath });
1812
+ });
1813
+ }
1814
+ if (defaultImport) {
1815
+ allImports.push({ name: defaultImport, module: modulePath });
1816
+ }
1817
+ }
1818
+
1819
+ // Find exported component definitions
1820
+ // Match: export (default)? (function|const) ComponentName = ...
1821
+ const componentPattern = /export\s+(default\s+)?(function|const)\s+(\w+)\s*[=\(]/g;
1822
+ const componentMatches = [...content.matchAll(componentPattern)];
1823
+
1824
+ componentMatches.forEach(match => {
1825
+ const componentName = match[3];
1826
+ const matchIndex = match.index;
1827
+
1828
+ // Skip if it's a test file or example file
1829
+ if (relativePath.includes('.test.') || relativePath.includes('.spec.') ||
1830
+ relativePath.includes('example') || relativePath.includes('Example')) {
1831
+ return;
1832
+ }
1833
+
1834
+ // Find the component body
1835
+ // Look for the opening brace after the function/const declaration
1836
+ let braceCount = 0;
1837
+ let bodyStart = -1;
1838
+ let bodyEnd = -1;
1839
+ let inBody = false;
1840
+
1841
+ for (let i = matchIndex + match[0].length; i < content.length; i++) {
1842
+ const char = content[i];
1843
+ if (char === '{' && !inBody) {
1844
+ bodyStart = i;
1845
+ inBody = true;
1846
+ braceCount = 1;
1847
+ } else if (char === '{') {
1848
+ braceCount++;
1849
+ } else if (char === '}') {
1850
+ braceCount--;
1851
+ if (braceCount === 0 && inBody) {
1852
+ bodyEnd = i;
1853
+ break;
1854
+ }
1855
+ }
1856
+ }
1857
+
1858
+ if (bodyStart === -1 || bodyEnd === -1) {
1859
+ return; // Couldn't find component body
1860
+ }
1861
+
1862
+ const componentBody = content.substring(bodyStart + 1, bodyEnd).trim();
1863
+
1864
+ // Check if body has significant logic (hooks, state, conditionals, etc.)
1865
+ const hasHooks = /use[A-Z]\w+/.test(componentBody);
1866
+ const hasState = /useState|useReducer|useRef/.test(componentBody);
1867
+ const hasConditionals = /if\s*\(|&&|\?|switch/.test(componentBody);
1868
+ const hasMultipleReturns = (componentBody.match(/return/g) || []).length > 1;
1869
+ const hasLoops = /for\s*\(|while\s*\(|\.map\s*\(/.test(componentBody);
1870
+
1871
+ // Find all JSX components used in the body
1872
+ // Match JSX opening tags: <ComponentName or <ComponentName>
1873
+ const jsxComponentPattern = /<([A-Z][a-zA-Z0-9]*)[\s>\/]/g;
1874
+ const jsxComponents = [];
1875
+ let jsxMatch;
1876
+ while ((jsxMatch = jsxComponentPattern.exec(componentBody)) !== null) {
1877
+ const jsxComponentName = jsxMatch[1];
1878
+ // Skip React fragments and the component itself
1879
+ if (jsxComponentName !== 'Fragment' &&
1880
+ jsxComponentName !== componentName &&
1881
+ !jsxComponents.includes(jsxComponentName)) {
1882
+ jsxComponents.push(jsxComponentName);
1883
+ }
1884
+ }
1885
+
1886
+ // Check if component is a simple wrapper around another component
1887
+ // Pattern 1: Just returns a single component (pace-core or local)
1888
+ // Pattern 2: Only does auth/permission checks and returns a component
1889
+ // Pattern 3: Minimal logic that just forwards to another component
1890
+
1891
+ // Check for auth/permission check patterns (common in page wrappers)
1892
+ const hasAuthCheck = /useUnifiedAuth|usePermissions|useCan|PagePermissionGuard|PermissionGuard|useRBAC/.test(componentBody);
1893
+ const hasEarlyReturn = /if\s*\([^)]*\)\s*return/.test(componentBody);
1894
+
1895
+ // Count conditional returns (early returns for auth checks)
1896
+ const conditionalReturns = (componentBody.match(/if\s*\([^)]*\)\s*return/g) || []).length;
1897
+
1898
+ // If there's only one JSX component used and minimal logic, it's likely a wrapper
1899
+ if (jsxComponents.length === 1) {
1900
+ const wrappedComponent = jsxComponents[0];
1901
+ const wrappedComponentCount = (componentBody.match(new RegExp(`<${wrappedComponent}`, 'gi')) || []).length;
1902
+
1903
+ // Check if it's a simple wrapper:
1904
+ // 1. Uses only one other component
1905
+ // 2. No state management
1906
+ // 3. No loops
1907
+ // 4. Component appears only once or twice (opening and closing tag)
1908
+ // 5. Either no logic at all, OR only auth/permission checks with early returns
1909
+
1910
+ // Allow auth/permission checks but still flag as wrapper if that's all it does
1911
+ // Pattern: uses auth hook, has early return(s) for auth checks, then returns the wrapped component
1912
+ const hasOnlyAuthLogic = hasAuthCheck &&
1913
+ !hasState &&
1914
+ !hasLoops &&
1915
+ (conditionalReturns === 0 || (conditionalReturns <= 2 && hasEarlyReturn)) &&
1916
+ (!hasConditionals || conditionalReturns <= 2);
1917
+
1918
+ // Check if hooks are only auth-related
1919
+ const authHookPattern = /use(UnifiedAuth|Permissions|Can|RBAC)/;
1920
+ const allHooks = componentBody.match(/use[A-Z]\w+/g) || [];
1921
+ const onlyAuthHooks = allHooks.length === 0 || allHooks.every(hook => authHookPattern.test(hook));
1922
+
1923
+ const isSimpleWrapper =
1924
+ wrappedComponentCount <= 2 && // Opening and closing tag, or self-closing
1925
+ !hasState &&
1926
+ !hasLoops &&
1927
+ (hasMultipleReturns === false || (hasMultipleReturns && hasOnlyAuthLogic && conditionalReturns <= 2)) &&
1928
+ (!hasConditionals || hasOnlyAuthLogic) &&
1929
+ (!hasHooks || (onlyAuthHooks && hasOnlyAuthLogic));
1930
+
1931
+ if (isSimpleWrapper) {
1932
+ // Check if the wrapped component is from pace-core or local
1933
+ const wrappedComponentImport = allImports.find(imp => imp.name === wrappedComponent);
1934
+ const isPaceCoreComponent = wrappedComponentImport &&
1935
+ wrappedComponentImport.module === '@jmruthers/pace-core';
1936
+
1937
+ let reason, recommendation;
1938
+ if (isPaceCoreComponent) {
1939
+ reason = `Component '${componentName}' appears to be an unnecessary wrapper around pace-core's '${wrappedComponent}'. It only forwards props without adding functionality.`;
1940
+ recommendation = `Use '${wrappedComponent}' directly from '@jmruthers/pace-core' instead of wrapping it in '${componentName}'.`;
1941
+ } else if (hasOnlyAuthLogic) {
1942
+ reason = `Component '${componentName}' is an unnecessary wrapper around '${wrappedComponent}'. It only performs auth/permission checks and returns the wrapped component.`;
1943
+ recommendation = `Merge the auth/permission logic into '${wrappedComponent}' and rename it to '${componentName}', or use PagePermissionGuard from pace-core to protect the route instead.`;
1944
+ } else {
1945
+ reason = `Component '${componentName}' appears to be an unnecessary wrapper around '${wrappedComponent}'. It only forwards props without adding functionality.`;
1946
+ recommendation = `Remove the wrapper and use '${wrappedComponent}' directly, or merge the logic into '${wrappedComponent}' and rename it to '${componentName}'.`;
1947
+ }
1948
+
1949
+ issues.push({
1950
+ component: componentName,
1951
+ wrappedComponent: wrappedComponent,
1952
+ file: relativePath,
1953
+ line: getLineNumber(content, match[0]),
1954
+ reason: reason,
1955
+ recommendation: recommendation
1956
+ });
1957
+ }
1958
+ }
1959
+ });
1960
+
1961
+ return issues;
1962
+ }
1963
+
1964
+ // Get line number for a match
1965
+ function getLineNumber(content, match) {
1966
+ const lines = content.substring(0, content.indexOf(match)).split('\n');
1967
+ return lines.length;
1968
+ }
1969
+
1970
+ // Generate report
1971
+ function generateReport(allViolations, manifest) {
1972
+ const totalRestricted = allViolations.restrictedImports.length;
1973
+ const totalDuplicates =
1974
+ allViolations.duplicateComponents.length +
1975
+ allViolations.duplicateHooks.length +
1976
+ allViolations.duplicateUtils.length;
1977
+ const totalSuggestions = allViolations.suggestions.length;
1978
+ const totalRbacAuth =
1979
+ allViolations.customAuthCode.length +
1980
+ allViolations.duplicateConfig.length +
1981
+ allViolations.unprotectedPages.length +
1982
+ allViolations.directSupabaseAuth.length;
1983
+ const totalSetupIssues =
1984
+ allViolations.providerSetupIssues.length +
1985
+ allViolations.viteConfigIssues.length +
1986
+ allViolations.routerSetupIssues.length;
1987
+ const totalUnnecessaryWrappers = allViolations.unnecessaryWrappers.length;
1988
+ const totalIssues = totalRestricted + totalDuplicates + totalSuggestions + totalRbacAuth + totalSetupIssues + totalUnnecessaryWrappers;
1989
+
1990
+ console.log(`\n${colors.bold}${colors.cyan}═══════════════════════════════════════════════════════════${colors.reset}`);
1991
+ console.log(`${colors.bold}${colors.cyan} pace-core Compliance Report${colors.reset}`);
1992
+ console.log(`${colors.bold}${colors.cyan}═══════════════════════════════════════════════════════════${colors.reset}\n`);
1993
+
1994
+ // Restricted Imports
1995
+ if (totalRestricted > 0) {
1996
+ console.log(`${colors.red}${colors.bold}❌ Restricted Imports Found: ${totalRestricted}${colors.reset}\n`);
1997
+ allViolations.restrictedImports.forEach(({ module, reason, file, line }) => {
1998
+ console.log(` ${colors.red}•${colors.reset} ${colors.yellow}${file}:${line}${colors.reset}`);
1999
+ console.log(` Import: ${colors.cyan}${module}${colors.reset}`);
2000
+ console.log(` ${colors.yellow}Reason:${colors.reset} ${reason}\n`);
2001
+ });
2002
+ } else {
2003
+ console.log(`${colors.green}✅ No restricted imports found${colors.reset}\n`);
2004
+ }
2005
+
2006
+ // Duplicate Components
2007
+ if (allViolations.duplicateComponents.length > 0) {
2008
+ console.log(`${colors.red}${colors.bold}❌ Duplicate Components Found: ${allViolations.duplicateComponents.length}${colors.reset}\n`);
2009
+ allViolations.duplicateComponents.forEach(({ component, file }) => {
2010
+ console.log(` ${colors.red}•${colors.reset} ${colors.yellow}${file}${colors.reset}`);
2011
+ console.log(` Component '${colors.cyan}${component}${colors.reset}' conflicts with pace-core component`);
2012
+ console.log(` ${colors.yellow}Suggestion:${colors.reset} Use '${component}' from '@jmruthers/pace-core' instead\n`);
2013
+ });
2014
+ }
2015
+
2016
+ // Duplicate Hooks
2017
+ if (allViolations.duplicateHooks.length > 0) {
2018
+ console.log(`${colors.red}${colors.bold}❌ Duplicate Hooks Found: ${allViolations.duplicateHooks.length}${colors.reset}\n`);
2019
+ allViolations.duplicateHooks.forEach(({ hook, file }) => {
2020
+ console.log(` ${colors.red}•${colors.reset} ${colors.yellow}${file}${colors.reset}`);
2021
+ console.log(` Hook '${colors.cyan}${hook}${colors.reset}' conflicts with pace-core hook`);
2022
+ console.log(` ${colors.yellow}Suggestion:${colors.reset} Use '${hook}' from '@jmruthers/pace-core' instead\n`);
2023
+ });
2024
+ }
2025
+
2026
+ // Duplicate Utils
2027
+ if (allViolations.duplicateUtils.length > 0) {
2028
+ console.log(`${colors.red}${colors.bold}❌ Duplicate Utils Found: ${allViolations.duplicateUtils.length}${colors.reset}\n`);
2029
+ allViolations.duplicateUtils.forEach(({ util, file }) => {
2030
+ console.log(` ${colors.red}•${colors.reset} ${colors.yellow}${file}${colors.reset}`);
2031
+ console.log(` Util '${colors.cyan}${util}${colors.reset}' conflicts with pace-core util`);
2032
+ console.log(` ${colors.yellow}Suggestion:${colors.reset} Use '${util}' from '@jmruthers/pace-core' instead\n`);
2033
+ });
2034
+ }
2035
+
2036
+ // Suggestions
2037
+ if (totalSuggestions > 0) {
2038
+ console.log(`${colors.yellow}${colors.bold}💡 Suggestions: ${totalSuggestions}${colors.reset}\n`);
2039
+ const grouped = {};
2040
+ allViolations.suggestions.forEach(s => {
2041
+ if (!grouped[s.file]) grouped[s.file] = [];
2042
+ grouped[s.file].push(s);
2043
+ });
2044
+ Object.entries(grouped).forEach(([file, suggestions]) => {
2045
+ console.log(` ${colors.yellow}•${colors.reset} ${colors.yellow}${file}${colors.reset}`);
2046
+ suggestions.forEach(s => {
2047
+ console.log(` ${s.suggestion}\n`);
2048
+ });
2049
+ });
2050
+ }
2051
+
2052
+ // Unnecessary Wrappers
2053
+ if (totalUnnecessaryWrappers > 0) {
2054
+ console.log(`${colors.yellow}${colors.bold}⚠️ Unnecessary Wrappers: ${totalUnnecessaryWrappers}${colors.reset}\n`);
2055
+ allViolations.unnecessaryWrappers.forEach(({ component, wrappedComponent, file, line, reason, recommendation }) => {
2056
+ console.log(` ${colors.yellow}•${colors.reset} ${colors.yellow}${file}:${line}${colors.reset}`);
2057
+ console.log(` Component: ${colors.cyan}${component}${colors.reset}`);
2058
+ console.log(` Wraps: ${colors.cyan}${wrappedComponent}${colors.reset}`);
2059
+ console.log(` ${colors.yellow}Issue:${colors.reset} ${reason}`);
2060
+ if (recommendation) {
2061
+ console.log(` ${colors.green}Recommendation:${colors.reset} ${recommendation}\n`);
2062
+ } else {
2063
+ console.log(` ${colors.green}Recommendation:${colors.reset} Remove the wrapper and use the component directly.\n`);
2064
+ }
2065
+ });
2066
+ }
2067
+
2068
+ // RBAC/Auth Compliance Section
2069
+ if (totalRbacAuth > 0) {
2070
+ console.log(`\n${colors.bold}${colors.cyan}═══════════════════════════════════════════════════════════${colors.reset}`);
2071
+ console.log(`${colors.bold}${colors.cyan} RBAC/Auth Compliance${colors.reset}`);
2072
+ console.log(`${colors.bold}${colors.cyan}═══════════════════════════════════════════════════════════${colors.reset}\n`);
2073
+
2074
+ // Custom Auth/RBAC Code
2075
+ if (allViolations.customAuthCode.length > 0) {
2076
+ // Separate by severity
2077
+ const errors = allViolations.customAuthCode.filter(v => !v.severity || v.severity === 'error');
2078
+ const warnings = allViolations.customAuthCode.filter(v => v.severity === 'warning');
2079
+ const info = allViolations.customAuthCode.filter(v => v.severity === 'info');
2080
+
2081
+ if (errors.length > 0) {
2082
+ console.log(`${colors.red}${colors.bold}❌ Custom Auth/RBAC Code Found: ${errors.length}${colors.reset}\n`);
2083
+ errors.forEach(({ name, type, file, line, reason, replacement, example }) => {
2084
+ console.log(` ${colors.red}•${colors.reset} ${colors.yellow}${file}:${line}${colors.reset}`);
2085
+ console.log(` ${type}: ${colors.cyan}${name}${colors.reset}`);
2086
+ console.log(` ${colors.yellow}Reason:${colors.reset} ${reason}`);
2087
+ if (replacement) {
2088
+ console.log(` ${colors.green}Fix:${colors.reset} ${replacement}`);
2089
+ // Add example code if provided
2090
+ if (example) {
2091
+ console.log(` ${colors.cyan}Example:${colors.reset}`);
2092
+ example.split('\n').forEach(line => {
2093
+ console.log(` ${colors.green}${line}${colors.reset}`);
2094
+ });
2095
+ } else {
2096
+ // Add example code for common cases
2097
+ if (type === 'rbac query' && name.includes('rbac_user_profiles')) {
2098
+ console.log(` ${colors.cyan}Example:${colors.reset}`);
2099
+ console.log(` ${colors.green}import { useSecureSupabase } from '@jmruthers/pace-core/rbac';${colors.reset}`);
2100
+ console.log(` ${colors.green}const supabase = useSecureSupabase();${colors.reset}`);
2101
+ console.log(` ${colors.green}const { data } = await supabase.from('rbac_user_profiles').select('*');${colors.reset}`);
2102
+ } else if (type === 'rbac query' && name.includes('rbac_user_login_history')) {
2103
+ console.log(` ${colors.cyan}Example:${colors.reset}`);
2104
+ console.log(` ${colors.green}import { useSecureSupabase } from '@jmruthers/pace-core/rbac';${colors.reset}`);
2105
+ console.log(` ${colors.green}const supabase = useSecureSupabase();${colors.reset}`);
2106
+ console.log(` ${colors.green}const { data } = await supabase.from('rbac_user_login_history').select('*').eq('user_id', userId);${colors.reset}`);
2107
+ console.log(` ${colors.yellow}Note:${colors.reset} Login history is automatically tracked by UnifiedAuthProvider.`);
2108
+ console.log(` ${colors.yellow} ${colors.reset} Queries should use useSecureSupabase to ensure organisation context is enforced.`);
2109
+ } else if (type === 'rbac query' && name.includes('rbac_user_units')) {
2110
+ console.log(` ${colors.cyan}Example:${colors.reset}`);
2111
+ console.log(` ${colors.green}import { useSecureSupabase } from '@jmruthers/pace-core/rbac';${colors.reset}`);
2112
+ console.log(` ${colors.green}const supabase = useSecureSupabase();${colors.reset}`);
2113
+ console.log(` ${colors.green}// For reading, use RPC:${colors.reset}`);
2114
+ console.log(` ${colors.green}const { data } = await supabase.rpc('data_user_unit_get', {${colors.reset}`);
2115
+ console.log(` ${colors.green} p_user_id: userId,${colors.reset}`);
2116
+ console.log(` ${colors.green} p_event_id: eventId${colors.reset}`);
2117
+ console.log(` ${colors.green}});${colors.reset}`);
2118
+ } else if (type === 'rbac query' && (name.includes('rbac_apps') || name.includes('rbac_app_pages') || name.includes('rbac_page_permissions'))) {
2119
+ console.log(` ${colors.cyan}Example (admin operations):${colors.reset}`);
2120
+ console.log(` ${colors.green}import { useSecureSupabase } from '@jmruthers/pace-core/rbac';${colors.reset}`);
2121
+ console.log(` ${colors.green}const supabase = useSecureSupabase();${colors.reset}`);
2122
+ console.log(` ${colors.green}const { data } = await supabase.from('${name.match(/rbac_\w+/)?.[0] || 'rbac_table'}').select('*');${colors.reset}`);
2123
+ }
2124
+ }
2125
+ }
2126
+ console.log('');
2127
+ });
2128
+ }
2129
+
2130
+ if (warnings.length > 0) {
2131
+ console.log(`${colors.yellow}${colors.bold}⚠️ Warnings (acceptable but should use secure methods): ${warnings.length}${colors.reset}\n`);
2132
+ warnings.forEach(({ name, type, file, line, reason, replacement, example }) => {
2133
+ console.log(` ${colors.yellow}•${colors.reset} ${colors.yellow}${file}:${line}${colors.reset}`);
2134
+ console.log(` ${type}: ${colors.cyan}${name}${colors.reset}`);
2135
+ console.log(` ${colors.yellow}Note:${colors.reset} ${reason}`);
2136
+ if (replacement) {
2137
+ console.log(` ${colors.green}Recommendation:${colors.reset} ${replacement}`);
2138
+ }
2139
+ if (example) {
2140
+ console.log(` ${colors.cyan}Example:${colors.reset}`);
2141
+ example.split('\n').forEach(line => {
2142
+ console.log(` ${colors.green}${line}${colors.reset}`);
2143
+ });
2144
+ }
2145
+ console.log('');
2146
+ });
2147
+ }
2148
+
2149
+ // Don't show info-level items (these are correct usage)
2150
+ // They're tracked but not displayed to avoid noise
2151
+ }
2152
+
2153
+ // Duplicate Configurations
2154
+ if (allViolations.duplicateConfig.length > 0) {
2155
+ console.log(`${colors.red}${colors.bold}❌ Duplicate Configurations Found: ${allViolations.duplicateConfig.length}${colors.reset}\n`);
2156
+ allViolations.duplicateConfig.forEach(({ type, file, count, reason }) => {
2157
+ console.log(` ${colors.red}•${colors.reset} ${colors.yellow}${file}${colors.reset}`);
2158
+ console.log(` Type: ${colors.cyan}${type}${colors.reset}${count ? ` (${count} instances)` : ''}`);
2159
+ console.log(` ${colors.yellow}Reason:${colors.reset} ${reason}\n`);
2160
+ });
2161
+ }
2162
+
2163
+ // Unprotected Pages
2164
+ if (allViolations.unprotectedPages.length > 0) {
2165
+ console.log(`${colors.red}${colors.bold}❌ Unprotected Pages Found: ${allViolations.unprotectedPages.length}${colors.reset}\n`);
2166
+ allViolations.unprotectedPages.forEach(({ file, reason }) => {
2167
+ console.log(` ${colors.red}•${colors.reset} ${colors.yellow}${file}${colors.reset}`);
2168
+ console.log(` ${colors.yellow}Reason:${colors.reset} ${reason}\n`);
2169
+ });
2170
+ }
2171
+
2172
+ // Direct Supabase Auth Usage
2173
+ if (allViolations.directSupabaseAuth.length > 0) {
2174
+ console.log(`${colors.red}${colors.bold}❌ Direct Supabase Auth Usage Found: ${allViolations.directSupabaseAuth.length}${colors.reset}\n`);
2175
+ allViolations.directSupabaseAuth.forEach(({ file, line, reason, method, recommendation }) => {
2176
+ console.log(` ${colors.red}•${colors.reset} ${colors.yellow}${file}:${line}${colors.reset}`);
2177
+ console.log(` ${colors.yellow}Reason:${colors.reset} ${reason}`);
2178
+ if (recommendation) {
2179
+ console.log(` ${colors.green}Fix:${colors.reset} ${recommendation}\n`);
2180
+ } else {
2181
+ 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`);
2182
+ }
2183
+ });
2184
+ }
2185
+ } else {
2186
+ console.log(`\n${colors.green}✅ RBAC/Auth compliance: All checks passed${colors.reset}\n`);
2187
+ }
2188
+
2189
+ // Setup/Configuration Issues Section
2190
+ if (totalSetupIssues > 0) {
2191
+ console.log(`\n${colors.bold}${colors.cyan}═══════════════════════════════════════════════════════════${colors.reset}`);
2192
+ console.log(`${colors.bold}${colors.cyan} Setup & Configuration Issues${colors.reset}`);
2193
+ console.log(`${colors.bold}${colors.cyan}═══════════════════════════════════════════════════════════${colors.reset}\n`);
2194
+
2195
+ // Provider Setup Issues
2196
+ if (allViolations.providerSetupIssues.length > 0) {
2197
+ console.log(`${colors.red}${colors.bold}❌ Provider Setup Issues Found: ${allViolations.providerSetupIssues.length}${colors.reset}\n`);
2198
+ allViolations.providerSetupIssues.forEach(({ file, line, type, provider, reason, recommendation }) => {
2199
+ console.log(` ${colors.red}•${colors.reset} ${colors.yellow}${file}:${line}${colors.reset}`);
2200
+ if (provider) {
2201
+ console.log(` Missing Provider: ${colors.cyan}${provider}${colors.reset}`);
2202
+ }
2203
+ if (type) {
2204
+ console.log(` Issue Type: ${colors.cyan}${type}${colors.reset}`);
2205
+ }
2206
+ console.log(` ${colors.yellow}Problem:${colors.reset} ${reason}`);
2207
+ console.log(` ${colors.green}Fix:${colors.reset} ${recommendation}\n`);
2208
+ });
2209
+ }
2210
+
2211
+ // Vite Configuration Issues
2212
+ if (allViolations.viteConfigIssues.length > 0) {
2213
+ console.log(`${colors.red}${colors.bold}❌ Vite Configuration Issues Found: ${allViolations.viteConfigIssues.length}${colors.reset}\n`);
2214
+ allViolations.viteConfigIssues.forEach(({ file, line, type, reason, recommendation }) => {
2215
+ const severity = type === 'recommendation' ? colors.yellow : colors.red;
2216
+ const icon = type === 'recommendation' ? '💡' : '❌';
2217
+ console.log(` ${severity}${icon}${colors.reset} ${colors.yellow}${file}:${line}${colors.reset}`);
2218
+ console.log(` ${colors.yellow}Issue:${colors.reset} ${reason}`);
2219
+ console.log(` ${colors.green}Fix:${colors.reset} ${recommendation}\n`);
2220
+ });
2221
+ }
2222
+
2223
+ // Router Setup Issues
2224
+ if (allViolations.routerSetupIssues.length > 0) {
2225
+ console.log(`${colors.red}${colors.bold}❌ Router Setup Issues Found: ${allViolations.routerSetupIssues.length}${colors.reset}\n`);
2226
+ allViolations.routerSetupIssues.forEach(({ file, line, type, reason, recommendation }) => {
2227
+ console.log(` ${colors.red}•${colors.reset} ${colors.yellow}${file}:${line}${colors.reset}`);
2228
+ console.log(` Issue Type: ${colors.cyan}${type}${colors.reset}`);
2229
+ console.log(` ${colors.yellow}Problem:${colors.reset} ${reason}`);
2230
+ console.log(` ${colors.green}Fix:${colors.reset} ${recommendation}\n`);
2231
+ });
2232
+ }
2233
+ } else {
2234
+ console.log(`\n${colors.green}✅ Setup & Configuration: All checks passed${colors.reset}\n`);
2235
+ }
2236
+
2237
+ // Summary
2238
+ console.log(`${colors.bold}${colors.cyan}═══════════════════════════════════════════════════════════${colors.reset}`);
2239
+ console.log(`${colors.bold}Summary:${colors.reset}`);
2240
+ console.log(` Total Issues: ${totalIssues > 0 ? colors.red : colors.green}${totalIssues}${colors.reset}`);
2241
+ console.log(` - Restricted Imports: ${totalRestricted > 0 ? colors.red : colors.green}${totalRestricted}${colors.reset}`);
2242
+ console.log(` - Duplicate Components/Hooks/Utils: ${totalDuplicates > 0 ? colors.red : colors.green}${totalDuplicates}${colors.reset}`);
2243
+ console.log(` - Suggestions: ${colors.yellow}${totalSuggestions}${colors.reset}`);
2244
+ console.log(` - RBAC/Auth Issues: ${totalRbacAuth > 0 ? colors.red : colors.green}${totalRbacAuth}${colors.reset}`);
2245
+ console.log(` - Setup/Configuration Issues: ${totalSetupIssues > 0 ? colors.red : colors.green}${totalSetupIssues}${colors.reset}`);
2246
+
2247
+ if (totalIssues === 0) {
2248
+ console.log(`\n${colors.green}${colors.bold}✅ Excellent! Your codebase is fully compliant with pace-core standards.${colors.reset}\n`);
2249
+ return 0;
2250
+ } else {
2251
+ console.log(`\n${colors.yellow}${colors.bold}⚠️ Please review the issues above and migrate to pace-core components/hooks/utils.${colors.reset}\n`);
2252
+ return 1;
2253
+ }
2254
+ }
2255
+
2256
+ // Recursively find source files
2257
+ function findSourceFiles(dir, fileList = []) {
2258
+ const ignoreDirs = ['node_modules', 'dist', 'build', '.next', 'coverage', '__tests__', '__mocks__'];
2259
+ const ignoreFiles = /\.(test|spec)\.(ts|tsx|js|jsx)$/;
2260
+ const sourceExtensions = /\.(ts|tsx|js|jsx)$/;
2261
+
2262
+ try {
2263
+ const items = fs.readdirSync(dir);
2264
+
2265
+ items.forEach(item => {
2266
+ const fullPath = path.join(dir, item);
2267
+ const stat = fs.statSync(fullPath);
2268
+
2269
+ if (stat.isDirectory()) {
2270
+ if (!ignoreDirs.includes(item) && !item.startsWith('.')) {
2271
+ findSourceFiles(fullPath, fileList);
2272
+ }
2273
+ } else if (stat.isFile()) {
2274
+ if (sourceExtensions.test(item) && !ignoreFiles.test(item)) {
2275
+ fileList.push(fullPath);
2276
+ }
2277
+ }
2278
+ });
2279
+ } catch (error) {
2280
+ // Skip directories we can't read
2281
+ }
2282
+
2283
+ return fileList;
2284
+ }
2285
+
2286
+ // Main function
2287
+ function main() {
2288
+ const manifest = loadManifest();
2289
+ const projectRoot = findProjectRoot();
2290
+
2291
+ console.log(`${colors.cyan}Scanning project at: ${projectRoot}${colors.reset}`);
2292
+
2293
+ // Find all TypeScript/JavaScript files (excluding node_modules, dist, etc.)
2294
+ const files = findSourceFiles(projectRoot);
2295
+
2296
+ console.log(`Found ${files.length} files to scan...\n`);
2297
+
2298
+ // Scan all files
2299
+ const allViolations = {
2300
+ restrictedImports: [],
2301
+ duplicateComponents: [],
2302
+ duplicateHooks: [],
2303
+ duplicateUtils: [],
2304
+ suggestions: [],
2305
+ customAuthCode: [],
2306
+ duplicateConfig: [],
2307
+ unprotectedPages: [],
2308
+ directSupabaseAuth: [],
2309
+ providerSetupIssues: [],
2310
+ viteConfigIssues: [],
2311
+ routerSetupIssues: [],
2312
+ unnecessaryWrappers: []
2313
+ };
2314
+
2315
+ files.forEach(file => {
2316
+ try {
2317
+ const violations = scanFile(file, manifest);
2318
+ allViolations.restrictedImports.push(...violations.restrictedImports);
2319
+ allViolations.duplicateComponents.push(...violations.duplicateComponents);
2320
+ allViolations.duplicateHooks.push(...violations.duplicateHooks);
2321
+ allViolations.duplicateUtils.push(...violations.duplicateUtils);
2322
+ allViolations.suggestions.push(...violations.suggestions);
2323
+ allViolations.customAuthCode.push(...violations.customAuthCode);
2324
+ allViolations.duplicateConfig.push(...violations.duplicateConfig);
2325
+ allViolations.unprotectedPages.push(...violations.unprotectedPages);
2326
+ allViolations.directSupabaseAuth.push(...violations.directSupabaseAuth);
2327
+ allViolations.providerSetupIssues.push(...violations.providerSetupIssues);
2328
+ allViolations.viteConfigIssues.push(...violations.viteConfigIssues);
2329
+ allViolations.routerSetupIssues.push(...violations.routerSetupIssues);
2330
+ allViolations.unnecessaryWrappers.push(...violations.unnecessaryWrappers);
2331
+ } catch (error) {
2332
+ console.error(`${colors.red}Error scanning ${file}: ${error.message}${colors.reset}`);
2333
+ }
2334
+ });
2335
+
2336
+ // Generate and display report
2337
+ const exitCode = generateReport(allViolations, manifest);
2338
+ process.exit(exitCode);
2339
+ }
2340
+
2341
+ // Run if called directly
2342
+ if (require.main === module) {
2343
+ try {
2344
+ main();
2345
+ } catch (error) {
2346
+ console.error(`${colors.red}Error: ${error.message}${colors.reset}`);
2347
+ console.error(error.stack);
2348
+ process.exit(1);
2349
+ }
2350
+ }
2351
+
2352
+ module.exports = { main, scanFile, generateReport };
2353
+