@jmruthers/pace-core 0.5.189 → 0.5.191

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (438) hide show
  1. package/core-usage-manifest.json +0 -4
  2. package/dist/{AuthService-B-cd2MA4.d.ts → AuthService-CbP_utw2.d.ts} +7 -3
  3. package/dist/{DataTable-IVYljGJ6.d.ts → DataTable-Be6dH_dR.d.ts} +1 -1
  4. package/dist/{DataTable-GUFUNZ3N.js → DataTable-WKRZD47S.js} +8 -8
  5. package/dist/{PublicPageProvider-B8HaLe69.d.ts → PublicPageProvider-ULXC_u6U.d.ts} +84 -25
  6. package/dist/{UnifiedAuthProvider-BG0AL5eE.d.ts → UnifiedAuthProvider-BYA9qB-o.d.ts} +4 -3
  7. package/dist/{UnifiedAuthProvider-643PUAIM.js → UnifiedAuthProvider-FTSG5XH7.js} +4 -2
  8. package/dist/{api-YP7XD5L6.js → api-IHKALJZD.js} +4 -2
  9. package/dist/{chunk-VGZZXKBR.js → chunk-6LTQQAT6.js} +351 -157
  10. package/dist/chunk-6LTQQAT6.js.map +1 -0
  11. package/dist/{chunk-MX64ZF6I.js → chunk-6TQDD426.js} +15 -15
  12. package/dist/chunk-6TQDD426.js.map +1 -0
  13. package/dist/{chunk-YHCN776L.js → chunk-G37KK66H.js} +2 -75
  14. package/dist/chunk-G37KK66H.js.map +1 -0
  15. package/dist/{chunk-THRPYOFK.js → chunk-HW3OVDUF.js} +5 -5
  16. package/dist/chunk-HW3OVDUF.js.map +1 -0
  17. package/dist/{chunk-F2IMUDXZ.js → chunk-I7PSE6JW.js} +75 -2
  18. package/dist/chunk-I7PSE6JW.js.map +1 -0
  19. package/dist/{chunk-IM4QE42D.js → chunk-LOMZXPSN.js} +141 -326
  20. package/dist/chunk-LOMZXPSN.js.map +1 -0
  21. package/dist/chunk-OETXORNB.js +614 -0
  22. package/dist/chunk-OETXORNB.js.map +1 -0
  23. package/dist/{chunk-HESYZWZW.js → chunk-QWWZ5CAQ.js} +2 -2
  24. package/dist/{chunk-HEHYGYOX.js → chunk-ROXMHMY2.js} +403 -46
  25. package/dist/chunk-ROXMHMY2.js.map +1 -0
  26. package/dist/{chunk-2UUZZJFT.js → chunk-ULHIJK66.js} +228 -177
  27. package/dist/{chunk-2UUZZJFT.js.map → chunk-ULHIJK66.js.map} +1 -1
  28. package/dist/{chunk-YGPFYGA6.js → chunk-VKB2CO4Z.js} +838 -503
  29. package/dist/chunk-VKB2CO4Z.js.map +1 -0
  30. package/dist/{chunk-3GOZZZYH.js → chunk-VRGWKHDB.js} +238 -301
  31. package/dist/chunk-VRGWKHDB.js.map +1 -0
  32. package/dist/{chunk-UCQSRW7Z.js → chunk-XNYQOL3Z.js} +431 -384
  33. package/dist/chunk-XNYQOL3Z.js.map +1 -0
  34. package/dist/{chunk-DDM4CCYT.js → chunk-XYXSXPUK.js} +79 -59
  35. package/dist/chunk-XYXSXPUK.js.map +1 -0
  36. package/dist/{chunk-SAUPYVLF.js → chunk-ZSAAAMVR.js} +1 -1
  37. package/dist/chunk-ZSAAAMVR.js.map +1 -0
  38. package/dist/components.d.ts +5 -6
  39. package/dist/components.js +19 -19
  40. package/dist/components.js.map +1 -1
  41. package/dist/{database.generated-DI89OQeI.d.ts → database.generated-CzIvgcPu.d.ts} +165 -201
  42. package/dist/eslint-rules/pace-core-compliance.cjs +0 -2
  43. package/dist/{file-reference-D037xOFK.d.ts → file-reference-BavO2eQj.d.ts} +13 -10
  44. package/dist/hooks.d.ts +20 -15
  45. package/dist/hooks.js +14 -8
  46. package/dist/hooks.js.map +1 -1
  47. package/dist/index.d.ts +17 -15
  48. package/dist/index.js +86 -81
  49. package/dist/index.js.map +1 -1
  50. package/dist/providers.d.ts +3 -3
  51. package/dist/providers.js +3 -1
  52. package/dist/rbac/index.d.ts +77 -13
  53. package/dist/rbac/index.js +12 -9
  54. package/dist/{types-Bwgl--Xo.d.ts → types-CEpcvwwF.d.ts} +1 -1
  55. package/dist/types.d.ts +3 -3
  56. package/dist/types.js +1 -1
  57. package/dist/{usePublicRouteParams-CTDELQ7H.d.ts → usePublicRouteParams-TZe0gy-4.d.ts} +17 -10
  58. package/dist/utils.d.ts +8 -8
  59. package/dist/utils.js +16 -16
  60. package/docs/README.md +2 -2
  61. package/docs/api/classes/ColumnFactory.md +1 -1
  62. package/docs/api/classes/ErrorBoundary.md +1 -1
  63. package/docs/api/classes/InvalidScopeError.md +2 -2
  64. package/docs/api/classes/Logger.md +1 -1
  65. package/docs/api/classes/MissingUserContextError.md +2 -2
  66. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  67. package/docs/api/classes/PermissionDeniedError.md +1 -1
  68. package/docs/api/classes/RBACAuditManager.md +2 -2
  69. package/docs/api/classes/RBACCache.md +1 -1
  70. package/docs/api/classes/RBACEngine.md +5 -5
  71. package/docs/api/classes/RBACError.md +1 -1
  72. package/docs/api/classes/RBACNotInitializedError.md +2 -2
  73. package/docs/api/classes/SecureSupabaseClient.md +25 -20
  74. package/docs/api/classes/StorageUtils.md +7 -4
  75. package/docs/api/enums/FileCategory.md +1 -1
  76. package/docs/api/enums/LogLevel.md +1 -1
  77. package/docs/api/enums/RBACErrorCode.md +1 -1
  78. package/docs/api/enums/RPCFunction.md +1 -1
  79. package/docs/api/interfaces/AddressFieldProps.md +1 -1
  80. package/docs/api/interfaces/AddressFieldRef.md +1 -1
  81. package/docs/api/interfaces/AggregateConfig.md +1 -1
  82. package/docs/api/interfaces/AutocompleteOptions.md +1 -1
  83. package/docs/api/interfaces/AvatarProps.md +1 -1
  84. package/docs/api/interfaces/BadgeProps.md +1 -1
  85. package/docs/api/interfaces/ButtonProps.md +1 -1
  86. package/docs/api/interfaces/CalendarProps.md +20 -6
  87. package/docs/api/interfaces/CardProps.md +1 -1
  88. package/docs/api/interfaces/ColorPalette.md +1 -1
  89. package/docs/api/interfaces/ColorShade.md +1 -1
  90. package/docs/api/interfaces/ComplianceResult.md +1 -1
  91. package/docs/api/interfaces/DataAccessRecord.md +9 -9
  92. package/docs/api/interfaces/DataRecord.md +1 -1
  93. package/docs/api/interfaces/DataTableAction.md +1 -1
  94. package/docs/api/interfaces/DataTableColumn.md +1 -1
  95. package/docs/api/interfaces/DataTableProps.md +1 -1
  96. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  97. package/docs/api/interfaces/DatabaseComplianceResult.md +1 -1
  98. package/docs/api/interfaces/DatabaseIssue.md +1 -1
  99. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  100. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  101. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  102. package/docs/api/interfaces/ExportColumn.md +1 -1
  103. package/docs/api/interfaces/ExportOptions.md +1 -1
  104. package/docs/api/interfaces/FileDisplayProps.md +62 -16
  105. package/docs/api/interfaces/FileMetadata.md +1 -1
  106. package/docs/api/interfaces/FileReference.md +2 -2
  107. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  108. package/docs/api/interfaces/FileUploadOptions.md +26 -12
  109. package/docs/api/interfaces/FileUploadProps.md +30 -19
  110. package/docs/api/interfaces/FooterProps.md +1 -1
  111. package/docs/api/interfaces/FormFieldProps.md +1 -1
  112. package/docs/api/interfaces/FormProps.md +1 -1
  113. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  114. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  115. package/docs/api/interfaces/InputProps.md +1 -1
  116. package/docs/api/interfaces/LabelProps.md +1 -1
  117. package/docs/api/interfaces/LoggerConfig.md +1 -1
  118. package/docs/api/interfaces/LoginFormProps.md +1 -1
  119. package/docs/api/interfaces/NavigationAccessRecord.md +10 -10
  120. package/docs/api/interfaces/NavigationContextType.md +9 -9
  121. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  122. package/docs/api/interfaces/NavigationItem.md +1 -1
  123. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  124. package/docs/api/interfaces/NavigationProviderProps.md +7 -7
  125. package/docs/api/interfaces/Organisation.md +1 -1
  126. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  127. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  128. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  129. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  130. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  131. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  132. package/docs/api/interfaces/PageAccessRecord.md +8 -8
  133. package/docs/api/interfaces/PagePermissionContextType.md +8 -8
  134. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  135. package/docs/api/interfaces/PagePermissionProviderProps.md +7 -7
  136. package/docs/api/interfaces/PaletteData.md +1 -1
  137. package/docs/api/interfaces/ParsedAddress.md +2 -2
  138. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  139. package/docs/api/interfaces/ProgressProps.md +3 -11
  140. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  141. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  142. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  143. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  144. package/docs/api/interfaces/QuickFix.md +1 -1
  145. package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
  146. package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
  147. package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
  148. package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
  149. package/docs/api/interfaces/RBACConfig.md +2 -2
  150. package/docs/api/interfaces/RBACContext.md +1 -1
  151. package/docs/api/interfaces/RBACLogger.md +1 -1
  152. package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
  153. package/docs/api/interfaces/RBACPerformanceMetrics.md +1 -1
  154. package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
  155. package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
  156. package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
  157. package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
  158. package/docs/api/interfaces/RBACResult.md +1 -1
  159. package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
  160. package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
  161. package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
  162. package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
  163. package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
  164. package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
  165. package/docs/api/interfaces/RBACRolesListParams.md +1 -1
  166. package/docs/api/interfaces/RBACRolesListResult.md +1 -1
  167. package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
  168. package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
  169. package/docs/api/interfaces/ResourcePermissions.md +1 -1
  170. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  171. package/docs/api/interfaces/RoleBasedRouterContextType.md +8 -8
  172. package/docs/api/interfaces/RoleBasedRouterProps.md +10 -10
  173. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  174. package/docs/api/interfaces/RouteAccessRecord.md +10 -10
  175. package/docs/api/interfaces/RouteConfig.md +10 -10
  176. package/docs/api/interfaces/RuntimeComplianceResult.md +1 -1
  177. package/docs/api/interfaces/SecureDataContextType.md +9 -9
  178. package/docs/api/interfaces/SecureDataProviderProps.md +8 -8
  179. package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
  180. package/docs/api/interfaces/SetupIssue.md +1 -1
  181. package/docs/api/interfaces/StorageConfig.md +4 -4
  182. package/docs/api/interfaces/StorageFileInfo.md +7 -7
  183. package/docs/api/interfaces/StorageFileMetadata.md +25 -14
  184. package/docs/api/interfaces/StorageListOptions.md +22 -9
  185. package/docs/api/interfaces/StorageListResult.md +4 -4
  186. package/docs/api/interfaces/StorageUploadOptions.md +21 -8
  187. package/docs/api/interfaces/StorageUploadResult.md +6 -6
  188. package/docs/api/interfaces/StorageUrlOptions.md +19 -6
  189. package/docs/api/interfaces/StyleImport.md +1 -1
  190. package/docs/api/interfaces/SwitchProps.md +1 -1
  191. package/docs/api/interfaces/TabsContentProps.md +1 -1
  192. package/docs/api/interfaces/TabsListProps.md +1 -1
  193. package/docs/api/interfaces/TabsProps.md +1 -1
  194. package/docs/api/interfaces/TabsTriggerProps.md +1 -1
  195. package/docs/api/interfaces/TextareaProps.md +1 -1
  196. package/docs/api/interfaces/ToastActionElement.md +1 -1
  197. package/docs/api/interfaces/ToastProps.md +1 -1
  198. package/docs/api/interfaces/UnifiedAuthContextType.md +53 -53
  199. package/docs/api/interfaces/UnifiedAuthProviderProps.md +13 -13
  200. package/docs/api/interfaces/UseFormDialogOptions.md +1 -1
  201. package/docs/api/interfaces/UseFormDialogReturn.md +1 -1
  202. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  203. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  204. package/docs/api/interfaces/UsePublicEventLogoOptions.md +2 -2
  205. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  206. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  207. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  208. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +2 -2
  209. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  210. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  211. package/docs/api/interfaces/UseResolvedScopeOptions.md +5 -5
  212. package/docs/api/interfaces/UseResolvedScopeReturn.md +4 -4
  213. package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
  214. package/docs/api/interfaces/UserEventAccess.md +11 -11
  215. package/docs/api/interfaces/UserMenuProps.md +1 -1
  216. package/docs/api/interfaces/UserProfile.md +1 -1
  217. package/docs/api/modules.md +165 -106
  218. package/docs/api-reference/components.md +15 -7
  219. package/docs/api-reference/providers.md +2 -2
  220. package/docs/api-reference/rpc-functions.md +1 -0
  221. package/docs/best-practices/README.md +1 -1
  222. package/docs/best-practices/deployment.md +8 -8
  223. package/docs/getting-started/examples/README.md +2 -2
  224. package/docs/getting-started/installation-guide.md +4 -4
  225. package/docs/getting-started/quick-start.md +3 -3
  226. package/docs/migration/MIGRATION_GUIDE.md +3 -3
  227. package/docs/migration/README.md +18 -0
  228. package/docs/migration/database-changes-december-2025.md +767 -0
  229. package/docs/migration/person-scoped-profiles-migration-guide.md +472 -0
  230. package/docs/rbac/compliance/compliance-guide.md +2 -2
  231. package/docs/rbac/event-based-apps.md +2 -2
  232. package/docs/rbac/getting-started.md +2 -2
  233. package/docs/rbac/quick-start.md +2 -2
  234. package/docs/security/README.md +4 -4
  235. package/docs/standards/07-rbac-and-rls-standard.md +430 -7
  236. package/docs/troubleshooting/README.md +2 -2
  237. package/docs/troubleshooting/migration.md +3 -3
  238. package/package.json +1 -3
  239. package/scripts/check-pace-core-compliance.cjs +1 -1
  240. package/scripts/check-pace-core-compliance.js +1 -1
  241. package/src/__tests__/fixtures/supabase.ts +301 -0
  242. package/src/__tests__/public-recipe-view.test.ts +19 -19
  243. package/src/__tests__/rls-policies.test.ts +210 -74
  244. package/src/components/AddressField/AddressField.test.tsx +42 -0
  245. package/src/components/AddressField/AddressField.tsx +71 -60
  246. package/src/components/AddressField/README.md +7 -6
  247. package/src/components/Alert/Alert.test.tsx +50 -10
  248. package/src/components/Alert/Alert.tsx +5 -3
  249. package/src/components/Avatar/Avatar.test.tsx +95 -43
  250. package/src/components/Avatar/Avatar.tsx +16 -16
  251. package/src/components/Button/Button.test.tsx +2 -1
  252. package/src/components/Button/Button.tsx +3 -3
  253. package/src/components/Calendar/Calendar.test.tsx +53 -37
  254. package/src/components/Calendar/Calendar.tsx +409 -82
  255. package/src/components/Card/Card.test.tsx +7 -4
  256. package/src/components/Card/Card.tsx +3 -6
  257. package/src/components/Checkbox/Checkbox.tsx +2 -2
  258. package/src/components/DataTable/components/ActionButtons.tsx +5 -5
  259. package/src/components/DataTable/components/BulkOperationsDropdown.tsx +2 -2
  260. package/src/components/DataTable/components/ColumnFilter.tsx +1 -1
  261. package/src/components/DataTable/components/ColumnVisibilityDropdown.tsx +3 -3
  262. package/src/components/DataTable/components/DataTableBody.tsx +12 -12
  263. package/src/components/DataTable/components/DataTableCore.tsx +3 -3
  264. package/src/components/DataTable/components/DataTableToolbar.tsx +5 -5
  265. package/src/components/DataTable/components/DraggableColumnHeader.tsx +3 -3
  266. package/src/components/DataTable/components/EditableRow.tsx +2 -2
  267. package/src/components/DataTable/components/EmptyState.tsx +3 -3
  268. package/src/components/DataTable/components/GroupHeader.tsx +2 -2
  269. package/src/components/DataTable/components/GroupingDropdown.tsx +1 -1
  270. package/src/components/DataTable/components/ImportModal.tsx +4 -4
  271. package/src/components/DataTable/components/LoadingState.tsx +1 -1
  272. package/src/components/DataTable/components/PaginationControls.tsx +11 -11
  273. package/src/components/DataTable/components/UnifiedTableBody.tsx +9 -9
  274. package/src/components/DataTable/components/ViewRowModal.tsx +2 -2
  275. package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +11 -37
  276. package/src/components/DataTable/components/__tests__/DataTableToolbar.test.tsx +157 -0
  277. package/src/components/DataTable/components/__tests__/LoadingState.test.tsx +2 -1
  278. package/src/components/DataTable/components/__tests__/VirtualizedDataTable.test.tsx +128 -0
  279. package/src/components/DataTable/core/__tests__/ActionManager.test.ts +19 -0
  280. package/src/components/DataTable/core/__tests__/ColumnFactory.test.ts +51 -0
  281. package/src/components/DataTable/core/__tests__/ColumnManager.test.ts +84 -0
  282. package/src/components/DataTable/core/__tests__/DataManager.test.ts +14 -0
  283. package/src/components/DataTable/core/__tests__/DataTableContext.test.tsx +136 -0
  284. package/src/components/DataTable/core/__tests__/LocalDataAdapter.test.ts +16 -0
  285. package/src/components/DataTable/core/__tests__/PluginRegistry.test.ts +18 -0
  286. package/src/components/DataTable/hooks/useDataTablePermissions.ts +28 -7
  287. package/src/components/DataTable/utils/__tests__/hierarchicalUtils.test.ts +30 -1
  288. package/src/components/DataTable/utils/hierarchicalUtils.ts +38 -10
  289. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.test.tsx +8 -3
  290. package/src/components/DatePickerWithTimezone/DatePickerWithTimezone.tsx +4 -4
  291. package/src/components/Dialog/Dialog.tsx +2 -2
  292. package/src/components/EventSelector/EventSelector.tsx +7 -7
  293. package/src/components/FileDisplay/FileDisplay.tsx +291 -179
  294. package/src/components/FileUpload/FileUpload.tsx +7 -4
  295. package/src/components/Header/Header.test.tsx +28 -0
  296. package/src/components/Header/Header.tsx +22 -9
  297. package/src/components/InactivityWarningModal/InactivityWarningModal.tsx +2 -2
  298. package/src/components/LoadingSpinner/LoadingSpinner.test.tsx +19 -14
  299. package/src/components/LoadingSpinner/LoadingSpinner.tsx +5 -5
  300. package/src/components/NavigationMenu/NavigationMenu.test.tsx +127 -1
  301. package/src/components/OrganisationSelector/OrganisationSelector.tsx +42 -22
  302. package/src/components/PaceAppLayout/PaceAppLayout.integration.test.tsx +4 -0
  303. package/src/components/PaceAppLayout/PaceAppLayout.performance.test.tsx +3 -0
  304. package/src/components/PaceAppLayout/PaceAppLayout.security.test.tsx +3 -0
  305. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +16 -6
  306. package/src/components/PaceAppLayout/PaceAppLayout.tsx +37 -3
  307. package/src/components/PaceAppLayout/test-setup.tsx +1 -0
  308. package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +66 -45
  309. package/src/components/PaceLoginPage/PaceLoginPage.tsx +6 -4
  310. package/src/components/Progress/Progress.test.tsx +18 -19
  311. package/src/components/Progress/Progress.tsx +31 -32
  312. package/src/components/PublicLayout/PublicLayout.test.tsx +6 -6
  313. package/src/components/PublicLayout/PublicPageProvider.tsx +5 -3
  314. package/src/components/Select/Select.test.tsx +4 -1
  315. package/src/components/Select/Select.tsx +65 -20
  316. package/src/components/Switch/Switch.test.tsx +2 -1
  317. package/src/components/Switch/Switch.tsx +1 -1
  318. package/src/components/Toast/Toast.tsx +1 -1
  319. package/src/components/Tooltip/Tooltip.test.tsx +8 -2
  320. package/src/components/UserMenu/UserMenu.tsx +3 -3
  321. package/src/eslint-rules/pace-core-compliance.cjs +0 -2
  322. package/src/eslint-rules/pace-core-compliance.js +0 -2
  323. package/src/hooks/__tests__/hooks.integration.test.tsx +4 -1
  324. package/src/hooks/__tests__/useAppConfig.unit.test.ts +76 -5
  325. package/src/hooks/__tests__/useDataTableState.test.ts +76 -0
  326. package/src/hooks/__tests__/useFileUrl.unit.test.ts +25 -69
  327. package/src/hooks/__tests__/useFileUrlCache.test.ts +129 -0
  328. package/src/hooks/__tests__/usePreventTabReload.test.ts +88 -0
  329. package/src/hooks/__tests__/usePublicEvent.simple.test.ts +1 -1
  330. package/src/hooks/__tests__/usePublicEvent.test.ts +608 -0
  331. package/src/hooks/__tests__/useQueryCache.test.ts +144 -0
  332. package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +67 -24
  333. package/src/hooks/index.ts +1 -1
  334. package/src/hooks/public/usePublicEvent.ts +10 -10
  335. package/src/hooks/public/usePublicFileDisplay.ts +173 -87
  336. package/src/hooks/useAppConfig.ts +24 -5
  337. package/src/hooks/useFileDisplay.ts +298 -36
  338. package/src/hooks/useFileReference.ts +56 -11
  339. package/src/hooks/useFileUrl.ts +1 -1
  340. package/src/hooks/useInactivityTracker.ts +16 -7
  341. package/src/hooks/usePermissionCache.test.ts +85 -8
  342. package/src/hooks/useQueryCache.ts +27 -6
  343. package/src/hooks/useSecureDataAccess.test.ts +87 -42
  344. package/src/hooks/useSecureDataAccess.ts +95 -48
  345. package/src/providers/__tests__/OrganisationProvider.test.tsx +27 -21
  346. package/src/providers/services/EventServiceProvider.tsx +37 -17
  347. package/src/providers/services/InactivityServiceProvider.tsx +4 -4
  348. package/src/providers/services/OrganisationServiceProvider.tsx +8 -1
  349. package/src/providers/services/UnifiedAuthProvider.tsx +115 -29
  350. package/src/rbac/__tests__/auth-rbac.e2e.test.tsx +451 -0
  351. package/src/rbac/__tests__/engine.comprehensive.test.ts +12 -0
  352. package/src/rbac/__tests__/rbac-engine-core-logic.test.ts +8 -0
  353. package/src/rbac/__tests__/rbac-engine-simplified.test.ts +4 -0
  354. package/src/rbac/api.ts +240 -36
  355. package/src/rbac/cache-invalidation.ts +21 -7
  356. package/src/rbac/compliance/quick-fix-suggestions.ts +1 -1
  357. package/src/rbac/components/NavigationGuard.tsx +23 -63
  358. package/src/rbac/components/NavigationProvider.test.tsx +52 -23
  359. package/src/rbac/components/NavigationProvider.tsx +13 -11
  360. package/src/rbac/components/PagePermissionGuard.tsx +77 -203
  361. package/src/rbac/components/PagePermissionProvider.tsx +13 -11
  362. package/src/rbac/components/PermissionEnforcer.tsx +24 -62
  363. package/src/rbac/components/RoleBasedRouter.tsx +14 -12
  364. package/src/rbac/components/SecureDataProvider.tsx +13 -11
  365. package/src/rbac/components/__tests__/NavigationGuard.test.tsx +104 -41
  366. package/src/rbac/components/__tests__/NavigationProvider.test.tsx +49 -12
  367. package/src/rbac/components/__tests__/PagePermissionGuard.race-condition.test.tsx +22 -1
  368. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +161 -82
  369. package/src/rbac/components/__tests__/PagePermissionGuard.verification.test.tsx +22 -1
  370. package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +77 -30
  371. package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +39 -5
  372. package/src/rbac/components/__tests__/SecureDataProvider.test.tsx +47 -4
  373. package/src/rbac/engine.ts +4 -2
  374. package/src/rbac/hooks/__tests__/useSecureSupabase.test.ts +144 -52
  375. package/src/rbac/hooks/index.ts +3 -0
  376. package/src/rbac/hooks/useCan.test.ts +101 -53
  377. package/src/rbac/hooks/usePermissions.ts +108 -41
  378. package/src/rbac/hooks/useRBAC.test.ts +11 -3
  379. package/src/rbac/hooks/useRBAC.ts +83 -40
  380. package/src/rbac/hooks/useResolvedScope.test.ts +189 -63
  381. package/src/rbac/hooks/useResolvedScope.ts +128 -70
  382. package/src/rbac/hooks/useSecureSupabase.ts +36 -19
  383. package/src/rbac/hooks/useSuperAdminBypass.ts +126 -0
  384. package/src/rbac/request-deduplication.ts +1 -1
  385. package/src/rbac/secureClient.ts +72 -12
  386. package/src/rbac/security.ts +29 -23
  387. package/src/rbac/types.ts +10 -0
  388. package/src/rbac/utils/__tests__/contextValidator.test.ts +150 -0
  389. package/src/rbac/utils/__tests__/deep-equal.test.ts +53 -0
  390. package/src/rbac/utils/__tests__/eventContext.test.ts +8 -3
  391. package/src/rbac/utils/__tests__/eventContext.unit.test.ts +74 -12
  392. package/src/rbac/utils/contextValidator.ts +288 -0
  393. package/src/rbac/utils/eventContext.ts +52 -3
  394. package/src/services/AuthService.ts +37 -8
  395. package/src/services/EventService.ts +165 -21
  396. package/src/services/OrganisationService.ts +125 -137
  397. package/src/services/__tests__/EventService.test.ts +26 -21
  398. package/src/services/__tests__/OrganisationService.pagination.test.ts +34 -8
  399. package/src/services/__tests__/OrganisationService.test.ts +218 -86
  400. package/src/types/database.generated.ts +166 -201
  401. package/src/types/file-reference.ts +13 -10
  402. package/src/types/supabase.ts +2 -2
  403. package/src/utils/__tests__/secureDataAccess.unit.test.ts +3 -2
  404. package/src/utils/app/appNameResolver.test.ts +346 -73
  405. package/src/utils/context/superAdminOverride.ts +58 -0
  406. package/src/utils/file-reference/index.ts +65 -37
  407. package/src/utils/google-places/googlePlacesUtils.test.ts +98 -0
  408. package/src/utils/google-places/googlePlacesUtils.ts +1 -1
  409. package/src/utils/google-places/loadGoogleMapsScript.test.ts +83 -0
  410. package/src/utils/google-places/types.ts +1 -1
  411. package/src/utils/request-deduplication.ts +4 -4
  412. package/src/utils/security/secureDataAccess.test.ts +1 -1
  413. package/src/utils/security/secureDataAccess.ts +7 -4
  414. package/src/utils/storage/README.md +1 -1
  415. package/src/utils/storage/helpers.test.ts +1 -1
  416. package/src/utils/storage/helpers.ts +38 -19
  417. package/src/utils/storage/types.ts +15 -8
  418. package/src/utils/validation/__tests__/csrf.test.ts +105 -0
  419. package/src/utils/validation/__tests__/sqlInjectionProtection.test.ts +92 -0
  420. package/src/vite-env.d.ts +2 -2
  421. package/dist/chunk-3GOZZZYH.js.map +0 -1
  422. package/dist/chunk-DDM4CCYT.js.map +0 -1
  423. package/dist/chunk-E7UAOUMY.js +0 -75
  424. package/dist/chunk-E7UAOUMY.js.map +0 -1
  425. package/dist/chunk-F2IMUDXZ.js.map +0 -1
  426. package/dist/chunk-HEHYGYOX.js.map +0 -1
  427. package/dist/chunk-IM4QE42D.js.map +0 -1
  428. package/dist/chunk-MX64ZF6I.js.map +0 -1
  429. package/dist/chunk-SAUPYVLF.js.map +0 -1
  430. package/dist/chunk-THRPYOFK.js.map +0 -1
  431. package/dist/chunk-UCQSRW7Z.js.map +0 -1
  432. package/dist/chunk-VGZZXKBR.js.map +0 -1
  433. package/dist/chunk-YGPFYGA6.js.map +0 -1
  434. package/dist/chunk-YHCN776L.js.map +0 -1
  435. /package/dist/{DataTable-GUFUNZ3N.js.map → DataTable-WKRZD47S.js.map} +0 -0
  436. /package/dist/{UnifiedAuthProvider-643PUAIM.js.map → UnifiedAuthProvider-FTSG5XH7.js.map} +0 -0
  437. /package/dist/{api-YP7XD5L6.js.map → api-IHKALJZD.js.map} +0 -0
  438. /package/dist/{chunk-HESYZWZW.js.map → chunk-QWWZ5CAQ.js.map} +0 -0
