@jmruthers/pace-core 0.5.189 → 0.5.191

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 (438) hide show
  1. package/core-usage-manifest.json +0 -4
  2. package/dist/{AuthService-B-cd2MA4.d.ts → AuthService-CbP_utw2.d.ts} +7 -3
  3. package/dist/{DataTable-IVYljGJ6.d.ts → DataTable-Be6dH_dR.d.ts} +1 -1
  4. package/dist/{DataTable-GUFUNZ3N.js → DataTable-WKRZD47S.js} +8 -8
  5. package/dist/{PublicPageProvider-B8HaLe69.d.ts → PublicPageProvider-ULXC_u6U.d.ts} +84 -25
  6. package/dist/{UnifiedAuthProvider-BG0AL5eE.d.ts → UnifiedAuthProvider-BYA9qB-o.d.ts} +4 -3
  7. package/dist/{UnifiedAuthProvider-643PUAIM.js → UnifiedAuthProvider-FTSG5XH7.js} +4 -2
  8. package/dist/{api-YP7XD5L6.js → api-IHKALJZD.js} +4 -2
  9. package/dist/{chunk-VGZZXKBR.js → chunk-6LTQQAT6.js} +351 -157
  10. package/dist/chunk-6LTQQAT6.js.map +1 -0
  11. package/dist/{chunk-MX64ZF6I.js → chunk-6TQDD426.js} +15 -15
  12. package/dist/chunk-6TQDD426.js.map +1 -0
  13. package/dist/{chunk-YHCN776L.js → chunk-G37KK66H.js} +2 -75
  14. package/dist/chunk-G37KK66H.js.map +1 -0
  15. package/dist/{chunk-THRPYOFK.js → chunk-HW3OVDUF.js} +5 -5
  16. package/dist/chunk-HW3OVDUF.js.map +1 -0
  17. package/dist/{chunk-F2IMUDXZ.js → chunk-I7PSE6JW.js} +75 -2
  18. package/dist/chunk-I7PSE6JW.js.map +1 -0
  19. package/dist/{chunk-IM4QE42D.js → chunk-LOMZXPSN.js} +141 -326
  20. package/dist/chunk-LOMZXPSN.js.map +1 -0
  21. package/dist/chunk-OETXORNB.js +614 -0
  22. package/dist/chunk-OETXORNB.js.map +1 -0
  23. package/dist/{chunk-HESYZWZW.js → chunk-QWWZ5CAQ.js} +2 -2
  24. package/dist/{chunk-HEHYGYOX.js → chunk-ROXMHMY2.js} +403 -46
  25. package/dist/chunk-ROXMHMY2.js.map +1 -0
  26. package/dist/{chunk-2UUZZJFT.js → chunk-ULHIJK66.js} +228 -177
  27. package/dist/{chunk-2UUZZJFT.js.map → chunk-ULHIJK66.js.map} +1 -1
  28. package/dist/{chunk-YGPFYGA6.js → chunk-VKB2CO4Z.js} +838 -503
  29. package/dist/chunk-VKB2CO4Z.js.map +1 -0
  30. package/dist/{chunk-3GOZZZYH.js → chunk-VRGWKHDB.js} +238 -301
  31. package/dist/chunk-VRGWKHDB.js.map +1 -0
  32. package/dist/{chunk-UCQSRW7Z.js → chunk-XNYQOL3Z.js} +431 -384
  33. package/dist/chunk-XNYQOL3Z.js.map +1 -0
  34. package/dist/{chunk-DDM4CCYT.js → chunk-XYXSXPUK.js} +79 -59
  35. package/dist/chunk-XYXSXPUK.js.map +1 -0
  36. package/dist/{chunk-SAUPYVLF.js → chunk-ZSAAAMVR.js} +1 -1
  37. package/dist/chunk-ZSAAAMVR.js.map +1 -0
  38. package/dist/components.d.ts +5 -6
  39. package/dist/components.js +19 -19
  40. package/dist/components.js.map +1 -1
  41. package/dist/{database.generated-DI89OQeI.d.ts → database.generated-CzIvgcPu.d.ts} +165 -201
  42. package/dist/eslint-rules/pace-core-compliance.cjs +0 -2
  43. package/dist/{file-reference-D037xOFK.d.ts → file-reference-BavO2eQj.d.ts} +13 -10
  44. package/dist/hooks.d.ts +20 -15
  45. package/dist/hooks.js +14 -8
  46. package/dist/hooks.js.map +1 -1
  47. package/dist/index.d.ts +17 -15
  48. package/dist/index.js +86 -81
  49. package/dist/index.js.map +1 -1
  50. package/dist/providers.d.ts +3 -3
  51. package/dist/providers.js +3 -1
  52. package/dist/rbac/index.d.ts +77 -13
  53. package/dist/rbac/index.js +12 -9
  54. package/dist/{types-Bwgl--Xo.d.ts → types-CEpcvwwF.d.ts} +1 -1
  55. package/dist/types.d.ts +3 -3
  56. package/dist/types.js +1 -1
  57. package/dist/{usePublicRouteParams-CTDELQ7H.d.ts → usePublicRouteParams-TZe0gy-4.d.ts} +17 -10
  58. package/dist/utils.d.ts +8 -8
  59. package/dist/utils.js +16 -16
  60. package/docs/README.md +2 -2
  61. package/docs/api/classes/ColumnFactory.md +1 -1
  62. package/docs/api/classes/ErrorBoundary.md +1 -1
  63. package/docs/api/classes/InvalidScopeError.md +2 -2
  64. package/docs/api/classes/Logger.md +1 -1
  65. package/docs/api/classes/MissingUserContextError.md +2 -2
  66. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  67. package/docs/api/classes/PermissionDeniedError.md +1 -1
  68. package/docs/api/classes/RBACAuditManager.md +2 -2
  69. package/docs/api/classes/RBACCache.md +1 -1
  70. package/docs/api/classes/RBACEngine.md +5 -5
  71. package/docs/api/classes/RBACError.md +1 -1
  72. package/docs/api/classes/RBACNotInitializedError.md +2 -2
  73. package/docs/api/classes/SecureSupabaseClient.md +25 -20
  74. package/docs/api/classes/StorageUtils.md +7 -4
  75. package/docs/api/enums/FileCategory.md +1 -1
  76. package/docs/api/enums/LogLevel.md +1 -1
  77. package/docs/api/enums/RBACErrorCode.md +1 -1
  78. package/docs/api/enums/RPCFunction.md +1 -1
  79. package/docs/api/interfaces/AddressFieldProps.md +1 -1
  80. package/docs/api/interfaces/AddressFieldRef.md +1 -1
  81. package/docs/api/interfaces/AggregateConfig.md +1 -1
  82. package/docs/api/interfaces/AutocompleteOptions.md +1 -1
  83. package/docs/api/interfaces/AvatarProps.md +1 -1
  84. package/docs/api/interfaces/BadgeProps.md +1 -1
  85. package/docs/api/interfaces/ButtonProps.md +1 -1
  86. package/docs/api/interfaces/CalendarProps.md +20 -6
  87. package/docs/api/interfaces/CardProps.md +1 -1
  88. package/docs/api/interfaces/ColorPalette.md +1 -1
  89. package/docs/api/interfaces/ColorShade.md +1 -1
  90. package/docs/api/interfaces/ComplianceResult.md +1 -1
  91. package/docs/api/interfaces/DataAccessRecord.md +9 -9
  92. package/docs/api/interfaces/DataRecord.md +1 -1
  93. package/docs/api/interfaces/DataTableAction.md +1 -1
  94. package/docs/api/interfaces/DataTableColumn.md +1 -1
  95. package/docs/api/interfaces/DataTableProps.md +1 -1
  96. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  97. package/docs/api/interfaces/DatabaseComplianceResult.md +1 -1
  98. package/docs/api/interfaces/DatabaseIssue.md +1 -1
  99. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  100. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  101. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  102. package/docs/api/interfaces/ExportColumn.md +1 -1
  103. package/docs/api/interfaces/ExportOptions.md +1 -1
  104. package/docs/api/interfaces/FileDisplayProps.md +62 -16
  105. package/docs/api/interfaces/FileMetadata.md +1 -1
  106. package/docs/api/interfaces/FileReference.md +2 -2
  107. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  108. package/docs/api/interfaces/FileUploadOptions.md +26 -12
  109. package/docs/api/interfaces/FileUploadProps.md +30 -19
  110. package/docs/api/interfaces/FooterProps.md +1 -1
  111. package/docs/api/interfaces/FormFieldProps.md +1 -1
  112. package/docs/api/interfaces/FormProps.md +1 -1
  113. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  114. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  115. package/docs/api/interfaces/InputProps.md +1 -1
  116. package/docs/api/interfaces/LabelProps.md +1 -1
  117. package/docs/api/interfaces/LoggerConfig.md +1 -1
  118. package/docs/api/interfaces/LoginFormProps.md +1 -1
  119. package/docs/api/interfaces/NavigationAccessRecord.md +10 -10
  120. package/docs/api/interfaces/NavigationContextType.md +9 -9
  121. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  122. package/docs/api/interfaces/NavigationItem.md +1 -1
  123. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  124. package/docs/api/interfaces/NavigationProviderProps.md +7 -7
  125. package/docs/api/interfaces/Organisation.md +1 -1
  126. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  127. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  128. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  129. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  130. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  131. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  132. package/docs/api/interfaces/PageAccessRecord.md +8 -8
  133. package/docs/api/interfaces/PagePermissionContextType.md +8 -8
  134. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  135. package/docs/api/interfaces/PagePermissionProviderProps.md +7 -7
  136. package/docs/api/interfaces/PaletteData.md +1 -1
  137. package/docs/api/interfaces/ParsedAddress.md +2 -2
  138. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  139. package/docs/api/interfaces/ProgressProps.md +3 -11
  140. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  141. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  142. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  143. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  144. package/docs/api/interfaces/QuickFix.md +1 -1
  145. package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
  146. package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
  147. package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
  148. package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
  149. package/docs/api/interfaces/RBACConfig.md +2 -2
  150. package/docs/api/interfaces/RBACContext.md +1 -1
  151. package/docs/api/interfaces/RBACLogger.md +1 -1
  152. package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
  153. package/docs/api/interfaces/RBACPerformanceMetrics.md +1 -1
  154. package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
  155. package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
  156. package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
  157. package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
  158. package/docs/api/interfaces/RBACResult.md +1 -1
  159. package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
  160. package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
  161. package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
  162. package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
  163. package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
  164. package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
  165. package/docs/api/interfaces/RBACRolesListParams.md +1 -1
  166. package/docs/api/interfaces/RBACRolesListResult.md +1 -1
  167. package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
  168. package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
  169. package/docs/api/interfaces/ResourcePermissions.md +1 -1
  170. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  171. package/docs/api/interfaces/RoleBasedRouterContextType.md +8 -8
  172. package/docs/api/interfaces/RoleBasedRouterProps.md +10 -10
  173. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  174. package/docs/api/interfaces/RouteAccessRecord.md +10 -10
  175. package/docs/api/interfaces/RouteConfig.md +10 -10
  176. package/docs/api/interfaces/RuntimeComplianceResult.md +1 -1
  177. package/docs/api/interfaces/SecureDataContextType.md +9 -9
  178. package/docs/api/interfaces/SecureDataProviderProps.md +8 -8
  179. package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
  180. package/docs/api/interfaces/SetupIssue.md +1 -1
  181. package/docs/api/interfaces/StorageConfig.md +4 -4
  182. package/docs/api/interfaces/StorageFileInfo.md +7 -7
  183. package/docs/api/interfaces/StorageFileMetadata.md +25 -14
  184. package/docs/api/interfaces/StorageListOptions.md +22 -9
  185. package/docs/api/interfaces/StorageListResult.md +4 -4
  186. package/docs/api/interfaces/StorageUploadOptions.md +21 -8
  187. package/docs/api/interfaces/StorageUploadResult.md +6 -6
  188. package/docs/api/interfaces/StorageUrlOptions.md +19 -6
  189. package/docs/api/interfaces/StyleImport.md +1 -1
  190. package/docs/api/interfaces/SwitchProps.md +1 -1
  191. package/docs/api/interfaces/TabsContentProps.md +1 -1
  192. package/docs/api/interfaces/TabsListProps.md +1 -1
  193. package/docs/api/interfaces/TabsProps.md +1 -1
  194. package/docs/api/interfaces/TabsTriggerProps.md +1 -1
  195. package/docs/api/interfaces/TextareaProps.md +1 -1
  196. package/docs/api/interfaces/ToastActionElement.md +1 -1
  197. package/docs/api/interfaces/ToastProps.md +1 -1
  198. package/docs/api/interfaces/UnifiedAuthContextType.md +53 -53
  199. package/docs/api/interfaces/UnifiedAuthProviderProps.md +13 -13
  200. package/docs/api/interfaces/UseFormDialogOptions.md +1 -1
  201. package/docs/api/interfaces/UseFormDialogReturn.md +1 -1
  202. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  203. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  204. package/docs/api/interfaces/UsePublicEventLogoOptions.md +2 -2
  205. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  206. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  207. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  208. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +2 -2
  209. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  210. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  211. package/docs/api/interfaces/UseResolvedScopeOptions.md +5 -5
  212. package/docs/api/interfaces/UseResolvedScopeReturn.md +4 -4
  213. package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
  214. package/docs/api/interfaces/UserEventAccess.md +11 -11
  215. package/docs/api/interfaces/UserMenuProps.md +1 -1
  216. package/docs/api/interfaces/UserProfile.md +1 -1
  217. package/docs/api/modules.md +165 -106
  218. package/docs/api-reference/components.md +15 -7
  219. package/docs/api-reference/providers.md +2 -2
  220. package/docs/api-reference/rpc-functions.md +1 -0
  221. package/docs/best-practices/README.md +1 -1
  222. package/docs/best-practices/deployment.md +8 -8
  223. package/docs/getting-started/examples/README.md +2 -2
  224. package/docs/getting-started/installation-guide.md +4 -4
  225. package/docs/getting-started/quick-start.md +3 -3
  226. package/docs/migration/MIGRATION_GUIDE.md +3 -3
  227. package/docs/migration/README.md +18 -0
  228. package/docs/migration/database-changes-december-2025.md +767 -0
  229. package/docs/migration/person-scoped-profiles-migration-guide.md +472 -0
  230. package/docs/rbac/compliance/compliance-guide.md +2 -2
  231. package/docs/rbac/event-based-apps.md +2 -2
  232. package/docs/rbac/getting-started.md +2 -2
  233. package/docs/rbac/quick-start.md +2 -2
  234. package/docs/security/README.md +4 -4
  235. package/docs/standards/07-rbac-and-rls-standard.md +430 -7
  236. package/docs/troubleshooting/README.md +2 -2
  237. package/docs/troubleshooting/migration.md +3 -3
  238. package/package.json +1 -3
  239. package/scripts/check-pace-core-compliance.cjs +1 -1
  240. package/scripts/check-pace-core-compliance.js +1 -1
  241. package/src/__tests__/fixtures/supabase.ts +301 -0
  242. package/src/__tests__/public-recipe-view.test.ts +19 -19
  243. package/src/__tests__/rls-policies.test.ts +210 -74
  244. package/src/components/AddressField/AddressField.test.tsx +42 -0
  245. package/src/components/AddressField/AddressField.tsx +71 -60
  246. package/src/components/AddressField/README.md +7 -6
  247. package/src/components/Alert/Alert.test.tsx +50 -10
  248. package/src/components/Alert/Alert.tsx +5 -3
  249. package/src/components/Avatar/Avatar.test.tsx +95 -43
  250. package/src/components/Avatar/Avatar.tsx +16 -16
  251. package/src/components/Button/Button.test.tsx +2 -1
  252. package/src/components/Button/Button.tsx +3 -3
  253. package/src/components/Calendar/Calendar.test.tsx +53 -37
  254. package/src/components/Calendar/Calendar.tsx +409 -82
  255. package/src/components/Card/Card.test.tsx +7 -4
  256. package/src/components/Card/Card.tsx +3 -6
  257. package/src/components/Checkbox/Checkbox.tsx +2 -2
  258. package/src/components/DataTable/components/ActionButtons.tsx +5 -5
  259. package/src/components/DataTable/components/BulkOperationsDropdown.tsx +2 -2
  260. package/src/components/DataTable/components/ColumnFilter.tsx +1 -1
  261. package/src/components/DataTable/components/ColumnVisibilityDropdown.tsx +3 -3
  262. package/src/components/DataTable/components/DataTableBody.tsx +12 -12
  263. package/src/components/DataTable/components/DataTableCore.tsx +3 -3
  264. package/src/components/DataTable/components/DataTableToolbar.tsx +5 -5
  265. package/src/components/DataTable/components/DraggableColumnHeader.tsx +3 -3
  266. package/src/components/DataTable/components/EditableRow.tsx +2 -2
  267. package/src/components/DataTable/components/EmptyState.tsx +3 -3
  268. package/src/components/DataTable/components/GroupHeader.tsx +2 -2
  269. package/src/components/DataTable/components/GroupingDropdown.tsx +1 -1
  270. package/src/components/DataTable/components/ImportModal.tsx +4 -4
  271. package/src/components/DataTable/components/LoadingState.tsx +1 -1
  272. package/src/components/DataTable/components/PaginationControls.tsx +11 -11
  273. package/src/components/DataTable/components/UnifiedTableBody.tsx +9 -9
  274. package/src/components/DataTable/components/ViewRowModal.tsx +2 -2
  275. package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +11 -37
  276. package/src/components/DataTable/components/__tests__/DataTableToolbar.test.tsx +157 -0
  277. package/src/components/DataTable/components/__tests__/LoadingState.test.tsx +2 -1
  278. package/src/components/DataTable/components/__tests__/VirtualizedDataTable.test.tsx +128 -0
  279. package/src/components/DataTable/core/__tests__/ActionManager.test.ts +19 -0
  280. package/src/components/DataTable/core/__tests__/ColumnFactory.test.ts +51 -0
  281. package/src/components/DataTable/core/__tests__/ColumnManager.test.ts +84 -0
  282. package/src/components/DataTable/core/__tests__/DataManager.test.ts +14 -0
  283. package/src/components/DataTable/core/__tests__/DataTableContext.test.tsx +136 -0
  284. package/src/components/DataTable/core/__tests__/LocalDataAdapter.test.ts +16 -0
  285. package/src/components/DataTable/core/__tests__/PluginRegistry.test.ts +18 -0
  286. package/src/components/DataTable/hooks/useDataTablePermissions.ts +28 -7
  287. package/src/components/DataTable/utils/__tests__/hierarchicalUtils.test.ts +30 -1
  288. package/src/components/DataTable/utils/hierarchicalUtils.ts +38 -10
  289. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +8 -3
  290. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.tsx +4 -4
  291. package/src/components/Dialog/Dialog.tsx +2 -2
  292. package/src/components/EventSelector/EventSelector.tsx +7 -7
  293. package/src/components/FileDisplay/FileDisplay.tsx +291 -179
  294. package/src/components/FileUpload/FileUpload.tsx +7 -4
  295. package/src/components/Header/Header.test.tsx +28 -0
  296. package/src/components/Header/Header.tsx +22 -9
  297. package/src/components/InactivityWarningModal/InactivityWarningModal.tsx +2 -2
  298. package/src/components/LoadingSpinner/LoadingSpinner.test.tsx +19 -14
  299. package/src/components/LoadingSpinner/LoadingSpinner.tsx +5 -5
  300. package/src/components/NavigationMenu/NavigationMenu.test.tsx +127 -1
  301. package/src/components/OrganisationSelector/OrganisationSelector.tsx +42 -22
  302. package/src/components/PaceAppLayout/PaceAppLayout.integration.test.tsx +4 -0
  303. package/src/components/PaceAppLayout/PaceAppLayout.performance.test.tsx +3 -0
  304. package/src/components/PaceAppLayout/PaceAppLayout.security.test.tsx +3 -0
  305. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +16 -6
  306. package/src/components/PaceAppLayout/PaceAppLayout.tsx +37 -3
  307. package/src/components/PaceAppLayout/test-setup.tsx +1 -0
  308. package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +66 -45
  309. package/src/components/PaceLoginPage/PaceLoginPage.tsx +6 -4
  310. package/src/components/Progress/Progress.test.tsx +18 -19
  311. package/src/components/Progress/Progress.tsx +31 -32
  312. package/src/components/PublicLayout/PublicLayout.test.tsx +6 -6
  313. package/src/components/PublicLayout/PublicPageProvider.tsx +5 -3
  314. package/src/components/Select/Select.test.tsx +4 -1
  315. package/src/components/Select/Select.tsx +65 -20
  316. package/src/components/Switch/Switch.test.tsx +2 -1
  317. package/src/components/Switch/Switch.tsx +1 -1
  318. package/src/components/Toast/Toast.tsx +1 -1
  319. package/src/components/Tooltip/Tooltip.test.tsx +8 -2
  320. package/src/components/UserMenu/UserMenu.tsx +3 -3
  321. package/src/eslint-rules/pace-core-compliance.cjs +0 -2
  322. package/src/eslint-rules/pace-core-compliance.js +0 -2
  323. package/src/hooks/__tests__/hooks.integration.test.tsx +4 -1
  324. package/src/hooks/__tests__/useAppConfig.unit.test.ts +76 -5
  325. package/src/hooks/__tests__/useDataTableState.test.ts +76 -0
  326. package/src/hooks/__tests__/useFileUrl.unit.test.ts +25 -69
  327. package/src/hooks/__tests__/useFileUrlCache.test.ts +129 -0
  328. package/src/hooks/__tests__/usePreventTabReload.test.ts +88 -0
  329. package/src/hooks/__tests__/usePublicEvent.simple.test.ts +1 -1
  330. package/src/hooks/__tests__/usePublicEvent.test.ts +608 -0
  331. package/src/hooks/__tests__/useQueryCache.test.ts +144 -0
  332. package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +67 -24
  333. package/src/hooks/index.ts +1 -1
  334. package/src/hooks/public/usePublicEvent.ts +10 -10
  335. package/src/hooks/public/usePublicFileDisplay.ts +173 -87
  336. package/src/hooks/useAppConfig.ts +24 -5
  337. package/src/hooks/useFileDisplay.ts +298 -36
  338. package/src/hooks/useFileReference.ts +56 -11
  339. package/src/hooks/useFileUrl.ts +1 -1
  340. package/src/hooks/useInactivityTracker.ts +16 -7
  341. package/src/hooks/usePermissionCache.test.ts +85 -8
  342. package/src/hooks/useQueryCache.ts +27 -6
  343. package/src/hooks/useSecureDataAccess.test.ts +87 -42
  344. package/src/hooks/useSecureDataAccess.ts +95 -48
  345. package/src/providers/__tests__/OrganisationProvider.test.tsx +27 -21
  346. package/src/providers/services/EventServiceProvider.tsx +37 -17
  347. package/src/providers/services/InactivityServiceProvider.tsx +4 -4
  348. package/src/providers/services/OrganisationServiceProvider.tsx +8 -1
  349. package/src/providers/services/UnifiedAuthProvider.tsx +115 -29
  350. package/src/rbac/__tests__/auth-rbac.e2e.test.tsx +451 -0
  351. package/src/rbac/__tests__/engine.comprehensive.test.ts +12 -0
  352. package/src/rbac/__tests__/rbac-engine-core-logic.test.ts +8 -0
  353. package/src/rbac/__tests__/rbac-engine-simplified.test.ts +4 -0
  354. package/src/rbac/api.ts +240 -36
  355. package/src/rbac/cache-invalidation.ts +21 -7
  356. package/src/rbac/compliance/quick-fix-suggestions.ts +1 -1
  357. package/src/rbac/components/NavigationGuard.tsx +23 -63
  358. package/src/rbac/components/NavigationProvider.test.tsx +52 -23
  359. package/src/rbac/components/NavigationProvider.tsx +13 -11
  360. package/src/rbac/components/PagePermissionGuard.tsx +77 -203
  361. package/src/rbac/components/PagePermissionProvider.tsx +13 -11
  362. package/src/rbac/components/PermissionEnforcer.tsx +24 -62
  363. package/src/rbac/components/RoleBasedRouter.tsx +14 -12
  364. package/src/rbac/components/SecureDataProvider.tsx +13 -11
  365. package/src/rbac/components/__tests__/NavigationGuard.test.tsx +104 -41
  366. package/src/rbac/components/__tests__/NavigationProvider.test.tsx +49 -12
  367. package/src/rbac/components/__tests__/PagePermissionGuard.race-condition.test.tsx +22 -1
  368. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +161 -82
  369. package/src/rbac/components/__tests__/PagePermissionGuard.verification.test.tsx +22 -1
  370. package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +77 -30
  371. package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +39 -5
  372. package/src/rbac/components/__tests__/SecureDataProvider.test.tsx +47 -4
  373. package/src/rbac/engine.ts +4 -2
  374. package/src/rbac/hooks/__tests__/useSecureSupabase.test.ts +144 -52
  375. package/src/rbac/hooks/index.ts +3 -0
  376. package/src/rbac/hooks/useCan.test.ts +101 -53
  377. package/src/rbac/hooks/usePermissions.ts +108 -41
  378. package/src/rbac/hooks/useRBAC.test.ts +11 -3
  379. package/src/rbac/hooks/useRBAC.ts +83 -40
  380. package/src/rbac/hooks/useResolvedScope.test.ts +189 -63
  381. package/src/rbac/hooks/useResolvedScope.ts +128 -70
  382. package/src/rbac/hooks/useSecureSupabase.ts +36 -19
  383. package/src/rbac/hooks/useSuperAdminBypass.ts +126 -0
  384. package/src/rbac/request-deduplication.ts +1 -1
  385. package/src/rbac/secureClient.ts +72 -12
  386. package/src/rbac/security.ts +29 -23
  387. package/src/rbac/types.ts +10 -0
  388. package/src/rbac/utils/__tests__/contextValidator.test.ts +150 -0
  389. package/src/rbac/utils/__tests__/deep-equal.test.ts +53 -0
  390. package/src/rbac/utils/__tests__/eventContext.test.ts +8 -3
  391. package/src/rbac/utils/__tests__/eventContext.unit.test.ts +74 -12
  392. package/src/rbac/utils/contextValidator.ts +288 -0
  393. package/src/rbac/utils/eventContext.ts +52 -3
  394. package/src/services/AuthService.ts +37 -8
  395. package/src/services/EventService.ts +165 -21
  396. package/src/services/OrganisationService.ts +125 -137
  397. package/src/services/__tests__/EventService.test.ts +26 -21
  398. package/src/services/__tests__/OrganisationService.pagination.test.ts +34 -8
  399. package/src/services/__tests__/OrganisationService.test.ts +218 -86
  400. package/src/types/database.generated.ts +166 -201
  401. package/src/types/file-reference.ts +13 -10
  402. package/src/types/supabase.ts +2 -2
  403. package/src/utils/__tests__/secureDataAccess.unit.test.ts +3 -2
  404. package/src/utils/app/appNameResolver.test.ts +346 -73
  405. package/src/utils/context/superAdminOverride.ts +58 -0
  406. package/src/utils/file-reference/index.ts +65 -37
  407. package/src/utils/google-places/googlePlacesUtils.test.ts +98 -0
  408. package/src/utils/google-places/googlePlacesUtils.ts +1 -1
  409. package/src/utils/google-places/loadGoogleMapsScript.test.ts +83 -0
  410. package/src/utils/google-places/types.ts +1 -1
  411. package/src/utils/request-deduplication.ts +4 -4
  412. package/src/utils/security/secureDataAccess.test.ts +1 -1
  413. package/src/utils/security/secureDataAccess.ts +7 -4
  414. package/src/utils/storage/README.md +1 -1
  415. package/src/utils/storage/helpers.test.ts +1 -1
  416. package/src/utils/storage/helpers.ts +38 -19
  417. package/src/utils/storage/types.ts +15 -8
  418. package/src/utils/validation/__tests__/csrf.test.ts +105 -0
  419. package/src/utils/validation/__tests__/sqlInjectionProtection.test.ts +92 -0
  420. package/src/vite-env.d.ts +2 -2
  421. package/dist/chunk-3GOZZZYH.js.map +0 -1
  422. package/dist/chunk-DDM4CCYT.js.map +0 -1
  423. package/dist/chunk-E7UAOUMY.js +0 -75
  424. package/dist/chunk-E7UAOUMY.js.map +0 -1
  425. package/dist/chunk-F2IMUDXZ.js.map +0 -1
  426. package/dist/chunk-HEHYGYOX.js.map +0 -1
  427. package/dist/chunk-IM4QE42D.js.map +0 -1
  428. package/dist/chunk-MX64ZF6I.js.map +0 -1
  429. package/dist/chunk-SAUPYVLF.js.map +0 -1
  430. package/dist/chunk-THRPYOFK.js.map +0 -1
  431. package/dist/chunk-UCQSRW7Z.js.map +0 -1
  432. package/dist/chunk-VGZZXKBR.js.map +0 -1
  433. package/dist/chunk-YGPFYGA6.js.map +0 -1
  434. package/dist/chunk-YHCN776L.js.map +0 -1
  435. /package/dist/{DataTable-GUFUNZ3N.js.map → DataTable-WKRZD47S.js.map} +0 -0
  436. /package/dist/{UnifiedAuthProvider-643PUAIM.js.map → UnifiedAuthProvider-FTSG5XH7.js.map} +0 -0
  437. /package/dist/{api-YP7XD5L6.js.map → api-IHKALJZD.js.map} +0 -0
  438. /package/dist/{chunk-HESYZWZW.js.map → chunk-QWWZ5CAQ.js.map} +0 -0
