@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,790 @@
1
+ /**
2
+ * RBAC-specific rules
3
+ * @package @jmruthers/pace-core
4
+ * @module ESLintRules/rules/rbac
5
+ */
6
+
7
+ const { isPaceCoreSourceFile } = require('../utils/helpers.cjs');
8
+
9
+ module.exports = {
10
+ rules: {
11
+ /**
12
+ * Disallow direct Supabase client creation - must use useSecureSupabase
13
+ */
14
+ 'no-direct-supabase-client': {
15
+ meta: {
16
+ type: 'problem',
17
+ docs: {
18
+ description: 'Disallow direct createClient calls from @supabase/supabase-js. Use useSecureSupabase() from pace-core instead to ensure organisation context and RLS policies are enforced.',
19
+ category: 'Security',
20
+ recommended: true
21
+ },
22
+ messages: {
23
+ directClientCreation: "Direct Supabase client creation detected. You MUST use useSecureSupabase() from '@jmruthers/pace-core/rbac' instead to ensure organisation context and RLS policies are enforced. This prevents cross-organisation data access.",
24
+ directClientImport: "Direct import of createClient from @supabase/supabase-js is not allowed. Use useSecureSupabase() from '@jmruthers/pace-core/rbac' instead."
25
+ },
26
+ hasSuggestions: true
27
+ },
28
+ create(context) {
29
+ const filename = context.getFilename();
30
+
31
+ // Exclude pace-core source files - these rules are for consuming apps
32
+ if (isPaceCoreSourceFile(filename)) {
33
+ return {};
34
+ }
35
+
36
+ // Allow createClient in specific config files (supabaseClient.ts/js, etc.)
37
+ const isConfigFile = /(supabase|client)\.(ts|js|tsx|jsx)$/i.test(filename) &&
38
+ (filename.includes('supabase') || filename.includes('client'));
39
+
40
+ return {
41
+ ImportDeclaration(node) {
42
+ const importSource = node.source.value;
43
+
44
+ // Check for @supabase/supabase-js import
45
+ if (importSource === '@supabase/supabase-js') {
46
+ // Check if createClient is imported
47
+ const hasCreateClient = node.specifiers.some(spec => {
48
+ if (spec.type === 'ImportSpecifier') {
49
+ return spec.imported.name === 'createClient';
50
+ }
51
+ if (spec.type === 'ImportNamespaceSpecifier') {
52
+ return true; // import * as supabase
53
+ }
54
+ return false;
55
+ });
56
+
57
+ if (hasCreateClient && !isConfigFile) {
58
+ context.report({
59
+ node: node.source,
60
+ messageId: 'directClientImport',
61
+ suggest: [{
62
+ desc: 'Replace with useSecureSupabase hook',
63
+ fix(fixer) {
64
+ return fixer.remove(node);
65
+ }
66
+ }]
67
+ });
68
+ }
69
+ }
70
+ },
71
+
72
+ CallExpression(node) {
73
+ // Check for createClient() calls
74
+ if (node.callee.type === 'Identifier' && node.callee.name === 'createClient') {
75
+ if (!isConfigFile) {
76
+ context.report({
77
+ node,
78
+ messageId: 'directClientCreation',
79
+ suggest: [{
80
+ desc: 'Use useSecureSupabase() hook instead',
81
+ fix(fixer) {
82
+ return null;
83
+ }
84
+ }]
85
+ });
86
+ }
87
+ }
88
+
89
+ // Check for supabase.createClient() or similar patterns
90
+ if (node.callee.type === 'MemberExpression' &&
91
+ node.callee.property?.name === 'createClient') {
92
+ if (!isConfigFile) {
93
+ context.report({
94
+ node,
95
+ messageId: 'directClientCreation',
96
+ suggest: [{
97
+ desc: 'Use useSecureSupabase() hook instead',
98
+ fix(fixer) {
99
+ return null;
100
+ }
101
+ }]
102
+ });
103
+ }
104
+ }
105
+ }
106
+ };
107
+ }
108
+ },
109
+
110
+ /**
111
+ * Check RBAC permission loading state usage
112
+ */
113
+ 'rbac-permission-loading': {
114
+ meta: {
115
+ type: 'problem',
116
+ docs: {
117
+ description: 'Require isLoading extraction from useResourcePermissions and check it before permission calls in mutations.',
118
+ category: 'Security',
119
+ recommended: true
120
+ },
121
+ messages: {
122
+ missingIsLoading: "useResourcePermissions is used but 'isLoading' is not extracted. Permission checks may fail if scope resolution is still in progress.",
123
+ missingLoadingCheck: "Permission check '{{permission}}()' is called without checking isLoading first. This can cause false negatives when scope resolution is still in progress."
124
+ },
125
+ hasSuggestions: true
126
+ },
127
+ create(context) {
128
+ let useResourcePermissionsFound = false;
129
+ let hasIsLoadingExtraction = false;
130
+ let isLoadingVarName = null;
131
+
132
+ return {
133
+ CallExpression(node) {
134
+ // Find useResourcePermissions calls
135
+ if (node.callee.type === 'Identifier' &&
136
+ node.callee.name === 'useResourcePermissions') {
137
+ useResourcePermissionsFound = true;
138
+
139
+ // Check parent to see if isLoading is destructured
140
+ const parent = node.parent;
141
+ if (parent && parent.type === 'VariableDeclarator' &&
142
+ parent.id && parent.id.type === 'ObjectPattern') {
143
+ const properties = parent.id.properties;
144
+ const isLoadingProp = properties.find(prop => {
145
+ if (prop.type === 'Property') {
146
+ const key = prop.key;
147
+ if (key.type === 'Identifier' && key.name === 'isLoading') {
148
+ return true;
149
+ }
150
+ // Also check for renamed: isLoading: permissionsLoading
151
+ if (key.type === 'Identifier' && key.name === 'isLoading') {
152
+ if (prop.value && prop.value.type === 'Identifier') {
153
+ isLoadingVarName = prop.value.name;
154
+ }
155
+ return true;
156
+ }
157
+ }
158
+ return false;
159
+ });
160
+
161
+ if (isLoadingProp) {
162
+ hasIsLoadingExtraction = true;
163
+ if (!isLoadingVarName) {
164
+ isLoadingVarName = 'isLoading';
165
+ }
166
+ }
167
+ }
168
+ }
169
+
170
+ // Check for permission function calls in mutations
171
+ if (useResourcePermissionsFound &&
172
+ node.callee.type === 'Identifier' &&
173
+ ['canCreate', 'canUpdate', 'canDelete', 'canRead'].includes(node.callee.name)) {
174
+
175
+ // Check if we're in a mutation context
176
+ const sourceCode = context.sourceCode || context.getSourceCode();
177
+ // ESLint 9: use sourceCode.getAncestors(node), ESLint 8: use context.getAncestors()
178
+ const ancestors = sourceCode.getAncestors ? sourceCode.getAncestors(node) : (context.getAncestors ? context.getAncestors() : []);
179
+ const isInMutation = ancestors.some(ancestor => {
180
+ if (ancestor.type === 'Property' && ancestor.key) {
181
+ const keyName = ancestor.key.name || (ancestor.key.type === 'Identifier' && ancestor.key.name);
182
+ return keyName === 'mutationFn' || keyName === 'mutateAsync';
183
+ }
184
+ return false;
185
+ });
186
+
187
+ if (isInMutation) {
188
+ // Check if isLoading is checked before this call
189
+ const nodeText = sourceCode.getText(node);
190
+ const beforeNode = sourceCode.getText().substring(0, sourceCode.getIndexFromLoc(node.loc.start));
191
+
192
+ // Look for isLoading check before this call
193
+ const hasLoadingCheck = new RegExp(
194
+ `(if\\s*\\(\\s*!?\\s*(${isLoadingVarName || 'isLoading'})|if\\s*\\(\\s*(${isLoadingVarName || 'isLoading'})\\s*===\\s*false|if\\s*\\(\\s*(${isLoadingVarName || 'isLoading'})\\s*!==\\s*true|await)`,
195
+ 'i'
196
+ ).test(beforeNode);
197
+
198
+ if (!hasIsLoadingExtraction) {
199
+ context.report({
200
+ node,
201
+ messageId: 'missingIsLoading',
202
+ suggest: [{
203
+ desc: `Extract 'isLoading' from useResourcePermissions`,
204
+ fix(fixer) {
205
+ return null; // Complex fix
206
+ }
207
+ }]
208
+ });
209
+ } else if (!hasLoadingCheck) {
210
+ context.report({
211
+ node,
212
+ messageId: 'missingLoadingCheck',
213
+ data: {
214
+ permission: node.callee.name
215
+ },
216
+ suggest: [{
217
+ desc: `Check ${isLoadingVarName || 'isLoading'} before calling permission function`,
218
+ fix(fixer) {
219
+ return null; // Complex fix
220
+ }
221
+ }]
222
+ });
223
+ }
224
+ }
225
+ }
226
+ }
227
+ };
228
+ }
229
+ },
230
+
231
+ /**
232
+ * Disallow direct RPC calls to RBAC functions
233
+ */
234
+ 'no-direct-rbac-rpc': {
235
+ meta: {
236
+ type: 'problem',
237
+ docs: {
238
+ description: 'Disallow direct calls to RBAC RPC functions. Use pace-core RBAC hooks instead.',
239
+ category: 'Security',
240
+ recommended: true
241
+ },
242
+ messages: {
243
+ directRbacRpc: "Direct RPC call to '{{rpcName}}' detected. Use pace-core RBAC hooks from '@jmruthers/pace-core/rbac' instead."
244
+ },
245
+ hasSuggestions: true
246
+ },
247
+ create(context) {
248
+ const filename = context.getFilename();
249
+
250
+ // Exclude pace-core source files - these rules are for consuming apps
251
+ if (isPaceCoreSourceFile(filename)) {
252
+ return {};
253
+ }
254
+
255
+ const rbacRpcPatterns = ['rbac_check_permission_simplified', 'rbac_'];
256
+
257
+ return {
258
+ CallExpression(node) {
259
+ // Check for supabase.rpc('rbac_*', ...)
260
+ if (node.callee.type === 'MemberExpression' &&
261
+ node.callee.property &&
262
+ node.callee.property.name === 'rpc' &&
263
+ node.arguments.length > 0) {
264
+
265
+ const firstArg = node.arguments[0];
266
+ if (firstArg.type === 'Literal' && typeof firstArg.value === 'string') {
267
+ const rpcName = firstArg.value;
268
+ if (rpcName.startsWith('rbac_')) {
269
+ context.report({
270
+ node: firstArg,
271
+ messageId: 'directRbacRpc',
272
+ data: {
273
+ rpcName
274
+ },
275
+ suggest: [{
276
+ desc: 'Use pace-core RBAC hooks instead',
277
+ fix(fixer) {
278
+ return null;
279
+ }
280
+ }]
281
+ });
282
+ }
283
+ }
284
+ }
285
+ }
286
+ };
287
+ }
288
+ },
289
+
290
+ /**
291
+ * Disallow direct queries to RBAC tables
292
+ */
293
+ 'no-direct-rbac-table': {
294
+ meta: {
295
+ type: 'problem',
296
+ docs: {
297
+ description: 'Disallow direct queries to RBAC tables. Use pace-core RBAC API functions or useSecureSupabase instead.',
298
+ category: 'Security',
299
+ recommended: true
300
+ },
301
+ messages: {
302
+ directRbacTable: "Direct query to RBAC table '{{tableName}}' detected. Use pace-core RBAC API functions from '@jmruthers/pace-core/rbac' or useSecureSupabase hook instead."
303
+ },
304
+ hasSuggestions: true
305
+ },
306
+ create(context) {
307
+ const filename = context.getFilename();
308
+
309
+ // Exclude pace-core source files - these rules are for consuming apps
310
+ if (isPaceCoreSourceFile(filename)) {
311
+ return {};
312
+ }
313
+
314
+ const rbacTables = [
315
+ 'rbac_organisation_roles',
316
+ 'rbac_event_app_roles',
317
+ 'rbac_global_roles',
318
+ 'rbac_apps',
319
+ 'rbac_app_pages',
320
+ 'rbac_page_permissions',
321
+ 'rbac_user_profiles'
322
+ ];
323
+
324
+ let hasSecureSupabase = false;
325
+ let secureSupabaseVarName = null;
326
+
327
+ return {
328
+ ImportDeclaration(node) {
329
+ const importSource = node.source.value;
330
+ if (importSource === '@jmruthers/pace-core/rbac' ||
331
+ importSource.startsWith('@jmruthers/pace-core/rbac/')) {
332
+ if (node.specifiers.some(spec =>
333
+ spec.type === 'ImportSpecifier' && spec.imported.name === 'useSecureSupabase'
334
+ )) {
335
+ hasSecureSupabase = true;
336
+ }
337
+ }
338
+ },
339
+ VariableDeclarator(node) {
340
+ if (node.init &&
341
+ node.init.type === 'CallExpression' &&
342
+ node.init.callee &&
343
+ node.init.callee.name === 'useSecureSupabase') {
344
+ if (node.id.type === 'Identifier') {
345
+ secureSupabaseVarName = node.id.name;
346
+ }
347
+ }
348
+ },
349
+ CallExpression(node) {
350
+ if (node.callee.type === 'MemberExpression' &&
351
+ node.callee.property &&
352
+ node.callee.property.name === 'from' &&
353
+ node.arguments.length > 0) {
354
+
355
+ // Check if this is called on secureSupabase
356
+ let isSecureClient = false;
357
+ if (node.callee.object.type === 'Identifier') {
358
+ const objName = node.callee.object.name;
359
+ if (objName === 'secureSupabase' ||
360
+ objName === secureSupabaseVarName ||
361
+ (hasSecureSupabase && objName.includes('secure'))) {
362
+ isSecureClient = true;
363
+ }
364
+ }
365
+
366
+ // Allow queries through secureSupabase
367
+ if (isSecureClient) {
368
+ return;
369
+ }
370
+
371
+ const firstArg = node.arguments[0];
372
+ if (firstArg.type === 'Literal' && typeof firstArg.value === 'string') {
373
+ const tableName = firstArg.value;
374
+ if (rbacTables.includes(tableName) || tableName.startsWith('rbac_')) {
375
+ context.report({
376
+ node: firstArg,
377
+ messageId: 'directRbacTable',
378
+ data: {
379
+ tableName
380
+ },
381
+ suggest: [{
382
+ desc: 'Use useSecureSupabase hook or pace-core RBAC API functions',
383
+ fix(fixer) {
384
+ return null;
385
+ }
386
+ }]
387
+ });
388
+ }
389
+ }
390
+ }
391
+ }
392
+ };
393
+ }
394
+ },
395
+
396
+ /**
397
+ * Disallow hardcoded role checks
398
+ */
399
+ 'no-hardcoded-role-checks': {
400
+ meta: {
401
+ type: 'problem',
402
+ docs: {
403
+ description: 'Disallow hardcoded role checks. Use useAccessLevel hook or getRoleContext API from pace-core instead.',
404
+ category: 'Security',
405
+ recommended: true
406
+ },
407
+ messages: {
408
+ hardcodedRoleCheck: "Hardcoded role check detected. Use useAccessLevel hook or getRoleContext API from '@jmruthers/pace-core/rbac' instead."
409
+ },
410
+ hasSuggestions: true
411
+ },
412
+ create(context) {
413
+ const filename = context.getFilename();
414
+
415
+ // Exclude pace-core source files - these rules are for consuming apps
416
+ if (isPaceCoreSourceFile(filename)) {
417
+ return {};
418
+ }
419
+
420
+ const roleNames = ['admin', 'org_admin', 'event_admin', 'user', 'member', 'viewer', 'editor'];
421
+ const roleCheckPattern = new RegExp(
422
+ `(?:role|user\\.role|userRole|currentRole|userRoleName)\\s*(?:===|!==|==|!=)\\s*['"](${roleNames.join('|')})['"]`,
423
+ 'i'
424
+ );
425
+
426
+ let hasPaceCoreImport = false;
427
+
428
+ return {
429
+ ImportDeclaration(node) {
430
+ const importSource = node.source.value;
431
+ if (importSource === '@jmruthers/pace-core/rbac' ||
432
+ importSource.startsWith('@jmruthers/pace-core/rbac/')) {
433
+ hasPaceCoreImport = true;
434
+ }
435
+ },
436
+ BinaryExpression(node) {
437
+ if (node.operator === '===' || node.operator === '!==' || node.operator === '==' || node.operator === '!=') {
438
+ const sourceCode = context.getSourceCode();
439
+ const leftText = sourceCode.getText(node.left);
440
+ const rightText = sourceCode.getText(node.right);
441
+
442
+ // Check if it's a role comparison
443
+ if (roleCheckPattern.test(leftText + ' ' + node.operator + ' ' + rightText)) {
444
+ // Check if using pace-core APIs
445
+ if (!hasPaceCoreImport ||
446
+ (!leftText.includes('useAccessLevel') && !leftText.includes('getRoleContext'))) {
447
+ context.report({
448
+ node,
449
+ messageId: 'hardcodedRoleCheck',
450
+ suggest: [{
451
+ desc: 'Use useAccessLevel hook or getRoleContext API',
452
+ fix(fixer) {
453
+ return null;
454
+ }
455
+ }]
456
+ });
457
+ }
458
+ }
459
+ }
460
+ }
461
+ };
462
+ }
463
+ },
464
+
465
+ /**
466
+ * Require RESOURCE_NAMES constants in useResourcePermissions
467
+ */
468
+ 'rbac-use-resource-names-constants': {
469
+ meta: {
470
+ type: 'problem',
471
+ docs: {
472
+ description: 'Require RESOURCE_NAMES constants instead of string literals in useResourcePermissions calls.',
473
+ category: 'Best Practices',
474
+ recommended: true
475
+ },
476
+ messages: {
477
+ resourcePermissionStringLiteral: "Resource permission string literal detected. Use RESOURCE_NAMES constant object instead (e.g., RESOURCE_NAMES.ORGANISATIONS, RESOURCE_NAMES.EVENTS)."
478
+ },
479
+ hasSuggestions: true
480
+ },
481
+ create(context) {
482
+ const filename = context.getFilename();
483
+
484
+ // Exclude pace-core source files - these rules are for consuming apps
485
+ if (isPaceCoreSourceFile(filename)) {
486
+ return {};
487
+ }
488
+
489
+ let hasResourceNamesImport = false;
490
+
491
+ return {
492
+ ImportDeclaration(node) {
493
+ const importSource = node.source.value;
494
+ if (node.specifiers.some(spec =>
495
+ spec.type === 'ImportSpecifier' && spec.imported.name === 'RESOURCE_NAMES'
496
+ )) {
497
+ hasResourceNamesImport = true;
498
+ }
499
+ },
500
+ CallExpression(node) {
501
+ if (node.callee.type === 'Identifier' &&
502
+ node.callee.name === 'useResourcePermissions') {
503
+ // Check if argument is a string literal
504
+ if (node.arguments.length > 0 &&
505
+ node.arguments[0].type === 'Literal' &&
506
+ typeof node.arguments[0].value === 'string') {
507
+ // Check if RESOURCE_NAMES is used in the file
508
+ const sourceCode = context.getSourceCode();
509
+ const fileText = sourceCode.getText();
510
+
511
+ if (!hasResourceNamesImport || !fileText.includes('RESOURCE_NAMES.')) {
512
+ context.report({
513
+ node: node.arguments[0],
514
+ messageId: 'resourcePermissionStringLiteral',
515
+ suggest: [{
516
+ desc: 'Use RESOURCE_NAMES constant instead',
517
+ fix(fixer) {
518
+ return null; // Complex fix
519
+ }
520
+ }]
521
+ });
522
+ }
523
+ }
524
+ }
525
+ }
526
+ };
527
+ }
528
+ },
529
+
530
+ /**
531
+ * Disallow wrapper components around PagePermissionGuard
532
+ */
533
+ 'no-rbac-wrapper-components': {
534
+ meta: {
535
+ type: 'problem',
536
+ docs: {
537
+ description: 'Disallow wrapper components around PagePermissionGuard. Use PagePermissionGuard directly.',
538
+ category: 'Security',
539
+ recommended: true
540
+ },
541
+ messages: {
542
+ wrapperComponent: "Wrapper component '{{componentName}}' detected around PagePermissionGuard. Must use PagePermissionGuard directly, not through wrappers."
543
+ },
544
+ hasSuggestions: true
545
+ },
546
+ create(context) {
547
+ const filename = context.getFilename();
548
+
549
+ // Allow in pace-core package itself
550
+ if (filename.includes('packages/core/src/rbac') || filename.includes('packages\\core\\src\\rbac')) {
551
+ return {};
552
+ }
553
+
554
+ let hasPagePermissionGuard = false;
555
+ let wrapperComponentName = null;
556
+
557
+ return {
558
+ JSXOpeningElement(node) {
559
+ if (node.name.type === 'JSXIdentifier' && node.name.name === 'PagePermissionGuard') {
560
+ hasPagePermissionGuard = true;
561
+
562
+ // Check if this is inside a wrapper component
563
+ const sourceCode = context.sourceCode || context.getSourceCode();
564
+ // ESLint 9: use sourceCode.getAncestors(node), ESLint 8: use context.getAncestors()
565
+ const ancestors = sourceCode.getAncestors ? sourceCode.getAncestors(node) : (context.getAncestors ? context.getAncestors() : []);
566
+ const componentAncestor = ancestors.find(ancestor =>
567
+ ancestor.type === 'FunctionDeclaration' ||
568
+ (ancestor.type === 'VariableDeclarator' && ancestor.init &&
569
+ (ancestor.init.type === 'ArrowFunctionExpression' || ancestor.init.type === 'FunctionExpression'))
570
+ );
571
+
572
+ if (componentAncestor) {
573
+ let componentName = null;
574
+ let componentParams = null;
575
+
576
+ if (componentAncestor.type === 'FunctionDeclaration' && componentAncestor.id) {
577
+ componentName = componentAncestor.id.name;
578
+ componentParams = componentAncestor.params;
579
+ } else if (componentAncestor.type === 'VariableDeclarator' && componentAncestor.id.type === 'Identifier') {
580
+ componentName = componentAncestor.id.name;
581
+ // For arrow functions and function expressions, get params from init
582
+ if (componentAncestor.init) {
583
+ if (componentAncestor.init.type === 'ArrowFunctionExpression' ||
584
+ componentAncestor.init.type === 'FunctionExpression') {
585
+ componentParams = componentAncestor.init.params;
586
+ }
587
+ }
588
+ }
589
+
590
+ // Check if component accepts pageName as a FUNCTION PARAMETER (not just uses it in JSX)
591
+ // This distinguishes wrapper components from legitimate page components
592
+ // Wrapper pattern: function Wrapper({ pageName }) { return <PagePermissionGuard pageName={pageName}> }
593
+ // Legitimate pattern: function Page() { return <PagePermissionGuard pageName={PAGE_NAMES.PAGE}> }
594
+ const acceptsPageNameAsParam = componentParams && componentParams.some(param => {
595
+ if (param.type === 'Identifier') {
596
+ return param.name === 'pageName';
597
+ }
598
+ // Handle destructured params: { pageName } or { pageName: pn }
599
+ if (param.type === 'ObjectPattern') {
600
+ return param.properties.some(prop => {
601
+ if (prop.type === 'Property') {
602
+ const key = prop.key;
603
+ if (key.type === 'Identifier') {
604
+ return key.name === 'pageName';
605
+ }
606
+ }
607
+ return false;
608
+ });
609
+ }
610
+ return false;
611
+ });
612
+
613
+ // Only flag if component accepts pageName as a parameter (wrapper pattern)
614
+ // Legitimate page components use constants like PAGE_NAMES.CONTACTS, not accept it as prop
615
+ if (acceptsPageNameAsParam) {
616
+ context.report({
617
+ node,
618
+ messageId: 'wrapperComponent',
619
+ data: {
620
+ componentName: componentName || 'Unknown'
621
+ },
622
+ suggest: [{
623
+ desc: 'Remove wrapper component and use PagePermissionGuard directly in pages',
624
+ fix(fixer) {
625
+ return null;
626
+ }
627
+ }]
628
+ });
629
+ }
630
+ }
631
+ }
632
+ }
633
+ };
634
+ }
635
+ },
636
+
637
+ /**
638
+ * Disallow wrapper functions around permission hooks
639
+ */
640
+ 'no-rbac-wrapper-functions': {
641
+ meta: {
642
+ type: 'problem',
643
+ docs: {
644
+ description: 'Disallow wrapper functions around pace-core permission hooks. Use hooks directly in components.',
645
+ category: 'Security',
646
+ recommended: true
647
+ },
648
+ messages: {
649
+ wrapperFunction: "Permission wrapper function '{{functionName}}' detected. Use pace-core hooks (useCan, useResourcePermissions) directly in components instead of wrapping them."
650
+ },
651
+ hasSuggestions: true
652
+ },
653
+ create(context) {
654
+ const filename = context.getFilename();
655
+
656
+ // Exclude pace-core source files - these rules are for consuming apps
657
+ if (isPaceCoreSourceFile(filename)) {
658
+ return {};
659
+ }
660
+
661
+ const permissionHookNames = ['useCan', 'useResourcePermissions', 'usePermissions', 'useMultiplePermissions'];
662
+ const permissionFunctionNames = ['canCreate', 'canUpdate', 'canDelete', 'canRead', 'can'];
663
+
664
+ let hasPaceCoreRBACImport = false;
665
+
666
+ return {
667
+ ImportDeclaration(node) {
668
+ const importSource = node.source.value;
669
+ if (importSource === '@jmruthers/pace-core/rbac' ||
670
+ importSource.startsWith('@jmruthers/pace-core/rbac/')) {
671
+ hasPaceCoreRBACImport = true;
672
+ }
673
+ },
674
+ FunctionDeclaration(node) {
675
+ if (!node.id || !hasPaceCoreRBACImport) return;
676
+
677
+ const functionName = node.id.name;
678
+ if (!functionName) return;
679
+
680
+ // Check if function body uses permission hooks/functions
681
+ const sourceCode = context.getSourceCode();
682
+ const functionText = sourceCode.getText(node.body);
683
+
684
+ // Check if function uses permission hooks or functions
685
+ const usesPermissionHooks = permissionHookNames.some(hook => functionText.includes(hook));
686
+ const usesPermissionFunctions = permissionFunctionNames.some(fn => functionText.includes(fn));
687
+
688
+ // Check if function has additional logic beyond permission checking
689
+ const hasAdditionalLogic = (
690
+ functionText.includes('&&') ||
691
+ functionText.includes('||') ||
692
+ functionText.includes('if') ||
693
+ (functionText.match(/return/g) || []).length > 1 ||
694
+ functionText.includes('.find') ||
695
+ functionText.includes('.filter') ||
696
+ functionText.includes('.map')
697
+ );
698
+
699
+ // Pattern: Functions that start with 'can' and use permission hooks/functions
700
+ // and have additional logic
701
+ const isPermissionWrapper = (
702
+ (functionName.toLowerCase().startsWith('can') ||
703
+ functionName.toLowerCase().includes('permission') ||
704
+ functionName.toLowerCase().includes('access')) &&
705
+ (usesPermissionHooks || usesPermissionFunctions) &&
706
+ hasAdditionalLogic &&
707
+ node.params.length > 0
708
+ );
709
+
710
+ if (isPermissionWrapper) {
711
+ context.report({
712
+ node: node.id,
713
+ messageId: 'wrapperFunction',
714
+ data: {
715
+ functionName
716
+ },
717
+ suggest: [{
718
+ desc: 'Use permission hooks directly in components',
719
+ fix(fixer) {
720
+ return null;
721
+ }
722
+ }]
723
+ });
724
+ }
725
+ },
726
+ VariableDeclarator(node) {
727
+ if (!hasPaceCoreRBACImport) return;
728
+ if (node.id.type !== 'Identifier') return;
729
+
730
+ const varName = node.id.name;
731
+ if (!varName) return;
732
+
733
+ // Only check arrow functions and function expressions
734
+ if (!node.init ||
735
+ (node.init.type !== 'ArrowFunctionExpression' &&
736
+ node.init.type !== 'FunctionExpression')) {
737
+ return;
738
+ }
739
+
740
+ const sourceCode = context.getSourceCode();
741
+ const functionText = sourceCode.getText(node.init);
742
+
743
+ // Check if function uses permission hooks or functions
744
+ const usesPermissionHooks = permissionHookNames.some(hook => functionText.includes(hook));
745
+ const usesPermissionFunctions = permissionFunctionNames.some(fn => functionText.includes(fn));
746
+
747
+ // Check if function has additional logic beyond permission checking
748
+ const hasAdditionalLogic = (
749
+ functionText.includes('&&') ||
750
+ functionText.includes('||') ||
751
+ functionText.includes('if') ||
752
+ (functionText.match(/return/g) || []).length > 1 ||
753
+ functionText.includes('.find') ||
754
+ functionText.includes('.filter') ||
755
+ functionText.includes('.map')
756
+ );
757
+
758
+ // Pattern: Variables that start with 'can' and use permission hooks/functions
759
+ // and have additional logic
760
+ const isPermissionWrapper = (
761
+ (varName.toLowerCase().startsWith('can') ||
762
+ varName.toLowerCase().includes('permission') ||
763
+ varName.toLowerCase().includes('access')) &&
764
+ (usesPermissionHooks || usesPermissionFunctions) &&
765
+ hasAdditionalLogic &&
766
+ node.init.params && node.init.params.length > 0
767
+ );
768
+
769
+ if (isPermissionWrapper) {
770
+ context.report({
771
+ node: node.id,
772
+ messageId: 'wrapperFunction',
773
+ data: {
774
+ functionName: varName
775
+ },
776
+ suggest: [{
777
+ desc: 'Use permission hooks directly in components',
778
+ fix(fixer) {
779
+ return null;
780
+ }
781
+ }]
782
+ });
783
+ }
784
+ }
785
+ };
786
+ }
787
+ }
788
+ }
789
+ };
790
+