@jmruthers/pace-core 0.5.188 → 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 (424) 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-DrLDztHt.d.ts → PublicPageProvider-C4uxosp6.d.ts} +129 -40
  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-UNOTYLQF.js → chunk-NIU6J6OX.js} +772 -725
  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-IPCH26AG.js → chunk-STYK4OH2.js} +11 -11
  30. package/dist/chunk-STYK4OH2.js.map +1 -0
  31. package/dist/{chunk-EFCLXK7F.js → chunk-VVBAW5A5.js} +4201 -3809
  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 -5
  38. package/dist/components.js +19 -23
  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 -12
  46. package/dist/index.js +79 -73
  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 +128 -0
  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 +155 -135
  214. package/docs/api-reference/components.md +72 -29
  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 -4
  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 +252 -226
  243. package/src/components/Avatar/Avatar.tsx +179 -53
  244. package/src/components/Avatar/index.ts +1 -1
  245. package/src/components/Button/Button.test.tsx +2 -1
  246. package/src/components/Button/Button.tsx +3 -3
  247. package/src/components/Calendar/Calendar.test.tsx +53 -37
  248. package/src/components/Calendar/Calendar.tsx +409 -82
  249. package/src/components/Card/Card.test.tsx +7 -4
  250. package/src/components/Card/Card.tsx +3 -6
  251. package/src/components/Checkbox/Checkbox.tsx +2 -2
  252. package/src/components/DataTable/components/ActionButtons.tsx +5 -5
  253. package/src/components/DataTable/components/BulkOperationsDropdown.tsx +2 -2
  254. package/src/components/DataTable/components/ColumnFilter.tsx +1 -1
  255. package/src/components/DataTable/components/ColumnVisibilityDropdown.tsx +3 -3
  256. package/src/components/DataTable/components/DataTableBody.tsx +12 -12
  257. package/src/components/DataTable/components/DataTableCore.tsx +3 -3
  258. package/src/components/DataTable/components/DataTableToolbar.tsx +5 -5
  259. package/src/components/DataTable/components/DraggableColumnHeader.tsx +3 -3
  260. package/src/components/DataTable/components/EditableRow.tsx +2 -2
  261. package/src/components/DataTable/components/EmptyState.tsx +3 -3
  262. package/src/components/DataTable/components/GroupHeader.tsx +2 -2
  263. package/src/components/DataTable/components/GroupingDropdown.tsx +1 -1
  264. package/src/components/DataTable/components/ImportModal.tsx +4 -4
  265. package/src/components/DataTable/components/LoadingState.tsx +1 -1
  266. package/src/components/DataTable/components/PaginationControls.tsx +11 -11
  267. package/src/components/DataTable/components/UnifiedTableBody.tsx +9 -9
  268. package/src/components/DataTable/components/ViewRowModal.tsx +2 -2
  269. package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +11 -37
  270. package/src/components/DataTable/components/__tests__/DataTableToolbar.test.tsx +157 -0
  271. package/src/components/DataTable/components/__tests__/LoadingState.test.tsx +2 -1
  272. package/src/components/DataTable/components/__tests__/VirtualizedDataTable.test.tsx +128 -0
  273. package/src/components/DataTable/core/__tests__/ActionManager.test.ts +19 -0
  274. package/src/components/DataTable/core/__tests__/ColumnFactory.test.ts +51 -0
  275. package/src/components/DataTable/core/__tests__/ColumnManager.test.ts +84 -0
  276. package/src/components/DataTable/core/__tests__/DataManager.test.ts +14 -0
  277. package/src/components/DataTable/core/__tests__/DataTableContext.test.tsx +136 -0
  278. package/src/components/DataTable/core/__tests__/LocalDataAdapter.test.ts +16 -0
  279. package/src/components/DataTable/core/__tests__/PluginRegistry.test.ts +18 -0
  280. package/src/components/DataTable/hooks/useDataTablePermissions.ts +28 -7
  281. package/src/components/DataTable/utils/__tests__/hierarchicalUtils.test.ts +30 -1
  282. package/src/components/DataTable/utils/hierarchicalUtils.ts +38 -10
  283. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +8 -3
  284. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.tsx +4 -4
  285. package/src/components/Dialog/Dialog.tsx +2 -2
  286. package/src/components/EventSelector/EventSelector.tsx +7 -7
  287. package/src/components/FileDisplay/FileDisplay.tsx +291 -179
  288. package/src/components/FileUpload/FileUpload.tsx +7 -4
  289. package/src/components/Header/Header.test.tsx +28 -0
  290. package/src/components/Header/Header.tsx +22 -9
  291. package/src/components/InactivityWarningModal/InactivityWarningModal.tsx +2 -2
  292. package/src/components/LoadingSpinner/LoadingSpinner.test.tsx +19 -14
  293. package/src/components/LoadingSpinner/LoadingSpinner.tsx +5 -5
  294. package/src/components/NavigationMenu/NavigationMenu.test.tsx +127 -1
  295. package/src/components/OrganisationSelector/OrganisationSelector.tsx +8 -8
  296. package/src/components/PaceAppLayout/PaceAppLayout.integration.test.tsx +4 -0
  297. package/src/components/PaceAppLayout/PaceAppLayout.performance.test.tsx +3 -0
  298. package/src/components/PaceAppLayout/PaceAppLayout.security.test.tsx +3 -0
  299. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +16 -6
  300. package/src/components/PaceAppLayout/PaceAppLayout.tsx +37 -3
  301. package/src/components/PaceAppLayout/test-setup.tsx +1 -0
  302. package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +66 -45
  303. package/src/components/PaceLoginPage/PaceLoginPage.tsx +6 -4
  304. package/src/components/Progress/Progress.test.tsx +18 -19
  305. package/src/components/Progress/Progress.tsx +31 -32
  306. package/src/components/PublicLayout/PublicLayout.test.tsx +6 -6
  307. package/src/components/PublicLayout/PublicPageProvider.tsx +5 -3
  308. package/src/components/Select/Select.tsx +5 -5
  309. package/src/components/Switch/Switch.test.tsx +2 -1
  310. package/src/components/Switch/Switch.tsx +1 -1
  311. package/src/components/Toast/Toast.tsx +1 -1
  312. package/src/components/Tooltip/Tooltip.test.tsx +8 -2
  313. package/src/components/UserMenu/UserMenu.test.tsx +7 -9
  314. package/src/components/UserMenu/UserMenu.tsx +10 -8
  315. package/src/components/index.ts +2 -1
  316. package/src/eslint-rules/pace-core-compliance.cjs +0 -2
  317. package/src/eslint-rules/pace-core-compliance.js +0 -2
  318. package/src/hooks/__tests__/hooks.integration.test.tsx +4 -1
  319. package/src/hooks/__tests__/useAppConfig.unit.test.ts +76 -5
  320. package/src/hooks/__tests__/useDataTableState.test.ts +76 -0
  321. package/src/hooks/__tests__/useFileUrl.unit.test.ts +25 -69
  322. package/src/hooks/__tests__/useFileUrlCache.test.ts +129 -0
  323. package/src/hooks/__tests__/usePreventTabReload.test.ts +88 -0
  324. package/src/hooks/__tests__/{usePublicEvent.unit.test.ts → usePublicEvent.test.ts} +28 -1
  325. package/src/hooks/__tests__/useQueryCache.test.ts +144 -0
  326. package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +58 -16
  327. package/src/hooks/index.ts +1 -1
  328. package/src/hooks/public/usePublicEvent.ts +2 -2
  329. package/src/hooks/public/usePublicFileDisplay.ts +173 -87
  330. package/src/hooks/useAppConfig.ts +24 -5
  331. package/src/hooks/useFileDisplay.ts +297 -34
  332. package/src/hooks/useFileReference.ts +56 -11
  333. package/src/hooks/useFileUrl.ts +1 -1
  334. package/src/hooks/useInactivityTracker.ts +16 -7
  335. package/src/hooks/usePermissionCache.test.ts +85 -8
  336. package/src/hooks/useQueryCache.ts +21 -0
  337. package/src/hooks/useSecureDataAccess.test.ts +80 -35
  338. package/src/hooks/useSecureDataAccess.ts +80 -37
  339. package/src/index.ts +2 -1
  340. package/src/providers/services/EventServiceProvider.tsx +37 -17
  341. package/src/providers/services/InactivityServiceProvider.tsx +4 -4
  342. package/src/providers/services/OrganisationServiceProvider.tsx +8 -1
  343. package/src/providers/services/UnifiedAuthProvider.tsx +115 -29
  344. package/src/rbac/__tests__/auth-rbac.e2e.test.tsx +451 -0
  345. package/src/rbac/__tests__/engine.comprehensive.test.ts +12 -0
  346. package/src/rbac/__tests__/rbac-engine-core-logic.test.ts +8 -0
  347. package/src/rbac/__tests__/rbac-engine-simplified.test.ts +4 -0
  348. package/src/rbac/api.ts +240 -36
  349. package/src/rbac/cache-invalidation.ts +21 -7
  350. package/src/rbac/compliance/quick-fix-suggestions.ts +1 -1
  351. package/src/rbac/components/NavigationGuard.tsx +23 -63
  352. package/src/rbac/components/NavigationProvider.test.tsx +52 -23
  353. package/src/rbac/components/NavigationProvider.tsx +13 -11
  354. package/src/rbac/components/PagePermissionGuard.tsx +77 -203
  355. package/src/rbac/components/PagePermissionProvider.tsx +13 -11
  356. package/src/rbac/components/PermissionEnforcer.tsx +24 -62
  357. package/src/rbac/components/RoleBasedRouter.tsx +14 -12
  358. package/src/rbac/components/SecureDataProvider.tsx +13 -11
  359. package/src/rbac/components/__tests__/NavigationGuard.test.tsx +104 -41
  360. package/src/rbac/components/__tests__/NavigationProvider.test.tsx +49 -12
  361. package/src/rbac/components/__tests__/PagePermissionGuard.race-condition.test.tsx +22 -1
  362. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +161 -82
  363. package/src/rbac/components/__tests__/PagePermissionGuard.verification.test.tsx +22 -1
  364. package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +77 -30
  365. package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +39 -5
  366. package/src/rbac/components/__tests__/SecureDataProvider.test.tsx +47 -4
  367. package/src/rbac/engine.ts +4 -2
  368. package/src/rbac/hooks/__tests__/useSecureSupabase.test.ts +144 -52
  369. package/src/rbac/hooks/index.ts +3 -0
  370. package/src/rbac/hooks/useCan.test.ts +101 -53
  371. package/src/rbac/hooks/usePermissions.ts +108 -41
  372. package/src/rbac/hooks/useRBAC.test.ts +11 -3
  373. package/src/rbac/hooks/useRBAC.ts +83 -40
  374. package/src/rbac/hooks/useResolvedScope.test.ts +189 -63
  375. package/src/rbac/hooks/useResolvedScope.ts +128 -70
  376. package/src/rbac/hooks/useSecureSupabase.ts +36 -19
  377. package/src/rbac/hooks/useSuperAdminBypass.ts +126 -0
  378. package/src/rbac/request-deduplication.ts +1 -1
  379. package/src/rbac/secureClient.ts +72 -12
  380. package/src/rbac/security.ts +29 -23
  381. package/src/rbac/types.ts +10 -0
  382. package/src/rbac/utils/__tests__/contextValidator.test.ts +150 -0
  383. package/src/rbac/utils/__tests__/deep-equal.test.ts +53 -0
  384. package/src/rbac/utils/__tests__/eventContext.test.ts +6 -1
  385. package/src/rbac/utils/contextValidator.ts +288 -0
  386. package/src/rbac/utils/eventContext.ts +48 -2
  387. package/src/services/EventService.ts +165 -21
  388. package/src/services/OrganisationService.ts +37 -2
  389. package/src/services/__tests__/EventService.test.ts +26 -21
  390. package/src/types/file-reference.ts +13 -10
  391. package/src/utils/app/appNameResolver.test.ts +346 -73
  392. package/src/utils/context/superAdminOverride.ts +58 -0
  393. package/src/utils/file-reference/index.ts +61 -33
  394. package/src/utils/google-places/googlePlacesUtils.test.ts +98 -0
  395. package/src/utils/google-places/loadGoogleMapsScript.test.ts +83 -0
  396. package/src/utils/storage/helpers.test.ts +1 -1
  397. package/src/utils/storage/helpers.ts +38 -19
  398. package/src/utils/storage/types.ts +15 -8
  399. package/src/utils/validation/__tests__/csrf.test.ts +105 -0
  400. package/src/utils/validation/__tests__/sqlInjectionProtection.test.ts +92 -0
  401. package/src/vite-env.d.ts +2 -2
  402. package/dist/chunk-3GOZZZYH.js.map +0 -1
  403. package/dist/chunk-DDM4CCYT.js.map +0 -1
  404. package/dist/chunk-E7UAOUMY.js +0 -75
  405. package/dist/chunk-E7UAOUMY.js.map +0 -1
  406. package/dist/chunk-EFCLXK7F.js.map +0 -1
  407. package/dist/chunk-F2IMUDXZ.js.map +0 -1
  408. package/dist/chunk-HEHYGYOX.js.map +0 -1
  409. package/dist/chunk-IM4QE42D.js.map +0 -1
  410. package/dist/chunk-IPCH26AG.js.map +0 -1
  411. package/dist/chunk-SAUPYVLF.js.map +0 -1
  412. package/dist/chunk-THRPYOFK.js.map +0 -1
  413. package/dist/chunk-UNOTYLQF.js.map +0 -1
  414. package/dist/chunk-VGZZXKBR.js.map +0 -1
  415. package/dist/chunk-YHCN776L.js.map +0 -1
  416. package/src/hooks/__tests__/usePermissionCache.simple.test.ts +0 -192
  417. package/src/hooks/__tests__/usePermissionCache.unit.test.ts +0 -741
  418. package/src/hooks/__tests__/usePublicEvent.simple.test.ts +0 -703
  419. package/src/rbac/hooks/useRBAC.simple.test.ts +0 -95
  420. package/src/rbac/utils/__tests__/eventContext.unit.test.ts +0 -428
  421. /package/dist/{DataTable-GUFUNZ3N.js.map → DataTable-ON3IXISJ.js.map} +0 -0
  422. /package/dist/{UnifiedAuthProvider-643PUAIM.js.map → UnifiedAuthProvider-X5NXANVI.js.map} +0 -0
  423. /package/dist/{api-YP7XD5L6.js.map → api-I6UCQ5S6.js.map} +0 -0
  424. /package/dist/{chunk-HESYZWZW.js.map → chunk-QWWZ5CAQ.js.map} +0 -0