@@ -4,7 +4,7 @@
4
4
  * @module Hooks
5
5
  *
6
6
  * A React hook for accessing file references in authenticated contexts.
7
- * Can handle both public and private files using the file_references system.
7
+ * Can handle both public and private files using the core_file_references system.
8
8
  *
9
9
  * Features:
10
10
  * - Works in authenticated contexts
@@ -20,7 +20,7 @@
20
20
  *
21
21
  * function FileView() {
22
22
  * const { fileUrl, fileReference, isLoading, error } = useFileDisplay(
23
- * 'event',
23
+ * 'core_events',
24
24
  * eventId,
25
25
  * organisationId,
26
26
  * FileCategory.EVENT_LOGOS
@@ -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,283 @@ 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('core_organisation_memberships')
251
+ .select('organisation_id')
252
+ .eq('user_id', user.id);
253
+
254
+ if (membershipError) {
255
+ logger.warn('useFileDisplay', 'Error querying organisation memberships:', membershipError);
256
+ }
257
+
258
+ if (memberships && memberships.length > 0) {
259
+ // Try each organisation the user belongs to
260
+ const orgIds = memberships.map(m => m.organisation_id).filter(Boolean) as string[];
261
+
262
+ // Query each organisation in parallel
263
+ const orgQueries = orgIds.map(async (orgId) => {
264
+ try {
265
+ if (category) {
266
+ return await service.getFilesByCategory(
267
+ table_name,
268
+ record_id,
269
+ category,
270
+ orgId
271
+ );
272
+ } else {
273
+ return await service.listFileReferences(
274
+ table_name,
275
+ record_id,
276
+ orgId
277
+ );
278
+ }
279
+ } catch (err) {
280
+ // Silently fail for individual org queries - user might not have access
281
+ return [];
282
+ }
283
+ });
284
+
285
+ const orgResults = await Promise.all(orgQueries);
286
+ orgScopedFiles = orgResults.flat();
287
+ } else {
288
+ // When user has no organisation memberships, try querying files with any organisation_id
289
+ // as a fallback - the file might be organisation-scoped but accessible via RLS
290
+ // This handles edge cases where RLS allows access but we can't enumerate organisations
291
+ try {
292
+ // Try querying without organisation_id filter - let RLS handle security
293
+ let fallbackQuery = supabase
294
+ .from('core_file_references')
295
+ .select('id, table_name, record_id, file_path, file_metadata, organisation_id, app_id, is_public, created_at, updated_at')
296
+ .eq('table_name', table_name)
297
+ .eq('record_id', record_id)
298
+ .order('created_at', { ascending: false });
299
+
300
+ if (category) {
301
+ fallbackQuery = fallbackQuery.eq('file_metadata->>category', category);
302
+ }
303
+
304
+ const { data: fallbackFiles } = await fallbackQuery;
305
+
306
+ if (fallbackFiles && fallbackFiles.length > 0) {
307
+ // Convert to FileReference format
308
+ orgScopedFiles = fallbackFiles.map((f: any) => ({
309
+ id: f.id,
310
+ table_name: f.table_name,
311
+ record_id: f.record_id,
312
+ file_path: f.file_path,
313
+ file_metadata: f.file_metadata || {},
314
+ organisation_id: f.organisation_id,
315
+ app_id: f.app_id,
316
+ is_public: f.is_public ?? false,
317
+ created_at: f.created_at,
318
+ updated_at: f.updated_at
319
+ })) as FileReference[];
320
+ }
321
+ } catch (err) {
322
+ // Silently fail - RLS may block or other error
323
+ }
324
+ }
325
+ }
326
+ } catch (err) {
327
+ logger.warn('useFileDisplay', 'Error querying organisation-scoped files:', err);
328
+ orgScopedFiles = [];
329
+ }
330
+
331
+ // Merge results: prefer organisation-scoped files if both exist, otherwise use user-scoped
332
+ // Sort by created_at DESC to get most recent first
333
+ const allFiles = [...userScopedFiles, ...orgScopedFiles];
334
+ allFiles.sort((a, b) => {
335
+ const aTime = new Date(a.created_at).getTime();
336
+ const bTime = new Date(b.created_at).getTime();
337
+ return bTime - aTime;
214
338
  });
