@jmruthers/pace-core 0.6.5 → 0.6.7

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 (473) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/README.md +5 -403
  3. package/audit-tool/00-dependencies.cjs +394 -0
  4. package/audit-tool/audits/01-pace-core-compliance.cjs +556 -0
  5. package/audit-tool/audits/02-project-structure.cjs +255 -0
  6. package/audit-tool/audits/03-architecture.cjs +196 -0
  7. package/audit-tool/audits/04-code-quality.cjs +149 -0
  8. package/audit-tool/audits/05-styling.cjs +224 -0
  9. package/audit-tool/audits/06-security-rbac.cjs +544 -0
  10. package/audit-tool/audits/07-api-tech-stack.cjs +301 -0
  11. package/audit-tool/audits/08-testing-documentation.cjs +202 -0
  12. package/audit-tool/audits/09-operations.cjs +208 -0
  13. package/audit-tool/index.cjs +291 -0
  14. package/audit-tool/utils/code-utils.cjs +218 -0
  15. package/audit-tool/utils/file-utils.cjs +230 -0
  16. package/audit-tool/utils/report-utils.cjs +241 -0
  17. package/core-usage-manifest.json +93 -0
  18. package/cursor-rules/00-standards-overview.mdc +156 -0
  19. package/cursor-rules/01-pace-core-compliance.mdc +586 -0
  20. package/cursor-rules/02-project-structure.mdc +42 -4
  21. package/cursor-rules/{03-solid-principles.mdc → 03-architecture.mdc} +126 -10
  22. package/cursor-rules/04-code-quality.mdc +419 -0
  23. package/cursor-rules/{08-markup-quality.mdc → 05-styling.mdc} +104 -34
  24. package/cursor-rules/06-security-rbac.mdc +518 -0
  25. package/cursor-rules/07-api-tech-stack.mdc +377 -0
  26. package/cursor-rules/08-testing-documentation.mdc +324 -0
  27. package/cursor-rules/09-operations.mdc +365 -0
  28. package/dist/{AuthService-Cb34EQs3.d.ts → AuthService-DmfO5rGS.d.ts} +10 -0
  29. package/dist/DataTable-7PMH7XN7.js +15 -0
  30. package/dist/{DataTable-BMRU8a1j.d.ts → DataTable-DRUIgtUH.d.ts} +1 -1
  31. package/dist/{PublicPageProvider-QTFVrL-Z.d.ts → PublicPageProvider-DlsCaR5v.d.ts} +33 -72
  32. package/dist/UnifiedAuthProvider-ZT6TIGM7.js +7 -0
  33. package/dist/api-Y4MQWOFW.js +4 -0
  34. package/dist/audit-MYQXYZFU.js +3 -0
  35. package/dist/{chunk-DGUM43GV.js → chunk-3RG5ZIWI.js} +1 -4
  36. package/dist/{chunk-QXHPKYJV.js → chunk-4SXLQIZO.js} +1 -26
  37. package/dist/{chunk-UPPMRMYG.js → chunk-5X4QLXRG.js} +73 -151
  38. package/dist/chunk-6F3IILHI.js +62 -0
  39. package/dist/{chunk-E66EQZE6.js → chunk-6GLLNA6U.js} +3 -9
  40. package/dist/{chunk-ZSAAAMVR.js → chunk-6QYDGKQY.js} +1 -4
  41. package/dist/{chunk-FMUCXFII.js → chunk-7ILTDCL2.js} +9 -5
  42. package/dist/{chunk-M43Y4SSO.js → chunk-A3W6LW53.js} +15 -13
  43. package/dist/{chunk-63FOKYGO.js → chunk-AHU7G2R5.js} +2 -11
  44. package/dist/{chunk-HU2C6SSC.js → chunk-BM4CQ5P3.js} +606 -559
  45. package/dist/chunk-C7NSAPTL.js +1 -0
  46. package/dist/{chunk-J36DSWQK.js → chunk-FEJLJNWA.js} +7 -41
  47. package/dist/{chunk-IHB5DR3H.js → chunk-FTCRZOG2.js} +188 -387
  48. package/dist/{chunk-G37KK66H.js → chunk-FYHN4DD5.js} +60 -19
  49. package/dist/chunk-GHYHJTYV.js +994 -0
  50. package/dist/{chunk-VBXEHIUJ.js → chunk-HF6O3O37.js} +6 -88
  51. package/dist/{chunk-FFQEQTNW.js → chunk-IUBRCBSY.js} +134 -45
  52. package/dist/{chunk-6COVEUS7.js → chunk-JGWDVX64.js} +983 -1034
  53. package/dist/{chunk-RGAWHO7N.js → chunk-L4XMVJKY.js} +77 -222
  54. package/dist/chunk-MBADTM7L.js +64 -0
  55. package/dist/{chunk-M7MPQISP.js → chunk-OJ4SKRSV.js} +3 -16
  56. package/dist/{chunk-IVOFDYWT.js → chunk-Q7Q7V5NV.js} +2109 -1604
  57. package/dist/{chunk-JGRYX5UX.js → chunk-S7DKJPLT.js} +29 -58
  58. package/dist/{chunk-PWLANIRT.js → chunk-TTRFSOKR.js} +1 -7
  59. package/dist/{chunk-5DRSZLL2.js → chunk-UH3NTO3F.js} +1 -6
  60. package/dist/{chunk-NTM7ZSB6.js → chunk-VBCS3DUA.js} +261 -168
  61. package/dist/{chunk-EFN2EIMK.js → chunk-ZFYPMX46.js} +271 -87
  62. package/dist/{chunk-L4OXEN46.js → chunk-ZKAWKYT4.js} +10 -24
  63. package/dist/components.d.ts +7 -5
  64. package/dist/components.js +46 -257
  65. package/dist/{database.generated-CzIvgcPu.d.ts → database.generated-CcnC_DRc.d.ts} +4795 -3691
  66. package/dist/eslint-rules/index.cjs +35 -0
  67. package/{src/eslint-rules/pace-core-compliance.cjs → dist/eslint-rules/rules/01-pace-core-compliance.cjs} +234 -235
  68. package/dist/eslint-rules/rules/04-code-quality.cjs +290 -0
  69. package/dist/eslint-rules/rules/05-styling.cjs +61 -0
  70. package/dist/eslint-rules/rules/06-security-rbac.cjs +806 -0
  71. package/dist/eslint-rules/rules/07-api-tech-stack.cjs +263 -0
  72. package/dist/eslint-rules/rules/08-testing.cjs +94 -0
  73. package/dist/eslint-rules/utils/helpers.cjs +42 -0
  74. package/dist/eslint-rules/utils/manifest-loader.cjs +75 -0
  75. package/dist/hooks.d.ts +6 -6
  76. package/dist/hooks.js +62 -172
  77. package/dist/icons/index.d.ts +1 -0
  78. package/dist/icons/index.js +1 -0
  79. package/dist/index.d.ts +12 -11
  80. package/dist/index.js +67 -660
  81. package/dist/providers.d.ts +2 -2
  82. package/dist/providers.js +8 -35
  83. package/dist/rbac/eslint-rules.d.ts +46 -44
  84. package/dist/rbac/eslint-rules.js +7 -4
  85. package/dist/rbac/index.d.ts +109 -586
  86. package/dist/rbac/index.js +14 -207
  87. package/dist/styles/index.js +2 -12
  88. package/dist/theming/runtime.d.ts +14 -1
  89. package/dist/theming/runtime.js +3 -19
  90. package/dist/{timezone-CHhWg6b4.d.ts → timezone-BZe_eUxx.d.ts} +175 -1
  91. package/dist/{types-CkbwOr4Y.d.ts → types-DXstZpNI.d.ts} +4 -17
  92. package/dist/types-t9H8qKRw.d.ts +55 -0
  93. package/dist/types.d.ts +1 -1
  94. package/dist/types.js +7 -94
  95. package/dist/{usePublicRouteParams-ClnV4tnv.d.ts → usePublicRouteParams-MamNgwqe.d.ts} +20 -20
  96. package/dist/utils.d.ts +24 -117
  97. package/dist/utils.js +54 -392
  98. package/docs/README.md +17 -7
  99. package/docs/api/README.md +4 -402
  100. package/docs/api/modules.md +301 -871
  101. package/docs/api-reference/components.md +21 -21
  102. package/docs/api-reference/deprecated.md +31 -6
  103. package/docs/api-reference/hooks.md +80 -80
  104. package/docs/api-reference/rpc-functions.md +78 -3
  105. package/docs/api-reference/types.md +1 -1
  106. package/docs/api-reference/utilities.md +1 -1
  107. package/docs/architecture/README.md +1 -1
  108. package/docs/core-concepts/events.md +3 -3
  109. package/docs/core-concepts/organisations.md +6 -6
  110. package/docs/core-concepts/permissions.md +6 -6
  111. package/docs/documentation-index.md +12 -18
  112. package/docs/getting-started/cursor-rules.md +3 -23
  113. package/docs/getting-started/dependencies.md +650 -0
  114. package/docs/getting-started/documentation-index.md +1 -1
  115. package/docs/getting-started/examples/README.md +4 -4
  116. package/docs/getting-started/examples/full-featured-app.md +1 -1
  117. package/docs/getting-started/faq.md +2 -2
  118. package/docs/getting-started/installation-guide.md +20 -7
  119. package/docs/getting-started/quick-reference.md +4 -4
  120. package/docs/getting-started/quick-start.md +23 -12
  121. package/docs/implementation-guides/authentication.md +15 -15
  122. package/docs/implementation-guides/component-styling.md +1 -1
  123. package/docs/implementation-guides/data-tables.md +126 -33
  124. package/docs/implementation-guides/datatable-rbac-usage.md +1 -1
  125. package/docs/implementation-guides/dynamic-colors.md +3 -3
  126. package/docs/implementation-guides/file-upload-storage.md +2 -2
  127. package/docs/implementation-guides/hierarchical-datatable.md +40 -60
  128. package/docs/implementation-guides/inactivity-tracking.md +3 -3
  129. package/docs/implementation-guides/large-datasets.md +3 -2
  130. package/docs/implementation-guides/organisation-security.md +2 -2
  131. package/docs/implementation-guides/performance.md +2 -2
  132. package/docs/implementation-guides/permission-enforcement.md +5 -1
  133. package/docs/migration/V0.3.44_organisation-context-timing-fix.md +1 -1
  134. package/docs/migration/V0.4.0_rbac-migration.md +6 -6
  135. package/docs/rbac/MIGRATION_GUIDE.md +819 -0
  136. package/docs/rbac/RBAC_CONTRACT.md +724 -0
  137. package/docs/rbac/README.md +17 -8
  138. package/docs/rbac/advanced-patterns.md +6 -6
  139. package/docs/rbac/api-reference.md +20 -20
  140. package/docs/rbac/edge-functions-guide.md +376 -0
  141. package/docs/rbac/event-based-apps.md +3 -3
  142. package/docs/rbac/examples.md +41 -41
  143. package/docs/rbac/getting-started.md +37 -37
  144. package/docs/rbac/performance.md +1 -1
  145. package/docs/rbac/quick-start.md +52 -52
  146. package/docs/rbac/secure-client-protection.md +1 -35
  147. package/docs/rbac/troubleshooting.md +1 -1
  148. package/docs/security/README.md +5 -5
  149. package/docs/standards/0-standards-overview.md +220 -0
  150. package/docs/standards/1-pace-core-compliance-standards.md +986 -0
  151. package/docs/standards/2-project-structure-standards.md +949 -0
  152. package/docs/standards/3-architecture-standards.md +606 -0
  153. package/docs/standards/4-code-quality-standards.md +728 -0
  154. package/docs/standards/5-styling-standards.md +348 -0
  155. package/docs/standards/{07-rbac-and-rls-standard.md → 6-security-rbac-standards.md} +269 -66
  156. package/docs/standards/7-api-tech-stack-standards.md +662 -0
  157. package/docs/standards/8-testing-documentation-standards.md +401 -0
  158. package/docs/standards/9-operations-standards.md +1102 -0
  159. package/docs/standards/README.md +185 -57
  160. package/docs/troubleshooting/README.md +4 -4
  161. package/docs/troubleshooting/common-issues.md +2 -2
  162. package/docs/troubleshooting/debugging.md +9 -9
  163. package/docs/troubleshooting/migration.md +4 -4
  164. package/docs/troubleshooting/organisation-context-setup.md +42 -19
  165. package/eslint-config-pace-core.cjs +33 -6
  166. package/package.json +35 -23
  167. package/scripts/install-cursor-rules.cjs +25 -6
  168. package/scripts/install-eslint-config.cjs +284 -0
  169. package/src/__tests__/fixtures/supabase.ts +1 -1
  170. package/src/__tests__/helpers/__tests__/component-test-utils.test.tsx +3 -3
  171. package/src/__tests__/helpers/__tests__/optimized-test-setup.test.ts +1 -1
  172. package/src/__tests__/helpers/__tests__/supabaseMock.test.ts +1 -1
  173. package/src/__tests__/helpers/__tests__/test-providers.test.tsx +2 -2
  174. package/src/__tests__/helpers/__tests__/test-utils.test.tsx +13 -13
  175. package/src/__tests__/helpers/component-test-utils.tsx +1 -1
  176. package/src/__tests__/helpers/supabaseMock.ts +2 -2
  177. package/src/__tests__/integration/UserProfile.test.tsx +14 -14
  178. package/src/__tests__/public-recipe-view.test.ts +38 -9
  179. package/src/__tests__/rbac/PagePermissionGuard.test.tsx +6 -6
  180. package/src/__tests__/templates/accessibility.test.template.tsx +9 -9
  181. package/src/__tests__/templates/component.test.template.tsx +18 -15
  182. package/src/components/Button/Button.tsx +5 -1
  183. package/src/components/Calendar/Calendar.tsx +201 -47
  184. package/src/components/ContextSelector/ContextSelector.tsx +106 -119
  185. package/src/components/DataTable/AUDIT_REPORT.md +293 -0
  186. package/src/components/DataTable/__tests__/DataTableCore.test.tsx +10 -2
  187. package/src/components/DataTable/__tests__/a11y.basic.test.tsx +10 -4
  188. package/src/components/DataTable/__tests__/test-utils/sharedTestUtils.tsx +9 -9
  189. package/src/components/DataTable/components/ColumnFilter.tsx +63 -74
  190. package/src/components/DataTable/components/ColumnVisibilityDropdown.tsx +43 -41
  191. package/src/components/DataTable/components/DataTableCore.tsx +186 -13
  192. package/src/components/DataTable/components/DataTableErrorBoundary.tsx +9 -11
  193. package/src/components/DataTable/components/DataTableLayout.tsx +35 -21
  194. package/src/components/DataTable/components/EditFields.tsx +23 -3
  195. package/src/components/DataTable/components/EditableRow.tsx +12 -9
  196. package/src/components/DataTable/components/EmptyState.tsx +10 -9
  197. package/src/components/DataTable/components/FilterRow.tsx +2 -4
  198. package/src/components/DataTable/components/ImportModal.tsx +124 -126
  199. package/src/components/DataTable/components/LoadingState.tsx +5 -6
  200. package/src/components/DataTable/components/RowComponent.tsx +12 -0
  201. package/src/components/DataTable/components/SortIndicator.tsx +50 -0
  202. package/src/components/DataTable/components/__tests__/COVERAGE_NOTE.md +4 -4
  203. package/src/components/DataTable/components/__tests__/ColumnFilter.test.tsx +23 -82
  204. package/src/components/DataTable/components/__tests__/DataTableErrorBoundary.test.tsx +37 -9
  205. package/src/components/DataTable/components/__tests__/EmptyState.test.tsx +7 -4
  206. package/src/components/DataTable/components/__tests__/FilterRow.test.tsx +12 -4
  207. package/src/components/DataTable/components/__tests__/LoadingState.test.tsx +41 -27
  208. package/src/components/DataTable/components/hooks/usePermissionTracking.ts +0 -4
  209. package/src/components/DataTable/components/index.ts +2 -1
  210. package/src/components/DataTable/hooks/__tests__/useDataTableState.test.ts +51 -47
  211. package/src/components/DataTable/hooks/useDataTablePermissions.ts +24 -21
  212. package/src/components/DataTable/hooks/useDataTableState.ts +125 -9
  213. package/src/components/DataTable/hooks/useTableColumns.ts +40 -2
  214. package/src/components/DataTable/hooks/useTableHandlers.ts +11 -0
  215. package/src/components/DataTable/types.ts +5 -18
  216. package/src/components/DataTable/utils/a11yUtils.ts +17 -0
  217. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +2 -1
  218. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.tsx +11 -15
  219. package/src/components/DateTimeField/DateTimeField.tsx +10 -9
  220. package/src/components/Dialog/Dialog.test.tsx +128 -104
  221. package/src/components/Dialog/Dialog.tsx +742 -24
  222. package/src/components/ErrorBoundary/ErrorBoundary.tsx +77 -79
  223. package/src/components/FileDisplay/FileDisplay.test.tsx +4 -2
  224. package/src/components/FileDisplay/FileDisplay.tsx +23 -17
  225. package/src/components/FileUpload/FileUpload.test.tsx +52 -14
  226. package/src/components/FileUpload/FileUpload.tsx +112 -130
  227. package/src/components/Form/Form.test.tsx +6 -8
  228. package/src/components/Form/Form.tsx +365 -4
  229. package/src/components/NavigationMenu/NavigationMenu.test.tsx +14 -13
  230. package/src/components/NavigationMenu/useNavigationFiltering.ts +11 -21
  231. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +6 -4
  232. package/src/components/PaceAppLayout/PaceAppLayout.tsx +11 -15
  233. package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +108 -61
  234. package/src/components/PaceLoginPage/PaceLoginPage.tsx +27 -3
  235. package/src/components/Progress/Progress.tsx +2 -4
  236. package/src/components/ProtectedRoute/ProtectedRoute.tsx +8 -8
  237. package/src/components/Select/Select.tsx +109 -98
  238. package/src/components/Select/types.ts +4 -1
  239. package/src/components/UserMenu/UserMenu.tsx +9 -6
  240. package/src/hooks/__tests__/ServiceHooks.test.tsx +16 -16
  241. package/src/hooks/__tests__/hooks.integration.test.tsx +55 -57
  242. package/src/hooks/__tests__/useAppConfig.unit.test.ts +129 -67
  243. package/src/hooks/__tests__/useFocusTrap.unit.test.tsx +97 -97
  244. package/src/hooks/__tests__/usePublicEvent.simple.test.ts +149 -67
  245. package/src/hooks/__tests__/usePublicEvent.test.ts +149 -79
  246. package/src/hooks/__tests__/usePublicEvent.unit.test.ts +158 -109
  247. package/src/hooks/__tests__/useSessionDraft.test.ts +163 -0
  248. package/src/hooks/__tests__/useSessionRestoration.unit.test.tsx +10 -5
  249. package/src/hooks/public/usePublicEvent.ts +67 -195
  250. package/src/hooks/public/usePublicEventLogo.test.ts +70 -17
  251. package/src/hooks/public/usePublicEventLogo.ts +24 -14
  252. package/src/hooks/public/usePublicFileDisplay.ts +2 -2
  253. package/src/hooks/public/usePublicRouteParams.ts +5 -5
  254. package/src/hooks/useAppConfig.ts +28 -26
  255. package/src/hooks/useEventTheme.test.ts +217 -239
  256. package/src/hooks/useEventTheme.ts +16 -28
  257. package/src/hooks/useFileDisplay.ts +2 -2
  258. package/src/hooks/useOrganisationPermissions.ts +5 -7
  259. package/src/hooks/useQueryCache.ts +0 -1
  260. package/src/hooks/useSessionDraft.ts +380 -0
  261. package/src/hooks/useSessionRestoration.ts +3 -1
  262. package/src/icons/index.ts +27 -0
  263. package/src/index.ts +5 -0
  264. package/src/providers/OrganisationProvider.tsx +23 -14
  265. package/src/providers/UnifiedAuthProvider.smoke.test.tsx +21 -21
  266. package/src/providers/__tests__/AuthProvider.test.tsx +21 -21
  267. package/src/providers/__tests__/EventProvider.test.tsx +61 -61
  268. package/src/providers/__tests__/InactivityProvider.test.tsx +56 -56
  269. package/src/providers/__tests__/OrganisationProvider.test.tsx +75 -75
  270. package/src/providers/__tests__/ProviderLifecycle.test.tsx +37 -37
  271. package/src/providers/__tests__/UnifiedAuthProvider.test.tsx +103 -103
  272. package/src/providers/services/EventServiceProvider.tsx +1 -24
  273. package/src/providers/services/UnifiedAuthProvider.tsx +5 -48
  274. package/src/providers/services/__tests__/AuthServiceProvider.integration.test.tsx +7 -7
  275. package/src/providers/services/__tests__/UnifiedAuthProvider.integration.test.tsx +13 -10
  276. package/src/rbac/__tests__/adapters.comprehensive.test.tsx +7 -457
  277. package/src/rbac/__tests__/auth-rbac.e2e.test.tsx +33 -7
  278. package/src/rbac/adapters.tsx +7 -295
  279. package/src/rbac/api.test.ts +44 -56
  280. package/src/rbac/api.ts +10 -17
  281. package/src/rbac/cache-invalidation.ts +0 -1
  282. package/src/rbac/compliance/index.ts +10 -0
  283. package/src/rbac/compliance/pattern-detector.ts +553 -0
  284. package/src/rbac/compliance/runtime-compliance.ts +22 -0
  285. package/src/rbac/components/AccessDenied.tsx +150 -0
  286. package/src/rbac/components/NavigationGuard.tsx +12 -20
  287. package/src/rbac/components/PagePermissionGuard.tsx +4 -24
  288. package/src/rbac/components/__tests__/NavigationGuard.test.tsx +21 -8
  289. package/src/rbac/components/index.ts +3 -41
  290. package/src/rbac/eslint-rules.js +1 -1
  291. package/src/rbac/hooks/index.ts +0 -3
  292. package/src/rbac/hooks/permissions/index.ts +0 -3
  293. package/src/rbac/hooks/permissions/useAccessLevel.ts +4 -8
  294. package/src/rbac/hooks/usePermissions.ts +0 -3
  295. package/src/rbac/hooks/useResolvedScope.test.ts +57 -47
  296. package/src/rbac/hooks/useResolvedScope.ts +58 -140
  297. package/src/rbac/hooks/useResourcePermissions.test.ts +124 -38
  298. package/src/rbac/hooks/useResourcePermissions.ts +139 -48
  299. package/src/rbac/hooks/useRoleManagement.test.ts +65 -22
  300. package/src/rbac/hooks/useRoleManagement.ts +147 -19
  301. package/src/rbac/hooks/useSecureSupabase.ts +4 -8
  302. package/src/rbac/index.ts +7 -9
  303. package/src/rbac/utils/contextValidator.ts +9 -7
  304. package/src/services/AuthService.ts +130 -18
  305. package/src/services/EventService.ts +4 -97
  306. package/src/services/InactivityService.ts +16 -0
  307. package/src/services/OrganisationService.ts +7 -44
  308. package/src/services/__tests__/OrganisationService.test.ts +26 -8
  309. package/src/services/base/BaseService.ts +0 -3
  310. package/src/styles/core.css +7 -0
  311. package/src/theming/__tests__/parseEventColours.test.ts +9 -3
  312. package/src/theming/parseEventColours.ts +22 -10
  313. package/src/types/database.generated.ts +4733 -3809
  314. package/src/utils/__tests__/lazyLoad.unit.test.tsx +42 -39
  315. package/src/utils/__tests__/organisationContext.unit.test.ts +9 -10
  316. package/src/utils/context/organisationContext.test.ts +13 -28
  317. package/src/utils/context/organisationContext.ts +21 -52
  318. package/src/utils/dynamic/dynamicUtils.ts +1 -1
  319. package/src/utils/file-reference/index.ts +39 -15
  320. package/src/utils/formatting/formatDateTime.test.ts +3 -2
  321. package/src/utils/google-places/loadGoogleMapsScript.ts +29 -4
  322. package/src/utils/index.ts +4 -1
  323. package/src/utils/persistence/__tests__/keyDerivation.test.ts +135 -0
  324. package/src/utils/persistence/__tests__/sensitiveFieldDetection.test.ts +123 -0
  325. package/src/utils/persistence/keyDerivation.ts +304 -0
  326. package/src/utils/persistence/sensitiveFieldDetection.ts +212 -0
  327. package/src/utils/security/secureStorage.ts +5 -5
  328. package/src/utils/storage/README.md +1 -1
  329. package/src/utils/storage/helpers.ts +3 -3
  330. package/src/utils/supabase/createBaseClient.ts +147 -0
  331. package/src/utils/timezone/timezone.test.ts +1 -2
  332. package/src/utils/timezone/timezone.ts +1 -1
  333. package/src/utils/validation/csrf.ts +4 -4
  334. package/cursor-rules/00-pace-core-compliance.mdc +0 -331
  335. package/cursor-rules/01-standards-compliance.mdc +0 -244
  336. package/cursor-rules/04-testing-standards.mdc +0 -268
  337. package/cursor-rules/05-bug-reports-and-features.mdc +0 -246
  338. package/cursor-rules/06-code-quality.mdc +0 -309
  339. package/cursor-rules/07-tech-stack-compliance.mdc +0 -214
  340. package/cursor-rules/CHANGELOG.md +0 -119
  341. package/cursor-rules/README.md +0 -192
  342. package/dist/DataTable-AOVNCPTX.js +0 -175
  343. package/dist/DataTable-AOVNCPTX.js.map +0 -1
  344. package/dist/UnifiedAuthProvider-4SBX4LU5.js +0 -18
  345. package/dist/UnifiedAuthProvider-4SBX4LU5.js.map +0 -1
  346. package/dist/api-O6HTBX5Y.js +0 -52
  347. package/dist/api-O6HTBX5Y.js.map +0 -1
  348. package/dist/audit-V53FV5AG.js +0 -17
  349. package/dist/audit-V53FV5AG.js.map +0 -1
  350. package/dist/chunk-5DRSZLL2.js.map +0 -1
  351. package/dist/chunk-63FOKYGO.js.map +0 -1
  352. package/dist/chunk-6COVEUS7.js.map +0 -1
  353. package/dist/chunk-AFVQODI2.js +0 -263
  354. package/dist/chunk-AFVQODI2.js.map +0 -1
  355. package/dist/chunk-DGUM43GV.js.map +0 -1
  356. package/dist/chunk-E66EQZE6.js.map +0 -1
  357. package/dist/chunk-EFN2EIMK.js.map +0 -1
  358. package/dist/chunk-FFQEQTNW.js.map +0 -1
  359. package/dist/chunk-FMUCXFII.js.map +0 -1
  360. package/dist/chunk-G37KK66H.js.map +0 -1
  361. package/dist/chunk-G7QEZTYQ.js +0 -2053
  362. package/dist/chunk-G7QEZTYQ.js.map +0 -1
  363. package/dist/chunk-HU2C6SSC.js.map +0 -1
  364. package/dist/chunk-IHB5DR3H.js.map +0 -1
  365. package/dist/chunk-IVOFDYWT.js.map +0 -1
  366. package/dist/chunk-J36DSWQK.js.map +0 -1
  367. package/dist/chunk-JGRYX5UX.js.map +0 -1
  368. package/dist/chunk-KQCRWDSA.js +0 -1
  369. package/dist/chunk-KQCRWDSA.js.map +0 -1
  370. package/dist/chunk-L4OXEN46.js.map +0 -1
  371. package/dist/chunk-LMC26NLJ.js +0 -84
  372. package/dist/chunk-LMC26NLJ.js.map +0 -1
  373. package/dist/chunk-M43Y4SSO.js.map +0 -1
  374. package/dist/chunk-M7MPQISP.js.map +0 -1
  375. package/dist/chunk-NTM7ZSB6.js.map +0 -1
  376. package/dist/chunk-PWLANIRT.js.map +0 -1
  377. package/dist/chunk-QXHPKYJV.js.map +0 -1
  378. package/dist/chunk-RGAWHO7N.js.map +0 -1
  379. package/dist/chunk-UPPMRMYG.js.map +0 -1
  380. package/dist/chunk-VBXEHIUJ.js.map +0 -1
  381. package/dist/chunk-ZSAAAMVR.js.map +0 -1
  382. package/dist/components.js.map +0 -1
  383. package/dist/contextValidator-5OGXSPKS.js +0 -9
  384. package/dist/contextValidator-5OGXSPKS.js.map +0 -1
  385. package/dist/eslint-rules/pace-core-compliance.cjs +0 -510
  386. package/dist/hooks.js.map +0 -1
  387. package/dist/index.js.map +0 -1
  388. package/dist/providers.js.map +0 -1
  389. package/dist/rbac/eslint-rules.js.map +0 -1
  390. package/dist/rbac/index.js.map +0 -1
  391. package/dist/styles/index.js.map +0 -1
  392. package/dist/theming/runtime.js.map +0 -1
  393. package/dist/types.js.map +0 -1
  394. package/dist/utils.js.map +0 -1
  395. package/docs/best-practices/README.md +0 -472
  396. package/docs/best-practices/accessibility.md +0 -601
  397. package/docs/best-practices/common-patterns.md +0 -516
  398. package/docs/best-practices/deployment.md +0 -1103
  399. package/docs/best-practices/performance.md +0 -1328
  400. package/docs/best-practices/security.md +0 -940
  401. package/docs/best-practices/testing.md +0 -1034
  402. package/docs/rbac/compliance/compliance-guide.md +0 -544
  403. package/docs/standards/01-architecture-standard.md +0 -44
  404. package/docs/standards/02-api-and-rpc-standard.md +0 -39
  405. package/docs/standards/03-component-standard.md +0 -32
  406. package/docs/standards/04-code-style-standard.md +0 -32
  407. package/docs/standards/05-security-standard.md +0 -44
  408. package/docs/standards/06-testing-and-docs-standard.md +0 -29
  409. package/docs/standards/pace-core-compliance.md +0 -432
  410. package/scripts/audit/core/checks/accessibility.cjs +0 -197
  411. package/scripts/audit/core/checks/api-usage.cjs +0 -191
  412. package/scripts/audit/core/checks/bundle.cjs +0 -142
  413. package/scripts/audit/core/checks/compliance.cjs +0 -2706
  414. package/scripts/audit/core/checks/config.cjs +0 -54
  415. package/scripts/audit/core/checks/coverage.cjs +0 -84
  416. package/scripts/audit/core/checks/dependencies.cjs +0 -994
  417. package/scripts/audit/core/checks/documentation.cjs +0 -268
  418. package/scripts/audit/core/checks/environment.cjs +0 -116
  419. package/scripts/audit/core/checks/error-handling.cjs +0 -340
  420. package/scripts/audit/core/checks/forms.cjs +0 -172
  421. package/scripts/audit/core/checks/heuristics.cjs +0 -68
  422. package/scripts/audit/core/checks/hooks.cjs +0 -334
  423. package/scripts/audit/core/checks/imports.cjs +0 -244
  424. package/scripts/audit/core/checks/performance.cjs +0 -325
  425. package/scripts/audit/core/checks/routes.cjs +0 -117
  426. package/scripts/audit/core/checks/state.cjs +0 -130
  427. package/scripts/audit/core/checks/structure.cjs +0 -65
  428. package/scripts/audit/core/checks/style.cjs +0 -584
  429. package/scripts/audit/core/checks/testing.cjs +0 -122
  430. package/scripts/audit/core/checks/typescript.cjs +0 -61
  431. package/scripts/audit/core/scanner.cjs +0 -199
  432. package/scripts/audit/core/utils.cjs +0 -137
  433. package/scripts/audit/index.cjs +0 -223
  434. package/scripts/audit/reporters/console.cjs +0 -151
  435. package/scripts/audit/reporters/json.cjs +0 -54
  436. package/scripts/audit/reporters/markdown.cjs +0 -124
  437. package/scripts/audit-consuming-app.cjs +0 -86
  438. package/src/components/DataTable/components/DataTableBody.tsx +0 -454
  439. package/src/components/DataTable/components/DraggableColumnHeader.tsx +0 -156
  440. package/src/components/DataTable/components/ExpandButton.tsx +0 -113
  441. package/src/components/DataTable/components/GroupHeader.tsx +0 -54
  442. package/src/components/DataTable/components/ViewRowModal.tsx +0 -68
  443. package/src/components/DataTable/components/VirtualizedDataTable.tsx +0 -525
  444. package/src/components/DataTable/components/__tests__/ExpandButton.test.tsx +0 -462
  445. package/src/components/DataTable/components/__tests__/GroupHeader.test.tsx +0 -393
  446. package/src/components/DataTable/components/__tests__/ViewRowModal.test.tsx +0 -476
  447. package/src/components/DataTable/components/__tests__/VirtualizedDataTable.test.tsx +0 -128
  448. package/src/components/DataTable/core/DataTableContext.tsx +0 -216
  449. package/src/components/DataTable/core/__tests__/DataTableContext.test.tsx +0 -136
  450. package/src/components/DataTable/hooks/__tests__/useColumnReordering.test.ts +0 -570
  451. package/src/components/DataTable/hooks/useColumnReordering.ts +0 -123
  452. package/src/components/DataTable/utils/debugTools.ts +0 -514
  453. package/src/eslint-rules/pace-core-compliance.js +0 -638
  454. package/src/rbac/components/EnhancedNavigationMenu.test.tsx +0 -555
  455. package/src/rbac/components/EnhancedNavigationMenu.tsx +0 -293
  456. package/src/rbac/components/NavigationProvider.test.tsx +0 -481
  457. package/src/rbac/components/NavigationProvider.tsx +0 -345
  458. package/src/rbac/components/PagePermissionProvider.test.tsx +0 -476
  459. package/src/rbac/components/PagePermissionProvider.tsx +0 -279
  460. package/src/rbac/components/PermissionEnforcer.tsx +0 -312
  461. package/src/rbac/components/RoleBasedRouter.tsx +0 -440
  462. package/src/rbac/components/SecureDataProvider.test.tsx +0 -543
  463. package/src/rbac/components/SecureDataProvider.tsx +0 -339
  464. package/src/rbac/components/__tests__/EnhancedNavigationMenu.test.tsx +0 -620
  465. package/src/rbac/components/__tests__/NavigationProvider.test.tsx +0 -726
  466. package/src/rbac/components/__tests__/PagePermissionProvider.test.tsx +0 -661
  467. package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +0 -881
  468. package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +0 -783
  469. package/src/rbac/components/__tests__/SecureDataProvider.fixed.test.tsx +0 -645
  470. package/src/rbac/components/__tests__/SecureDataProvider.test.tsx +0 -659
  471. package/src/rbac/hooks/permissions/useCachedPermissions.ts +0 -79
  472. package/src/rbac/hooks/permissions/useHasAllPermissions.ts +0 -90
  473. package/src/rbac/hooks/permissions/useHasAnyPermission.ts +0 -90
