@jmruthers/pace-core 0.5.189 → 0.5.190

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 (420) 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-GUFUNZ3N.js → DataTable-ON3IXISJ.js} +8 -8
  4. package/dist/{PublicPageProvider-B8HaLe69.d.ts → PublicPageProvider-C4uxosp6.d.ts} +83 -24
  5. package/dist/{UnifiedAuthProvider-BG0AL5eE.d.ts → UnifiedAuthProvider-BYA9qB-o.d.ts} +4 -3
  6. package/dist/{UnifiedAuthProvider-643PUAIM.js → UnifiedAuthProvider-X5NXANVI.js} +4 -2
  7. package/dist/{api-YP7XD5L6.js → api-I6UCQ5S6.js} +4 -2
  8. package/dist/{chunk-DDM4CCYT.js → chunk-4QYC5L4K.js} +60 -35
  9. package/dist/chunk-4QYC5L4K.js.map +1 -0
  10. package/dist/{chunk-IM4QE42D.js → chunk-73HSNNOQ.js} +141 -326
  11. package/dist/chunk-73HSNNOQ.js.map +1 -0
  12. package/dist/{chunk-YHCN776L.js → chunk-DZWK57KZ.js} +2 -75
  13. package/dist/chunk-DZWK57KZ.js.map +1 -0
  14. package/dist/{chunk-3GOZZZYH.js → chunk-HQVPB5MZ.js} +238 -301
  15. package/dist/chunk-HQVPB5MZ.js.map +1 -0
  16. package/dist/{chunk-THRPYOFK.js → chunk-HW3OVDUF.js} +5 -5
  17. package/dist/chunk-HW3OVDUF.js.map +1 -0
  18. package/dist/{chunk-F2IMUDXZ.js → chunk-I7PSE6JW.js} +75 -2
  19. package/dist/chunk-I7PSE6JW.js.map +1 -0
  20. package/dist/{chunk-VGZZXKBR.js → chunk-J2XXC7R5.js} +280 -52
  21. package/dist/chunk-J2XXC7R5.js.map +1 -0
  22. package/dist/{chunk-UCQSRW7Z.js → chunk-NIU6J6OX.js} +425 -378
  23. package/dist/chunk-NIU6J6OX.js.map +1 -0
  24. package/dist/{chunk-HESYZWZW.js → chunk-QWWZ5CAQ.js} +2 -2
  25. package/dist/{chunk-HEHYGYOX.js → chunk-RUYZKXOD.js} +401 -46
  26. package/dist/chunk-RUYZKXOD.js.map +1 -0
  27. package/dist/{chunk-2UUZZJFT.js → chunk-SDMHPX3X.js} +176 -160
  28. package/dist/{chunk-2UUZZJFT.js.map → chunk-SDMHPX3X.js.map} +1 -1
  29. package/dist/{chunk-MX64ZF6I.js → chunk-STYK4OH2.js} +11 -11
  30. package/dist/chunk-STYK4OH2.js.map +1 -0
  31. package/dist/{chunk-YGPFYGA6.js → chunk-VVBAW5A5.js} +822 -498
  32. package/dist/chunk-VVBAW5A5.js.map +1 -0
  33. package/dist/chunk-Y4BUBBHD.js +614 -0
  34. package/dist/chunk-Y4BUBBHD.js.map +1 -0
  35. package/dist/{chunk-SAUPYVLF.js → chunk-ZSAAAMVR.js} +1 -1
  36. package/dist/chunk-ZSAAAMVR.js.map +1 -0
  37. package/dist/components.d.ts +3 -4
  38. package/dist/components.js +19 -19
  39. package/dist/components.js.map +1 -1
  40. package/dist/eslint-rules/pace-core-compliance.cjs +0 -2
  41. package/dist/{file-reference-D037xOFK.d.ts → file-reference-BavO2eQj.d.ts} +13 -10
  42. package/dist/hooks.d.ts +10 -5
  43. package/dist/hooks.js +14 -8
  44. package/dist/hooks.js.map +1 -1
  45. package/dist/index.d.ts +13 -11
  46. package/dist/index.js +79 -69
  47. package/dist/index.js.map +1 -1
  48. package/dist/providers.d.ts +3 -3
  49. package/dist/providers.js +3 -1
  50. package/dist/rbac/index.d.ts +76 -12
  51. package/dist/rbac/index.js +12 -9
  52. package/dist/types.d.ts +1 -1
  53. package/dist/types.js +1 -1
  54. package/dist/{usePublicRouteParams-CTDELQ7H.d.ts → usePublicRouteParams-DxIDS4bC.d.ts} +16 -9
  55. package/dist/utils.js +16 -16
  56. package/docs/README.md +2 -2
  57. package/docs/api/classes/ColumnFactory.md +1 -1
  58. package/docs/api/classes/ErrorBoundary.md +1 -1
  59. package/docs/api/classes/InvalidScopeError.md +2 -2
  60. package/docs/api/classes/Logger.md +1 -1
  61. package/docs/api/classes/MissingUserContextError.md +2 -2
  62. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  63. package/docs/api/classes/PermissionDeniedError.md +1 -1
  64. package/docs/api/classes/RBACAuditManager.md +1 -1
  65. package/docs/api/classes/RBACCache.md +1 -1
  66. package/docs/api/classes/RBACEngine.md +4 -4
  67. package/docs/api/classes/RBACError.md +1 -1
  68. package/docs/api/classes/RBACNotInitializedError.md +2 -2
  69. package/docs/api/classes/SecureSupabaseClient.md +21 -16
  70. package/docs/api/classes/StorageUtils.md +7 -4
  71. package/docs/api/enums/FileCategory.md +1 -1
  72. package/docs/api/enums/LogLevel.md +1 -1
  73. package/docs/api/enums/RBACErrorCode.md +1 -1
  74. package/docs/api/enums/RPCFunction.md +1 -1
  75. package/docs/api/interfaces/AddressFieldProps.md +1 -1
  76. package/docs/api/interfaces/AddressFieldRef.md +1 -1
  77. package/docs/api/interfaces/AggregateConfig.md +1 -1
  78. package/docs/api/interfaces/AutocompleteOptions.md +1 -1
  79. package/docs/api/interfaces/AvatarProps.md +1 -1
  80. package/docs/api/interfaces/BadgeProps.md +1 -1
  81. package/docs/api/interfaces/ButtonProps.md +1 -1
  82. package/docs/api/interfaces/CalendarProps.md +20 -6
  83. package/docs/api/interfaces/CardProps.md +1 -1
  84. package/docs/api/interfaces/ColorPalette.md +1 -1
  85. package/docs/api/interfaces/ColorShade.md +1 -1
  86. package/docs/api/interfaces/ComplianceResult.md +1 -1
  87. package/docs/api/interfaces/DataAccessRecord.md +9 -9
  88. package/docs/api/interfaces/DataRecord.md +1 -1
  89. package/docs/api/interfaces/DataTableAction.md +1 -1
  90. package/docs/api/interfaces/DataTableColumn.md +1 -1
  91. package/docs/api/interfaces/DataTableProps.md +1 -1
  92. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  93. package/docs/api/interfaces/DatabaseComplianceResult.md +1 -1
  94. package/docs/api/interfaces/DatabaseIssue.md +1 -1
  95. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  96. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  97. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  98. package/docs/api/interfaces/ExportColumn.md +1 -1
  99. package/docs/api/interfaces/ExportOptions.md +1 -1
  100. package/docs/api/interfaces/FileDisplayProps.md +62 -16
  101. package/docs/api/interfaces/FileMetadata.md +1 -1
  102. package/docs/api/interfaces/FileReference.md +2 -2
  103. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  104. package/docs/api/interfaces/FileUploadOptions.md +26 -12
  105. package/docs/api/interfaces/FileUploadProps.md +30 -19
  106. package/docs/api/interfaces/FooterProps.md +1 -1
  107. package/docs/api/interfaces/FormFieldProps.md +1 -1
  108. package/docs/api/interfaces/FormProps.md +1 -1
  109. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  110. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  111. package/docs/api/interfaces/InputProps.md +1 -1
  112. package/docs/api/interfaces/LabelProps.md +1 -1
  113. package/docs/api/interfaces/LoggerConfig.md +1 -1
  114. package/docs/api/interfaces/LoginFormProps.md +1 -1
  115. package/docs/api/interfaces/NavigationAccessRecord.md +10 -10
  116. package/docs/api/interfaces/NavigationContextType.md +9 -9
  117. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  118. package/docs/api/interfaces/NavigationItem.md +1 -1
  119. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  120. package/docs/api/interfaces/NavigationProviderProps.md +7 -7
  121. package/docs/api/interfaces/Organisation.md +1 -1
  122. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  123. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  124. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  125. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  126. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  127. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  128. package/docs/api/interfaces/PageAccessRecord.md +8 -8
  129. package/docs/api/interfaces/PagePermissionContextType.md +8 -8
  130. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  131. package/docs/api/interfaces/PagePermissionProviderProps.md +7 -7
  132. package/docs/api/interfaces/PaletteData.md +1 -1
  133. package/docs/api/interfaces/ParsedAddress.md +1 -1
  134. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  135. package/docs/api/interfaces/ProgressProps.md +3 -11
  136. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  137. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  138. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  139. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  140. package/docs/api/interfaces/QuickFix.md +1 -1
  141. package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
  142. package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
  143. package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
  144. package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
  145. package/docs/api/interfaces/RBACConfig.md +1 -1
  146. package/docs/api/interfaces/RBACContext.md +1 -1
  147. package/docs/api/interfaces/RBACLogger.md +1 -1
  148. package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
  149. package/docs/api/interfaces/RBACPerformanceMetrics.md +1 -1
  150. package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
  151. package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
  152. package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
  153. package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
  154. package/docs/api/interfaces/RBACResult.md +1 -1
  155. package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
  156. package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
  157. package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
  158. package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
  159. package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
  160. package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
  161. package/docs/api/interfaces/RBACRolesListParams.md +1 -1
  162. package/docs/api/interfaces/RBACRolesListResult.md +1 -1
  163. package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
  164. package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
  165. package/docs/api/interfaces/ResourcePermissions.md +1 -1
  166. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  167. package/docs/api/interfaces/RoleBasedRouterContextType.md +8 -8
  168. package/docs/api/interfaces/RoleBasedRouterProps.md +10 -10
  169. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  170. package/docs/api/interfaces/RouteAccessRecord.md +10 -10
  171. package/docs/api/interfaces/RouteConfig.md +10 -10
  172. package/docs/api/interfaces/RuntimeComplianceResult.md +1 -1
  173. package/docs/api/interfaces/SecureDataContextType.md +9 -9
  174. package/docs/api/interfaces/SecureDataProviderProps.md +8 -8
  175. package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
  176. package/docs/api/interfaces/SetupIssue.md +1 -1
  177. package/docs/api/interfaces/StorageConfig.md +4 -4
  178. package/docs/api/interfaces/StorageFileInfo.md +7 -7
  179. package/docs/api/interfaces/StorageFileMetadata.md +25 -14
  180. package/docs/api/interfaces/StorageListOptions.md +22 -9
  181. package/docs/api/interfaces/StorageListResult.md +4 -4
  182. package/docs/api/interfaces/StorageUploadOptions.md +21 -8
  183. package/docs/api/interfaces/StorageUploadResult.md +6 -6
  184. package/docs/api/interfaces/StorageUrlOptions.md +19 -6
  185. package/docs/api/interfaces/StyleImport.md +1 -1
  186. package/docs/api/interfaces/SwitchProps.md +1 -1
  187. package/docs/api/interfaces/TabsContentProps.md +1 -1
  188. package/docs/api/interfaces/TabsListProps.md +1 -1
  189. package/docs/api/interfaces/TabsProps.md +1 -1
  190. package/docs/api/interfaces/TabsTriggerProps.md +1 -1
  191. package/docs/api/interfaces/TextareaProps.md +1 -1
  192. package/docs/api/interfaces/ToastActionElement.md +1 -1
  193. package/docs/api/interfaces/ToastProps.md +1 -1
  194. package/docs/api/interfaces/UnifiedAuthContextType.md +53 -53
  195. package/docs/api/interfaces/UnifiedAuthProviderProps.md +13 -13
  196. package/docs/api/interfaces/UseFormDialogOptions.md +1 -1
  197. package/docs/api/interfaces/UseFormDialogReturn.md +1 -1
  198. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  199. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  200. package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
  201. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  202. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  203. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  204. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  205. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  206. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  207. package/docs/api/interfaces/UseResolvedScopeOptions.md +4 -4
  208. package/docs/api/interfaces/UseResolvedScopeReturn.md +4 -4
  209. package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
  210. package/docs/api/interfaces/UserEventAccess.md +11 -11
  211. package/docs/api/interfaces/UserMenuProps.md +1 -1
  212. package/docs/api/interfaces/UserProfile.md +1 -1
  213. package/docs/api/modules.md +151 -92
  214. package/docs/api-reference/components.md +15 -7
  215. package/docs/api-reference/providers.md +2 -2
  216. package/docs/api-reference/rpc-functions.md +1 -0
  217. package/docs/best-practices/README.md +1 -1
  218. package/docs/best-practices/deployment.md +8 -8
  219. package/docs/getting-started/examples/README.md +2 -2
  220. package/docs/getting-started/installation-guide.md +4 -4
  221. package/docs/getting-started/quick-start.md +3 -3
  222. package/docs/migration/MIGRATION_GUIDE.md +3 -3
  223. package/docs/rbac/compliance/compliance-guide.md +2 -2
  224. package/docs/rbac/event-based-apps.md +2 -2
  225. package/docs/rbac/getting-started.md +2 -2
  226. package/docs/rbac/quick-start.md +2 -2
  227. package/docs/security/README.md +4 -4
  228. package/docs/standards/07-rbac-and-rls-standard.md +430 -7
  229. package/docs/troubleshooting/README.md +2 -2
  230. package/docs/troubleshooting/migration.md +3 -3
  231. package/package.json +1 -3
  232. package/scripts/check-pace-core-compliance.cjs +1 -1
  233. package/scripts/check-pace-core-compliance.js +1 -1
  234. package/src/__tests__/fixtures/supabase.ts +301 -0
  235. package/src/__tests__/public-recipe-view.test.ts +9 -9
  236. package/src/__tests__/rls-policies.test.ts +197 -61
  237. package/src/components/AddressField/AddressField.test.tsx +42 -0
  238. package/src/components/AddressField/AddressField.tsx +71 -60
  239. package/src/components/AddressField/README.md +1 -0
  240. package/src/components/Alert/Alert.test.tsx +50 -10
  241. package/src/components/Alert/Alert.tsx +5 -3
  242. package/src/components/Avatar/Avatar.test.tsx +95 -43
  243. package/src/components/Avatar/Avatar.tsx +16 -16
  244. package/src/components/Button/Button.test.tsx +2 -1
  245. package/src/components/Button/Button.tsx +3 -3
  246. package/src/components/Calendar/Calendar.test.tsx +53 -37
  247. package/src/components/Calendar/Calendar.tsx +409 -82
  248. package/src/components/Card/Card.test.tsx +7 -4
  249. package/src/components/Card/Card.tsx +3 -6
  250. package/src/components/Checkbox/Checkbox.tsx +2 -2
  251. package/src/components/DataTable/components/ActionButtons.tsx +5 -5
  252. package/src/components/DataTable/components/BulkOperationsDropdown.tsx +2 -2
  253. package/src/components/DataTable/components/ColumnFilter.tsx +1 -1
  254. package/src/components/DataTable/components/ColumnVisibilityDropdown.tsx +3 -3
  255. package/src/components/DataTable/components/DataTableBody.tsx +12 -12
  256. package/src/components/DataTable/components/DataTableCore.tsx +3 -3
  257. package/src/components/DataTable/components/DataTableToolbar.tsx +5 -5
  258. package/src/components/DataTable/components/DraggableColumnHeader.tsx +3 -3
  259. package/src/components/DataTable/components/EditableRow.tsx +2 -2
  260. package/src/components/DataTable/components/EmptyState.tsx +3 -3
  261. package/src/components/DataTable/components/GroupHeader.tsx +2 -2
  262. package/src/components/DataTable/components/GroupingDropdown.tsx +1 -1
  263. package/src/components/DataTable/components/ImportModal.tsx +4 -4
  264. package/src/components/DataTable/components/LoadingState.tsx +1 -1
  265. package/src/components/DataTable/components/PaginationControls.tsx +11 -11
  266. package/src/components/DataTable/components/UnifiedTableBody.tsx +9 -9
  267. package/src/components/DataTable/components/ViewRowModal.tsx +2 -2
  268. package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +11 -37
  269. package/src/components/DataTable/components/__tests__/DataTableToolbar.test.tsx +157 -0
  270. package/src/components/DataTable/components/__tests__/LoadingState.test.tsx +2 -1
  271. package/src/components/DataTable/components/__tests__/VirtualizedDataTable.test.tsx +128 -0
  272. package/src/components/DataTable/core/__tests__/ActionManager.test.ts +19 -0
  273. package/src/components/DataTable/core/__tests__/ColumnFactory.test.ts +51 -0
  274. package/src/components/DataTable/core/__tests__/ColumnManager.test.ts +84 -0
  275. package/src/components/DataTable/core/__tests__/DataManager.test.ts +14 -0
  276. package/src/components/DataTable/core/__tests__/DataTableContext.test.tsx +136 -0
  277. package/src/components/DataTable/core/__tests__/LocalDataAdapter.test.ts +16 -0
  278. package/src/components/DataTable/core/__tests__/PluginRegistry.test.ts +18 -0
  279. package/src/components/DataTable/hooks/useDataTablePermissions.ts +28 -7
  280. package/src/components/DataTable/utils/__tests__/hierarchicalUtils.test.ts +30 -1
  281. package/src/components/DataTable/utils/hierarchicalUtils.ts +38 -10
  282. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +8 -3
  283. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.tsx +4 -4
  284. package/src/components/Dialog/Dialog.tsx +2 -2
  285. package/src/components/EventSelector/EventSelector.tsx +7 -7
  286. package/src/components/FileDisplay/FileDisplay.tsx +291 -179
  287. package/src/components/FileUpload/FileUpload.tsx +7 -4
  288. package/src/components/Header/Header.test.tsx +28 -0
  289. package/src/components/Header/Header.tsx +22 -9
  290. package/src/components/InactivityWarningModal/InactivityWarningModal.tsx +2 -2
  291. package/src/components/LoadingSpinner/LoadingSpinner.test.tsx +19 -14
  292. package/src/components/LoadingSpinner/LoadingSpinner.tsx +5 -5
  293. package/src/components/NavigationMenu/NavigationMenu.test.tsx +127 -1
  294. package/src/components/OrganisationSelector/OrganisationSelector.tsx +8 -8
  295. package/src/components/PaceAppLayout/PaceAppLayout.integration.test.tsx +4 -0
  296. package/src/components/PaceAppLayout/PaceAppLayout.performance.test.tsx +3 -0
  297. package/src/components/PaceAppLayout/PaceAppLayout.security.test.tsx +3 -0
  298. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +16 -6
  299. package/src/components/PaceAppLayout/PaceAppLayout.tsx +37 -3
  300. package/src/components/PaceAppLayout/test-setup.tsx +1 -0
  301. package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +66 -45
  302. package/src/components/PaceLoginPage/PaceLoginPage.tsx +6 -4
  303. package/src/components/Progress/Progress.test.tsx +18 -19
  304. package/src/components/Progress/Progress.tsx +31 -32
  305. package/src/components/PublicLayout/PublicLayout.test.tsx +6 -6
  306. package/src/components/PublicLayout/PublicPageProvider.tsx +5 -3
  307. package/src/components/Select/Select.tsx +5 -5
  308. package/src/components/Switch/Switch.test.tsx +2 -1
  309. package/src/components/Switch/Switch.tsx +1 -1
  310. package/src/components/Toast/Toast.tsx +1 -1
  311. package/src/components/Tooltip/Tooltip.test.tsx +8 -2
  312. package/src/components/UserMenu/UserMenu.tsx +3 -3
  313. package/src/eslint-rules/pace-core-compliance.cjs +0 -2
  314. package/src/eslint-rules/pace-core-compliance.js +0 -2
  315. package/src/hooks/__tests__/hooks.integration.test.tsx +4 -1
  316. package/src/hooks/__tests__/useAppConfig.unit.test.ts +76 -5
  317. package/src/hooks/__tests__/useDataTableState.test.ts +76 -0
  318. package/src/hooks/__tests__/useFileUrl.unit.test.ts +25 -69
  319. package/src/hooks/__tests__/useFileUrlCache.test.ts +129 -0
  320. package/src/hooks/__tests__/usePreventTabReload.test.ts +88 -0
  321. package/src/hooks/__tests__/{usePublicEvent.unit.test.ts → usePublicEvent.test.ts} +28 -1
  322. package/src/hooks/__tests__/useQueryCache.test.ts +144 -0
  323. package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +58 -16
  324. package/src/hooks/index.ts +1 -1
  325. package/src/hooks/public/usePublicEvent.ts +2 -2
  326. package/src/hooks/public/usePublicFileDisplay.ts +173 -87
  327. package/src/hooks/useAppConfig.ts +24 -5
  328. package/src/hooks/useFileDisplay.ts +297 -34
  329. package/src/hooks/useFileReference.ts +56 -11
  330. package/src/hooks/useFileUrl.ts +1 -1
  331. package/src/hooks/useInactivityTracker.ts +16 -7
  332. package/src/hooks/usePermissionCache.test.ts +85 -8
  333. package/src/hooks/useQueryCache.ts +21 -0
  334. package/src/hooks/useSecureDataAccess.test.ts +80 -35
  335. package/src/hooks/useSecureDataAccess.ts +80 -37
  336. package/src/providers/services/EventServiceProvider.tsx +37 -17
  337. package/src/providers/services/InactivityServiceProvider.tsx +4 -4
  338. package/src/providers/services/OrganisationServiceProvider.tsx +8 -1
  339. package/src/providers/services/UnifiedAuthProvider.tsx +115 -29
  340. package/src/rbac/__tests__/auth-rbac.e2e.test.tsx +451 -0
  341. package/src/rbac/__tests__/engine.comprehensive.test.ts +12 -0
  342. package/src/rbac/__tests__/rbac-engine-core-logic.test.ts +8 -0
  343. package/src/rbac/__tests__/rbac-engine-simplified.test.ts +4 -0
  344. package/src/rbac/api.ts +240 -36
  345. package/src/rbac/cache-invalidation.ts +21 -7
  346. package/src/rbac/compliance/quick-fix-suggestions.ts +1 -1
  347. package/src/rbac/components/NavigationGuard.tsx +23 -63
  348. package/src/rbac/components/NavigationProvider.test.tsx +52 -23
  349. package/src/rbac/components/NavigationProvider.tsx +13 -11
  350. package/src/rbac/components/PagePermissionGuard.tsx +77 -203
  351. package/src/rbac/components/PagePermissionProvider.tsx +13 -11
  352. package/src/rbac/components/PermissionEnforcer.tsx +24 -62
  353. package/src/rbac/components/RoleBasedRouter.tsx +14 -12
  354. package/src/rbac/components/SecureDataProvider.tsx +13 -11
  355. package/src/rbac/components/__tests__/NavigationGuard.test.tsx +104 -41
  356. package/src/rbac/components/__tests__/NavigationProvider.test.tsx +49 -12
  357. package/src/rbac/components/__tests__/PagePermissionGuard.race-condition.test.tsx +22 -1
  358. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +161 -82
  359. package/src/rbac/components/__tests__/PagePermissionGuard.verification.test.tsx +22 -1
  360. package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +77 -30
  361. package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +39 -5
  362. package/src/rbac/components/__tests__/SecureDataProvider.test.tsx +47 -4
  363. package/src/rbac/engine.ts +4 -2
  364. package/src/rbac/hooks/__tests__/useSecureSupabase.test.ts +144 -52
  365. package/src/rbac/hooks/index.ts +3 -0
  366. package/src/rbac/hooks/useCan.test.ts +101 -53
  367. package/src/rbac/hooks/usePermissions.ts +108 -41
  368. package/src/rbac/hooks/useRBAC.test.ts +11 -3
  369. package/src/rbac/hooks/useRBAC.ts +83 -40
  370. package/src/rbac/hooks/useResolvedScope.test.ts +189 -63
  371. package/src/rbac/hooks/useResolvedScope.ts +128 -70
  372. package/src/rbac/hooks/useSecureSupabase.ts +36 -19
  373. package/src/rbac/hooks/useSuperAdminBypass.ts +126 -0
  374. package/src/rbac/request-deduplication.ts +1 -1
  375. package/src/rbac/secureClient.ts +72 -12
  376. package/src/rbac/security.ts +29 -23
  377. package/src/rbac/types.ts +10 -0
  378. package/src/rbac/utils/__tests__/contextValidator.test.ts +150 -0
  379. package/src/rbac/utils/__tests__/deep-equal.test.ts +53 -0
  380. package/src/rbac/utils/__tests__/eventContext.test.ts +6 -1
  381. package/src/rbac/utils/contextValidator.ts +288 -0
  382. package/src/rbac/utils/eventContext.ts +48 -2
  383. package/src/services/EventService.ts +165 -21
  384. package/src/services/OrganisationService.ts +37 -2
  385. package/src/services/__tests__/EventService.test.ts +26 -21
  386. package/src/types/file-reference.ts +13 -10
  387. package/src/utils/app/appNameResolver.test.ts +346 -73
  388. package/src/utils/context/superAdminOverride.ts +58 -0
  389. package/src/utils/file-reference/index.ts +61 -33
  390. package/src/utils/google-places/googlePlacesUtils.test.ts +98 -0
  391. package/src/utils/google-places/loadGoogleMapsScript.test.ts +83 -0
  392. package/src/utils/storage/helpers.test.ts +1 -1
  393. package/src/utils/storage/helpers.ts +38 -19
  394. package/src/utils/storage/types.ts +15 -8
  395. package/src/utils/validation/__tests__/csrf.test.ts +105 -0
  396. package/src/utils/validation/__tests__/sqlInjectionProtection.test.ts +92 -0
  397. package/src/vite-env.d.ts +2 -2
  398. package/dist/chunk-3GOZZZYH.js.map +0 -1
  399. package/dist/chunk-DDM4CCYT.js.map +0 -1
  400. package/dist/chunk-E7UAOUMY.js +0 -75
  401. package/dist/chunk-E7UAOUMY.js.map +0 -1
  402. package/dist/chunk-F2IMUDXZ.js.map +0 -1
  403. package/dist/chunk-HEHYGYOX.js.map +0 -1
  404. package/dist/chunk-IM4QE42D.js.map +0 -1
  405. package/dist/chunk-MX64ZF6I.js.map +0 -1
  406. package/dist/chunk-SAUPYVLF.js.map +0 -1
  407. package/dist/chunk-THRPYOFK.js.map +0 -1
  408. package/dist/chunk-UCQSRW7Z.js.map +0 -1
  409. package/dist/chunk-VGZZXKBR.js.map +0 -1
  410. package/dist/chunk-YGPFYGA6.js.map +0 -1
  411. package/dist/chunk-YHCN776L.js.map +0 -1
  412. package/src/hooks/__tests__/usePermissionCache.simple.test.ts +0 -192
  413. package/src/hooks/__tests__/usePermissionCache.unit.test.ts +0 -741
  414. package/src/hooks/__tests__/usePublicEvent.simple.test.ts +0 -703
  415. package/src/rbac/hooks/useRBAC.simple.test.ts +0 -95
  416. package/src/rbac/utils/__tests__/eventContext.unit.test.ts +0 -428
  417. /package/dist/{DataTable-GUFUNZ3N.js.map → DataTable-ON3IXISJ.js.map} +0 -0
  418. /package/dist/{UnifiedAuthProvider-643PUAIM.js.map → UnifiedAuthProvider-X5NXANVI.js.map} +0 -0
  419. /package/dist/{api-YP7XD5L6.js.map → api-I6UCQ5S6.js.map} +0 -0
  420. /package/dist/{chunk-HESYZWZW.js.map → chunk-QWWZ5CAQ.js.map} +0 -0