215
- files = await service.getFilesByCategory(
216
- table_name,
217
- record_id,
218
- category,
219
- organisation_id
220
- );
339
+
340
+ // If we have both types, prefer organisation-scoped (non-null organisation_id)
341
+ // Otherwise, use whatever we found
342
+ if (orgScopedFiles.length > 0 && userScopedFiles.length > 0) {
343
+ // Prefer organisation-scoped files - filter to only those with organisation_id
344
+ files = allFiles.filter(f => f.organisation_id !== null);
345
+ } else {
346
+ // Use all files found (either user-scoped or org-scoped, but not both)
347
+ files = allFiles;
348
+ }
349
+
350
+ // If no files found through RPC, try a direct query as fallback
351
+ // This handles cases where RLS policy allows access but RPC security check is too strict
352
+ // (e.g., core_person files where user owns the person record but record_id != user_id)
353
+ if (files.length === 0) {
354
+ try {
355
+ let directQuery = supabase
356
+ .from('core_file_references')
357
+ .select('id, table_name, record_id, file_path, file_metadata, organisation_id, app_id, is_public, created_at, updated_at')
358
+ .eq('table_name', table_name)
359
+ .eq('record_id', record_id)
360
+ .order('created_at', { ascending: false });
361
+
362
+ // If category is provided, filter by category in metadata
363
+ if (category) {
364
+ directQuery = directQuery.eq('file_metadata->>category', category);
365
+ }
366
+
367
+ const { data: directFiles } = await directQuery;
368
+
369
+ if (directFiles && directFiles.length > 0) {
370
+ // Convert to FileReference format
371
+ files = directFiles.map((f: any) => ({
372
+ id: f.id,
373
+ table_name: f.table_name,
374
+ record_id: f.record_id,
375
+ file_path: f.file_path,
376
+ file_metadata: f.file_metadata || {},
377
+ organisation_id: f.organisation_id,
378
+ app_id: f.app_id,
379
+ is_public: f.is_public ?? false,
380
+ created_at: f.created_at,
381
+ updated_at: f.updated_at
382
+ })) as FileReference[];
383
+ }
384
+ } catch (err) {
385
+ // Silently fail - RLS may block or other error
386
+ }
387
+ }
221
388
  } 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
