@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
@@ -11,6 +11,7 @@ import { renderHook, waitFor } from '@testing-library/react';
11
11
  import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
12
12
  import { useResourcePermissions } from './useResourcePermissions';
13
13
  import type { Scope } from '../types';
14
+ import { isSuperAdmin } from '../api';
14
15
 
15
16
  // Mock dependencies
16
17
  vi.mock('../../providers/services/UnifiedAuthProvider', () => ({
@@ -33,6 +34,12 @@ vi.mock('./usePermissions', () => ({
33
34
  useCan: vi.fn(),
34
35
  }));
35
36
 
37
+ vi.mock('../api', () => ({
38
+ isSuperAdmin: vi.fn(),
39
+ }));
40
+
41
+ const mockIsSuperAdmin = vi.mocked(isSuperAdmin);
42
+
36
43
  import { useUnifiedAuth } from '../../providers/services/UnifiedAuthProvider';
37
44
  import { useOrganisations } from '../../hooks/useOrganisations';
38
45
  import { useEvents } from '../../hooks/useEvents';
@@ -89,11 +96,14 @@ describe('useResourcePermissions Hook', () => {
89
96
  } as any);
90
97
 
91
98
  mockUseResolvedScope.mockReturnValue({
92
- resolvedScope: mockScope,
99
+ resolvedScope: mockScope, // Use correct property name from UseResolvedScopeReturn interface
93
100
  isLoading: false,
94
101
  error: null,
95
102
  });
96
103
 
104
+ // Mock isSuperAdmin to resolve immediately (prevents isLoading from being true due to super admin check)
105
+ mockIsSuperAdmin.mockResolvedValue(false);
106
+
97
107
  // Default useCan mocks - all permissions allowed
98
108
  mockUseCan.mockReturnValue({
99
109
  can: true,
@@ -117,9 +127,16 @@ describe('useResourcePermissions Hook', () => {
117
127
  expect(result.current.canRead).toBeTypeOf('function');
118
128
  });
119
129
 
120
- it('returns scope object', () => {
130
+ it('returns scope object', async () => {
121
131
  const { result } = renderHook(() => useResourcePermissions('contacts'));
122
132
 
133
+ // Wait for super admin check to complete
134
+ await waitFor(() => {
135
+ expect(result.current.isLoading).toBe(false);
136
+ });
137
+
138
+ // The hook returns resolvedScope if available, otherwise fallback scope
139
+ // Since mockScope has appId, it should be returned as-is
123
140
  expect(result.current.scope).toEqual(mockScope);
124
141
  });
125
142
 
@@ -130,27 +147,46 @@ describe('useResourcePermissions Hook', () => {
130
147
  supabase: mockSupabase,
131
148
  selectedOrganisationId: 'org-123',
132
149
  selectedEventId: 'event-123',
150
+ selectedEventOrganisationId: null, // Mock event doesn't have organisation_id, so it becomes null
133
151
  });
134
152
  });
135
153
 
136
- it('calls useCan for each permission type', () => {
154
+ it('calls useCan for each permission type', async () => {
137
155
  renderHook(() => useResourcePermissions('contacts'));
138
156
 
139
- expect(mockUseCan).toHaveBeenCalledTimes(4); // create, update, delete, read
140
- // When appId is available in scope, resource name is passed as pageId to enable page permission checks
157
+ // Wait for super admin check to complete
158
+ await waitFor(() => {
159
+ expect(mockUseCan).toHaveBeenCalled();
160
+ });
161
+
162
+ // The hook calls useCan for each permission type (create, update, delete, read)
163
+ // Note: It may be called multiple times due to re-renders during super admin check,
164
+ // so we verify that all four permission types are checked rather than exact call count
165
+ const calls = mockUseCan.mock.calls;
166
+ expect(calls.length).toBeGreaterThanOrEqual(4);
167
+
168
+ // When appId is available in resolvedScope, permission strings include page. prefix
169
+ // and resource name is passed as pageId to enable page permission checks
141
170
  expect(mockUseCan).toHaveBeenCalledWith(
142
171
  'user-123',
143
- mockScope,
144
- 'create:contacts',
145
- 'contacts', // pageId is resource name when appId is available
172
+ mockScope, // resolvedScope is used when available
173
+ 'create:page.contacts',
174
+ 'contacts', // pageId is resource name when appId is available in resolvedScope
146
175
  true,
147
- null, // precomputedSuperAdmin
176
+ false, // precomputedSuperAdmin (resolved after async check)
148
177
  undefined // appName
149
178
  );
179
+
180
+ // Verify all four permission types are checked
181
+ const permissions = calls.map(call => call[2]); // permission is the 3rd argument
182
+ expect(permissions).toContain('create:page.contacts');
183
+ expect(permissions).toContain('update:page.contacts');
184
+ expect(permissions).toContain('delete:page.contacts');
185
+ expect(permissions).toContain('read:page.contacts');
150
186
  expect(mockUseCan).toHaveBeenCalledWith(
151
187
  'user-123',
152
188
  mockScope,
153
- 'update:contacts',
189
+ 'update:page.contacts',
154
190
  'contacts', // pageId is resource name when appId is available
155
191
  true,
156
192
  null, // precomputedSuperAdmin
@@ -159,7 +195,7 @@ describe('useResourcePermissions Hook', () => {
159
195
  expect(mockUseCan).toHaveBeenCalledWith(
160
196
  'user-123',
161
197
  mockScope,
162
- 'delete:contacts',
198
+ 'delete:page.contacts',
163
199
  'contacts', // pageId is resource name when appId is available
164
200
  true,
165
201
  null, // precomputedSuperAdmin
@@ -168,7 +204,7 @@ describe('useResourcePermissions Hook', () => {
168
204
  expect(mockUseCan).toHaveBeenCalledWith(
169
205
  'user-123',
170
206
  mockScope,
171
- 'read:contacts',
207
+ 'read:page.contacts',
172
208
  'contacts', // pageId is resource name when appId is available
173
209
  true,
174
210
  null, // precomputedSuperAdmin
@@ -191,9 +227,10 @@ describe('useResourcePermissions Hook', () => {
191
227
  expect(result.current.canCreate('contacts')).toBe(true);
192
228
  });
193
229
 
194
- it('returns false when user cannot create resource', () => {
230
+ it('returns false when user cannot create resource', async () => {
195
231
  mockUseCan.mockImplementation((userId, scope, permission) => {
196
- if (permission === 'create:contacts') {
232
+ // When appId is available in resolvedScope, permission format is 'create:page.contacts'
233
+ if (permission === 'create:page.contacts' || permission === 'create:contacts') {
197
234
  return {
198
235
  can: false,
199
236
  isLoading: false,
@@ -211,6 +248,11 @@ describe('useResourcePermissions Hook', () => {
211
248
 
212
249
  const { result } = renderHook(() => useResourcePermissions('contacts'));
213
250
 
251
+ // Wait for super admin check to complete
252
+ await waitFor(() => {
253
+ expect(result.current.isLoading).toBe(false);
254
+ });
255
+
214
256
  expect(result.current.canCreate('contacts')).toBe(false);
215
257
  expect(result.current.canUpdate('contacts')).toBe(true);
216
258
  expect(result.current.canDelete('contacts')).toBe(true);
@@ -232,9 +274,10 @@ describe('useResourcePermissions Hook', () => {
232
274
  expect(result.current.canRead('contacts')).toBe(true);
233
275
  });
234
276
 
235
- it('checks read permissions when enableRead is true', () => {
277
+ it('checks read permissions when enableRead is true', async () => {
236
278
  mockUseCan.mockImplementation((userId, scope, permission) => {
237
- if (permission === 'read:contacts') {
279
+ // When appId is available in resolvedScope, permission format is 'read:page.contacts'
280
+ if (permission === 'read:page.contacts' || permission === 'read:contacts') {
238
281
  return {
239
282
  can: true,
240
283
  isLoading: false,
@@ -254,12 +297,18 @@ describe('useResourcePermissions Hook', () => {
254
297
  useResourcePermissions('contacts', { enableRead: true })
255
298
  );
256
299
 
300
+ // Wait for super admin check to complete
301
+ await waitFor(() => {
302
+ expect(result.current.isLoading).toBe(false);
303
+ });
304
+
257
305
  expect(result.current.canRead('contacts')).toBe(true);
258
306
  });
259
307
 
260
- it('returns false for read when permission is denied and enableRead is true', () => {
308
+ it('returns false for read when permission is denied and enableRead is true', async () => {
261
309
  mockUseCan.mockImplementation((userId, scope, permission) => {
262
- if (permission === 'read:contacts') {
310
+ // When appId is available in resolvedScope, permission format is 'read:page.contacts'
311
+ if (permission === 'read:page.contacts' || permission === 'read:contacts') {
263
312
  return {
264
313
  can: false,
265
314
  isLoading: false,
@@ -279,6 +328,11 @@ describe('useResourcePermissions Hook', () => {
279
328
  useResourcePermissions('contacts', { enableRead: true })
280
329
  );
281
330
 
331
+ // Wait for super admin check to complete
332
+ await waitFor(() => {
333
+ expect(result.current.isLoading).toBe(false);
334
+ });
335
+
282
336
  expect(result.current.canRead('contacts')).toBe(false);
283
337
  });
284
338
  });
@@ -398,7 +452,7 @@ describe('useResourcePermissions Hook', () => {
398
452
  });
399
453
 
400
454
  describe('Missing User Context', () => {
401
- it('handles missing user gracefully', () => {
455
+ it('handles missing user gracefully', async () => {
402
456
  mockUseUnifiedAuth.mockReturnValue({
403
457
  user: null,
404
458
  supabase: mockSupabase,
@@ -406,21 +460,26 @@ describe('useResourcePermissions Hook', () => {
406
460
 
407
461
  const { result } = renderHook(() => useResourcePermissions('contacts'));
408
462
 
463
+ // Wait for super admin check to complete (will be false when user is null)
464
+ await waitFor(() => {
465
+ expect(result.current.isLoading).toBe(false);
466
+ });
467
+
409
468
  // When user is null, userId is empty string, but scope and permissions are still checked
410
- // Since mockScope has appId, pageId will be the resource name ('contacts')
469
+ // Since resolvedScope (mockScope) has appId, permission strings include page. prefix
411
470
  expect(mockUseCan).toHaveBeenCalledWith(
412
471
  '',
413
- mockScope,
414
- 'create:contacts',
415
- 'contacts', // pageId is resource name when appId is available in scope
472
+ mockScope, // resolvedScope is used when available
473
+ 'create:page.contacts',
474
+ 'contacts', // pageId is resource name when appId is available in resolvedScope
416
475
  true,
417
- null, // precomputedSuperAdmin
476
+ false, // precomputedSuperAdmin (false when user is null)
418
477
  undefined // appName
419
478
  );
420
479
  expect(mockUseCan).toHaveBeenCalledWith(
421
480
  '',
422
481
  mockScope,
423
- 'update:contacts',
482
+ 'update:page.contacts',
424
483
  'contacts',
425
484
  true,
426
485
  null, // precomputedSuperAdmin
@@ -429,7 +488,7 @@ describe('useResourcePermissions Hook', () => {
429
488
  expect(mockUseCan).toHaveBeenCalledWith(
430
489
  '',
431
490
  mockScope,
432
- 'delete:contacts',
491
+ 'delete:page.contacts',
433
492
  'contacts',
434
493
  true,
435
494
  null, // precomputedSuperAdmin
@@ -438,7 +497,7 @@ describe('useResourcePermissions Hook', () => {
438
497
  expect(mockUseCan).toHaveBeenCalledWith(
439
498
  '',
440
499
  mockScope,
441
- 'read:contacts',
500
+ 'read:page.contacts',
442
501
  'contacts',
443
502
  true,
444
503
  null, // precomputedSuperAdmin
@@ -453,11 +512,11 @@ describe('useResourcePermissions Hook', () => {
453
512
  throw new Error('Event provider not available');
454
513
  });
455
514
 
456
- mockUseResolvedScope.mockReturnValue({
457
- resolvedScope: mockScope,
458
- isLoading: false,
459
- error: null,
460
- });
515
+ mockUseResolvedScope.mockReturnValue({
516
+ resolvedScope: mockScope,
517
+ isLoading: false, // Scope is resolved
518
+ error: null,
519
+ });
461
520
 
462
521
  const { result } = renderHook(() => useResourcePermissions('contacts'));
463
522
 
@@ -481,7 +540,7 @@ describe('useResourcePermissions Hook', () => {
481
540
 
482
541
  it('aggregates loading states from permission checks', () => {
483
542
  mockUseCan.mockImplementation((userId, scope, permission) => {
484
- if (permission === 'create:contacts') {
543
+ if (permission === 'create:page.contacts') {
485
544
  return {
486
545
  can: false,
487
546
  isLoading: true,
@@ -504,7 +563,7 @@ describe('useResourcePermissions Hook', () => {
504
563
 
505
564
  it('includes read loading state when enableRead is true', () => {
506
565
  mockUseCan.mockImplementation((userId, scope, permission) => {
507
- if (permission === 'read:contacts') {
566
+ if (permission === 'read:page.contacts') {
508
567
  return {
509
568
  can: false,
510
569
  isLoading: true,
@@ -527,12 +586,20 @@ describe('useResourcePermissions Hook', () => {
527
586
  expect(result.current.isLoading).toBe(true);
528
587
  });
529
588
 
530
- it('excludes read loading state when enableRead is false', () => {
589
+ it('excludes read loading state when enableRead is false', async () => {
590
+ // Ensure scope is resolved (not loading) - this prevents scopeLoading from contributing to isLoading
591
+ mockUseResolvedScope.mockReturnValue({
592
+ resolvedScope: mockScope, // Use correct property name
593
+ isLoading: false,
594
+ error: null,
595
+ });
596
+
531
597
  mockUseCan.mockImplementation((userId, scope, permission) => {
532
- if (permission === 'read:contacts') {
598
+ // Read permission check should not contribute to isLoading when enableRead is false
599
+ if (permission === 'read:page.contacts' || permission === 'read:contacts') {
533
600
  return {
534
601
  can: false,
535
- isLoading: true,
602
+ isLoading: true, // Read is loading, but should be excluded
536
603
  error: null,
537
604
  refetch: vi.fn(),
538
605
  } as any;
@@ -545,9 +612,12 @@ describe('useResourcePermissions Hook', () => {
545
612
  } as any;
546
613
  });
547
614
 
548
- const { result } = renderHook(() => useResourcePermissions('contacts'));
615
+ const { result } = renderHook(() => useResourcePermissions('contacts', { enableRead: false }));
549
616
 
550
- expect(result.current.isLoading).toBe(false);
617
+ // Wait for super admin check to complete
618
+ await waitFor(() => {
619
+ expect(result.current.isLoading).toBe(false);
620
+ });
551
621
  });
552
622
  });
553
623
 
@@ -565,10 +635,11 @@ describe('useResourcePermissions Hook', () => {
565
635
  expect(result.current.error).toEqual(scopeError);
566
636
  });
567
637
 
568
- it('aggregates errors from permission checks', () => {
638
+ it('aggregates errors from permission checks', async () => {
569
639
  const permissionError = new Error('Permission check failed');
570
640
  mockUseCan.mockImplementation((userId, scope, permission) => {
571
- if (permission === 'create:contacts') {
641
+ // When appId is available in resolvedScope, permission format is 'create:page.contacts'
642
+ if (permission === 'create:page.contacts' || permission === 'create:contacts') {
572
643
  return {
573
644
  can: false,
574
645
  isLoading: false,
@@ -586,6 +657,11 @@ describe('useResourcePermissions Hook', () => {
586
657
 
587
658
  const { result } = renderHook(() => useResourcePermissions('contacts'));
588
659
 
660
+ // Wait for super admin check to complete
661
+ await waitFor(() => {
662
+ expect(result.current.isLoading).toBe(false);
663
+ });
664
+
589
665
  expect(result.current.error).toEqual(permissionError);
590
666
  });
591
667
 
@@ -600,7 +676,7 @@ describe('useResourcePermissions Hook', () => {
600
676
  });
601
677
 
602
678
  mockUseCan.mockImplementation((userId, scope, permission) => {
603
- if (permission === 'create:contacts') {
679
+ if (permission === 'create:page.contacts') {
604
680
  return {
605
681
  can: false,
606
682
  isLoading: false,
@@ -629,17 +705,22 @@ describe('useResourcePermissions Hook', () => {
629
705
  });
630
706
 
631
707
  describe('Options', () => {
632
- it('respects enableRead option', () => {
708
+ it('respects enableRead option', async () => {
633
709
  renderHook(() => useResourcePermissions('contacts', { enableRead: true }));
634
710
 
635
- // Should still call useCan for read permission
711
+ // Wait for super admin check to complete
712
+ await waitFor(() => {
713
+ expect(mockUseCan).toHaveBeenCalled();
714
+ });
715
+
716
+ // Should call useCan for read permission with page. prefix when appId is available in resolvedScope
636
717
  expect(mockUseCan).toHaveBeenCalledWith(
637
718
  expect.any(String),
638
719
  expect.any(Object),
639
- 'read:contacts',
640
- 'contacts', // pageId is resource name when appId is available
720
+ 'read:page.contacts',
721
+ 'contacts', // pageId is resource name when appId is available in resolvedScope
641
722
  true,
642
- null, // precomputedSuperAdmin
723
+ false, // precomputedSuperAdmin (resolved after async check)
643
724
  undefined // appName
644
725
  );
645
726
  });
@@ -653,22 +734,28 @@ describe('useResourcePermissions Hook', () => {
653
734
  });
654
735
 
655
736
  describe('Different Resources', () => {
656
- it('works with different resource names', () => {
737
+ it('works with different resource names', async () => {
657
738
  renderHook(() => useResourcePermissions('risks'));
658
739
 
740
+ // Wait for super admin check to complete
741
+ await waitFor(() => {
742
+ expect(mockUseCan).toHaveBeenCalled();
743
+ });
744
+
745
+ // When appId is available in resolvedScope, permission format includes page. prefix
659
746
  expect(mockUseCan).toHaveBeenCalledWith(
660
747
  expect.any(String),
661
748
  expect.any(Object),
662
- 'create:risks',
663
- 'risks', // pageId is resource name when appId is available
749
+ 'create:page.risks',
750
+ 'risks', // pageId is resource name when appId is available in resolvedScope
664
751
  true,
665
- null, // precomputedSuperAdmin
752
+ false, // precomputedSuperAdmin (resolved after async check)
666
753
  undefined // appName
667
754
  );
668
755
  expect(mockUseCan).toHaveBeenCalledWith(
669
756
  expect.any(String),
670
757
  expect.any(Object),
671
- 'update:risks',
758
+ 'update:page.risks',
672
759
  'risks', // pageId is resource name when appId is available
673
760
  true,
674
761
  null, // precomputedSuperAdmin
@@ -677,7 +764,7 @@ describe('useResourcePermissions Hook', () => {
677
764
  expect(mockUseCan).toHaveBeenCalledWith(
678
765
  expect.any(String),
679
766
  expect.any(Object),
680
- 'delete:risks',
767
+ 'delete:page.risks',
681
768
  'risks', // pageId is resource name when appId is available
682
769
  true,
683
770
  null, // precomputedSuperAdmin
@@ -687,7 +774,7 @@ describe('useResourcePermissions Hook', () => {
687
774
  });
688
775
 
689
776
  describe('Page Permission Support', () => {
690
- it('passes resource name as pageId when appId is available in scope', () => {
777
+ it('constructs permission strings with page. prefix when appId is available', () => {
691
778
  const scopeWithAppId: Scope = {
692
779
  organisationId: 'org-123',
693
780
  eventId: 'event-123',
@@ -702,20 +789,38 @@ describe('useResourcePermissions Hook', () => {
702
789
 
703
790
  renderHook(() => useResourcePermissions('planning'));
704
791
 
705
- // When appId is available, resource name should be passed as pageId
706
- // This enables the RPC function to check page permissions
792
+ // When appId is available, permission strings should include page. prefix
793
+ // and resource name should be passed as pageId to enable page permission checks
707
794
  expect(mockUseCan).toHaveBeenCalledWith(
708
795
  'user-123',
709
796
  scopeWithAppId,
710
- 'create:planning',
797
+ 'create:page.planning',
711
798
  'planning', // Resource name passed as pageId to enable page permission checks
712
799
  true,
713
800
  null, // precomputedSuperAdmin
714
801
  undefined // appName
715
802
  );
803
+ expect(mockUseCan).toHaveBeenCalledWith(
804
+ 'user-123',
805
+ scopeWithAppId,
806
+ 'update:page.planning',
807
+ 'planning',
808
+ true,
809
+ null,
810
+ undefined
811
+ );
812
+ expect(mockUseCan).toHaveBeenCalledWith(
813
+ 'user-123',
814
+ scopeWithAppId,
815
+ 'delete:page.planning',
816
+ 'planning',
817
+ true,
818
+ null,
819
+ undefined
820
+ );
716
821
  });
717
822
 
718
- it('does not pass pageId when appId is not available', () => {
823
+ it('does not add page. prefix when appId is not available', () => {
719
824
  const scopeWithoutAppId: Scope = {
720
825
  organisationId: 'org-123',
721
826
  eventId: 'event-123',
@@ -730,8 +835,8 @@ describe('useResourcePermissions Hook', () => {
730
835
 
731
836
  renderHook(() => useResourcePermissions('planning'));
732
837
 
733
- // When appId is not available, pageId should be undefined
734
- // This falls back to resource-based permission checking
838
+ // When appId is not available, permission strings should NOT include page. prefix
839
+ // and pageId should be undefined - falls back to resource-based permission checking
735
840
  expect(mockUseCan).toHaveBeenCalledWith(
736
841
  'user-123',
737
842
  scopeWithoutAppId,
@@ -741,6 +846,82 @@ describe('useResourcePermissions Hook', () => {
741
846
  null, // precomputedSuperAdmin
742
847
  undefined // appName
743
848
  );
849
+ expect(mockUseCan).toHaveBeenCalledWith(
850
+ 'user-123',
851
+ scopeWithoutAppId,
852
+ 'update:planning',
853
+ undefined,
854
+ true,
855
+ null,
856
+ undefined
857
+ );
858
+ expect(mockUseCan).toHaveBeenCalledWith(
859
+ 'user-123',
860
+ scopeWithoutAppId,
861
+ 'delete:planning',
862
+ undefined,
863
+ true,
864
+ null,
865
+ undefined
866
+ );
867
+ });
868
+
869
+ it('waits for scope resolution before using page permissions (timing fix)', () => {
870
+ // Simulate scope resolution in progress (resolvedScope is null, isLoading is true)
871
+ mockUseResolvedScope.mockReturnValue({
872
+ resolvedScope: null,
873
+ isLoading: true,
874
+ error: null,
875
+ });
876
+
877
+ renderHook(() => useResourcePermissions('planning'));
878
+
879
+ // During scope loading, should use resource-based permissions (no page. prefix)
880
+ // because resolvedScope is null, so hasAppId is false
881
+ const fallbackScope: Scope = {
882
+ organisationId: 'org-123',
883
+ eventId: 'event-123',
884
+ appId: undefined,
885
+ };
886
+
887
+ expect(mockUseCan).toHaveBeenCalledWith(
888
+ 'user-123',
889
+ fallbackScope,
890
+ 'create:planning', // Resource-based permission (no page. prefix) during loading
891
+ undefined, // No pageId when appId is not available
892
+ true,
893
+ null,
894
+ undefined
895
+ );
896
+
897
+ // Now simulate scope resolution completing with appId
898
+ const scopeWithAppId: Scope = {
899
+ organisationId: 'org-123',
900
+ eventId: 'event-123',
901
+ appId: 'app-123',
902
+ };
903
+
904
+ mockUseResolvedScope.mockReturnValue({
905
+ resolvedScope: scopeWithAppId,
906
+ isLoading: false,
907
+ error: null,
908
+ });
909
+
910
+ // Re-render to trigger update
911
+ const { rerender } = renderHook(() => useResourcePermissions('planning'));
912
+ rerender();
913
+
914
+ // After scope resolution, should use page permissions (with page. prefix)
915
+ // because resolvedScope.appId is now available, so hasAppId is true
916
+ expect(mockUseCan).toHaveBeenCalledWith(
917
+ 'user-123',
918
+ scopeWithAppId,
919
+ 'create:page.planning', // Page-based permission (with page. prefix) after resolution
920
+ 'planning', // pageId is resource name when appId is available
921
+ true,
922
+ null,
923
+ undefined
924
+ );
744
925
  });
745
926
  });
746
927
  });