@@ -15,7 +15,9 @@ import {
15
15
  getOrganisationFromEvent,
16
16
  createScopeFromEvent,
17
17
  isEventBasedScope,
18
- isValidEventBasedScope
18
+ isValidEventBasedScope,
19
+ clearAllOrgDerivationCache,
20
+ clearOrgDerivationCache
19
21
  } from '../eventContext';
20
22
 
21
23
  // Mock Supabase client
@@ -26,8 +28,10 @@ const createMockSupabaseClient = () => {
26
28
  single: vi.fn()
27
29
  };
28
30
 
31
+ const fromMock = vi.fn().mockReturnValue(mockQuery);
32
+
29
33
  return {
30
- from: vi.fn().mockReturnValue(mockQuery),
34
+ from: fromMock,
31
35
  query: mockQuery
32
36
  } as unknown as SupabaseClient<Database>;
33
37
  };
@@ -37,12 +41,23 @@ describe('Event Context Utilities', () => {
37
41
  let mockQuery: any;
38
42
 
39
43
  beforeEach(() => {
44
+ // Clear cache before each test
45
+ clearAllOrgDerivationCache();
46
+
40
47
  mockSupabase = createMockSupabaseClient();
41
- mockQuery = mockSupabase.from('event');
48
+ // Reset mockQuery to get a fresh query builder for each test
49
+ mockQuery = {
50
+ select: vi.fn().mockReturnThis(),
51
+ eq: vi.fn().mockReturnThis(),
52
+ single: vi.fn()
53
+ };
54
+ (mockSupabase.from as any).mockReturnValue(mockQuery);
42
55
  });
43
56
 
44
57
  afterEach(() => {
45
58
  vi.clearAllMocks();
59
+ // Clear cache after each test
60
+ clearAllOrgDerivationCache();
46
61
  });
47
62
 
48
63
  describe('getOrganisationFromEvent', () => {
@@ -58,7 +73,7 @@ describe('Event Context Utilities', () => {
58
73
  const result = await getOrganisationFromEvent(mockSupabase, eventId);
59
74
 
60
75
  expect(result).toBe(organisationId);
61
- expect(mockSupabase.from).toHaveBeenCalledWith('event');
76
+ expect(mockSupabase.from).toHaveBeenCalledWith('core_events');
62
77
  expect(mockQuery.select).toHaveBeenCalledWith('organisation_id');
63
78
  expect(mockQuery.eq).toHaveBeenCalledWith('event_id', eventId);
64
79
  expect(mockQuery.single).toHaveBeenCalled();
@@ -80,6 +95,9 @@ describe('Event Context Utilities', () => {
80
95
  it('should return null when data is null', async () => {
81
96
  const eventId = 'event-123';
82
97
 
98
+ // Clear cache for this specific test
99
+ clearOrgDerivationCache(eventId);
100
+
83
101
  mockQuery.single.mockResolvedValue({
84
102
  data: null,
85
103
  error: null
@@ -94,6 +112,9 @@ describe('Event Context Utilities', () => {
94
112
  const eventId = 'event-123';
95
113
  const dbError = new Error('Database connection failed');
96
114
 
115
+ // Clear cache for this specific test
116
+ clearOrgDerivationCache(eventId);
117
+
97
118
  mockQuery.single.mockRejectedValue(dbError);
98
119
 
99
120
  await expect(getOrganisationFromEvent(mockSupabase, eventId))
@@ -103,6 +124,9 @@ describe('Event Context Utilities', () => {
103
124
  it('should handle empty organisation_id', async () => {
104
125
  const eventId = 'event-123';
105
126
 
127
+ // Clear cache for this specific test
128
+ clearOrgDerivationCache(eventId);
129
+
106
130
  mockQuery.single.mockResolvedValue({
107
131
  data: { organisation_id: null },
108
132
  error: null
@@ -168,6 +192,9 @@ describe('Event Context Utilities', () => {
168
192
  it('should return null when organisation lookup fails', async () => {
169
193
  const eventId = 'event-123';
170
194
 
195
+ // Clear cache for this specific test
196
+ clearOrgDerivationCache(eventId);
197
+
171
198
  mockQuery.single.mockResolvedValue({
172
199
  data: { organisation_id: null },
173
200
  error: null
@@ -182,6 +209,9 @@ describe('Event Context Utilities', () => {
182
209
  const eventId = 'event-123';
183
210
  const dbError = new Error('Database connection failed');
184
211
 
212
+ // Clear cache for this specific test
213
+ clearOrgDerivationCache(eventId);
214
+
185
215
  mockQuery.single.mockRejectedValue(dbError);
186
216
 
187
217
  await expect(createScopeFromEvent(mockSupabase, eventId))
@@ -349,15 +379,31 @@ describe('Event Context Utilities', () => {
349
379
  const organisationId1 = 'org-123';
350
380
  const organisationId2 = 'org-456';
351
381
 
352
- mockQuery.single
353
- .mockResolvedValueOnce({
382
+ // Clear cache for these specific events
383
+ clearOrgDerivationCache(eventId1);
384
+ clearOrgDerivationCache(eventId2);
385
+
386
+ // Create separate query builders for concurrent calls
387
+ const mockQuery1 = {
388
+ select: vi.fn().mockReturnThis(),
389
+ eq: vi.fn().mockReturnThis(),
390
+ single: vi.fn().mockResolvedValue({
354
391
  data: { organisation_id: organisationId1 },
355
392
  error: null
356
393
  })
357
- .mockResolvedValueOnce({
394
+ };
395
+ const mockQuery2 = {
396
+ select: vi.fn().mockReturnThis(),
397
+ eq: vi.fn().mockReturnThis(),
398
+ single: vi.fn().mockResolvedValue({
358
399
  data: { organisation_id: organisationId2 },
359
400
  error: null
360
- });
401
+ })
402
+ };
403
+
404
+ (mockSupabase.from as any)
405
+ .mockReturnValueOnce(mockQuery1)
406
+ .mockReturnValueOnce(mockQuery2);
361
407
 
362
408
  const [result1, result2] = await Promise.all([
363
409
  getOrganisationFromEvent(mockSupabase, eventId1),
@@ -376,15 +422,31 @@ describe('Event Context Utilities', () => {
376
422
  const appId1 = 'app-123';
377
423
  const appId2 = 'app-456';
378
424
 
379
- mockQuery.single
380
- .mockResolvedValueOnce({
425
+ // Clear cache for these specific events
426
+ clearOrgDerivationCache(eventId1);
427
+ clearOrgDerivationCache(eventId2);
428
+
429
+ // Create separate query builders for concurrent calls
430
+ const mockQuery1 = {
431
+ select: vi.fn().mockReturnThis(),
432
+ eq: vi.fn().mockReturnThis(),
433
+ single: vi.fn().mockResolvedValue({
381
434
  data: { organisation_id: organisationId1 },
382
435
  error: null
383
436
  })
384
- .mockResolvedValueOnce({
437
+ };
438
+ const mockQuery2 = {
439
+ select: vi.fn().mockReturnThis(),
440
+ eq: vi.fn().mockReturnThis(),
441
+ single: vi.fn().mockResolvedValue({
385
442
  data: { organisation_id: organisationId2 },
386
443
  error: null
387
- });
444
+ })
445
+ };
446
+
447
+ (mockSupabase.from as any)
448
+ .mockReturnValueOnce(mockQuery1)
449
+ .mockReturnValueOnce(mockQuery2);
388
450
 
389
451
  const [result1, result2] = await Promise.all([
390
452
  createScopeFromEvent(mockSupabase, eventId1, appId1),
@@ -0,0 +1,288 @@
1
+ /**
2
+ * Context Validator for RBAC
3
+ * @package @jmruthers/pace-core
4
+ * @module RBAC/ContextValidator
5
+ * @since 1.0.0
6
+ *
7
+ * Centralized validation for RBAC context requirements based on app configuration.
8
+ * Enforces app-specific context rules with single primary context:
9
+ * - requires_event = TRUE: Event is PRIMARY context, org derived from event (org not required in input)
10
+ * - requires_event = FALSE: Organisation is PRIMARY context, event optional
11
+ * - PORTAL/ADMIN apps: Both contexts optional (allows users to view/edit own profiles, super admin access)
12
+ *
13
+ * Key principle: Only one primary context is required based on app config. The other is derived or optional.
14
+ */
15
+
16
+ import { SupabaseClient } from '@supabase/supabase-js';
17
+ import { Database } from '../../types/database';
18
+ import { UUID, Scope } from '../types';
19
+ import { EventContextRequiredError, OrganisationContextRequiredError } from '../types';
20
+ import { getOrganisationFromEvent } from './eventContext';
21
+ import { createLogger } from '../../utils/core/logger';
22
+
23
+ const log = createLogger('ContextValidator');
24
+
25
+ export interface AppConfig {
26
+ requires_event: boolean;
27
+ }
28
+
29
+ /**
30
+ * Check if an app allows optional contexts (both organisation and event optional)
31
+ * @param appName - App name to check
32
+ * @returns True if app allows optional contexts
33
+ */
34
+ function allowsOptionalContexts(appName?: string): boolean {
35
+ return appName === 'PORTAL' || appName === 'ADMIN';
36
+ }
37
+
38
+ export interface ContextValidationResult {
39
+ isValid: boolean;
40
+ resolvedScope: Scope | null;
41
+ error: Error | null;
42
+ }
43
+
44
+ /**
45
+ * Context Validator class
46
+ *
47
+ * Validates and resolves RBAC scope based on app configuration requirements.
48
+ */
49
+ export class ContextValidator {
50
+ /**
51
+ * Validate scope against app requirements
52
+ *
53
+ * @param scope - Current scope
54
+ * @param appConfig - App configuration (requires_event flag)
55
+ * @param appName - App name (for PORTAL/ADMIN special case)
56
+ * @returns Validation result with resolved scope
57
+ */
58
+ static async validateScope(
59
+ scope: Scope,
60
+ appConfig: AppConfig | null,
61
+ appName?: string
62
+ ): Promise<ContextValidationResult> {
63
+ // PORTAL/ADMIN special case: both contexts optional
64
+ if (allowsOptionalContexts(appName)) {
65
+ return {
66
+ isValid: true,
67
+ resolvedScope: {
68
+ organisationId: scope.organisationId,
69
+ eventId: scope.eventId,
70
+ appId: scope.appId
71
+ },
72
+ error: null
73
+ };
74
+ }
75
+
76
+ // If no app config, default to requiring org context
77
+ if (!appConfig) {
78
+ if (!scope.organisationId) {
79
+ return {
80
+ isValid: false,
81
+ resolvedScope: null,
82
+ error: new OrganisationContextRequiredError()
83
+ };
84
+ }
85
+ return {
86
+ isValid: true,
87
+ resolvedScope: scope,
88
+ error: null
89
+ };
90
+ }
91
+
92
+ // Event-required apps: must have eventId, derive org from event
93
+ if (appConfig.requires_event) {
94
+ if (!scope.eventId) {
95
+ return {
96
+ isValid: false,
97
+ resolvedScope: null,
98
+ error: new EventContextRequiredError()
99
+ };
100
+ }
101
+
102
+ // If org is not provided, we'll need to derive it from event
103
+ // But for validation, we just check that eventId exists
104
+ // The actual derivation happens in resolveRequiredContext
105
+ return {
106
+ isValid: true,
107
+ resolvedScope: scope,
108
+ error: null
109
+ };
110
+ }
111
+
112
+ // Org-required apps: must have organisationId, eventId optional
113
+ if (!scope.organisationId) {
114
+ return {
115
+ isValid: false,
116
+ resolvedScope: null,
117
+ error: new OrganisationContextRequiredError()
118
+ };
119
+ }
120
+
121
+ return {
122
+ isValid: true,
123
+ resolvedScope: scope,
124
+ error: null
125
+ };
126
+ }
127
+
128
+ /**
129
+ * Resolve required context and derive missing values
130
+ *
131
+ * @param scope - Current scope
132
+ * @param appConfig - App configuration
133
+ * @param appName - App name (for PORTAL/ADMIN special case)
134
+ * @param supabase - Supabase client (for deriving org from event)
135
+ * @returns Resolved scope with all required context
136
+ */
137
+ static async resolveRequiredContext(
138
+ scope: Scope,
139
+ appConfig: AppConfig | null,
140
+ appName?: string,
141
+ supabase?: SupabaseClient<Database> | null
142
+ ): Promise<ContextValidationResult> {
143
+ // PORTAL/ADMIN special case: both contexts optional
144
+ if (allowsOptionalContexts(appName)) {
145
+ return {
146
+ isValid: true,
147
+ resolvedScope: {
148
+ organisationId: scope.organisationId,
149
+ eventId: scope.eventId,
150
+ appId: scope.appId
151
+ },
152
+ error: null
153
+ };
154
+ }
155
+
156
+ // If no app config, default to requiring org context
157
+ if (!appConfig) {
158
+ if (!scope.organisationId) {
159
+ return {
160
+ isValid: false,
161
+ resolvedScope: null,
162
+ error: new OrganisationContextRequiredError()
163
+ };
164
+ }
165
+ return {
166
+ isValid: true,
167
+ resolvedScope: scope,
168
+ error: null
169
+ };
170
+ }
171
+
172
+ // Event-required apps: must have eventId, derive org from event
173
+ if (appConfig.requires_event) {
174
+ if (!scope.eventId) {
175
+ return {
176
+ isValid: false,
177
+ resolvedScope: null,
178
+ error: new EventContextRequiredError()
179
+ };
180
+ }
181
+
182
+ // Derive organisationId from event if not provided
183
+ let organisationId: UUID | undefined = scope.organisationId;
184
+ if (!organisationId && supabase && scope.eventId) {
185
+ try {
186
+ const derivedOrgId = await this.deriveOrgFromEvent(supabase, scope.eventId);
187
+ organisationId = derivedOrgId || undefined;
188
+ if (!organisationId) {
189
+ return {
190
+ isValid: false,
191
+ resolvedScope: null,
192
+ error: new Error('Could not resolve organisation from event context')
193
+ };
194
+ }
195
+ } catch (error) {
196
+ log.error('Failed to derive org from event:', error);
197
+ return {
198
+ isValid: false,
199
+ resolvedScope: null,
200
+ error: error instanceof Error ? error : new Error('Failed to derive organisation from event')
201
+ };
202
+ }
203
+ } else if (!organisationId) {
204
+ return {
205
+ isValid: false,
206
+ resolvedScope: null,
207
+ error: new Error('Event context requires organisationId but it could not be derived (supabase client not available)')
208
+ };
209
+ }
210
+
211
+ return {
212
+ isValid: true,
213
+ resolvedScope: {
214
+ organisationId,
215
+ eventId: scope.eventId,
216
+ appId: scope.appId
217
+ },
218
+ error: null
219
+ };
220
+ }
221
+
222
+ // Org-required apps: must have organisationId, eventId optional
223
+ if (!scope.organisationId) {
224
+ return {
225
+ isValid: false,
226
+ resolvedScope: null,
227
+ error: new OrganisationContextRequiredError()
228
+ };
229
+ }
230
+
231
+ return {
232
+ isValid: true,
233
+ resolvedScope: scope,
234
+ error: null
235
+ };
236
+ }
237
+
238
+ /**
239
+ * Derive organisation ID from event ID
240
+ *
241
+ * @param supabase - Supabase client
242
+ * @param eventId - Event ID
243
+ * @returns Organisation ID or null
244
+ */
245
+ static async deriveOrgFromEvent(
246
+ supabase: SupabaseClient<Database>,
247
+ eventId: string
248
+ ): Promise<UUID | null> {
249
+ return getOrganisationFromEvent(supabase, eventId);
250
+ }
251
+
252
+ /**
253
+ * Check if context is ready for permission checks
254
+ *
255
+ * @param scope - Current scope
256
+ * @param appConfig - App configuration
257
+ * @param appName - App name
258
+ * @param hasSelectedEvent - Whether event is selected
259
+ * @param hasSelectedOrganisation - Whether organisation is selected
260
+ * @returns True if context is ready
261
+ */
262
+ static isContextReady(
263
+ scope: Scope,
264
+ appConfig: AppConfig | null,
265
+ appName?: string,
266
+ hasSelectedEvent?: boolean,
267
+ hasSelectedOrganisation?: boolean
268
+ ): boolean {
269
+ // PORTAL/ADMIN special case: context is always ready
270
+ if (allowsOptionalContexts(appName)) {
271
+ return true;
272
+ }
273
+
274
+ // If no app config, default to requiring org context
275
+ if (!appConfig) {
276
+ return !!hasSelectedOrganisation || !!scope.organisationId;
277
+ }
278
+
279
+ // Event-required apps: need event context
280
+ if (appConfig.requires_event) {
281
+ return !!hasSelectedEvent || !!scope.eventId;
282
+ }
283
+
284
+ // Org-required apps: need org context
285
+ return !!hasSelectedOrganisation || !!scope.organisationId;
286
+ }
287
+ }
288
+
@@ -12,9 +12,35 @@ import { SupabaseClient } from '@supabase/supabase-js';
12
12
  import { Database } from '../../types/database';
13
13
  import { UUID, Scope } from '../types';
14
14
 
15
+ /**
16
+ * Cache for organisation derivation from event
17
+ * Key: eventId, Value: organisationId | null
18
+ * Maximum cache size to prevent memory leaks
19
+ */
20
+ const orgDerivationCache = new Map<string, UUID | null>();
21
+ const MAX_CACHE_SIZE = 100; // Limit cache to 100 entries
22
+
23
+ /**
24
+ * Clear cache entry for a specific event (useful if event's org changes)
25
+ * @param eventId - Event ID to clear from cache
26
+ */
27
+ export function clearOrgDerivationCache(eventId: string): void {
28
+ orgDerivationCache.delete(eventId);
29
+ }
30
+
31
+ /**
32
+ * Clear all cached organisation derivations
33
+ */
34
+ export function clearAllOrgDerivationCache(): void {
35
+ orgDerivationCache.clear();
36
+ }
37
+
15
38
  /**
16
39
  * Get organization ID from event ID
17
40
  *
41
+ * Uses caching to avoid repeated database queries for the same event.
42
+ * Cache is limited to prevent memory leaks.
43
+ *
18
44
  * @param supabase - Supabase client
19
45
  * @param eventId - Event ID
20
46
  * @returns Promise resolving to organization ID or null
@@ -23,17 +49,40 @@ export async function getOrganisationFromEvent(
23
49
  supabase: SupabaseClient<Database>,
24
50
  eventId: string
25
51
  ): Promise<UUID | null> {
52
+ // Check cache first
53
+ if (orgDerivationCache.has(eventId)) {
54
+ return orgDerivationCache.get(eventId) ?? null;
55
+ }
56
+
57
+ // Query database
26
58
  const { data, error } = await supabase
27
- .from('event')
59
+ .from('core_events')
28
60
  .select('organisation_id')
29
61
  .eq('event_id', eventId)
30
62
  .single() as { data: { organisation_id: string } | null; error: any };
31
63
 
64
+ let organisationId: UUID | null = null;
65
+
32
66
  if (error || !data) {
33
- return null;
67
+ organisationId = null;
68
+ } else if (data.organisation_id) {
69
+ organisationId = data.organisation_id;
70
+ } else {
71
+ // organisation_id is null or undefined
72
+ organisationId = null;
73
+ }
74
+
75
+ // Cache the result (with size limit to prevent memory leaks)
76
+ if (orgDerivationCache.size >= MAX_CACHE_SIZE) {
77
+ // Remove oldest entry (first key in Map)
78
+ const firstKey = orgDerivationCache.keys().next().value;
79
+ if (firstKey) {
80
+ orgDerivationCache.delete(firstKey);
81
+ }
34
82
  }
83
+ orgDerivationCache.set(eventId, organisationId);
35
84
 
36
- return data.organisation_id;
85
+ return organisationId;
37
86
  }
38
87
 
39
88
  /**
@@ -299,7 +299,15 @@ export class AuthService extends BaseService implements IAuthService {
299
299
  // Lifecycle methods
300
300
  async initialize(): Promise<void> {
301
301
  await super.initialize();
302
+ // Set loading to true before starting session restoration
303
+ // This ensures ProtectedRoute shows loading state while session is being restored
304
+ this.authLoading = true;
305
+ this.notify();
306
+
307
+ // Setup auth state listener first - this will receive INITIAL_SESSION event
302
308
  await this.setupAuthStateListener();
309
+
310
+ // Then restore session - this will trigger INITIAL_SESSION event if session exists
303
311
  await this.restoreSession();
304
312
  }
305
313
 
@@ -431,17 +439,25 @@ export class AuthService extends BaseService implements IAuthService {
431
439
  this.sessionRestorationState.restorationError ||
432
440
  (hasTimeoutError && session)) {
433
441
  this.finishSessionRestoration();
434
- return;
442
+ }
443
+ } else {
444
+ // No session in INITIAL_SESSION event - user is not authenticated
445
+ // Finish restoration to clear loading state
446
+ if (this.sessionRestorationState.isRestoring) {
447
+ this.finishSessionRestoration();
435
448
  }
436
449
  }
437
450
 
438
- if (this.sessionRestorationState.isRestoring) {
439
- this.finishSessionRestoration();
440
- return;
441
- }
451
+ // CRITICAL: Set loading to false AFTER handling INITIAL_SESSION
452
+ // This ensures ProtectedRoute waits for session restoration to complete
453
+ // before checking authentication state
454
+ this.authLoading = false;
455
+ this.notify();
456
+ return; // Return early to avoid setting loading to false again below
442
457
  }
443
458
 
444
- // Always set loading to false after any auth state change
459
+ // For other events (SIGNED_IN, SIGNED_OUT, TOKEN_REFRESHED), set loading to false
460
+ // INITIAL_SESSION is handled above and returns early
445
461
  this.authLoading = false;
446
462
  this.notify();
447
463
  } catch (error) {
@@ -519,8 +535,21 @@ export class AuthService extends BaseService implements IAuthService {
519
535
  this.authError = null;
520
536
  }
521
537
 
522
- // Finish successfully even if earlier calls reported an error, to avoid noisy warnings in benign cases
523
- this.finishSessionRestoration();
538
+ // CRITICAL FIX: Don't finish restoration here - wait for INITIAL_SESSION event
539
+ // The INITIAL_SESSION event should fire when the auth state listener is set up
540
+ // However, if it doesn't fire within a short delay (e.g., edge case), we'll finish it
541
+ // This ensures ProtectedRoute waits for the event before checking auth state
542
+
543
+ // Set a short fallback timeout to finish restoration if INITIAL_SESSION doesn't fire
544
+ // This handles edge cases where the event might be delayed or not fire
545
+ setTimeout(() => {
546
+ // Only finish if restoration is still in progress and INITIAL_SESSION hasn't fired
547
+ // The INITIAL_SESSION handler will have set restorationComplete to true if it fired
548
+ if (this.sessionRestorationState.isRestoring && !this.sessionRestorationState.restorationComplete) {
549
+ logger.debug('AuthService', 'INITIAL_SESSION event did not fire, finishing restoration');
550
+ this.finishSessionRestoration();
551
+ }
552
+ }, 100); // 100ms fallback - INITIAL_SESSION should fire immediately when listener is set up
524
553
  } catch (error) {
525
554
  const restorationError = error instanceof Error
526
555
  ? error