- );
389
+ // organisation_id is provided (or explicitly null) - use normal query
390
+ // CRITICAL: When category is provided, MUST use RPC function, not direct queries
391
+ // Category is stored in file_metadata JSONB field, not a direct column
392
+ if (category) {
393
+ // Single file mode - get files by category using RPC
394
+ logger.debug('useFileDisplay', 'Using RPC function for category filtering:', {
395
+ table_name,
396
+ record_id,
397
+ category,
398
+ organisation_id
399
+ });
400
+ files = await service.getFilesByCategory(
401
+ table_name,
402
+ record_id,
403
+ category,
404
+ organisation_id
405
+ );
406
+ } else {
407
+ // Multiple files mode - get all files using RPC
408
+ files = await service.listFileReferences(
409
+ table_name,
410
+ record_id,
411
+ organisation_id
412
+ );
413
+ }
414
+
415
+ // Fallback: If no files found and organisation_id is falsy (undefined, null, empty string),
416
+ // try searching both scopes as a fallback
417
+ // This handles cases where the prop might be passed as empty string or the check above didn't catch it
418
+ if (files.length === 0 && (!organisation_id || organisation_id === '')) {
419
+ // Try the dual-scope search logic
420
+ let userScopedFiles: FileReference[] = [];
421
+ let orgScopedFiles: FileReference[] = [];
422
+
423
+ try {
424
+ if (category) {
425
+ userScopedFiles = await service.getFilesByCategory(
426
+ table_name,
427
+ record_id,
428
+ category,
429
+ undefined
430
+ );
431
+ } else {
432
+ userScopedFiles = await service.listFileReferences(
433
+ table_name,
434
+ record_id,
435
+ undefined
436
+ );
437
+ }
438
+ } catch (err) {
439
+ // Silently fail
440
+ }
441
+
442
+ try {
443
+ const { data: { user } } = await supabase.auth.getUser();
444
+ if (user) {
445
+ const { data: memberships } = await supabase
446
+ .from('core_organisation_memberships')
447
+ .select('organisation_id')
448
+ .eq('user_id', user.id)
449
+ .or('status.is.null,status.eq.active');
450
+
451
+ if (memberships && memberships.length > 0) {
452
+ const orgIds = memberships.map(m => m.organisation_id).filter(Boolean) as string[];
453
+ const orgQueries = orgIds.map(async (orgId) => {
454
+ try {
455
+ if (category) {
456
+ return await service.getFilesByCategory(table_name, record_id, category, orgId);
457
+ } else {
458
+ return await service.listFileReferences(table_name, record_id, orgId);
459
+ }
460
+ } catch (err) {
461
+ return [];
462
+ }
463
+ });
464
+ const orgResults = await Promise.all(orgQueries);
465
+ orgScopedFiles = orgResults.flat();
466
+ }
467
+ }
468
+ } catch (err) {
469
+ // Silently fail
470
+ }
471
+
472
+ // Merge results
473
+ const allFiles = [...userScopedFiles, ...orgScopedFiles];
474
+ allFiles.sort((a, b) => {
475
+ const aTime = new Date(a.created_at).getTime();
476
+ const bTime = new Date(b.created_at).getTime();
477
+ return bTime - aTime;
478
+ });
479
+
480
+ if (orgScopedFiles.length > 0 && userScopedFiles.length > 0) {
481
+ files = allFiles.filter(f => f.organisation_id !== null);
482
+ } else {
483
+ files = allFiles;
484
+ }
485
+ }
228
486
  }