@@ -9,7 +9,7 @@
9
9
  * consistent scope resolution logic.
10
10
  */
11
11
 
12
- import { useEffect, useState, useRef, useMemo } from 'react';
12
+ import { useEffect, useState, useMemo } from 'react';
13
13
  import { SupabaseClient } from '@supabase/supabase-js';
14
14
  import type { Database } from '../../types/database';
15
15
  import type { Scope } from '../types';
@@ -35,6 +35,8 @@ export interface UseResolvedScopeOptions {
35
35
  selectedOrganisationId: string | null;
36
36
  /** Selected event ID */
37
37
  selectedEventId: string | null;
38
+ /** Selected event organisation ID (from selectedEvent.organisation_id) - allows immediate context without querying */
39
+ selectedEventOrganisationId?: string | null;
38
40
  }
39
41
 
40
42
  export interface UseResolvedScopeReturn {
@@ -73,72 +75,42 @@ export interface UseResolvedScopeReturn {
73
75
  export function useResolvedScope({
74
76
  supabase,
75
77
  selectedOrganisationId,
76
- selectedEventId
78
+ selectedEventId,
79
+ selectedEventOrganisationId
77
80
  }: UseResolvedScopeOptions): UseResolvedScopeReturn {
78
- const [resolvedScope, setResolvedScope] = useState<Scope | null>(null);
79
- const [isLoading, setIsLoading] = useState(true);
80
- const [error, setError] = useState<Error | null>(null);
81
-
82
- // Use a ref to track the stable scope and only update it when it actually changes
83
- const stableScopeRef = useRef<{ organisationId: string; appId: string; eventId: string | undefined }>({
84
- organisationId: '',
85
- appId: '',
86
- eventId: undefined
87
- });
81
+ // Get immediate context (synchronous) - allows secure client creation immediately
82
+ const immediateOrganisationId = selectedEventOrganisationId || selectedOrganisationId || undefined;
83
+ const immediateEventId = selectedEventId || undefined;
88
84
 
89
- // Update stable scope ref in useEffect to avoid updates during render
90
- // For PORTAL, allow scopes without organisationId
91
- useEffect(() => {
92
- if (resolvedScope) {
93
- const newScope = {
94
- organisationId: resolvedScope.organisationId || '',
95
- appId: resolvedScope.appId || '',
96
- eventId: resolvedScope.eventId
97
- };
98
-
99
- // Only update if the scope has actually changed
100
- if (stableScopeRef.current.organisationId !== newScope.organisationId ||
101
- stableScopeRef.current.eventId !== newScope.eventId ||
102
- stableScopeRef.current.appId !== newScope.appId) {
103
- stableScopeRef.current = {
104
- organisationId: newScope.organisationId,
105
- appId: newScope.appId,
106
- eventId: newScope.eventId
107
- };
108
- }
109
- } else {
110
- // Reset to empty scope when no resolved scope
111
- stableScopeRef.current = { organisationId: '', appId: '', eventId: undefined };
112
- }
113
- }, [resolvedScope]);
85
+ const [appId, setAppId] = useState<string | undefined>(undefined);
86
+ const [isResolvingAppId, setIsResolvingAppId] = useState(false);
87
+ const [error, setError] = useState<Error | null>(null);
114
88
 
115
89
  // Get app name to check if it's PORTAL (needed for return logic)
116
90
  const appName = getCurrentAppName();
117
91
 
118
- const stableScope = stableScopeRef.current;
119
-
92
+ // Resolve appId in background (non-blocking)
120
93
  useEffect(() => {
121
94
  let cancelled = false;
122
95
 
123
- const resolveScope = async () => {
124
- // OPTIMIZATION: If all inputs are null/undefined, immediately return empty scope
125
- // This indicates pre-filtered mode where we don't need to resolve scope
96
+ const resolveAppId = async () => {
97
+ // OPTIMIZATION: If all inputs are null/undefined, skip appId resolution
126
98
  if (!supabase && !selectedOrganisationId && !selectedEventId) {
127
99
  if (!cancelled) {
128
- setResolvedScope(null);
129
- setIsLoading(false);
100
+ setAppId(undefined);
101
+ setIsResolvingAppId(false);
130
102
  setError(null);
131
103
  }
132
104
  return;
133
105
  }
134
106
 
135
- setIsLoading(true);
107
+ setIsResolvingAppId(true);
136
108
  setError(null);
137
109
 
138
110
  try {
139
111
  // Get app name and resolve appId
140
112
  const appName = getCurrentAppName();
141
- let appId: string | undefined = undefined;
113
+ let resolvedAppId: string | undefined = undefined;
142
114
 
143
115
  // Try to resolve appId from database (with caching)
144
116
  // Only query if user is authenticated (RLS policies require authentication)
@@ -156,7 +128,7 @@ export function useResolvedScope({
156
128
  const cached = appIdCache.get(appName);
157
129
  const now = Date.now();
158
130
  if (cached && (now - cached.timestamp) < CACHE_TTL) {
159
- appId = cached.appId;
131
+ resolvedAppId = cached.appId;
160
132
  } else {
161
133
  // Cache miss or expired - fetch from database
162
134
  const { data: app, error } = await supabase
@@ -172,7 +144,7 @@ export function useResolvedScope({
172
144
  if (error.code === '406' || error.code === 'PGRST116' || error.message?.includes('406')) {
173
145
  log.debug(`App resolution blocked by RLS for "${appName}" - user may not be authenticated`);
174
146
  // Don't cache - will retry after authentication
175
- appId = undefined;
147
+ resolvedAppId = undefined;
176
148
  } else {
177
149
  // Check if app exists but is inactive
178
150
  const { data: inactiveApp } = await supabase
@@ -184,17 +156,17 @@ export function useResolvedScope({
184
156
  if (inactiveApp) {
185
157
  log.error(`App "${appName}" exists but is inactive (is_active: ${inactiveApp.is_active})`);
186
158
  // Don't cache inactive apps - set appId to undefined
187
- appId = undefined;
159
+ resolvedAppId = undefined;
188
160
  } else {
189
161
  log.error(`App "${appName}" not found in rbac_apps table`, { error });
190
162
  // Don't cache missing apps - set appId to undefined
191
- appId = undefined;
163
+ resolvedAppId = undefined;
192
164
  }
193
165
  }
194
166
  } else if (app) {
195
- appId = app.id;
167
+ resolvedAppId = app.id;
196
168
  // Only cache successful lookups of active apps
197
- appIdCache.set(appName, { appId, timestamp: now });
169
+ appIdCache.set(appName, { appId: resolvedAppId, timestamp: now });
198
170
  }
199
171
  }
200
172
  }
@@ -210,119 +182,65 @@ export function useResolvedScope({
210
182
  }
211
183
  }
212
184
 
213
- // Build initial scope from available context
214
- // Scope is now page-level only - use whatever context is available
215
- // Default to organisation scope if both are available (safest default)
216
- const initialScope: Scope = {
217
- organisationId: selectedOrganisationId || undefined,
218
- eventId: selectedEventId || undefined,
219
- appId: appId
220
- };
221
-
222
- // For PORTAL/ADMIN apps, allow scope without org/event
223
- if (appName === 'PORTAL' || appName === 'ADMIN') {
224
- if (!cancelled) {
225
- const optionalContextScope: Scope = {
226
- organisationId: undefined,
227
- eventId: undefined,
228
- appId: appId || undefined
229
- };
230
- setResolvedScope(optionalContextScope);
231
- setError(null);
232
- setIsLoading(false);
233
- }
234
- return;
235
- }
236
-
237
- // For other apps, default to organisation scope validation (safest default)
238
- // Page-level scope will be validated during permission checks
239
- // ContextValidator is already imported at the top
240
- const { ContextValidator } = await import('../utils/contextValidator');
241
- const validation = await ContextValidator.resolveScopeForPage(
242
- initialScope,
243
- 'organisation', // Default to organisation scope when no page context
244
- appName || undefined,
245
- supabase
246
- );
247
-
248
- if (!validation.isValid) {
249
- // If validation fails but we have an eventId, return scope with eventId
250
- // The organisation will be derived later during permission checks
251
- if (selectedEventId) {
252
- if (!cancelled) {
253
- const eventScope: Scope = {
254
- organisationId: undefined, // Will be derived from event during permission check
255
- eventId: selectedEventId,
256
- appId: appId || undefined
257
- };
258
- setResolvedScope(eventScope);
259
- setError(null); // Don't set error - let permission check handle derivation
260
- setIsLoading(false);
261
- }
262
- return;
263
- }
264
-
265
- if (!cancelled) {
266
- setResolvedScope(null);
267
- setError(validation.error || new Error('Context validation failed'));
268
- setIsLoading(false);
269
- }
270
- return;
271
- }
272
-
273
- // Set resolved scope
185
+ // Update appId state when resolved
274
186
  if (!cancelled) {
275
- setResolvedScope(validation.resolvedScope);
187
+ setAppId(resolvedAppId);
188
+ setIsResolvingAppId(false);
276
189
  setError(null);
277
- setIsLoading(false);
278
190
  }
279
191
  } catch (err) {
280
192
  if (!cancelled) {
281
193
  setError(err as Error);
282
- setIsLoading(false);
194
+ setIsResolvingAppId(false);
283
195
  }
284
196
  }
285
197
  };
286
198
 
287
- resolveScope();
199
+ resolveAppId();
288
200
 
289
201
  return () => {
290
202
  cancelled = true;
291
203
  };
292
- }, [selectedOrganisationId, selectedEventId, supabase]);
293
-
294
- // Return scope if it has appId (for PORTAL/ADMIN) or organisationId (for other apps)
295
- // For PORTAL/ADMIN, always return a scope (even if empty) so components can use contextAppId
296
- const allowsOptionalContexts = appName === 'PORTAL' || appName === 'ADMIN';
297
- const hasValidScope = allowsOptionalContexts
298
- ? true // PORTAL/ADMIN always have valid scope (even without org/event/appId)
299
- : (stableScope.appId || stableScope.organisationId);
204
+ }, [supabase, selectedOrganisationId, selectedEventId]);
300
205
 
301
- // CRITICAL FIX: Memoize finalScope to prevent creating new object references on every render
302
- // This prevents infinite loops in useCan and other hooks that depend on scope equality
303
- const finalScope: Scope | null = useMemo(() => {
304
- if (!hasValidScope) {
305
- return allowsOptionalContexts ? {} : null;
206
+ // Build scope immediately with synchronous context + async appId
207
+ // This allows secure client creation immediately while appId resolves
208
+ const immediateScope: Scope | null = useMemo(() => {
209
+ // For PORTAL/ADMIN apps, allow scope without org/event
210
+ if (appName === 'PORTAL' || appName === 'ADMIN') {
211
+ return {
212
+ organisationId: undefined,
213
+ eventId: undefined,
214
+ appId: appId || undefined
215
+ };
306
216
  }
307
217
 
308
- // Build scope object only with defined values to ensure stable reference
218
+ // Build scope with immediate context
309
219
  const scope: Scope = {};
310
- if (stableScope.organisationId) {
311
- scope.organisationId = stableScope.organisationId;
220
+ if (immediateOrganisationId) {
221
+ scope.organisationId = immediateOrganisationId;
312
222
  }
313
- if (stableScope.eventId) {
314
- scope.eventId = stableScope.eventId;
223
+ if (immediateEventId) {
224
+ scope.eventId = immediateEventId;
315
225
  }
316
- if (stableScope.appId) {
317
- scope.appId = stableScope.appId;
226
+ if (appId) {
227
+ scope.appId = appId;
228
+ }
229
+
230
+ // For non-PORTAL/ADMIN apps, require at least orgId or appId
231
+ if (!scope.organisationId && !scope.appId) {
232
+ return null;
318
233
  }
319
234
 
320
235
  return scope;
321
- }, [hasValidScope, allowsOptionalContexts, stableScope.organisationId, stableScope.eventId, stableScope.appId]);
236
+ }, [immediateOrganisationId, immediateEventId, appId, appName]);
322
237
 
238
+ // Return scope immediately - appId resolves in background
239
+ // Navigation and permissions will wait for appId (correct behavior)
240
+ // But secure client can be created immediately, allowing data queries to run
323
241
  return {
324
- resolvedScope: finalScope,
325
- isLoading,
242
+ resolvedScope: immediateScope,
243
+ isLoading: isResolvingAppId, // Only true while appId resolves
326
244
  error
327
245
  };
328
246
  }
@@ -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,24 +147,42 @@ 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, permission strings include page. prefix
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
141
169
  // and resource name is passed as pageId to enable page permission checks
142
170
  expect(mockUseCan).toHaveBeenCalledWith(
143
171
  'user-123',
144
- mockScope,
172
+ mockScope, // resolvedScope is used when available
145
173
  'create:page.contacts',
146
- 'contacts', // pageId is resource name when appId is available
174
+ 'contacts', // pageId is resource name when appId is available in resolvedScope
147
175
  true,
148
- null, // precomputedSuperAdmin
176
+ false, // precomputedSuperAdmin (resolved after async check)
149
177
  undefined // appName
150
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');
151
186
  expect(mockUseCan).toHaveBeenCalledWith(
152
187
  'user-123',
153
188
  mockScope,
@@ -192,9 +227,10 @@ describe('useResourcePermissions Hook', () => {
192
227
  expect(result.current.canCreate('contacts')).toBe(true);
193
228
  });
194
229
 
195
- it('returns false when user cannot create resource', () => {
230
+ it('returns false when user cannot create resource', async () => {
196
231
  mockUseCan.mockImplementation((userId, scope, permission) => {
197
- if (permission === 'create:page.contacts') {
232
+ // When appId is available in resolvedScope, permission format is 'create:page.contacts'
233
+ if (permission === 'create:page.contacts' || permission === 'create:contacts') {
198
234
  return {
199
235
  can: false,
200
236
  isLoading: false,
@@ -212,6 +248,11 @@ describe('useResourcePermissions Hook', () => {
212
248
 
213
249
  const { result } = renderHook(() => useResourcePermissions('contacts'));
214
250
 
251
+ // Wait for super admin check to complete
252
+ await waitFor(() => {
253
+ expect(result.current.isLoading).toBe(false);
254
+ });
255
+
215
256
  expect(result.current.canCreate('contacts')).toBe(false);
216
257
  expect(result.current.canUpdate('contacts')).toBe(true);
217
258
  expect(result.current.canDelete('contacts')).toBe(true);
@@ -233,9 +274,10 @@ describe('useResourcePermissions Hook', () => {
233
274
  expect(result.current.canRead('contacts')).toBe(true);
234
275
  });
235
276
 
236
- it('checks read permissions when enableRead is true', () => {
277
+ it('checks read permissions when enableRead is true', async () => {
237
278
  mockUseCan.mockImplementation((userId, scope, permission) => {
238
- if (permission === 'read:page.contacts') {
279
+ // When appId is available in resolvedScope, permission format is 'read:page.contacts'
280
+ if (permission === 'read:page.contacts' || permission === 'read:contacts') {
239
281
  return {
240
282
  can: true,
241
283
  isLoading: false,
@@ -255,12 +297,18 @@ describe('useResourcePermissions Hook', () => {
255
297
  useResourcePermissions('contacts', { enableRead: true })
256
298
  );
257
299
 
300
+ // Wait for super admin check to complete
301
+ await waitFor(() => {
302
+ expect(result.current.isLoading).toBe(false);
303
+ });
304
+
258
305
  expect(result.current.canRead('contacts')).toBe(true);
259
306
  });
260
307
 
261
- 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 () => {
262
309
  mockUseCan.mockImplementation((userId, scope, permission) => {
263
- if (permission === 'read:page.contacts') {
310
+ // When appId is available in resolvedScope, permission format is 'read:page.contacts'
311
+ if (permission === 'read:page.contacts' || permission === 'read:contacts') {
264
312
  return {
265
313
  can: false,
266
314
  isLoading: false,
@@ -280,6 +328,11 @@ describe('useResourcePermissions Hook', () => {
280
328
  useResourcePermissions('contacts', { enableRead: true })
281
329
  );
282
330
 
331
+ // Wait for super admin check to complete
332
+ await waitFor(() => {
333
+ expect(result.current.isLoading).toBe(false);
334
+ });
335
+
283
336
  expect(result.current.canRead('contacts')).toBe(false);
284
337
  });
285
338
  });
@@ -399,7 +452,7 @@ describe('useResourcePermissions Hook', () => {
399
452
  });
400
453
 
401
454
  describe('Missing User Context', () => {
402
- it('handles missing user gracefully', () => {
455
+ it('handles missing user gracefully', async () => {
403
456
  mockUseUnifiedAuth.mockReturnValue({
404
457
  user: null,
405
458
  supabase: mockSupabase,
@@ -407,15 +460,20 @@ describe('useResourcePermissions Hook', () => {
407
460
 
408
461
  const { result } = renderHook(() => useResourcePermissions('contacts'));
409
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
+
410
468
  // When user is null, userId is empty string, but scope and permissions are still checked
411
- // Since mockScope has appId, permission strings include page. prefix and pageId will be the resource name ('contacts')
469
+ // Since resolvedScope (mockScope) has appId, permission strings include page. prefix
412
470
  expect(mockUseCan).toHaveBeenCalledWith(
413
471
  '',
414
- mockScope,
472
+ mockScope, // resolvedScope is used when available
415
473
  'create:page.contacts',
416
- 'contacts', // pageId is resource name when appId is available in scope
474
+ 'contacts', // pageId is resource name when appId is available in resolvedScope
417
475
  true,
418
- null, // precomputedSuperAdmin
476
+ false, // precomputedSuperAdmin (false when user is null)
419
477
  undefined // appName
420
478
  );
421
479
  expect(mockUseCan).toHaveBeenCalledWith(
@@ -454,11 +512,11 @@ describe('useResourcePermissions Hook', () => {
454
512
  throw new Error('Event provider not available');
455
513
  });
456
514
 
457
- mockUseResolvedScope.mockReturnValue({
458
- resolvedScope: mockScope,
459
- isLoading: false,
460
- error: null,
461
- });
515
+ mockUseResolvedScope.mockReturnValue({
516
+ resolvedScope: mockScope,
517
+ isLoading: false, // Scope is resolved
518
+ error: null,
519
+ });
462
520
 
463
521
  const { result } = renderHook(() => useResourcePermissions('contacts'));
464
522
 
@@ -528,12 +586,20 @@ describe('useResourcePermissions Hook', () => {
528
586
  expect(result.current.isLoading).toBe(true);
529
587
  });
530
588
 
531
- 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
+
532
597
  mockUseCan.mockImplementation((userId, scope, permission) => {
533
- if (permission === 'read:page.contacts') {
598
+ // Read permission check should not contribute to isLoading when enableRead is false
599
+ if (permission === 'read:page.contacts' || permission === 'read:contacts') {
534
600
  return {
535
601
  can: false,
536
- isLoading: true,
602
+ isLoading: true, // Read is loading, but should be excluded
537
603
  error: null,
538
604
  refetch: vi.fn(),
539
605
  } as any;
@@ -546,9 +612,12 @@ describe('useResourcePermissions Hook', () => {
546
612
  } as any;
547
613
  });
548
614
 
549
- const { result } = renderHook(() => useResourcePermissions('contacts'));
615
+ const { result } = renderHook(() => useResourcePermissions('contacts', { enableRead: false }));
550
616
 
551
- 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
+ });
552
621
  });
553
622
  });
554
623
 
@@ -566,10 +635,11 @@ describe('useResourcePermissions Hook', () => {
566
635
  expect(result.current.error).toEqual(scopeError);
567
636
  });
568
637
 
569
- it('aggregates errors from permission checks', () => {
638
+ it('aggregates errors from permission checks', async () => {
570
639
  const permissionError = new Error('Permission check failed');
571
640
  mockUseCan.mockImplementation((userId, scope, permission) => {
572
- if (permission === 'create:page.contacts') {
641
+ // When appId is available in resolvedScope, permission format is 'create:page.contacts'
642
+ if (permission === 'create:page.contacts' || permission === 'create:contacts') {
573
643
  return {
574
644
  can: false,
575
645
  isLoading: false,
@@ -587,6 +657,11 @@ describe('useResourcePermissions Hook', () => {
587
657
 
588
658
  const { result } = renderHook(() => useResourcePermissions('contacts'));
589
659
 
660
+ // Wait for super admin check to complete
661
+ await waitFor(() => {
662
+ expect(result.current.isLoading).toBe(false);
663
+ });
664
+
590
665
  expect(result.current.error).toEqual(permissionError);
591
666
  });
592
667
 
@@ -630,17 +705,22 @@ describe('useResourcePermissions Hook', () => {
630
705
  });
631
706
 
632
707
  describe('Options', () => {
633
- it('respects enableRead option', () => {
708
+ it('respects enableRead option', async () => {
634
709
  renderHook(() => useResourcePermissions('contacts', { enableRead: true }));
635
710
 
636
- // Should still call useCan for read permission with page. prefix when appId is available
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
637
717
  expect(mockUseCan).toHaveBeenCalledWith(
638
718
  expect.any(String),
639
719
  expect.any(Object),
640
720
  'read:page.contacts',
641
- 'contacts', // pageId is resource name when appId is available
721
+ 'contacts', // pageId is resource name when appId is available in resolvedScope
642
722
  true,
643
- null, // precomputedSuperAdmin
723
+ false, // precomputedSuperAdmin (resolved after async check)
644
724
  undefined // appName
645
725
  );
646
726
  });
@@ -654,16 +734,22 @@ describe('useResourcePermissions Hook', () => {
654
734
  });
655
735
 
656
736
  describe('Different Resources', () => {
657
- it('works with different resource names', () => {
737
+ it('works with different resource names', async () => {
658
738
  renderHook(() => useResourcePermissions('risks'));
659
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
660
746
  expect(mockUseCan).toHaveBeenCalledWith(
661
747
  expect.any(String),
662
748
  expect.any(Object),
663
749
  'create:page.risks',
664
- 'risks', // pageId is resource name when appId is available
750
+ 'risks', // pageId is resource name when appId is available in resolvedScope
665
751
  true,
666
- null, // precomputedSuperAdmin
752
+ false, // precomputedSuperAdmin (resolved after async check)
667
753
  undefined // appName
668
754
  );
669
755
  expect(mockUseCan).toHaveBeenCalledWith(