@jmruthers/pace-core 0.6.4 → 0.6.6

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