@@ -18,6 +18,9 @@ import { OrganisationContextRequiredError } from './types';
18
18
  *
19
19
  * This client automatically injects organisation context into all requests
20
20
  * and prevents queries that don't have the required context.
21
+ *
22
+ * Note: Callers should derive organisationId from eventId before creating this client
23
+ * if working with event-required apps. The client requires organisationId.
21
24
  */
22
25
  export class SecureSupabaseClient {
23
26
  private supabase: SupabaseClient<Database>;
@@ -27,19 +30,22 @@ export class SecureSupabaseClient {
27
30
  private organisationId: UUID;
28
31
  private eventId?: string;
29
32
  private appId?: UUID;
33
+ private isSuperAdmin: boolean;
30
34
 
31
35
  constructor(
32
36
  supabaseUrl: string,
33
37
  supabaseKey: string,
34
38
  organisationId: UUID,
35
39
  eventId?: string,
36
- appId?: UUID
40
+ appId?: UUID,
41
+ isSuperAdmin: boolean = false
37
42
  ) {
38
43
  this.supabaseUrl = supabaseUrl;
39
44
  this.supabaseKey = supabaseKey;
40
45
  this.organisationId = organisationId;
41
46
  this.eventId = eventId;
42
47
  this.appId = appId;
48
+ this.isSuperAdmin = isSuperAdmin;
43
49
 
44
50
  // Create the base Supabase client with context headers
45
51
  // Note: We'll override functions.invoke to exclude headers for Edge Functions
@@ -75,8 +81,11 @@ export class SecureSupabaseClient {
75
81
  // Type assertion needed because table is a string but Supabase expects specific table names
76
82
  const query = originalFrom(table as any);
77
83
 
84
+ // Store table name on query object so we can access it in filter methods
85
+ (query as any)._tableName = table;
86
+
78
87
  // Inject organisation context into all queries
79
- return this.injectContext(query);
88
+ return this.injectContext(query, table);
80
89
  };
81
90
 
82
91
  const originalRpc = this.supabase.rpc.bind(this.supabase);
@@ -129,7 +138,7 @@ export class SecureSupabaseClient {
129
138
  /**
130
139
  * Inject organisation context into a query
131
140
  */
132
- private injectContext(query: any) {
141
+ private injectContext(query: any, tableName: string) {
133
142
  const originalSelect = query.select.bind(query);
134
143
  const originalInsert = query.insert.bind(query);
135
144
  const originalUpdate = query.update.bind(query);
@@ -138,11 +147,26 @@ export class SecureSupabaseClient {
138
147
  // Override select to add organisation filter
139
148
  query.select = (columns?: string) => {
140
149
  const result = originalSelect(columns);
141
- return this.addOrganisationFilter(result);
150
+ return this.addOrganisationFilter(result, tableName);
142
151
  };
143
152
 
144
153
  // Override insert to add organisation context
145
154
  query.insert = (values: any) => {
155
+ // For rbac_user_profiles, only add organisation_id if not super admin
156
+ // Super admins can create users in any org, non-super-admins are restricted
157
+ if (tableName === 'rbac_user_profiles') {
158
+ if (this.isSuperAdmin) {
159
+ // Super admin: Don't force organisation_id (can be set explicitly if needed)
160
+ return originalInsert(values);
161
+ }
162
+ // Non-super-admin: Add organisation_id as defense in depth
163
+ const contextValues = Array.isArray(values)
164
+ ? values.map(v => ({ ...v, organisation_id: this.organisationId }))
165
+ : { ...values, organisation_id: this.organisationId };
166
+ return originalInsert(contextValues);
167
+ }
168
+
169
+ // For other tables, always add organisation_id
146
170
  const contextValues = Array.isArray(values)
147
171
  ? values.map(v => ({ ...v, organisation_id: this.organisationId }))
148
172
  : { ...values, organisation_id: this.organisationId };
@@ -153,13 +177,13 @@ export class SecureSupabaseClient {
153
177
  // Override update to add organisation filter
154
178
  query.update = (values: any) => {
155
179
  const result = originalUpdate(values);
156
- return this.addOrganisationFilter(result);
180
+ return this.addOrganisationFilter(result, tableName);
157
181
  };
158
182
 
159
183
  // Override delete to add organisation filter
160
184
  query.delete = () => {
161
185
  const result = originalDelete();
162
- return this.addOrganisationFilter(result);
186
+ return this.addOrganisationFilter(result, tableName);
163
187
  };
164
188
 
165
189
  return query;
@@ -167,9 +191,40 @@ export class SecureSupabaseClient {
167
191
 
168
192
  /**
169
193
  * Add organisation filter to a query
194
+ *
195
+ * Defense in depth strategy:
196
+ * - RLS policies are the primary security layer (cannot be bypassed)
197
+ * - Application-level filtering adds an additional layer of protection
198
+ *
199
+ * For rbac_user_profiles:
200
+ * - Super admins: No org filter (see all users) - RLS will allow access
201
+ * - Non-super-admins: Apply org filter as defense in depth - RLS will also filter
202
+ *
203
+ * For other tables:
204
+ * - Always apply org filter unless super admin bypasses it
170
205
  */
171
- private addOrganisationFilter(query: any) {
172
- // Add organisation_id filter to all queries
206
+ private addOrganisationFilter(query: any, tableName: string) {
207
+ // For rbac_user_profiles, use conditional filtering based on super admin status
208
+ if (tableName === 'rbac_user_profiles') {
209
+ // Super admins: No org filter (see all users via RLS)
210
+ // Non-super-admins: Apply org filter as defense in depth (RLS also filters)
211
+ if (this.isSuperAdmin) {
212
+ return query; // No filter - RLS handles access control
213
+ }
214
+ // Apply org filter for non-super-admins as additional security layer
215
+ return query.eq('organisation_id', this.organisationId);
216
+ }
217
+
218
+ // For all other tables, apply organisation filter
219
+ // Super admins can still bypass via RLS if needed, but we filter at app level too
220
+ if (this.isSuperAdmin) {
221
+ // Super admins might want to see cross-org data, but for most tables
222
+ // we still apply the filter as a safety measure (they can override if needed)
223
+ // For now, we'll apply it - super admins can use direct queries if they need cross-org access
224
+ return query.eq('organisation_id', this.organisationId);
225
+ }
226
+
227
+ // Non-super-admins: Always apply org filter
173
228
  return query.eq('organisation_id', this.organisationId);
174
229
  }
175
230
 
@@ -210,13 +265,15 @@ export class SecureSupabaseClient {
210
265
  organisationId?: UUID;
211
266
  eventId?: string;
212
267
  appId?: UUID;
268
+ isSuperAdmin?: boolean;
213
269
  }): SecureSupabaseClient {
214
270
  return new SecureSupabaseClient(
215
271
  this.supabaseUrl,
216
272
  this.supabaseKey,
217
273
  updates.organisationId || this.organisationId,
218
274
  updates.eventId !== undefined ? updates.eventId : this.eventId,
219
- updates.appId !== undefined ? updates.appId : this.appId
275
+ updates.appId !== undefined ? updates.appId : this.appId,
276
+ updates.isSuperAdmin !== undefined ? updates.isSuperAdmin : this.isSuperAdmin
220
277
  );
221
278
  }
222
279
 
@@ -249,6 +306,7 @@ export class SecureSupabaseClient {
249
306
  * @param organisationId - Required organisation ID
250
307
  * @param eventId - Optional event ID
251
308
  * @param appId - Optional app ID
309
+ * @param isSuperAdmin - Optional super admin flag (defaults to false)
252
310
  * @returns SecureSupabaseClient instance
253
311
  *
254
312
  * @example
@@ -258,7 +316,8 @@ export class SecureSupabaseClient {
258
316
  * 'your-publishable-key-or-anon-key',
259
317
  * 'org-123',
260
318
  * 'event-456',
261
- * 'app-789'
319
+ * 'app-789',
320
+ * false // isSuperAdmin
262
321
  * );
263
322
  * ```
264
323
  */
@@ -267,9 +326,10 @@ export function createSecureClient(
267
326
  supabaseKey: string,
268
327
  organisationId: UUID,
269
328
  eventId?: string,
270
- appId?: UUID
329
+ appId?: UUID,
330
+ isSuperAdmin: boolean = false
271
331
  ): SecureSupabaseClient {
272
- return new SecureSupabaseClient(supabaseUrl, supabaseKey, organisationId, eventId, appId);
332
+ return new SecureSupabaseClient(supabaseUrl, supabaseKey, organisationId, eventId, appId, isSuperAdmin);
273
333
  }
274
334
 
275
335
  /**
@@ -9,6 +9,8 @@
9
9
 
10
10
  import { UUID, Permission, Scope } from './types';
11
11
  import { createLogger } from '../utils/core/logger';
12
+ import { ContextValidator } from './utils/contextValidator';
13
+ import type { AppConfig } from './utils/contextValidator';
12
14
 
13
15
  const log = createLogger('RBACSecurity');
14
16
 
@@ -162,25 +164,17 @@ export class RBACSecurityValidator {
162
164
  /**
163
165
  * Validate context requirements for security
164
166
  * @param scope - Scope object
165
- * @param appId - Application ID
167
+ * @param appConfig - App configuration
168
+ * @param appName - App name (for PORTAL special case)
166
169
  * @returns True if context is valid, false otherwise
167
170
  */
168
- static validateContextRequirements(scope: Scope, appId?: UUID): boolean {
169
- // Organisation context is always required
170
- if (!scope.organisationId) {
171
- return false;
172
- }
173
-
174
- // If app requires event, event context must be provided
175
- if (appId && scope.appId === appId) {
176
- // This would need to check app configuration
177
- // For now, we'll assume event context is required if appId is provided
178
- if (!scope.eventId) {
179
- return false;
180
- }
181
- }
182
-
183
- return true;
171
+ static async validateContextRequirements(
172
+ scope: Scope,
173
+ appConfig?: AppConfig | null,
174
+ appName?: string
175
+ ): Promise<boolean> {
176
+ const validation = await ContextValidator.validateScope(scope, appConfig || null, appName);
177
+ return validation.isValid;
184
178
  }
185
179
 
186
180
  /**
@@ -309,7 +303,7 @@ export const DEFAULT_SECURITY_CONFIG: RBACSecurityConfig = {
309
303
  */
310
304
  export interface SecurityContext {
311
305
  userId: UUID;
312
- organisationId: UUID; // Required - can always be derived from event context
306
+ organisationId: UUID | null; // Required for resource-level permissions, null for page-level permissions (database handles NULL)
313
307
  ipAddress?: string;
314
308
  userAgent?: string;
315
309
  timestamp: Date;
@@ -353,11 +347,23 @@ export class RBACSecurityMiddleware {
353
347
  errors.push('Invalid user ID format');
354
348
  }
355
349
 
356
- // OrganisationId is required
357
- if (!context.organisationId) {
358
- errors.push('Organisation ID is required');
359
- } else if (!RBACSecurityValidator.validateUUID(context.organisationId)) {
360
- errors.push('Invalid organisation ID format');
350
+ // For page-level permissions, organisationId can be null (database handles it)
351
+ // For resource-level permissions, organisationId is required
352
+ const isPagePermission = input.permission?.includes(':page.') || !!input.pageId;
353
+ const requiresOrgId = !isPagePermission;
354
+
355
+ // OrganisationId validation - only required for resource-level permissions
356
+ if (requiresOrgId) {
357
+ if (!context.organisationId) {
358
+ errors.push('Organisation ID is required for resource-level permissions');
359
+ } else if (!RBACSecurityValidator.validateUUID(context.organisationId)) {
360
+ errors.push('Invalid organisation ID format');
361
+ }
362
+ } else {
363
+ // For page-level permissions, organisationId can be null, but if provided, it must be valid
364
+ if (context.organisationId && !RBACSecurityValidator.validateUUID(context.organisationId)) {
365
+ errors.push('Invalid organisation ID format');
366
+ }
361
367
  }
362
368
 
363
369
  if (input.permission && !RBACSecurityValidator.validatePermission(input.permission)) {
package/src/rbac/types.ts CHANGED
@@ -322,6 +322,16 @@ export class OrganisationContextRequiredError extends RBACError {
322
322
  }
323
323
  }
324
324
 
325
+ export class EventContextRequiredError extends RBACError {
326
+ constructor() {
327
+ super(
328
+ 'Event context is required for this operation',
329
+ 'EVENT_CONTEXT_REQUIRED'
330
+ );
331
+ this.name = 'EventContextRequiredError';
332
+ }
333
+ }
334
+
325
335
  export class RBACNotInitializedError extends RBACError {
326
336
  constructor() {
327
337
  super(
@@ -0,0 +1,150 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import type { SupabaseClient } from '@supabase/supabase-js';
3
+ import { ContextValidator, type AppConfig } from '../contextValidator';
4
+ import { EventContextRequiredError, OrganisationContextRequiredError, type Scope } from '../../types';
5
+ import type { Database } from '../../../types/database';
6
+
7
+ describe('ContextValidator', () => {
8
+ const eventRequiredConfig: AppConfig = { requires_event: true };
9
+ const orgRequiredConfig: AppConfig = { requires_event: false };
10
+ const scopeWithOrg: Scope = { organisationId: 'org-1', eventId: 'event-1', appId: 'app-1' };
11
+
12
+ const createSupabaseMock = () => ({}) as SupabaseClient<Database>;
13
+
14
+ beforeEach(() => {
15
+ vi.restoreAllMocks();
16
+ });
17
+
18
+ afterEach(() => {
19
+ vi.clearAllMocks();
20
+ });
21
+
22
+ describe('validateScope', () => {
23
+ it('treats PORTAL and ADMIN apps as always valid', async () => {
24
+ const baseScope: Scope = { appId: 'portal-app' };
25
+ const result = await ContextValidator.validateScope(baseScope, eventRequiredConfig, 'PORTAL');
26
+
27
+ expect(result).toEqual({
28
+ isValid: true,
29
+ resolvedScope: { organisationId: undefined, eventId: undefined, appId: 'portal-app' },
30
+ error: null
31
+ });
32
+ });
33
+
34
+ it('requires organisation context when no app config exists', async () => {
35
+ const baseScope: Scope = { eventId: 'event-1' };
36
+ const result = await ContextValidator.validateScope(baseScope, null);
37
+
38
+ expect(result.isValid).toBe(false);
39
+ expect(result.error).toBeInstanceOf(OrganisationContextRequiredError);
40
+ });
41
+
42
+ it('requires event context for event-based apps', async () => {
43
+ const baseScope: Scope = { organisationId: 'org-1' };
44
+ const result = await ContextValidator.validateScope(baseScope, eventRequiredConfig);
45
+
46
+ expect(result.isValid).toBe(false);
47
+ expect(result.error).toBeInstanceOf(EventContextRequiredError);
48
+ });
49
+
50
+ it('accepts scopes that satisfy event-based requirements', async () => {
51
+ const result = await ContextValidator.validateScope({ eventId: 'event-1' }, eventRequiredConfig);
52
+
53
+ expect(result.isValid).toBe(true);
54
+ expect(result.resolvedScope?.eventId).toBe('event-1');
55
+ });
56
+
57
+ it('enforces organisation requirements for org-based apps', async () => {
58
+ const result = await ContextValidator.validateScope({ eventId: 'event-1' }, orgRequiredConfig);
59
+
60
+ expect(result.isValid).toBe(false);
61
+ expect(result.error).toBeInstanceOf(OrganisationContextRequiredError);
62
+ });
63
+ });
64
+
65
+ describe('resolveRequiredContext', () => {
66
+ it('passes through scope for optional contexts', async () => {
67
+ const baseScope: Scope = { appId: 'admin-app' };
68
+ const result = await ContextValidator.resolveRequiredContext(baseScope, eventRequiredConfig, 'ADMIN');
69
+
70
+ expect(result).toEqual({
71
+ isValid: true,
72
+ resolvedScope: { organisationId: undefined, eventId: undefined, appId: 'admin-app' },
73
+ error: null
74
+ });
75
+ });
76
+
77
+ it('returns organisation error when no app config and org is missing', async () => {
78
+ const result = await ContextValidator.resolveRequiredContext({ eventId: 'event-1' }, null);
79
+
80
+ expect(result.isValid).toBe(false);
81
+ expect(result.error).toBeInstanceOf(OrganisationContextRequiredError);
82
+ });
83
+
84
+ it('derives organisation id for event-required apps when missing', async () => {
85
+ const supabase = createSupabaseMock();
86
+ const deriveSpy = vi.spyOn(ContextValidator, 'deriveOrgFromEvent').mockResolvedValue('derived-org');
87
+
88
+ const result = await ContextValidator.resolveRequiredContext({ eventId: 'event-1' }, eventRequiredConfig, undefined, supabase);
89
+
90
+ expect(deriveSpy).toHaveBeenCalledWith(supabase, 'event-1');
91
+ expect(result).toEqual({
92
+ isValid: true,
93
+ resolvedScope: { organisationId: 'derived-org', eventId: 'event-1', appId: undefined },
94
+ error: null
95
+ });
96
+ });
97
+
98
+ it('surfaces derivation failures when supabase is unavailable', async () => {
99
+ const result = await ContextValidator.resolveRequiredContext({ eventId: 'event-1' }, eventRequiredConfig, undefined, null);
100
+
101
+ expect(result.isValid).toBe(false);
102
+ expect(result.error?.message).toContain('supabase client not available');
103
+ });
104
+
105
+ it('returns derivation error messages when fetch fails', async () => {
106
+ const supabase = createSupabaseMock();
107
+ vi.spyOn(ContextValidator, 'deriveOrgFromEvent').mockRejectedValue(new Error('network down'));
108
+
109
+ const result = await ContextValidator.resolveRequiredContext({ eventId: 'event-1' }, eventRequiredConfig, undefined, supabase);
110
+
111
+ expect(result.isValid).toBe(false);
112
+ expect(result.error).toBeInstanceOf(Error);
113
+ expect(result.error?.message).toContain('network down');
114
+ });
115
+
116
+ it('enforces organisation requirement for org-based apps', async () => {
117
+ const result = await ContextValidator.resolveRequiredContext({ eventId: 'event-1' }, orgRequiredConfig);
118
+
119
+ expect(result.isValid).toBe(false);
120
+ expect(result.error).toBeInstanceOf(OrganisationContextRequiredError);
121
+ });
122
+
123
+ it('returns valid result when org context is present', async () => {
124
+ const result = await ContextValidator.resolveRequiredContext(scopeWithOrg, orgRequiredConfig);
125
+
126
+ expect(result).toEqual({ isValid: true, resolvedScope: scopeWithOrg, error: null });
127
+ });
128
+ });
129
+
130
+ describe('isContextReady', () => {
131
+ it('is always ready for PORTAL/ADMIN apps', () => {
132
+ expect(ContextValidator.isContextReady({}, eventRequiredConfig, 'PORTAL')).toBe(true);
133
+ });
134
+
135
+ it('checks event selection for event-required apps', () => {
136
+ expect(ContextValidator.isContextReady({}, eventRequiredConfig, undefined, true)).toBe(true);
137
+ expect(ContextValidator.isContextReady({}, eventRequiredConfig, undefined, false)).toBe(false);
138
+ });
139
+
140
+ it('checks organisation selection for org-required apps', () => {
141
+ expect(ContextValidator.isContextReady({}, orgRequiredConfig, undefined, false, true)).toBe(true);
142
+ expect(ContextValidator.isContextReady({}, orgRequiredConfig, undefined, false, false)).toBe(false);
143
+ });
144
+
145
+ it('defaults to organisation requirement when no config', () => {
146
+ expect(ContextValidator.isContextReady({}, null, undefined, false, true)).toBe(true);
147
+ expect(ContextValidator.isContextReady({}, null, undefined, false, false)).toBe(false);
148
+ });
149
+ });
150
+ });
@@ -0,0 +1,53 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { deepEqual, scopeEqual } from '../deep-equal';
3
+ import type { Scope } from '../../types';
4
+
5
+ describe('deepEqual', () => {
6
+ it('returns true for identical primitives and references', () => {
7
+ expect(deepEqual(5, 5)).toBe(true);
8
+ expect(deepEqual('test', 'test')).toBe(true);
9
+ const obj = { value: 1 };
10
+ expect(deepEqual(obj, obj)).toBe(true);
11
+ });
12
+
13
+ it('returns false for different primitive values or types', () => {
14
+ expect(deepEqual(5, 6)).toBe(false);
15
+ expect(deepEqual(5, '5')).toBe(false);
16
+ expect(deepEqual(null, undefined)).toBe(false);
17
+ });
18
+
19
+ it('compares arrays by value and order', () => {
20
+ expect(deepEqual([1, 2, 3], [1, 2, 3])).toBe(true);
21
+ expect(deepEqual([1, 2, 3], [3, 2, 1])).toBe(false);
22
+ expect(deepEqual([1, 2], [1, 2, 3])).toBe(false);
23
+ });
24
+
25
+ it('performs deep comparisons on nested objects', () => {
26
+ const left = { id: 1, nested: { active: true, tags: ['a', 'b'] } };
27
+ const right = { nested: { tags: ['a', 'b'], active: true }, id: 1 };
28
+ expect(deepEqual(left, right)).toBe(true);
29
+ });
30
+
31
+ it('detects differences in object structure or values', () => {
32
+ const base = { id: 1, nested: { active: true } };
33
+ expect(deepEqual(base, { id: 1, nested: { active: false } })).toBe(false);
34
+ expect(deepEqual(base, { id: 1, nested: { active: true }, extra: 'field' })).toBe(false);
35
+ expect(deepEqual(base, { id: 1, other: { active: true } })).toBe(false);
36
+ });
37
+ });
38
+
39
+ describe('scopeEqual', () => {
40
+ it('returns true for identical scopes including nulls', () => {
41
+ const scope: Scope = { organisationId: 'org', eventId: 'event', appId: 'app' };
42
+ expect(scopeEqual(scope, { ...scope })).toBe(true);
43
+ expect(scopeEqual(null, null)).toBe(true);
44
+ expect(scopeEqual(undefined, undefined)).toBe(true);
45
+ });
46
+
47
+ it('returns false when any scope property differs', () => {
48
+ const base: Scope = { organisationId: 'org', eventId: 'event', appId: 'app' };
49
+ expect(scopeEqual(base, { ...base, eventId: 'other' })).toBe(false);
50
+ expect(scopeEqual(base, { organisationId: 'org', eventId: 'event' })).toBe(false);
51
+ expect(scopeEqual(base, null)).toBe(false);
52
+ });
53
+ });
@@ -15,7 +15,8 @@ import {
15
15
  getOrganisationFromEvent,
16
16
  createScopeFromEvent,
17
17
  isEventBasedScope,
18
- isValidEventBasedScope
18
+ isValidEventBasedScope,
19
+ clearAllOrgDerivationCache
19
20
  } from '../eventContext';
20
21
 
21
22
  // Mock Supabase client
@@ -37,11 +38,15 @@ describe('Event Context Utilities', () => {
37
38
  let mockQuery: any;
38
39
 
39
40
  beforeEach(() => {
41
+ // Clear cache before each test to prevent test interference
42
+ clearAllOrgDerivationCache();
40
43
  mockSupabase = createMockSupabaseClient();
41
44
  mockQuery = mockSupabase.from('event');
42
45
  });
43
46
 
44
47
  afterEach(() => {
48
+ // Clear cache after each test
49
+ clearAllOrgDerivationCache();
45
50
  vi.clearAllMocks();
46
51
  });
47
52