@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,954 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * RBAC Compliance Audit Script
5
+ *
6
+ * Audits consuming apps for RBAC compliance according to 09-rbac-compliance.mdc.
7
+ * Checks for:
8
+ * - PagePermissionGuard usage on all protected pages
9
+ * - Wrapper components/functions around RBAC components
10
+ * - Direct RBAC RPC calls and table queries
11
+ * - Hardcoded role checks
12
+ * - RESOURCE_NAMES constants usage
13
+ * - AccessDenied component usage
14
+ * - enforcePermissions configuration
15
+ * - Edge Functions RBAC usage
16
+ *
17
+ * Usage:
18
+ * const { runRBACAudit } = require('./audit-rbac.cjs');
19
+ * const issues = runRBACAudit(consumingAppPath);
20
+ */
21
+
22
+ const fs = require('fs');
23
+ const path = require('path');
24
+
25
+ /**
26
+ * Recursively find all TypeScript/JavaScript files in a directory
27
+ */
28
+ function findSourceFiles(dir, fileList = []) {
29
+ if (!fs.existsSync(dir)) {
30
+ return fileList;
31
+ }
32
+
33
+ const files = fs.readdirSync(dir);
34
+
35
+ files.forEach(file => {
36
+ const filePath = path.join(dir, file);
37
+ const stat = fs.statSync(filePath);
38
+
39
+ if (stat.isDirectory()) {
40
+ // Skip node_modules, dist, build, .git, etc.
41
+ if (!['node_modules', 'dist', 'build', '.git', '.next', '.vite', 'coverage', '.turbo'].includes(file)) {
42
+ findSourceFiles(filePath, fileList);
43
+ }
44
+ } else if (/\.(ts|tsx|js|jsx)$/.test(file)) {
45
+ fileList.push(filePath);
46
+ }
47
+ });
48
+
49
+ return fileList;
50
+ }
51
+
52
+ /**
53
+ * Get line number from index in content
54
+ */
55
+ function getLineNumber(content, index) {
56
+ return content.substring(0, index).split('\n').length;
57
+ }
58
+
59
+ /**
60
+ * Get code snippet around a match for context
61
+ */
62
+ function getCodeSnippet(content, index, before = 30, after = 50) {
63
+ const start = Math.max(0, index - before);
64
+ const end = Math.min(content.length, index + after);
65
+ return content.substring(start, end).trim();
66
+ }
67
+
68
+ /**
69
+ * Check if content is in a comment or string
70
+ */
71
+ function isInCommentOrString(content, index) {
72
+ const before = content.substring(0, index);
73
+
74
+ // Check for line comments
75
+ const lastLineComment = before.lastIndexOf('//');
76
+ const lastNewline = before.lastIndexOf('\n');
77
+ if (lastLineComment > lastNewline) {
78
+ return true;
79
+ }
80
+
81
+ // Check for block comments
82
+ const lastBlockCommentStart = before.lastIndexOf('/*');
83
+ const lastBlockCommentEnd = before.lastIndexOf('*/');
84
+ if (lastBlockCommentStart > lastBlockCommentEnd) {
85
+ return true;
86
+ }
87
+
88
+ // Check for string literals (simple check)
89
+ const singleQuoteMatches = [...before.matchAll(/'/g)];
90
+ const doubleQuoteMatches = [...before.matchAll(/"/g)];
91
+ const backtickMatches = [...before.matchAll(/`/g)];
92
+
93
+ // Simple heuristic: if odd number of quotes before, might be in string
94
+ const inSingleQuote = singleQuoteMatches.length % 2 === 1;
95
+ const inDoubleQuote = doubleQuoteMatches.length % 2 === 1;
96
+ const inBacktick = backtickMatches.length % 2 === 1;
97
+
98
+ return inSingleQuote || inDoubleQuote || inBacktick;
99
+ }
100
+
101
+ /**
102
+ * Check if file imports a specific component/hook/util from pace-core
103
+ */
104
+ function importsFromPaceCore(content, name) {
105
+ const patterns = [
106
+ new RegExp(`import\\s+.*\\b${name}\\b.*from\\s+['"]@jmruthers/pace-core`),
107
+ new RegExp(`import\\s+.*\\b${name}\\b.*from\\s+['"]@jmruthers/pace-core/rbac`),
108
+ new RegExp(`import\\s+.*\\b${name}\\b.*from\\s+['"]@jmruthers/pace-core/components`),
109
+ new RegExp(`import\\s+.*\\b${name}\\b.*from\\s+['"]@jmruthers/pace-core/hooks`),
110
+ ];
111
+
112
+ return patterns.some(pattern => pattern.test(content));
113
+ }
114
+
115
+ /**
116
+ * Check if a file is likely a page/route component
117
+ * Excludes: providers, routing components, shared/reusable components
118
+ */
119
+ function isPageComponent(filePath, content) {
120
+ const fileName = path.basename(filePath);
121
+ const dirName = path.dirname(filePath);
122
+ const dirParts = dirName.split(path.sep);
123
+
124
+ // EXCLUDE: Provider components
125
+ if (dirParts.some(part => part.toLowerCase() === 'providers' || part.toLowerCase() === 'provider')) {
126
+ return false;
127
+ }
128
+
129
+ // EXCLUDE: Routing components (files matching *Route.tsx or *Routes.tsx)
130
+ if (/Route(s)?\.(tsx?|jsx?)$/i.test(fileName)) {
131
+ return false;
132
+ }
133
+
134
+ // EXCLUDE: Shared/reusable components
135
+ if (dirParts.some(part => part.toLowerCase() === 'shared' || part.toLowerCase() === 'components')) {
136
+ // Only exclude if it's in a components directory (not pages/components)
137
+ const componentsIndex = dirParts.findIndex(part => part.toLowerCase() === 'components');
138
+ const pagesIndex = dirParts.findIndex(part => part.toLowerCase() === 'pages');
139
+ if (componentsIndex !== -1 && (pagesIndex === -1 || componentsIndex < pagesIndex)) {
140
+ return false;
141
+ }
142
+ }
143
+
144
+ // EXCLUDE: Files that are clearly providers (contain Provider in name and setup context)
145
+ if (/Provider\.(tsx?|jsx?)$/i.test(fileName) ||
146
+ /.*Provider.*\.(tsx?|jsx?)$/i.test(fileName)) {
147
+ // Check if it actually sets up providers (contains Provider components)
148
+ if (/<.*Provider|Provider\s*</.test(content) || /createContext|Context\.Provider/.test(content)) {
149
+ return false;
150
+ }
151
+ }
152
+
153
+ // INCLUDE: Files in pages/ directory (definitely pages)
154
+ if (dirParts.some(part => part.toLowerCase() === 'pages')) {
155
+ return true;
156
+ }
157
+
158
+ // INCLUDE: Files matching *Page.tsx pattern (but not *Route.tsx which we already excluded)
159
+ if (/Page\.(tsx?|jsx?)$/i.test(fileName)) {
160
+ return true;
161
+ }
162
+
163
+ // INCLUDE: Files in features/ directory that export page components
164
+ // (features often contain page components)
165
+ if (dirParts.some(part => part.toLowerCase() === 'features')) {
166
+ // Check if it exports a page-like component (has PagePermissionGuard or is a page)
167
+ if (/export\s+(function|const)\s+\w+Page/.test(content) ||
168
+ /PagePermissionGuard/.test(content)) {
169
+ return true;
170
+ }
171
+ }
172
+
173
+ // EXCLUDE: Files in app/ directory that are NOT pages
174
+ // (app/ can contain routes, providers, etc.)
175
+ if (dirParts.some(part => part.toLowerCase() === 'app')) {
176
+ // Only include if it's clearly a page (has PagePermissionGuard or matches page patterns)
177
+ if (/PagePermissionGuard/.test(content) ||
178
+ /export\s+(function|const)\s+\w+Page/.test(content)) {
179
+ return true;
180
+ }
181
+ // Otherwise exclude (likely a route, provider, or other non-page component)
182
+ return false;
183
+ }
184
+
185
+ // EXCLUDE: Files that are clearly routing components (contain Routes, Route, Router)
186
+ if (/Routes|Router/.test(content) && !/PagePermissionGuard/.test(content)) {
187
+ // If it contains routing setup but no PagePermissionGuard, it's a routing component
188
+ if (/<Routes|<Route|<Router/.test(content)) {
189
+ return false;
190
+ }
191
+ }
192
+
193
+ // EXCLUDE: Files that export routing components
194
+ if (/export\s+(function|const)\s+\w+Route(s)?/.test(content)) {
195
+ return false;
196
+ }
197
+
198
+ return false;
199
+ }
200
+
201
+ /**
202
+ * Check 1: PagePermissionGuard Usage (Security Critical)
203
+ */
204
+ function checkPagePermissionGuard(content, filePath, consumingAppPath) {
205
+ const issues = [];
206
+ const relativePath = path.relative(consumingAppPath || process.cwd(), filePath);
207
+
208
+ // Only check page components
209
+ if (!isPageComponent(filePath, content)) {
210
+ return issues;
211
+ }
212
+
213
+ // Check if PagePermissionGuard is used
214
+ const hasPagePermissionGuard = /<PagePermissionGuard/.test(content) ||
215
+ /PagePermissionGuard\s*</.test(content);
216
+
217
+ // Check if RBAC hooks are imported (indicates this should be protected)
218
+ const hasRBACHooks = importsFromPaceCore(content, 'useCan') ||
219
+ importsFromPaceCore(content, 'useResourcePermissions') ||
220
+ importsFromPaceCore(content, 'usePermissions') ||
221
+ importsFromPaceCore(content, 'useRBAC');
222
+
223
+ // Check if component returns JSX (likely a page)
224
+ const returnsJSX = /return\s*\(/.test(content) || /return\s+</.test(content);
225
+
226
+ if (returnsJSX && !hasPagePermissionGuard) {
227
+ // Check if this is a public page (no RBAC hooks)
228
+ if (hasRBACHooks) {
229
+ // Definitely should be protected
230
+ issues.push({
231
+ type: 'rbacPageGuard',
232
+ file: relativePath,
233
+ line: 1,
234
+ message: 'Page component missing PagePermissionGuard wrapper. Pages using RBAC hooks must be protected.',
235
+ code: getCodeSnippet(content, content.indexOf('return')),
236
+ severity: 'error',
237
+ fix: 'Wrap page content with <PagePermissionGuard pageName="page-name" operation="read">',
238
+ });
239
+ } else {
240
+ // Might be a public page, but flag for review
241
+ issues.push({
242
+ type: 'rbacPageGuard',
243
+ file: relativePath,
244
+ line: 1,
245
+ message: 'Page component does not use PagePermissionGuard. Verify if this page should be protected.',
246
+ code: getCodeSnippet(content, content.indexOf('return') || 0),
247
+ severity: 'warning',
248
+ fix: 'If page should be protected, wrap with <PagePermissionGuard pageName="page-name" operation="read">',
249
+ });
250
+ }
251
+ }
252
+
253
+ // Check if PagePermissionGuard is used incorrectly (missing required props)
254
+ if (hasPagePermissionGuard) {
255
+ const pageGuardPattern = /<PagePermissionGuard[^>]*>/g;
256
+ let match;
257
+ while ((match = pageGuardPattern.exec(content)) !== null) {
258
+ if (isInCommentOrString(content, match.index)) {
259
+ continue;
260
+ }
261
+
262
+ const guardProps = match[0];
263
+ const hasPageName = /pageName\s*=/.test(guardProps);
264
+ const hasOperation = /operation\s*=/.test(guardProps);
265
+
266
+ if (!hasPageName || !hasOperation) {
267
+ issues.push({
268
+ type: 'rbacPageGuard',
269
+ file: relativePath,
270
+ line: getLineNumber(content, match.index),
271
+ message: 'PagePermissionGuard missing required props (pageName or operation)',
272
+ code: guardProps,
273
+ severity: 'error',
274
+ fix: 'Add required props: <PagePermissionGuard pageName="page-name" operation="read">',
275
+ });
276
+ }
277
+ }
278
+ }
279
+
280
+ return issues;
281
+ }
282
+
283
+ /**
284
+ * Check 2: Wrapper Components Around PagePermissionGuard (Security Risk)
285
+ *
286
+ * MIGRATED TO ESLINT: This check is now handled by 'no-rbac-wrapper-components' ESLint rule.
287
+ * Kept for reference only.
288
+ */
289
+ function checkWrapperComponents_MIGRATED_TO_ESLINT(content, filePath, consumingAppPath) {
290
+ const issues = [];
291
+ const relativePath = path.relative(consumingAppPath || process.cwd(), filePath);
292
+
293
+ // Pattern to find components that wrap PagePermissionGuard
294
+ // Look for function/const definitions that return PagePermissionGuard
295
+ const wrapperPattern = /(?:function|const|export\s+(?:function|const))\s+(\w+)\s*[=(][^)]*\)\s*\{[^}]*<PagePermissionGuard/gs;
296
+ let match;
297
+
298
+ while ((match = wrapperPattern.exec(content)) !== null) {
299
+ if (isInCommentOrString(content, match.index)) {
300
+ continue;
301
+ }
302
+
303
+ const componentName = match[1];
304
+ const componentBody = match[0];
305
+
306
+ // Check if this component accepts pageName as prop (wrapper pattern)
307
+ if (/pageName\s*[:=]/.test(componentBody) || /\{[^}]*pageName/.test(componentBody)) {
308
+ issues.push({
309
+ type: 'rbacWrapperComponent',
310
+ file: relativePath,
311
+ line: getLineNumber(content, match.index),
312
+ message: `Wrapper component '${componentName}' detected around PagePermissionGuard. Must use PagePermissionGuard directly, not through wrappers.`,
313
+ code: getCodeSnippet(content, match.index, 0, 100),
314
+ severity: 'error',
315
+ fix: `Remove wrapper component and use PagePermissionGuard directly in pages.`,
316
+ });
317
+ }
318
+ }
319
+
320
+ return issues;
321
+ }
322
+
323
+ /**
324
+ * Check 3: Wrapper Functions Around Permission Hooks (Security Risk)
325
+ *
326
+ * MIGRATED TO ESLINT: This check is now handled by 'no-rbac-wrapper-functions' ESLint rule.
327
+ * Kept for reference only.
328
+ */
329
+ function checkWrapperFunctions_MIGRATED_TO_ESLINT(content, filePath, consumingAppPath) {
330
+ const issues = [];
331
+ const relativePath = path.relative(consumingAppPath || process.cwd(), filePath);
332
+
333
+ // Pattern to find functions that use permission hooks
334
+ const permissionHooks = ['useCan', 'useResourcePermissions', 'usePermissions', 'useRBAC'];
335
+
336
+ permissionHooks.forEach(hookName => {
337
+ // Check if hook is imported
338
+ if (!importsFromPaceCore(content, hookName)) {
339
+ return;
340
+ }
341
+
342
+ // Find function definitions that might wrap the hook
343
+ // Pattern: function/const name = (...) => { ... useHook ... }
344
+ const functionPattern = new RegExp(`(?:function|const|export\\s+(?:function|const))\\s+(\\w+)\\s*[=(][^)]*\\)\\s*=>\\s*\\{[^}]*${hookName}`, 'gs');
345
+ let match;
346
+
347
+ while ((match = functionPattern.exec(content)) !== null) {
348
+ if (isInCommentOrString(content, match.index)) {
349
+ continue;
350
+ }
351
+
352
+ const functionName = match[1];
353
+ const functionBody = match[0];
354
+
355
+ // Check if this function returns a permission check result (wrapper pattern)
356
+ // Look for patterns like: const canEdit = (...) => { const { can } = useCan(...); return can; }
357
+ if (/return\s+(can|hasPermission|isPermitted|canCreate|canUpdate|canDelete)/.test(functionBody) ||
358
+ /:\s*(can|hasPermission|isPermitted|boolean)/.test(functionBody)) {
359
+ issues.push({
360
+ type: 'rbacWrapperFunction',
361
+ file: relativePath,
362
+ line: getLineNumber(content, match.index),
363
+ message: `Wrapper function '${functionName}' detected around ${hookName} hook. Must use hooks directly, not through wrapper functions.`,
364
+ code: getCodeSnippet(content, match.index, 0, 150),
365
+ severity: 'error',
366
+ fix: `Remove wrapper function and use ${hookName} hook directly in components.`,
367
+ });
368
+ }
369
+ }
370
+
371
+ // Also check for custom permission utility functions
372
+ const utilityPattern = /(?:function|const|export\s+(?:function|const))\s+(checkPermission|canEdit|canDelete|hasAccess|checkAccess)\s*[=(]/g;
373
+ let utilMatch;
374
+ while ((utilMatch = utilityPattern.exec(content)) !== null) {
375
+ if (isInCommentOrString(content, utilMatch.index)) {
376
+ continue;
377
+ }
378
+
379
+ // Check if this function uses permission hooks or RPC calls
380
+ const funcStart = utilMatch.index;
381
+ const funcEnd = content.indexOf('}', funcStart);
382
+ if (funcEnd === -1) continue;
383
+
384
+ const funcBody = content.substring(funcStart, funcEnd);
385
+ if (new RegExp(hookName).test(funcBody) || /rbac_check_permission/.test(funcBody)) {
386
+ issues.push({
387
+ type: 'rbacWrapperFunction',
388
+ file: relativePath,
389
+ line: getLineNumber(content, utilMatch.index),
390
+ message: `Custom permission utility function '${utilMatch[1]}' detected. Must use pace-core hooks directly, not custom utilities.`,
391
+ code: getCodeSnippet(content, utilMatch.index, 0, 150),
392
+ severity: 'error',
393
+ fix: `Remove custom utility and use ${hookName} hook directly.`,
394
+ });
395
+ }
396
+ }
397
+ });
398
+
399
+ return issues;
400
+ }
401
+
402
+ /**
403
+ * Check 4: RESOURCE_NAMES Constants Usage (Type Safety)
404
+ *
405
+ * MIGRATED TO ESLINT: This check is now handled by 'rbac-use-resource-names-constants' ESLint rule.
406
+ * Kept for reference only.
407
+ */
408
+ function checkResourceNamesConstants_MIGRATED_TO_ESLINT(content, filePath, consumingAppPath) {
409
+ const issues = [];
410
+ const relativePath = path.relative(consumingAppPath || process.cwd(), filePath);
411
+
412
+ // Find all useResourcePermissions calls
413
+ const useResourcePermissionsPattern = /useResourcePermissions\s*\(/g;
414
+ let match;
415
+
416
+ while ((match = useResourcePermissionsPattern.exec(content)) !== null) {
417
+ if (isInCommentOrString(content, match.index)) {
418
+ continue;
419
+ }
420
+
421
+ // Get the argument to useResourcePermissions
422
+ const callStart = match.index;
423
+ const callEnd = content.indexOf(')', callStart);
424
+ if (callEnd === -1) continue;
425
+
426
+ const callContent = content.substring(callStart, callEnd + 1);
427
+
428
+ // Check if argument is a string literal (not a constant)
429
+ const stringLiteralPattern = /useResourcePermissions\s*\(\s*['"]([^'"]+)['"]/;
430
+ const stringMatch = callContent.match(stringLiteralPattern);
431
+
432
+ if (stringMatch) {
433
+ const resourceName = stringMatch[1];
434
+
435
+ // Check if RESOURCE_NAMES is imported
436
+ const hasResourceNames = importsFromPaceCore(content, 'RESOURCE_NAMES') ||
437
+ /RESOURCE_NAMES\s*\./.test(content);
438
+
439
+ issues.push({
440
+ type: 'rbacResourceNames',
441
+ file: relativePath,
442
+ line: getLineNumber(content, match.index),
443
+ message: `useResourcePermissions called with string literal '${resourceName}'. Must use RESOURCE_NAMES constant instead.`,
444
+ code: getCodeSnippet(content, match.index, 0, 80),
445
+ severity: 'error',
446
+ fix: `Import RESOURCE_NAMES from '@/config/resource-names' and use RESOURCE_NAMES.${resourceName.toUpperCase()} instead.`,
447
+ });
448
+ }
449
+ }
450
+
451
+ return issues;
452
+ }
453
+
454
+ /**
455
+ * Check 5: AccessDenied Component Usage (Consistency)
456
+ */
457
+ function checkAccessDeniedComponent(content, filePath, consumingAppPath) {
458
+ const issues = [];
459
+ const relativePath = path.relative(consumingAppPath || process.cwd(), filePath);
460
+
461
+ // Find custom access denied components
462
+ const customAccessDeniedPattern = /(?:function|const|export\s+(?:function|const))\s+(\w*(?:AccessDenied|PermissionDenied|Unauthorized|Forbidden)\w*)\s*[=(]/g;
463
+ let match;
464
+
465
+ while ((match = customAccessDeniedPattern.exec(content)) !== null) {
466
+ if (isInCommentOrString(content, match.index)) {
467
+ continue;
468
+ }
469
+
470
+ const componentName = match[1];
471
+
472
+ // Skip if it's the pace-core AccessDenied import
473
+ if (importsFromPaceCore(content, 'AccessDenied') && componentName === 'AccessDenied') {
474
+ continue;
475
+ }
476
+
477
+ issues.push({
478
+ type: 'rbacAccessDenied',
479
+ file: relativePath,
480
+ line: getLineNumber(content, match.index),
481
+ message: `Custom access denied component '${componentName}' detected. Must use AccessDenied from pace-core instead.`,
482
+ code: getCodeSnippet(content, match.index, 0, 100),
483
+ severity: 'warning',
484
+ fix: `Remove custom component and use AccessDenied from '@jmruthers/pace-core/rbac'`,
485
+ });
486
+ }
487
+
488
+ // Check PagePermissionGuard fallback prop for custom components
489
+ const pageGuardPattern = /<PagePermissionGuard[^>]*fallback\s*=\s*\{?<(\w+)/g;
490
+ let fallbackMatch;
491
+ while ((fallbackMatch = pageGuardPattern.exec(content)) !== null) {
492
+ if (isInCommentOrString(content, fallbackMatch.index)) {
493
+ continue;
494
+ }
495
+
496
+ const fallbackComponent = fallbackMatch[1];
497
+ if (fallbackComponent !== 'AccessDenied' && !importsFromPaceCore(content, fallbackComponent)) {
498
+ issues.push({
499
+ type: 'rbacAccessDenied',
500
+ file: relativePath,
501
+ line: getLineNumber(content, fallbackMatch.index),
502
+ message: `PagePermissionGuard uses custom fallback component '${fallbackComponent}'. Must use AccessDenied from pace-core.`,
503
+ code: getCodeSnippet(content, fallbackMatch.index, 0, 80),
504
+ severity: 'warning',
505
+ fix: `Use <AccessDenied /> from '@jmruthers/pace-core/rbac' as fallback prop.`,
506
+ });
507
+ }
508
+ }
509
+
510
+ return issues;
511
+ }
512
+
513
+ /**
514
+ * Check 6: Direct RBAC RPC Calls (Security Critical)
515
+ *
516
+ * MIGRATED TO ESLINT: This check is now handled by 'no-direct-rbac-rpc' ESLint rule.
517
+ * Kept for reference only.
518
+ */
519
+ function checkDirectRBACRPC_MIGRATED_TO_ESLINT(content, filePath, consumingAppPath) {
520
+ const issues = [];
521
+ const relativePath = path.relative(consumingAppPath || process.cwd(), filePath);
522
+
523
+ // Find direct RPC calls to RBAC functions
524
+ const rpcPattern = /\.rpc\s*\(\s*['"](rbac_[^'"]+)['"]/g;
525
+ let match;
526
+
527
+ while ((match = rpcPattern.exec(content)) !== null) {
528
+ if (isInCommentOrString(content, match.index)) {
529
+ continue;
530
+ }
531
+
532
+ const rpcName = match[1];
533
+
534
+ // Check if this is the forbidden RPC
535
+ if (rpcName === 'rbac_check_permission_simplified' || rpcName.startsWith('rbac_')) {
536
+ // Check if it's in an Edge Function (where setupRBAC + isPermitted should be used)
537
+ const isEdgeFunction = /supabase\/functions/.test(filePath);
538
+
539
+ issues.push({
540
+ type: 'rbacDirectRPC',
541
+ file: relativePath,
542
+ line: getLineNumber(content, match.index),
543
+ message: `Direct RBAC RPC call '${rpcName}' detected. Must use pace-core API functions (isPermitted, isPermittedCached) instead.`,
544
+ code: getCodeSnippet(content, match.index, 0, 100),
545
+ severity: 'error',
546
+ fix: isEdgeFunction
547
+ ? `Use isPermitted() API: import { setupRBAC, isPermitted } from 'npm:@jmruthers/pace-core@^0.6.0/rbac'; setupRBAC(supabase); const hasPermission = await isPermitted({...});`
548
+ : `Use isPermitted() or isPermittedCached() from '@jmruthers/pace-core/rbac' instead of direct RPC calls.`,
549
+ });
550
+ }
551
+ }
552
+
553
+ return issues;
554
+ }
555
+
556
+ /**
557
+ * Check 7: Direct RBAC Table Queries (Security Critical)
558
+ *
559
+ * MIGRATED TO ESLINT: This check is now handled by 'no-direct-rbac-table' ESLint rule.
560
+ * Kept for reference only.
561
+ */
562
+ function checkDirectRBACTables_MIGRATED_TO_ESLINT(content, filePath, consumingAppPath) {
563
+ const issues = [];
564
+ const relativePath = path.relative(consumingAppPath || process.cwd(), filePath);
565
+
566
+ // RBAC tables that must use useSecureSupabase
567
+ const rbacTables = [
568
+ 'rbac_organisation_roles',
569
+ 'rbac_event_app_roles',
570
+ 'rbac_global_roles',
571
+ 'rbac_apps',
572
+ 'rbac_app_pages',
573
+ 'rbac_page_permissions',
574
+ 'rbac_user_profiles',
575
+ ];
576
+
577
+ rbacTables.forEach(tableName => {
578
+ // Find .from('rbac_*') calls
579
+ const tablePattern = new RegExp(`\\.from\\s*\\(\\s*['"]${tableName}['"]`, 'g');
580
+ let match;
581
+
582
+ while ((match = tablePattern.exec(content)) !== null) {
583
+ if (isInCommentOrString(content, match.index)) {
584
+ continue;
585
+ }
586
+
587
+ // Check if this is using useSecureSupabase
588
+ const beforeMatch = content.substring(Math.max(0, match.index - 200), match.index);
589
+ const hasSecureSupabase = /useSecureSupabase\s*\(/.test(beforeMatch) ||
590
+ /secureSupabase/.test(beforeMatch) ||
591
+ /supabase\s*=\s*useSecureSupabase/.test(beforeMatch);
592
+
593
+ if (!hasSecureSupabase) {
594
+ issues.push({
595
+ type: 'rbacDirectTable',
596
+ file: relativePath,
597
+ line: getLineNumber(content, match.index),
598
+ message: `Direct query to RBAC table '${tableName}' detected. Must use useSecureSupabase() hook instead.`,
599
+ code: getCodeSnippet(content, match.index, 0, 100),
600
+ severity: 'error',
601
+ fix: `Use useSecureSupabase() from '@jmruthers/pace-core/rbac' for all RBAC table queries.`,
602
+ });
603
+ }
604
+ }
605
+ });
606
+
607
+ return issues;
608
+ }
609
+
610
+ /**
611
+ * Check 8: Hardcoded Role Checks (Security Risk)
612
+ *
613
+ * MIGRATED TO ESLINT: This check is now handled by 'no-hardcoded-role-checks' ESLint rule.
614
+ * Kept for reference only.
615
+ */
616
+ function checkHardcodedRoleChecks_MIGRATED_TO_ESLINT(content, filePath, consumingAppPath) {
617
+ const issues = [];
618
+ const relativePath = path.relative(consumingAppPath || process.cwd(), filePath);
619
+
620
+ // Find hardcoded role comparisons
621
+ const rolePatterns = [
622
+ /(?:user|role|userRole)\.role\s*===\s*['"](admin|super_admin|superadmin|owner|manager)['"]/gi,
623
+ /role\s*===\s*['"](admin|super_admin|superadmin|owner|manager)['"]/gi,
624
+ /(?:user|role|userRole)\.role\s*!==\s*['"](admin|super_admin|superadmin|owner|manager)['"]/gi,
625
+ /role\s*!==\s*['"](admin|super_admin|superadmin|owner|manager)['"]/gi,
626
+ /(?:user|role|userRole)\.role\s*==\s*['"](admin|super_admin|superadmin|owner|manager)['"]/gi,
627
+ /role\s*==\s*['"](admin|super_admin|superadmin|owner|manager)['"]/gi,
628
+ ];
629
+
630
+ rolePatterns.forEach(pattern => {
631
+ let match;
632
+ while ((match = pattern.exec(content)) !== null) {
633
+ if (isInCommentOrString(content, match.index)) {
634
+ continue;
635
+ }
636
+
637
+ const roleValue = match[1];
638
+
639
+ issues.push({
640
+ type: 'rbacHardcodedRole',
641
+ file: relativePath,
642
+ line: getLineNumber(content, match.index),
643
+ message: `Hardcoded role check for '${roleValue}' detected. Must use pace-core APIs (useAccessLevel, getRoleContext) instead.`,
644
+ code: getCodeSnippet(content, match.index, 0, 80),
645
+ severity: 'error',
646
+ fix: `Use useAccessLevel(userId, scope) or getRoleContext({ userId, scope }) from '@jmruthers/pace-core/rbac' instead of hardcoded role checks.`,
647
+ });
648
+ }
649
+ });
650
+
651
+ return issues;
652
+ }
653
+
654
+ /**
655
+ * Check 9: enforcePermissions Configuration (App Type)
656
+ */
657
+ function checkEnforcePermissions(content, filePath, consumingAppPath) {
658
+ const issues = [];
659
+ const relativePath = path.relative(consumingAppPath || process.cwd(), filePath);
660
+
661
+ // Find PaceAppLayout usage
662
+ const paceAppLayoutPattern = /<PaceAppLayout[^>]*>/g;
663
+ let match;
664
+
665
+ while ((match = paceAppLayoutPattern.exec(content)) !== null) {
666
+ if (isInCommentOrString(content, match.index)) {
667
+ continue;
668
+ }
669
+
670
+ const layoutProps = match[0];
671
+ const hasEnforcePermissions = /enforcePermissions\s*=/.test(layoutProps);
672
+
673
+ if (!hasEnforcePermissions) {
674
+ issues.push({
675
+ type: 'rbacEnforcePermissions',
676
+ file: relativePath,
677
+ line: getLineNumber(content, match.index),
678
+ message: 'PaceAppLayout missing enforcePermissions prop. Must configure based on app type (event-based vs organisation-based).',
679
+ code: layoutProps,
680
+ severity: 'error',
681
+ fix: 'Add enforcePermissions prop: enforcePermissions={false} for event-based apps, enforcePermissions={true} for organisation-based apps.',
682
+ });
683
+ } else {
684
+ // Check if value is correct (heuristic: event-based apps typically have eventId in scope)
685
+ // This is a warning, not an error, as we can't definitively determine app type
686
+ const enforceTrue = /enforcePermissions\s*=\s*\{?\s*true\s*\}?/.test(layoutProps);
687
+ const enforceFalse = /enforcePermissions\s*=\s*\{?\s*false\s*\}?/.test(layoutProps);
688
+
689
+ // Check context for event-based indicators
690
+ const hasEventContext = /eventId|selectedEventId|useEvents/.test(content);
691
+
692
+ if (hasEventContext && enforceTrue) {
693
+ issues.push({
694
+ type: 'rbacEnforcePermissions',
695
+ file: relativePath,
696
+ line: getLineNumber(content, match.index),
697
+ message: 'PaceAppLayout has enforcePermissions={true} but app appears to be event-based. Event-based apps should use enforcePermissions={false} (pages handle checks).',
698
+ code: layoutProps,
699
+ severity: 'warning',
700
+ fix: 'Set enforcePermissions={false} for event-based apps. Pages should handle checks via PagePermissionGuard.',
701
+ });
702
+ }
703
+ }
704
+ }
705
+
706
+ return issues;
707
+ }
708
+
709
+ /**
710
+ * Check 10: Edge Functions RBAC Usage (Security Critical)
711
+ */
712
+ function checkEdgeFunctionsRBAC(consumingAppPath) {
713
+ const issues = [];
714
+
715
+ // Find Edge Function files
716
+ const functionsPath = path.join(consumingAppPath, 'supabase', 'functions');
717
+ if (!fs.existsSync(functionsPath)) {
718
+ return issues;
719
+ }
720
+
721
+ const edgeFunctionFiles = findSourceFiles(functionsPath);
722
+
723
+ edgeFunctionFiles.forEach(filePath => {
724
+ try {
725
+ const content = fs.readFileSync(filePath, 'utf8');
726
+ const relativePath = path.relative(consumingAppPath || process.cwd(), filePath);
727
+
728
+ // Check for setupRBAC call
729
+ const hasSetupRBAC = /setupRBAC\s*\(/.test(content);
730
+
731
+ // Check for isPermitted usage
732
+ const hasIsPermitted = /\bisPermitted\s*\(/.test(content);
733
+
734
+ // Check for custom RBAC helpers
735
+ const customHelperPattern = /(?:function|const|export\s+(?:function|const))\s+(checkPermission|canEdit|canDelete|hasAccess|checkAccess|rbacCheck)\s*[=(]/g;
736
+ const hasCustomHelper = customHelperPattern.test(content);
737
+
738
+ // Check for direct RPC calls
739
+ const hasDirectRPC = /\.rpc\s*\(\s*['"]rbac_/.test(content);
740
+
741
+ if (!hasSetupRBAC && (hasIsPermitted || hasCustomHelper || hasDirectRPC)) {
742
+ issues.push({
743
+ type: 'rbacEdgeFunction',
744
+ file: relativePath,
745
+ line: 1,
746
+ message: 'Edge Function uses RBAC but missing setupRBAC() call. Must call setupRBAC(supabase) before using isPermitted().',
747
+ code: '',
748
+ severity: 'error',
749
+ fix: 'Add: import { setupRBAC, isPermitted } from \'npm:@jmruthers/pace-core@^0.6.0/rbac\'; setupRBAC(supabase);',
750
+ });
751
+ }
752
+
753
+ if (hasCustomHelper) {
754
+ issues.push({
755
+ type: 'rbacEdgeFunction',
756
+ file: relativePath,
757
+ line: 1,
758
+ message: 'Edge Function contains custom RBAC helper functions. Must use isPermitted() API directly, not custom helpers.',
759
+ code: getCodeSnippet(content, content.indexOf('function') || 0, 0, 100),
760
+ severity: 'error',
761
+ fix: 'Remove custom RBAC helpers and use isPermitted() API from pace-core directly.',
762
+ });
763
+ }
764
+
765
+ if (hasDirectRPC) {
766
+ issues.push({
767
+ type: 'rbacEdgeFunction',
768
+ file: relativePath,
769
+ line: 1,
770
+ message: 'Edge Function uses direct RBAC RPC calls. Must use isPermitted() API instead.',
771
+ code: '',
772
+ severity: 'error',
773
+ fix: 'Replace direct RPC calls with isPermitted() API from pace-core.',
774
+ });
775
+ }
776
+
777
+ if (hasSetupRBAC && !hasIsPermitted && !hasDirectRPC && !hasCustomHelper) {
778
+ // setupRBAC called but no RBAC usage - might be incomplete
779
+ issues.push({
780
+ type: 'rbacEdgeFunction',
781
+ file: relativePath,
782
+ line: 1,
783
+ message: 'Edge Function calls setupRBAC() but does not use isPermitted(). Verify if RBAC checks are needed.',
784
+ code: '',
785
+ severity: 'warning',
786
+ fix: 'If RBAC checks are needed, use isPermitted() API. Otherwise, remove setupRBAC() call.',
787
+ });
788
+ }
789
+
790
+ } catch (error) {
791
+ // Skip files that can't be read
792
+ }
793
+ });
794
+
795
+ return issues;
796
+ }
797
+
798
+ /**
799
+ * Main audit function
800
+ */
801
+ function runRBACAudit(consumingAppPath = process.cwd()) {
802
+ const srcPath = path.join(consumingAppPath, 'src');
803
+ const searchPath = fs.existsSync(srcPath) ? srcPath : consumingAppPath;
804
+
805
+ if (!fs.existsSync(searchPath)) {
806
+ return {
807
+ error: `Source directory not found at ${searchPath}`,
808
+ issues: {
809
+ rbacPageGuard: [],
810
+ rbacWrapperComponent: [],
811
+ rbacWrapperFunction: [],
812
+ rbacResourceNames: [],
813
+ rbacAccessDenied: [],
814
+ rbacDirectRPC: [],
815
+ rbacDirectTable: [],
816
+ rbacHardcodedRole: [],
817
+ rbacEnforcePermissions: [],
818
+ rbacEdgeFunction: [],
819
+ },
820
+ };
821
+ }
822
+
823
+ // Find all source files
824
+ const sourceFiles = findSourceFiles(searchPath);
825
+
826
+ if (sourceFiles.length === 0) {
827
+ return {
828
+ error: `No source files found in ${searchPath}`,
829
+ issues: {
830
+ rbacPageGuard: [],
831
+ rbacWrapperComponent: [],
832
+ rbacWrapperFunction: [],
833
+ rbacResourceNames: [],
834
+ rbacAccessDenied: [],
835
+ rbacDirectRPC: [],
836
+ rbacDirectTable: [],
837
+ rbacHardcodedRole: [],
838
+ rbacEnforcePermissions: [],
839
+ rbacEdgeFunction: [],
840
+ },
841
+ };
842
+ }
843
+
844
+ const issues = {
845
+ rbacPageGuard: [],
846
+ rbacWrapperComponent: [],
847
+ rbacWrapperFunction: [],
848
+ rbacResourceNames: [],
849
+ rbacAccessDenied: [],
850
+ rbacDirectRPC: [],
851
+ rbacDirectTable: [],
852
+ rbacHardcodedRole: [],
853
+ rbacEnforcePermissions: [],
854
+ rbacEdgeFunction: [],
855
+ };
856
+
857
+ // Check each file
858
+ sourceFiles.forEach(filePath => {
859
+ try {
860
+ const content = fs.readFileSync(filePath, 'utf8');
861
+
862
+ // Run all checks - wrap each in try-catch to prevent one failure from breaking others
863
+ try {
864
+ const pageGuardIssues = checkPagePermissionGuard(content, filePath, consumingAppPath);
865
+ if (Array.isArray(pageGuardIssues)) {
866
+ issues.rbacPageGuard.push(...pageGuardIssues);
867
+ }
868
+ } catch (e) {
869
+ // Skip this check for this file
870
+ }
871
+
872
+ // NOTE: wrapperComponent, wrapperFunction, and resourceNames checks migrated to ESLint
873
+ // checkWrapperComponents → ESLint: no-rbac-wrapper-components
874
+ // checkWrapperFunctions → ESLint: no-rbac-wrapper-functions
875
+ // checkResourceNamesConstants → ESLint: rbac-use-resource-names-constants
876
+
877
+ try {
878
+ const accessDeniedIssues = checkAccessDeniedComponent(content, filePath, consumingAppPath);
879
+ if (Array.isArray(accessDeniedIssues)) {
880
+ issues.rbacAccessDenied.push(...accessDeniedIssues);
881
+ }
882
+ } catch (e) {
883
+ // Skip this check for this file
884
+ }
885
+
886
+ // NOTE: directRPC, directTable, and hardcodedRole checks migrated to ESLint
887
+ // checkDirectRBACRPC → ESLint: no-direct-rbac-rpc
888
+ // checkDirectRBACTables → ESLint: no-direct-rbac-table
889
+ // checkHardcodedRoleChecks → ESLint: no-hardcoded-role-checks
890
+
891
+ try {
892
+ const enforcePermissionsIssues = checkEnforcePermissions(content, filePath, consumingAppPath);
893
+ if (Array.isArray(enforcePermissionsIssues)) {
894
+ issues.rbacEnforcePermissions.push(...enforcePermissionsIssues);
895
+ }
896
+ } catch (e) {
897
+ // Skip this check for this file
898
+ }
899
+
900
+ } catch (error) {
901
+ // Skip files that can't be read
902
+ console.warn(`Warning: Could not read ${filePath}: ${error.message}`);
903
+ }
904
+ });
905
+
906
+ // Check Edge Functions (separate scan)
907
+ issues.rbacEdgeFunction.push(...checkEdgeFunctionsRBAC(consumingAppPath));
908
+
909
+ return {
910
+ issues,
911
+ };
912
+ }
913
+
914
+ // Export for use by other scripts
915
+ if (typeof module !== 'undefined' && module.exports) {
916
+ module.exports = { runRBACAudit };
917
+ }
918
+
919
+ // If run directly, output results
920
+ if (require.main === module) {
921
+ const consumingAppPath = process.argv[2] || process.cwd();
922
+ const result = runRBACAudit(consumingAppPath);
923
+
924
+ if (result.error) {
925
+ console.error(`Error: ${result.error}`);
926
+ process.exit(1);
927
+ }
928
+
929
+ const { issues } = result;
930
+
931
+ // Count total issues
932
+ const totalIssues = Object.values(issues).reduce((sum, arr) => sum + arr.length, 0);
933
+
934
+ if (totalIssues === 0) {
935
+ console.log('✅ No RBAC compliance issues found!');
936
+ process.exit(0);
937
+ }
938
+
939
+ console.log(`\n❌ Found ${totalIssues} RBAC compliance issue(s):\n`);
940
+
941
+ // Group by type
942
+ Object.entries(issues).forEach(([type, typeIssues]) => {
943
+ if (typeIssues.length > 0) {
944
+ console.log(`\n${type}: ${typeIssues.length} issue(s)`);
945
+ typeIssues.forEach(issue => {
946
+ console.log(` ${issue.file}:${issue.line}`);
947
+ console.log(` ${issue.message}`);
948
+ });
949
+ }
950
+ });
951
+
952
+ process.exit(1);
953
+ }
954
+