@@ -135,7 +135,7 @@ export function useFileDisplay(
135
135
  const [error, setError] = useState<Error | null>(null);
136
136
 
137
137
  const fetchFiles = useCallback(async (): Promise<void> => {
138
- if (!table_name || !record_id || !organisation_id || !supabase) {
138
+ if (!table_name || !record_id || !supabase) {
139
139
  setFileUrl(null);
140
140
  setFileReference(null);
141
141
  setFileReferences([]);
@@ -145,14 +145,17 @@ export function useFileDisplay(
145
145
  return;
146
146
  }
147
147
 
148
- // Validate UUID format for organisationId to prevent database errors
149
- const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
150
- if (!uuidRegex.test(organisation_id)) {
151
- logger.warn('useFileDisplay', 'Invalid organisationId format (not a valid UUID):', organisation_id);
148
+ // Validate UUID format for organisationId to prevent database errors (only if provided)
149
+ if (organisation_id) {
150
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
151
+ if (!uuidRegex.test(organisation_id)) {
152
+ logger.warn('useFileDisplay', 'Invalid organisationId format (not a valid UUID):', organisation_id);
153
+ }
152
154
  }
153
155
 
154
156
  // Check cache first
155
- const cacheKey = `file_${table_name}_${record_id}_${organisation_id}_${category || 'all'}`;
157
+ // When organisation_id is undefined, use 'undefined' in cache key to distinguish from explicit null
158
+ const cacheKey = `file_${table_name}_${record_id}_${organisation_id === undefined ? 'undefined' : (organisation_id ?? 'null')}_${category || 'all'}`;
156
159
  if (enableCache) {
157
160
  const cached = authenticatedFileCache.get(cacheKey);
158
161
  if (cached && Date.now() - cached.timestamp < cached.ttl) {
@@ -166,6 +169,7 @@ export function useFileDisplay(
166
169
  const signedUrlResult = await getSignedUrl(supabase, cachedData.fileReference.file_path, {
167
170
  appName: 'pace-core',
168
171
  orgId: organisation_id,
172
+ userId: organisation_id ? undefined : record_id,
169
173
  expiresIn: 3600
170
174
  });
171
175
  const regeneratedUrl = signedUrlResult?.url || null;
@@ -202,29 +206,284 @@ export function useFileDisplay(
202
206
  const service = createFileReferenceService(supabase);
203
207
  let files: FileReference[] = [];
204
208
 
205
- // CRITICAL: When category is provided, MUST use RPC function, not direct queries
206
- // Category is stored in file_metadata JSONB field, not a direct column
207
- if (category) {
208
- // Single file mode - get files by category using RPC
209
- logger.debug('useFileDisplay', 'Using RPC function for category filtering:', {
210
- table_name,
211
- record_id,
212
- category,
213
- organisation_id
209
+ // When organisation_id is undefined (not provided), search both user-scoped (null) and organisation-scoped files
210
+ // This allows FileDisplay to work without requiring the organisation_id prop
211
+ // Note: Explicitly passing null or empty string should only search user-scoped files
212
+ const shouldSearchBothScopes = organisation_id === undefined;
213
+
214
+ if (shouldSearchBothScopes) {
215
+ // First, try user-scoped files (organisation_id = null)
216
+ let userScopedFiles: FileReference[] = [];
217
+ let orgScopedFiles: FileReference[] = [];
218
+
219
+ try {
220
+ if (category) {
221
+ userScopedFiles = await service.getFilesByCategory(
222
+ table_name,
223
+ record_id,
224
+ category,
225
+ undefined // Explicitly pass undefined for user-scoped files (service converts to null for RPC)
226
+ );
227
+ } else {
228
+ userScopedFiles = await service.listFileReferences(
229
+ table_name,
230
+ record_id,
231
+ undefined // Explicitly pass undefined for user-scoped files (service converts to null for RPC)
232
+ );
233
+ }
234
+ } catch (err) {
235
+ logger.warn('useFileDisplay', 'Error querying user-scoped files:', err);
236
+ userScopedFiles = [];
237
+ }
238
+
239
+ // For organisation-scoped files, we need to query the user's organisations
240
+ // Get user's organisations from the authenticated session
241
+ try {
242
+ const { data: { user }, error: userError } = await supabase.auth.getUser();
243
+ if (userError) {
244
+ logger.warn('useFileDisplay', 'Error getting user:', userError);
245
+ }
246
+
247
+ if (user) {
248
+ // Query user's active organisation memberships
249
+ const { data: memberships, error: membershipError } = await supabase
250
+ .from('organisation_memberships')
251
+ .select('organisation_id')
252
+ .eq('user_id', user.id)
253
+ .or('status.is.null,status.eq.active');
254
+
255
+ if (membershipError) {
256
+ logger.warn('useFileDisplay', 'Error querying organisation memberships:', membershipError);
257
+ }
258
+
259
+ if (memberships && memberships.length > 0) {
260
+ // Try each organisation the user belongs to
261
+ const orgIds = memberships.map(m => m.organisation_id).filter(Boolean) as string[];
262
+
263
+ // Query each organisation in parallel
264
+ const orgQueries = orgIds.map(async (orgId) => {
265
+ try {
266
+ if (category) {
267
+ return await service.getFilesByCategory(
268
+ table_name,
269
+ record_id,
270
+ category,
271
+ orgId
272
+ );
273
+ } else {
274
+ return await service.listFileReferences(
275
+ table_name,
276
+ record_id,
277
+ orgId
278
+ );
279
+ }
280
+ } catch (err) {
281
+ // Silently fail for individual org queries - user might not have access
282
+ return [];
283
+ }
284
+ });
285
+
286
+ const orgResults = await Promise.all(orgQueries);
287
+ orgScopedFiles = orgResults.flat();
288
+ } else {
289
+ // When user has no organisation memberships, try querying files with any organisation_id
290
+ // as a fallback - the file might be organisation-scoped but accessible via RLS
291
+ // This handles edge cases where RLS allows access but we can't enumerate organisations
292
+ try {
293
+ // Try querying without organisation_id filter - let RLS handle security
294
+ let fallbackQuery = supabase
295
+ .from('file_references')
296
+ .select('id, table_name, record_id, file_path, file_metadata, organisation_id, app_id, is_public, created_at, updated_at')
297
+ .eq('table_name', table_name)
298
+ .eq('record_id', record_id)
299
+ .order('created_at', { ascending: false });
300
+
301
+ if (category) {
302
+ fallbackQuery = fallbackQuery.eq('file_metadata->>category', category);
303
+ }
304
+
305
+ const { data: fallbackFiles } = await fallbackQuery;
306
+
307
+ if (fallbackFiles && fallbackFiles.length > 0) {
308
+ // Convert to FileReference format
309
+ orgScopedFiles = fallbackFiles.map((f: any) => ({
310
+ id: f.id,
311
+ table_name: f.table_name,
312
+ record_id: f.record_id,
313
+ file_path: f.file_path,
314
+ file_metadata: f.file_metadata || {},
315
+ organisation_id: f.organisation_id,
316
+ app_id: f.app_id,
317
+ is_public: f.is_public ?? false,
318
+ created_at: f.created_at,
319
+ updated_at: f.updated_at
320
+ })) as FileReference[];
321
+ }
322
+ } catch (err) {
323
+ // Silently fail - RLS may block or other error
324
+ }
325
+ }
326
+ }
327
+ } catch (err) {
328
+ logger.warn('useFileDisplay', 'Error querying organisation-scoped files:', err);
329
+ orgScopedFiles = [];
330
+ }
331
+
332
+ // Merge results: prefer organisation-scoped files if both exist, otherwise use user-scoped
333
+ // Sort by created_at DESC to get most recent first
334
+ const allFiles = [...userScopedFiles, ...orgScopedFiles];
335
+ allFiles.sort((a, b) => {
336
+ const aTime = new Date(a.created_at).getTime();
337
+ const bTime = new Date(b.created_at).getTime();
338
+ return bTime - aTime;
214
339
  });
215
- files = await service.getFilesByCategory(
216
- table_name,
217
- record_id,
218
- category,
219
- organisation_id
220
- );
340
+
341
+ // If we have both types, prefer organisation-scoped (non-null organisation_id)
342
+ // Otherwise, use whatever we found
343
+ if (orgScopedFiles.length > 0 && userScopedFiles.length > 0) {
344
+ // Prefer organisation-scoped files - filter to only those with organisation_id
345
+ files = allFiles.filter(f => f.organisation_id !== null);
346
+ } else {
347
+ // Use all files found (either user-scoped or org-scoped, but not both)
348
+ files = allFiles;
349
+ }
350
+
351
+ // If no files found through RPC, try a direct query as fallback
352
+ // This handles cases where RLS policy allows access but RPC security check is too strict
353
+ // (e.g., pace_person files where user owns the person record but record_id != user_id)
354
+ if (files.length === 0) {
355
+ try {
356
+ let directQuery = supabase
357
+ .from('file_references')
358
+ .select('id, table_name, record_id, file_path, file_metadata, organisation_id, app_id, is_public, created_at, updated_at')
359
+ .eq('table_name', table_name)
360
+ .eq('record_id', record_id)
361
+ .order('created_at', { ascending: false });
362
+
363
+ // If category is provided, filter by category in metadata
364
+ if (category) {
365
+ directQuery = directQuery.eq('file_metadata->>category', category);
366
+ }
367
+
368
+ const { data: directFiles } = await directQuery;
369
+
370
+ if (directFiles && directFiles.length > 0) {
371
+ // Convert to FileReference format
372
+ files = directFiles.map((f: any) => ({
373
+ id: f.id,
374
+ table_name: f.table_name,
375
+ record_id: f.record_id,
376
+ file_path: f.file_path,
377
+ file_metadata: f.file_metadata || {},
378
+ organisation_id: f.organisation_id,
379
+ app_id: f.app_id,
380
+ is_public: f.is_public ?? false,
381
+ created_at: f.created_at,
382
+ updated_at: f.updated_at
383
+ })) as FileReference[];
384
+ }
385
+ } catch (err) {
386
+ // Silently fail - RLS may block or other error
387
+ }
388
+ }
221
389
  } else {
222
- // Multiple files mode - get all files using RPC
223
- files = await service.listFileReferences(
224
- table_name,
225
- record_id,
226
- organisation_id
227
- );
390
+ // organisation_id is provided (or explicitly null) - use normal query
391
+ // CRITICAL: When category is provided, MUST use RPC function, not direct queries
392
+ // Category is stored in file_metadata JSONB field, not a direct column
393
+ if (category) {
394
+ // Single file mode - get files by category using RPC
395
+ logger.debug('useFileDisplay', 'Using RPC function for category filtering:', {
396
+ table_name,
397
+ record_id,
398
+ category,
399
+ organisation_id
400
+ });
401
+ files = await service.getFilesByCategory(
402
+ table_name,
403
+ record_id,
404
+ category,
405
+ organisation_id
406
+ );
407
+ } else {
408
+ // Multiple files mode - get all files using RPC
409
+ files = await service.listFileReferences(
410
+ table_name,
411
+ record_id,
412
+ organisation_id
413
+ );
414
+ }
415
+
416
+ // Fallback: If no files found and organisation_id is falsy (undefined, null, empty string),
417
+ // try searching both scopes as a fallback
418
+ // This handles cases where the prop might be passed as empty string or the check above didn't catch it
419
+ if (files.length === 0 && (!organisation_id || organisation_id === '')) {
420
+ // Try the dual-scope search logic
421
+ let userScopedFiles: FileReference[] = [];
422
+ let orgScopedFiles: FileReference[] = [];
423
+
424
+ try {
425
+ if (category) {
426
+ userScopedFiles = await service.getFilesByCategory(
427
+ table_name,
428
+ record_id,
429
+ category,
430
+ undefined
431
+ );
432
+ } else {
433
+ userScopedFiles = await service.listFileReferences(
434
+ table_name,
435
+ record_id,
436
+ undefined
437
+ );
438
+ }
439
+ } catch (err) {
440
+ // Silently fail
441
+ }
442
+
443
+ try {
444
+ const { data: { user } } = await supabase.auth.getUser();
445
+ if (user) {
446
+ const { data: memberships } = await supabase
447
+ .from('organisation_memberships')
448
+ .select('organisation_id')
449
+ .eq('user_id', user.id)
450
+ .or('status.is.null,status.eq.active');
451
+
452
+ if (memberships && memberships.length > 0) {
453
+ const orgIds = memberships.map(m => m.organisation_id).filter(Boolean) as string[];
454
+ const orgQueries = orgIds.map(async (orgId) => {
455
+ try {
456
+ if (category) {
457
+ return await service.getFilesByCategory(table_name, record_id, category, orgId);
458
+ } else {
459
+ return await service.listFileReferences(table_name, record_id, orgId);
460
+ }
461
+ } catch (err) {
462
+ return [];
463
+ }
464
+ });
465
+ const orgResults = await Promise.all(orgQueries);
466
+ orgScopedFiles = orgResults.flat();
467
+ }
468
+ }
469
+ } catch (err) {
470
+ // Silently fail
471
+ }
472
+
473
+ // Merge results
474
+ const allFiles = [...userScopedFiles, ...orgScopedFiles];
475
+ allFiles.sort((a, b) => {
476
+ const aTime = new Date(a.created_at).getTime();
477
+ const bTime = new Date(b.created_at).getTime();
478
+ return bTime - aTime;
479
+ });
480
+
481
+ if (orgScopedFiles.length > 0 && userScopedFiles.length > 0) {
482
+ files = allFiles.filter(f => f.organisation_id !== null);
483
+ } else {
484
+ files = allFiles;
485
+ }
486
+ }
228
487
  }
229
488
 
230
489
  if (files.length === 0) {
@@ -271,6 +530,7 @@ export function useFileDisplay(
271
530
  const signedUrlResult = await getSignedUrl(supabase, firstFile.file_path, {
272
531
  appName: 'pace-core',
273
532
  orgId: organisation_id,
533
+ userId: organisation_id ? undefined : record_id,
274
534
  expiresIn: 3600
275
535
  });
276
536
  url = signedUrlResult?.url || null;
@@ -283,6 +543,7 @@ export function useFileDisplay(
283
543
  const urlMap = await generateFileUrlsBatch(supabase, files, {
284
544
  appName: 'pace-core',
285
545
  orgId: organisation_id,
546
+ userId: organisation_id ? undefined : record_id,
286
547
  expiresIn: 3600
287
548
  });
288
549
  setFileUrls(urlMap);
@@ -344,7 +605,7 @@ export function useFileDisplay(
344
605
 
345
606
  // Fetch files when parameters change
346
607
  useEffect(() => {
347
- if (table_name && record_id && organisation_id && supabase) {
608
+ if (table_name && record_id && supabase) {
348
609
  fetchFiles();
349
610
  } else {
350
611
  setFileUrl(null);
@@ -355,14 +616,16 @@ export function useFileDisplay(
355
616
  setIsLoading(false);
356
617
  setError(null);
357
618
  }
358
- }, [fetchFiles, table_name, record_id, organisation_id, supabase]);
619
+ // fetchFiles is memoized; we only need to re-run when parameters change
620
+ // eslint-disable-next-line react-hooks/exhaustive-deps
621
+ }, [table_name, record_id, organisation_id, supabase]);
359
622
 
360
623
  const refetch = useCallback(async (): Promise<void> => {
361
- if (!table_name || !record_id || !organisation_id || !supabase) return;
624
+ if (!table_name || !record_id || !supabase) return;
362
625
 
363
626
  // Clear cache for this file
364
627
  if (enableCache) {
365
- const cacheKey = `file_${table_name}_${record_id}_${organisation_id}_${category || 'all'}`;
628
+ const cacheKey = `file_${table_name}_${record_id}_${organisation_id === undefined ? 'undefined' : (organisation_id ?? 'null')}_${category || 'all'}`;
366
629
  authenticatedFileCache.delete(cacheKey);
367
630
  }
368
631
  await fetchFiles();
@@ -415,14 +678,14 @@ export function getFileDisplayCacheStats(): { size: number; keys: string[] } {
415
678
  export function invalidateFileDisplayCache(
416
679
  table_name: string,
417
680
  record_id: string,
418
- organisation_id: string,
681
+ organisation_id: string | null | undefined,
419
682
  category?: FileCategory
420
683
  ): void {
421
- const cacheKey = `file_${table_name}_${record_id}_${organisation_id}_${category || 'all'}`;
684
+ const cacheKey = `file_${table_name}_${record_id}_${organisation_id === undefined ? 'undefined' : (organisation_id ?? 'null')}_${category || 'all'}`;
422
685
  authenticatedFileCache.delete(cacheKey);
423
686
  // Also invalidate 'all' category if specific category invalidated
424
687
  if (category) {
425
- const allCategoryKey = `file_${table_name}_${record_id}_${organisation_id}_all`;
688
+ const allCategoryKey = `file_${table_name}_${record_id}_${organisation_id === undefined ? 'undefined' : (organisation_id ?? 'null')}_all`;
426
689
  authenticatedFileCache.delete(allCategoryKey);
427
690
  }
428
691
  }
@@ -16,6 +16,49 @@ import { createLogger } from '../utils/core/logger';
16
16
 
17
17
  const log = createLogger('useFileReference');
18
18
 
19
+ type UrlRefreshCallback = () => void;
20
+
21
+ /**
22
+ * Shared interval manager to avoid spawning multiple intervals for the same file reference
23
+ */
24
+ const urlRefreshManager = {
25
+ subscriptions: new Map<string, { callbacks: Set<UrlRefreshCallback>; intervalId: NodeJS.Timeout | null }>(),
26
+
27
+ subscribe(key: string, callback: UrlRefreshCallback) {
28
+ let entry = this.subscriptions.get(key);
29
+ if (!entry) {
30
+ entry = { callbacks: new Set(), intervalId: null };
31
+ this.subscriptions.set(key, entry);
32
+ }
33
+
34
+ entry.callbacks.add(callback);
35
+
36
+ if (!entry.intervalId) {
37
+ entry.intervalId = setInterval(() => {
38
+ entry?.callbacks.forEach((cb) => cb());
39
+ }, 55 * 60 * 1000);
40
+ }
41
+
42
+ return () => {
43
+ this.unsubscribe(key, callback);
44
+ };
45
+ },
46
+
47
+ unsubscribe(key: string, callback: UrlRefreshCallback) {
48
+ const entry = this.subscriptions.get(key);
49
+ if (!entry) return;
50
+
51
+ entry.callbacks.delete(callback);
52
+
53
+ if (entry.callbacks.size === 0) {
54
+ if (entry.intervalId) {
55
+ clearInterval(entry.intervalId);
56
+ }
57
+ this.subscriptions.delete(key);
58
+ }
59
+ }
60
+ };
61
+
19
62
  export function useFileReference(supabase: SupabaseClient) {
20
63
  const [isLoading, setIsLoading] = useState(false);
21
64
  const [error, setError] = useState<string | null>(null);
@@ -221,7 +264,7 @@ export function useFileReferenceForRecord(
221
264
  const [fileReference, setFileReference] = useState<FileReference | null>(null);
222
265
  const [fileReferences, setFileReferences] = useState<FileReference[]>([]);
223
266
  const [fileCount, setFileCount] = useState<number>(0);
224
- const urlRefreshIntervalRef = useRef<NodeJS.Timeout | null>(null);
267
+ const refreshSubscriptionRef = useRef<(() => void) | null>(null);
225
268
 
226
269
  const loadFileReference = useCallback(async () => {
227
270
  const reference = await getFileReference(table_name, record_id, organisation_id);
@@ -259,27 +302,29 @@ export function useFileReferenceForRecord(
259
302
 
260
303
  // Auto-refresh signed URLs before expiration (refresh 5 minutes before 1 hour expiry)
261
304
  useEffect(() => {
305
+ if (refreshSubscriptionRef.current) {
306
+ refreshSubscriptionRef.current();
307
+ refreshSubscriptionRef.current = null;
308
+ }
309
+
262
310
  if (!fileReference || fileReference.is_public) {
263
311
  // Only refresh private files (signed URLs expire)
264
- if (urlRefreshIntervalRef.current) {
265
- clearInterval(urlRefreshIntervalRef.current);
266
- urlRefreshIntervalRef.current = null;
267
- }
268
312
  return;
269
313
  }
270
314
 
271
315
  // Refresh signed URL 5 minutes before expiration (55 minutes into the 1-hour expiry)
272
- urlRefreshIntervalRef.current = setInterval(() => {
316
+ const key = `${fileReference.table_name}:${fileReference.record_id}:${organisation_id}`;
317
+ refreshSubscriptionRef.current = urlRefreshManager.subscribe(key, () => {
273
318
  loadFileUrl();
274
- }, 55 * 60 * 1000); // 55 minutes
319
+ });
275
320
 
276
321
  return () => {
277
- if (urlRefreshIntervalRef.current) {
278
- clearInterval(urlRefreshIntervalRef.current);
279
- urlRefreshIntervalRef.current = null;
322
+ if (refreshSubscriptionRef.current) {
323
+ refreshSubscriptionRef.current();
324
+ refreshSubscriptionRef.current = null;
280
325
  }
281
326
  };
282
- }, [fileReference, loadFileUrl]);
327
+ }, [fileReference, loadFileUrl, organisation_id]);
283
328
 
284
329
  return {
285
330
  isLoading,
@@ -17,7 +17,7 @@ const log = createLogger('useFileUrl');
17
17
 
18
18
  export interface UseFileUrlOptions {
19
19
  /** Organisation ID for signed URL generation */
20
- organisation_id: string;
20
+ organisation_id: string | undefined;
21
21
  /** Supabase client instance */
22
22
  supabase: SupabaseClient;
23
23
  /** Whether to auto-load URLs on mount */
@@ -156,6 +156,15 @@ export function useInactivityTracker({
156
156
  const lastActivityRef = useRef<number>(Date.now());
157
157
  const channelRef = useRef<BroadcastChannel | null>(null);
158
158
  const throttledResetActivityRef = useRef<((event: Event) => void) | null>(null);
159
+ const onIdleRef = useRef(onIdle);
160
+ const onWarningRef = useRef(onWarning);
161
+ const onActivityRef = useRef(onActivity);
162
+
163
+ useEffect(() => {
164
+ onIdleRef.current = onIdle;
165
+ onWarningRef.current = onWarning;
166
+ onActivityRef.current = onActivity;
167
+ }, [onIdle, onWarning, onActivity]);
159
168
 
160
169
  // Clear all timers
161
170
  const clearTimers = useCallback(() => {
@@ -190,7 +199,7 @@ export function useInactivityTracker({
190
199
 
191
200
  // Notify activity callback (unless skipped for initial setup)
192
201
  if (!skipActivityCallback) {
193
- onActivity?.();
202
+ onActivityRef.current?.();
194
203
  }
195
204
 
196
205
  // Set up warning timer
@@ -198,14 +207,14 @@ export function useInactivityTracker({
198
207
  if (warningTime > 0) {
199
208
  warningTimeoutRef.current = setTimeout(() => {
200
209
  setShowWarning(true);
201
- onWarning?.();
210
+ onWarningRef.current?.();
202
211
  }, warningTime);
203
212
  }
204
213
 
205
214
  // Set up idle timeout
206
215
  timeoutRef.current = setTimeout(() => {
207
216
  setIsIdle(true);
208
- onIdle?.();
217
+ onIdleRef.current?.();
209
218
  }, idleTimeoutMs);
210
219
 
211
220
  // Start countdown interval for time remaining
@@ -234,7 +243,7 @@ export function useInactivityTracker({
234
243
  } catch (error) {
235
244
  logger.warn('useInactivityTracker', 'Failed to broadcast activity:', error);
236
245
  }
237
- }, [enabled, idleTimeoutMs, warnBeforeMs, onIdle, onWarning, onActivity, storageKey, clearTimers]);
246
+ }, [enabled, idleTimeoutMs, warnBeforeMs, storageKey, clearTimers]);
238
247
 
239
248
  // Start tracking
240
249
  const startTracking = useCallback(() => {
@@ -281,12 +290,12 @@ export function useInactivityTracker({
281
290
 
282
291
  if (remaining <= warnBeforeMs) {
283
292
  setShowWarning(true);
284
- onWarning?.();
293
+ onWarningRef.current?.();
285
294
  }
286
295
 
287
296
  if (remaining <= 0) {
288
297
  setIsIdle(true);
289
- onIdle?.();
298
+ onIdleRef.current?.();
290
299
  return;
291
300
  }
292
301
  }
@@ -330,7 +339,7 @@ export function useInactivityTracker({
330
339
  channelRef.current = null;
331
340
  }
332
341
  };
333
- }, [enabled, isTracking, channelName, storageKey, idleTimeoutMs, warnBeforeMs, onIdle, onWarning, onActivity, resetActivity]);
342
+ }, [enabled, channelName, storageKey, idleTimeoutMs, warnBeforeMs, resetActivity, clearTimers]);
334
343
 
335
344
  // Stop tracking
336
345
  const stopTracking = useCallback(() => {