229
487
 
230
488
  if (files.length === 0) {
@@ -271,6 +529,7 @@ export function useFileDisplay(
271
529
  const signedUrlResult = await getSignedUrl(supabase, firstFile.file_path, {
272
530
  appName: 'pace-core',
273
531
  orgId: organisation_id,
532
+ userId: organisation_id ? undefined : record_id,
274
533
  expiresIn: 3600
275
534
  });
276
535
  url = signedUrlResult?.url || null;
@@ -283,6 +542,7 @@ export function useFileDisplay(
283
542
  const urlMap = await generateFileUrlsBatch(supabase, files, {
284
543
  appName: 'pace-core',
285
544
  orgId: organisation_id,
545
+ userId: organisation_id ? undefined : record_id,
286
546
  expiresIn: 3600
287
547
  });
288
548
  setFileUrls(urlMap);
@@ -344,7 +604,7 @@ export function useFileDisplay(
344
604
 
345
605
  // Fetch files when parameters change
346
606
  useEffect(() => {
347
- if (table_name && record_id && organisation_id && supabase) {
607
+ if (table_name && record_id && supabase) {
348
608
  fetchFiles();
349
609
  } else {
350
610
  setFileUrl(null);
@@ -355,14 +615,16 @@ export function useFileDisplay(
355
615
  setIsLoading(false);
356
616
  setError(null);
357
617
  }
358
- }, [fetchFiles, table_name, record_id, organisation_id, supabase]);
618
+ // fetchFiles is memoized; we only need to re-run when parameters change
619
+ // eslint-disable-next-line react-hooks/exhaustive-deps
620
+ }, [table_name, record_id, organisation_id, supabase]);
359
621
 
360
622
  const refetch = useCallback(async (): Promise<void> => {
361
- if (!table_name || !record_id || !organisation_id || !supabase) return;
623
+ if (!table_name || !record_id || !supabase) return;
362
624
 
363
625
  // Clear cache for this file
364
626
  if (enableCache) {
365
- const cacheKey = `file_${table_name}_${record_id}_${organisation_id}_${category || 'all'}`;
627
+ const cacheKey = `file_${table_name}_${record_id}_${organisation_id === undefined ? 'undefined' : (organisation_id ?? 'null')}_${category || 'all'}`;
366
628
  authenticatedFileCache.delete(cacheKey);
367
629
  }
368
630
  await fetchFiles();
@@ -415,14 +677,14 @@ export function getFileDisplayCacheStats(): { size: number; keys: string[] } {
415
677
  export function invalidateFileDisplayCache(
416
678
  table_name: string,
417
679
  record_id: string,
418
- organisation_id: string,
680
+ organisation_id: string | null | undefined,
419
681
  category?: FileCategory
420
682
  ): void {
421
- const cacheKey = `file_${table_name}_${record_id}_${organisation_id}_${category || 'all'}`;
683
+ const cacheKey = `file_${table_name}_${record_id}_${organisation_id === undefined ? 'undefined' : (organisation_id ?? 'null')}_${category || 'all'}`;
422
684
  authenticatedFileCache.delete(cacheKey);
423
685
  // Also invalidate 'all' category if specific category invalidated
424
686
  if (category) {
425
- const allCategoryKey = `file_${table_name}_${record_id}_${organisation_id}_all`;
687
+ const allCategoryKey = `file_${table_name}_${record_id}_${organisation_id === undefined ? 'undefined' : (organisation_id ?? 'null')}_all`;
426
688
  authenticatedFileCache.delete(allCategoryKey);
427
689
  }
428
690
  }
@@ -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(() => {