@jmruthers/pace-core 0.5.136 → 0.5.139

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 (292) hide show
  1. package/dist/{DataTable-CYOHOX3O.js → DataTable-JXFCA2BJ.js} +10 -9
  2. package/dist/{EventLogo-801uofbR.d.ts → EventLogo-rFL_kRjk.d.ts} +73 -1
  3. package/dist/{UnifiedAuthProvider-5E5TUNMS.js → UnifiedAuthProvider-XIQQ7LVU.js} +4 -5
  4. package/dist/{chunk-YLKIDTUK.js → chunk-22WKWKRX.js} +4 -4
  5. package/dist/{chunk-TVYPTYOY.js → chunk-4C7EXCAR.js} +60 -24
  6. package/dist/chunk-4C7EXCAR.js.map +1 -0
  7. package/dist/{chunk-NOHEVYVX.js → chunk-5JMOHWDI.js} +417 -319
  8. package/dist/chunk-5JMOHWDI.js.map +1 -0
  9. package/dist/{chunk-FHWWBIHA.js → chunk-6DXZ6V5Q.js} +5 -5
  10. package/dist/{chunk-2TWNJ46Y.js → chunk-6LAAY47Q.js} +2 -2
  11. package/dist/{chunk-444EZN6N.js → chunk-7QCC6MCP.js} +88 -1
  12. package/dist/chunk-7QCC6MCP.js.map +1 -0
  13. package/dist/chunk-BJPBT3CU.js +21 -0
  14. package/dist/chunk-BJPBT3CU.js.map +1 -0
  15. package/dist/{chunk-L6PGMCMD.js → chunk-BOOI7GK2.js} +38 -12
  16. package/dist/chunk-BOOI7GK2.js.map +1 -0
  17. package/dist/{chunk-XARJS7CD.js → chunk-INQLMHPF.js} +2 -2
  18. package/dist/chunk-JISYG63F.js +70 -0
  19. package/dist/chunk-JISYG63F.js.map +1 -0
  20. package/dist/{chunk-SL2YQDR6.js → chunk-MA6EPSGZ.js} +2 -2
  21. package/dist/{chunk-5DPZ5EAT.js → chunk-OWAG3GSU.js} +1 -3
  22. package/dist/{chunk-LTV3XIJJ.js → chunk-T6JN6LH6.js} +4 -4
  23. package/dist/{chunk-HJGGOMQ6.js → chunk-TLT2ZR3L.js} +147 -103
  24. package/dist/chunk-TLT2ZR3L.js.map +1 -0
  25. package/dist/{chunk-4MT5BGGL.js → chunk-YCWDTTUK.js} +4 -6
  26. package/dist/{chunk-4MT5BGGL.js.map → chunk-YCWDTTUK.js.map} +1 -1
  27. package/dist/components.d.ts +1 -1
  28. package/dist/components.js +12 -11
  29. package/dist/components.js.map +1 -1
  30. package/dist/hooks.js +8 -9
  31. package/dist/hooks.js.map +1 -1
  32. package/dist/index.d.ts +2 -2
  33. package/dist/index.js +15 -14
  34. package/dist/index.js.map +1 -1
  35. package/dist/providers.js +3 -4
  36. package/dist/rbac/index.js +8 -9
  37. package/dist/schema-DTDZQe2u.d.ts +28 -0
  38. package/dist/types.d.ts +152 -3
  39. package/dist/types.js +51 -16
  40. package/dist/types.js.map +1 -1
  41. package/dist/utils.d.ts +89 -4
  42. package/dist/utils.js +214 -96
  43. package/dist/utils.js.map +1 -1
  44. package/dist/validation.d.ts +1 -343
  45. package/dist/validation.js +3 -100
  46. package/docs/api/classes/ColumnFactory.md +1 -1
  47. package/docs/api/classes/ErrorBoundary.md +1 -1
  48. package/docs/api/classes/InvalidScopeError.md +1 -1
  49. package/docs/api/classes/MissingUserContextError.md +1 -1
  50. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  51. package/docs/api/classes/PermissionDeniedError.md +1 -1
  52. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  53. package/docs/api/classes/RBACAuditManager.md +1 -1
  54. package/docs/api/classes/RBACCache.md +1 -1
  55. package/docs/api/classes/RBACEngine.md +1 -1
  56. package/docs/api/classes/RBACError.md +1 -1
  57. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  58. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  59. package/docs/api/classes/StorageUtils.md +1 -1
  60. package/docs/api/enums/FileCategory.md +1 -1
  61. package/docs/api/interfaces/AggregateConfig.md +1 -1
  62. package/docs/api/interfaces/BadgeProps.md +27 -0
  63. package/docs/api/interfaces/ButtonProps.md +1 -1
  64. package/docs/api/interfaces/CardProps.md +1 -1
  65. package/docs/api/interfaces/ColorPalette.md +1 -1
  66. package/docs/api/interfaces/ColorShade.md +1 -1
  67. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  68. package/docs/api/interfaces/DataRecord.md +1 -1
  69. package/docs/api/interfaces/DataTableAction.md +1 -1
  70. package/docs/api/interfaces/DataTableColumn.md +1 -1
  71. package/docs/api/interfaces/DataTableProps.md +1 -1
  72. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  73. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  74. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  75. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  76. package/docs/api/interfaces/EventLogoProps.md +1 -1
  77. package/docs/api/interfaces/ExportColumn.md +1 -1
  78. package/docs/api/interfaces/ExportOptions.md +1 -1
  79. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  80. package/docs/api/interfaces/FileMetadata.md +1 -1
  81. package/docs/api/interfaces/FileReference.md +1 -1
  82. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  83. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  84. package/docs/api/interfaces/FileUploadProps.md +1 -1
  85. package/docs/api/interfaces/FooterProps.md +1 -1
  86. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  87. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  88. package/docs/api/interfaces/InputProps.md +1 -1
  89. package/docs/api/interfaces/LabelProps.md +1 -1
  90. package/docs/api/interfaces/LoginFormProps.md +1 -1
  91. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  92. package/docs/api/interfaces/NavigationContextType.md +1 -1
  93. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  94. package/docs/api/interfaces/NavigationItem.md +1 -1
  95. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  96. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  97. package/docs/api/interfaces/Organisation.md +1 -1
  98. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  99. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  100. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  101. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  102. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  103. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  104. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  105. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  106. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  107. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  108. package/docs/api/interfaces/PaletteData.md +1 -1
  109. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  110. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  111. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  112. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  113. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  114. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  115. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  116. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  117. package/docs/api/interfaces/RBACConfig.md +1 -1
  118. package/docs/api/interfaces/RBACLogger.md +1 -1
  119. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  120. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  121. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  122. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  123. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  124. package/docs/api/interfaces/RouteConfig.md +1 -1
  125. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  126. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  127. package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
  128. package/docs/api/interfaces/StorageConfig.md +1 -1
  129. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  130. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  131. package/docs/api/interfaces/StorageListOptions.md +1 -1
  132. package/docs/api/interfaces/StorageListResult.md +1 -1
  133. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  134. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  135. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  136. package/docs/api/interfaces/StyleImport.md +1 -1
  137. package/docs/api/interfaces/SwitchProps.md +1 -1
  138. package/docs/api/interfaces/ToastActionElement.md +1 -1
  139. package/docs/api/interfaces/ToastProps.md +1 -1
  140. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  141. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  142. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  143. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  144. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  145. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  146. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  147. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  148. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  149. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  150. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  151. package/docs/api/interfaces/UserEventAccess.md +1 -1
  152. package/docs/api/interfaces/UserMenuProps.md +1 -1
  153. package/docs/api/interfaces/UserProfile.md +1 -1
  154. package/docs/api/modules.md +84 -15
  155. package/docs/architecture/README.md +0 -1
  156. package/docs/styles/README.md +0 -2
  157. package/examples/RBAC/CompleteRBACExample.tsx +324 -0
  158. package/examples/RBAC/EventBasedApp.tsx +239 -0
  159. package/examples/RBAC/PermissionExample.tsx +151 -0
  160. package/examples/RBAC/index.ts +13 -0
  161. package/examples/public-pages/CorrectPublicPageImplementation.tsx +301 -0
  162. package/examples/public-pages/PublicEventPage.tsx +274 -0
  163. package/examples/public-pages/PublicPageApp.tsx +308 -0
  164. package/examples/public-pages/PublicPageUsageExample.tsx +216 -0
  165. package/examples/public-pages/index.ts +14 -0
  166. package/package.json +1 -10
  167. package/src/__tests__/TEST_STANDARD.md +92 -0
  168. package/src/components/Badge/Badge.test.tsx +314 -0
  169. package/src/components/Badge/Badge.tsx +304 -0
  170. package/src/components/Badge/index.ts +3 -0
  171. package/src/components/DataTable/__tests__/DataTableCore.test-setup.ts +217 -0
  172. package/src/components/DataTable/__tests__/styles.test.ts +1 -1
  173. package/src/components/DataTable/components/ColumnFilter.tsx +8 -4
  174. package/src/components/DataTable/components/DataTableBody.tsx +461 -0
  175. package/src/components/DataTable/components/DraggableColumnHeader.tsx +144 -0
  176. package/src/components/DataTable/components/FilterRow.tsx +9 -3
  177. package/src/components/DataTable/components/PaginationControls.tsx +1 -0
  178. package/src/components/DataTable/components/VirtualizedDataTable.tsx +513 -0
  179. package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +14 -68
  180. package/src/components/DataTable/components/__tests__/ColumnFilter.test.tsx +62 -0
  181. package/src/components/DataTable/components/__tests__/FilterRow.test.tsx +43 -0
  182. package/src/components/DataTable/core/ActionManager.ts +235 -0
  183. package/src/components/DataTable/core/ColumnManager.ts +205 -0
  184. package/src/components/DataTable/core/DataManager.ts +188 -0
  185. package/src/components/DataTable/core/DataTableContext.tsx +181 -0
  186. package/src/components/DataTable/core/LocalDataAdapter.ts +273 -0
  187. package/src/components/DataTable/core/PluginRegistry.ts +229 -0
  188. package/src/components/DataTable/core/StateManager.ts +311 -0
  189. package/src/components/DataTable/core/interfaces.ts +338 -0
  190. package/src/components/DataTable/styles.ts +27 -6
  191. package/src/components/DataTable/utils/__tests__/columnUtils.test.ts +94 -0
  192. package/src/components/DataTable/utils/columnUtils.ts +40 -0
  193. package/src/components/DataTable/utils/debugTools.ts +609 -0
  194. package/src/components/DataTable/utils/index.ts +1 -0
  195. package/src/components/Dialog/README.md +804 -0
  196. package/src/components/Dialog/utils/__tests__/safeHtml.unit.test.ts +611 -0
  197. package/src/components/Dialog/utils/safeHtml.ts +185 -0
  198. package/src/components/Footer/Footer.test.tsx +1 -1
  199. package/src/components/Form/Form.test.tsx +1 -1
  200. package/src/components/Form/FormErrorSummary.tsx +113 -0
  201. package/src/components/Form/FormFieldset.tsx +127 -0
  202. package/src/components/Form/FormLiveRegion.tsx +198 -0
  203. package/src/components/LoginForm/LoginForm.test.tsx +1 -1
  204. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +76 -10
  205. package/src/components/PaceLoginPage/PaceLoginPage.tsx +1 -1
  206. package/src/components/PasswordReset/PasswordResetForm.test.tsx +597 -0
  207. package/src/components/PasswordReset/PasswordResetForm.tsx +201 -0
  208. package/src/components/PublicLayout/PublicPageDebugger.tsx +104 -0
  209. package/src/components/PublicLayout/PublicPageDiagnostic.tsx +162 -0
  210. package/src/components/PublicLayout/__tests__/PublicPageFooter.test.tsx +1 -1
  211. package/src/components/Select/Select.test.tsx +1 -1
  212. package/src/components/Select/Select.tsx +20 -8
  213. package/src/components/Table/__tests__/Table.test.tsx +1 -1
  214. package/src/components/index.ts +3 -0
  215. package/src/hooks/__tests__/useFileUrl.unit.test.ts +83 -85
  216. package/src/index.ts +4 -0
  217. package/src/rbac/hooks/useCan.test.ts +24 -0
  218. package/src/rbac/hooks/usePermissions.ts +49 -12
  219. package/src/styles/core.css +3 -0
  220. package/src/utils/appConfig.ts +47 -0
  221. package/src/utils/appIdResolver.test.ts +499 -0
  222. package/src/utils/appIdResolver.ts +130 -0
  223. package/src/utils/appNameResolver.simple.test.ts +212 -0
  224. package/src/utils/appNameResolver.test.ts +121 -0
  225. package/src/utils/appNameResolver.ts +191 -0
  226. package/src/utils/audit.ts +127 -0
  227. package/src/utils/auth-utils.ts +96 -0
  228. package/src/utils/bundleAnalysis.ts +129 -0
  229. package/src/utils/cn.ts +7 -0
  230. package/src/utils/debugLogger.ts +67 -0
  231. package/src/utils/deviceFingerprint.ts +215 -0
  232. package/src/utils/dynamicUtils.ts +105 -0
  233. package/src/utils/file-reference.test.ts +788 -0
  234. package/src/utils/file-reference.ts +519 -0
  235. package/src/utils/formatDate.test.ts +237 -0
  236. package/src/utils/formatting.ts +133 -0
  237. package/src/utils/index.ts +7 -0
  238. package/src/utils/lazyLoad.tsx +44 -0
  239. package/src/utils/logger.ts +179 -0
  240. package/src/utils/organisationContext.test.ts +322 -0
  241. package/src/utils/organisationContext.ts +153 -0
  242. package/src/utils/performanceBenchmark.ts +64 -0
  243. package/src/utils/performanceBudgets.ts +110 -0
  244. package/src/utils/permissionTypes.ts +37 -0
  245. package/src/utils/permissionUtils.test.ts +393 -0
  246. package/src/utils/permissionUtils.ts +34 -0
  247. package/src/utils/sanitization.ts +264 -0
  248. package/src/utils/schemaUtils.ts +37 -0
  249. package/src/utils/secureDataAccess.test.ts +711 -0
  250. package/src/utils/secureDataAccess.ts +377 -0
  251. package/src/utils/secureErrors.ts +79 -0
  252. package/src/utils/secureStorage.ts +244 -0
  253. package/src/utils/security.ts +156 -0
  254. package/src/utils/securityMonitor.ts +45 -0
  255. package/src/utils/sessionTracking.ts +126 -0
  256. package/src/utils/validation.ts +111 -0
  257. package/src/utils/validationUtils.ts +120 -0
  258. package/src/validation/index.ts +2 -2
  259. package/dist/chunk-444EZN6N.js.map +0 -1
  260. package/dist/chunk-APIBCTL2.js +0 -670
  261. package/dist/chunk-APIBCTL2.js.map +0 -1
  262. package/dist/chunk-HJGGOMQ6.js.map +0 -1
  263. package/dist/chunk-K2WWTH7O.js +0 -94
  264. package/dist/chunk-K2WWTH7O.js.map +0 -1
  265. package/dist/chunk-L6PGMCMD.js.map +0 -1
  266. package/dist/chunk-LMC26NLJ.js +0 -84
  267. package/dist/chunk-LMC26NLJ.js.map +0 -1
  268. package/dist/chunk-NOHEVYVX.js.map +0 -1
  269. package/dist/chunk-TVYPTYOY.js.map +0 -1
  270. package/dist/validation-8npbysjg.d.ts +0 -177
  271. /package/dist/{DataTable-CYOHOX3O.js.map → DataTable-JXFCA2BJ.js.map} +0 -0
  272. /package/dist/{UnifiedAuthProvider-5E5TUNMS.js.map → UnifiedAuthProvider-XIQQ7LVU.js.map} +0 -0
  273. /package/dist/{chunk-YLKIDTUK.js.map → chunk-22WKWKRX.js.map} +0 -0
  274. /package/dist/{chunk-FHWWBIHA.js.map → chunk-6DXZ6V5Q.js.map} +0 -0
  275. /package/dist/{chunk-2TWNJ46Y.js.map → chunk-6LAAY47Q.js.map} +0 -0
  276. /package/dist/{chunk-XARJS7CD.js.map → chunk-INQLMHPF.js.map} +0 -0
  277. /package/dist/{chunk-SL2YQDR6.js.map → chunk-MA6EPSGZ.js.map} +0 -0
  278. /package/dist/{chunk-5DPZ5EAT.js.map → chunk-OWAG3GSU.js.map} +0 -0
  279. /package/dist/{chunk-LTV3XIJJ.js.map → chunk-T6JN6LH6.js.map} +0 -0
  280. /package/examples/{components → components 2}/DataTable/HierarchicalActionsExample.tsx +0 -0
  281. /package/examples/{components → components 2}/DataTable/HierarchicalExample.tsx +0 -0
  282. /package/examples/{components → components 2}/DataTable/InitialPageSizeExample.tsx +0 -0
  283. /package/examples/{components → components 2}/DataTable/PerformanceExample.tsx +0 -0
  284. /package/examples/{components → components 2}/DataTable/index.ts +0 -0
  285. /package/examples/{components → components 2}/Dialog/BasicHtmlTest.tsx +0 -0
  286. /package/examples/{components → components 2}/Dialog/DebugHtmlExample.tsx +0 -0
  287. /package/examples/{components → components 2}/Dialog/HtmlDialogExample.tsx +0 -0
  288. /package/examples/{components → components 2}/Dialog/ScrollableDialogExample.tsx +0 -0
  289. /package/examples/{components → components 2}/Dialog/SimpleHtmlTest.tsx +0 -0
  290. /package/examples/{components → components 2}/Dialog/SmartDialogExample.tsx +0 -0
  291. /package/examples/{components → components 2}/Dialog/index.ts +0 -0
  292. /package/examples/{components → components 2}/index.ts +0 -0
@@ -0,0 +1,377 @@
1
+ /**
2
+ * @file Secure Data Access Utility
3
+ * @package @jmruthers/pace-core
4
+ * @module Utils/SecureDataAccess
5
+ * @since 0.4.0
6
+ *
7
+ * Secure data access utilities that enforce organisation context for all database operations.
8
+ * Prevents data leakage between organisations and ensures proper access validation.
9
+ */
10
+
11
+ import type { SupabaseClient } from '@supabase/supabase-js';
12
+
13
+ // Generic database record type
14
+ export interface DatabaseRecord {
15
+ id: string;
16
+ organisation_id: string;
17
+ [key: string]: unknown;
18
+ }
19
+
20
+ // Generic data for insert/update operations
21
+ export interface DatabaseData {
22
+ [key: string]: unknown;
23
+ }
24
+
25
+ // Generic filters for queries
26
+ export interface DatabaseFilters {
27
+ [key: string]: unknown;
28
+ }
29
+
30
+ // Secure query options
31
+ export interface SecureQueryOptions {
32
+ table: string;
33
+ select: string;
34
+ organisationId: string;
35
+ filters?: DatabaseFilters;
36
+ orderBy?: string;
37
+ limit?: number;
38
+ offset?: number;
39
+ }
40
+
41
+ export interface SecureDataAccess {
42
+ // Secure query methods
43
+ secureQuery: <T extends DatabaseRecord = DatabaseRecord>(options: SecureQueryOptions) => Promise<T[]>;
44
+ secureSingleQuery: <T extends DatabaseRecord = DatabaseRecord>(options: SecureQueryOptions) => Promise<T | null>;
45
+
46
+ // Secure mutation methods
47
+ secureInsert: <T extends DatabaseRecord = DatabaseRecord>(table: string, data: DatabaseData, organisationId: string) => Promise<T | null>;
48
+ secureUpdate: <T extends DatabaseRecord = DatabaseRecord>(table: string, data: DatabaseData, filters: DatabaseFilters, organisationId: string) => Promise<T | null>;
49
+ secureDelete: (table: string, filters: DatabaseFilters, organisationId: string) => Promise<boolean>;
50
+
51
+ // Organisation-scoped queries
52
+ queryByOrganisation: <T extends DatabaseRecord = DatabaseRecord>(table: string, select: string, organisationId: string, filters?: DatabaseFilters) => Promise<T[]>;
53
+
54
+ // Validation helpers
55
+ validateOrganisationContext: (organisationId: string) => void;
56
+ ensureOrganisationColumn: (table: string) => boolean;
57
+ }
58
+
59
+ export interface SecureQueryBuilder {
60
+ table: string;
61
+ select: string;
62
+ organisationId: string;
63
+ filters?: DatabaseFilters;
64
+ orderBy?: string;
65
+ limit?: number;
66
+ offset?: number;
67
+ }
68
+
69
+ /**
70
+ * Create a secure data access instance
71
+ * @param supabase - Supabase client instance
72
+ * @param organisationId - Current organisation context
73
+ * @param isSuperAdmin - Whether user has super admin privileges
74
+ * @returns Secure data access utilities
75
+ */
76
+ export const createSecureDataAccess = (
77
+ supabase: SupabaseClient,
78
+ organisationId: string,
79
+ isSuperAdmin: boolean = false
80
+ ): SecureDataAccess => {
81
+
82
+ // Validate organisation context
83
+ const validateOrganisationContext = (orgId: string): void => {
84
+ if (!orgId) {
85
+ throw new Error('Organisation context is required for secure data access');
86
+ }
87
+
88
+ if (!isSuperAdmin && !orgId) {
89
+ throw new Error('Organisation context is mandatory for non-super admin users');
90
+ }
91
+ };
92
+
93
+ // Check if table has organisation_id column
94
+ const ensureOrganisationColumn = (table: string): boolean => {
95
+ // This is a simplified check - in production you might want to cache this
96
+ const tablesWithOrganisation = [
97
+ 'event', 'organisation_settings',
98
+ 'rbac_event_app_roles', 'rbac_organisation_roles',
99
+ // SECURITY: Phase 2 additions - complete organisation table mapping
100
+ 'organisation_audit_log', 'organisation_invitations', 'organisation_app_access',
101
+ // SECURITY: Emergency additions for Phase 1 fixes
102
+ 'cake_meal', 'cake_mealtype', 'pace_person', 'pace_member',
103
+ // SECURITY: Phase 3A additions - medical and personal data
104
+ 'medi_profile', 'medi_condition', 'medi_diet', 'medi_action_plan', 'medi_profile_versions',
105
+ 'pace_consent', 'pace_contact', 'pace_id_documents', 'pace_qualifications',
106
+ 'form_responses', 'form_response_values', 'forms',
107
+ // SECURITY: Phase 3B additions - remaining critical tables
108
+ 'invoice', 'line_item', 'credit_balance', 'payment_method',
109
+ 'form_contexts', 'form_field_config', 'form_fields',
110
+ 'cake_delivery', 'cake_diettype', 'cake_diner', 'cake_dish', 'cake_item',
111
+ 'cake_logistics', 'cake_mealplan', 'cake_package', 'cake_recipe', 'cake_supplier',
112
+ 'cake_supply', 'cake_unit', 'event_app_access', 'base_application', 'base_questions'
113
+ ];
114
+
115
+ return tablesWithOrganisation.includes(table);
116
+ };
117
+
118
+ // Build secure query with organisation context
119
+ const buildSecureQuery = (options: SecureQueryBuilder) => {
120
+ const { table, select, organisationId: orgId, filters, orderBy, limit, offset } = options;
121
+
122
+ validateOrganisationContext(orgId);
123
+
124
+ let query = supabase
125
+ .from(table)
126
+ .select(select);
127
+
128
+ // Add organisation filter (unless super admin)
129
+ if (!isSuperAdmin && ensureOrganisationColumn(table)) {
130
+ query = query.eq('organisation_id', orgId);
131
+ }
132
+
133
+ // Add additional filters
134
+ if (filters) {
135
+ Object.entries(filters).forEach(([key, value]) => {
136
+ if (value !== undefined && value !== null) {
137
+ // Handle qualified column names (e.g., 'users.role')
138
+ const columnName = key.includes('.') ? key.split('.').pop()! : key;
139
+ query = query.eq(columnName, value);
140
+ }
141
+ });
142
+ }
143
+
144
+ // Add ordering
145
+ if (orderBy) {
146
+ // Only use the column name, not a qualified name
147
+ const orderByColumn = orderBy.split('.').pop();
148
+ if (orderByColumn) {
149
+ query = query.order(orderByColumn);
150
+ }
151
+ }
152
+
153
+ // Add pagination
154
+ if (limit) {
155
+ query = query.limit(limit);
156
+ }
157
+
158
+ if (offset) {
159
+ query = query.range(offset, offset + (limit || 10) - 1);
160
+ }
161
+
162
+ return query;
163
+ };
164
+
165
+ // Secure query for multiple results
166
+ const secureQuery = async <T extends DatabaseRecord = DatabaseRecord>(options: SecureQueryOptions): Promise<T[]> => {
167
+ const { table, select, organisationId: orgId, filters, orderBy, limit, offset } = options;
168
+
169
+ try {
170
+ const query = buildSecureQuery({
171
+ table,
172
+ select,
173
+ organisationId: orgId,
174
+ filters,
175
+ orderBy,
176
+ limit,
177
+ offset
178
+ });
179
+
180
+ const { data, error } = await query;
181
+
182
+ if (error) {
183
+ throw error;
184
+ }
185
+
186
+ // Ensure data is an array and not an error type
187
+ if (Array.isArray(data)) {
188
+ return data as unknown as T[];
189
+ }
190
+
191
+ return [];
192
+ } catch (error) {
193
+ throw error;
194
+ }
195
+ };
196
+
197
+ // Secure query for single result
198
+ const secureSingleQuery = async <T extends DatabaseRecord = DatabaseRecord>(options: SecureQueryOptions): Promise<T | null> => {
199
+ const { table, select, organisationId: orgId, filters, orderBy, limit, offset } = options;
200
+
201
+ try {
202
+ const query = buildSecureQuery({
203
+ table,
204
+ select,
205
+ organisationId: orgId,
206
+ filters,
207
+ orderBy,
208
+ limit,
209
+ offset
210
+ });
211
+
212
+ const { data, error } = await query.single();
213
+
214
+ if (error) {
215
+ if (error.code === 'PGRST116') {
216
+ // No rows returned
217
+ return null;
218
+ }
219
+ throw error;
220
+ }
221
+
222
+ // Ensure data is not an error type
223
+ if (data && typeof data === 'object' && !('code' in data)) {
224
+ return data as unknown as T;
225
+ }
226
+
227
+ return null;
228
+ } catch (error) {
229
+ throw error;
230
+ }
231
+ };
232
+
233
+ // Secure insert with organisation context
234
+ const secureInsert = async <T extends DatabaseRecord = DatabaseRecord>(
235
+ table: string,
236
+ data: DatabaseData,
237
+ organisationId: string
238
+ ): Promise<T | null> => {
239
+ validateOrganisationContext(organisationId);
240
+
241
+ try {
242
+ const insertData = {
243
+ ...data,
244
+ organisation_id: organisationId
245
+ };
246
+
247
+ const { data: result, error } = await supabase
248
+ .from(table)
249
+ .insert(insertData)
250
+ .select()
251
+ .single();
252
+
253
+ if (error) {
254
+ throw error;
255
+ }
256
+
257
+ return result;
258
+ } catch (error) {
259
+ throw error;
260
+ }
261
+ };
262
+
263
+ // Secure update with organisation context
264
+ const secureUpdate = async <T extends DatabaseRecord = DatabaseRecord>(
265
+ table: string,
266
+ data: DatabaseData,
267
+ filters: DatabaseFilters,
268
+ organisationId: string
269
+ ): Promise<T | null> => {
270
+ validateOrganisationContext(organisationId);
271
+
272
+ try {
273
+ let query = supabase
274
+ .from(table)
275
+ .update(data);
276
+
277
+ // Add organisation filter (unless super admin)
278
+ if (!isSuperAdmin && ensureOrganisationColumn(table)) {
279
+ query = query.eq('organisation_id', organisationId);
280
+ }
281
+
282
+ // Add additional filters
283
+ if (filters) {
284
+ Object.entries(filters).forEach(([key, value]) => {
285
+ if (value !== undefined && value !== null) {
286
+ query = query.eq(key, value);
287
+ }
288
+ });
289
+ }
290
+
291
+ const { data: result, error } = await query.select().single();
292
+
293
+ if (error) {
294
+ throw error;
295
+ }
296
+
297
+ return result;
298
+ } catch (error) {
299
+ throw error;
300
+ }
301
+ };
302
+
303
+ // Secure delete with organisation context
304
+ const secureDelete = async (
305
+ table: string,
306
+ filters: DatabaseFilters,
307
+ organisationId: string
308
+ ): Promise<boolean> => {
309
+ validateOrganisationContext(organisationId);
310
+
311
+ try {
312
+ let query = supabase
313
+ .from(table)
314
+ .delete();
315
+
316
+ // Add organisation filter (unless super admin)
317
+ if (!isSuperAdmin && ensureOrganisationColumn(table)) {
318
+ query = query.eq('organisation_id', organisationId);
319
+ }
320
+
321
+ // Add additional filters
322
+ if (filters) {
323
+ Object.entries(filters).forEach(([key, value]) => {
324
+ if (value !== undefined && value !== null) {
325
+ query = query.eq(key, value);
326
+ }
327
+ });
328
+ }
329
+
330
+ const { error } = await query;
331
+
332
+ if (error) {
333
+ throw error;
334
+ }
335
+
336
+ return true;
337
+ } catch (error) {
338
+ throw error;
339
+ }
340
+ };
341
+
342
+ // Organisation-scoped query helper
343
+ const queryByOrganisation = async <T extends DatabaseRecord = DatabaseRecord>(
344
+ table: string,
345
+ select: string,
346
+ organisationId: string,
347
+ filters?: DatabaseFilters
348
+ ): Promise<T[]> => {
349
+ return secureQuery<T>({
350
+ table,
351
+ select,
352
+ organisationId,
353
+ filters
354
+ });
355
+ };
356
+
357
+ return {
358
+ secureQuery,
359
+ secureSingleQuery,
360
+ secureInsert,
361
+ secureUpdate,
362
+ secureDelete,
363
+ queryByOrganisation,
364
+ validateOrganisationContext,
365
+ ensureOrganisationColumn
366
+ };
367
+ };
368
+
369
+ /**
370
+ * Hook for secure data access
371
+ * @returns Secure data access utilities
372
+ */
373
+ export const useSecureDataAccess = (): SecureDataAccess => {
374
+ // This would typically get the context from providers
375
+ // For now, we'll create a placeholder that can be used with explicit parameters
376
+ throw new Error('useSecureDataAccess must be used with explicit parameters. Use createSecureDataAccess instead.');
377
+ };
@@ -0,0 +1,79 @@
1
+ /**
2
+ * @file Secure error handling utilities
3
+ */
4
+
5
+ import { AuthError, AuthErrorCode, RequestId } from '../types/unified';
6
+
7
+ export class SecureError extends Error {
8
+ public readonly code: AuthErrorCode;
9
+ public readonly statusCode: number;
10
+ public readonly userMessage: string;
11
+
12
+ constructor(message: string, code: AuthErrorCode, statusCode: number = 500, userMessage?: string) {
13
+ super(message);
14
+ this.name = 'SecureError';
15
+ this.code = code;
16
+ this.statusCode = statusCode;
17
+ this.userMessage = userMessage || message;
18
+ }
19
+ }
20
+
21
+ export function createSecureError(
22
+ message: string,
23
+ code: AuthErrorCode,
24
+ statusCode: number = 500,
25
+ userMessage?: string
26
+ ): SecureError {
27
+ return new SecureError(message, code, statusCode, userMessage);
28
+ }
29
+
30
+ export function convertToAuthError(error: SecureError | Error | unknown): AuthError {
31
+ if (error instanceof SecureError) {
32
+ return {
33
+ message: error.message,
34
+ code: error.code,
35
+ user_message: error.userMessage,
36
+ __isAuthError: true,
37
+ name: 'AuthError',
38
+ timestamp: Date.now()
39
+ };
40
+ }
41
+
42
+ if (error instanceof Error) {
43
+ return {
44
+ message: error.message,
45
+ code: AuthErrorCode.UNKNOWN_ERROR,
46
+ user_message: error.message,
47
+ __isAuthError: true,
48
+ name: 'AuthError',
49
+ timestamp: Date.now()
50
+ };
51
+ }
52
+
53
+ return {
54
+ message: 'Unknown error occurred',
55
+ code: AuthErrorCode.UNKNOWN_ERROR,
56
+ user_message: 'Unknown error occurred',
57
+ __isAuthError: true,
58
+ name: 'AuthError',
59
+ timestamp: Date.now()
60
+ };
61
+ }
62
+
63
+ export function isSecureError(error: unknown): error is SecureError {
64
+ return error instanceof SecureError;
65
+ }
66
+
67
+ export function sanitizeError(error: unknown): AuthError {
68
+ return convertToAuthError(error);
69
+ }
70
+
71
+ export function generateRequestId(): RequestId {
72
+ return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` as RequestId;
73
+ }
74
+
75
+ export function logSecurityEvent(event: string, details?: unknown): void {
76
+ // In production, this would send to a proper logging service
77
+ // For now, we'll log to console.warn for testing purposes
78
+ console.warn(`[SECURITY] ${event}`, details);
79
+ }
@@ -0,0 +1,244 @@
1
+
2
+ /**
3
+ * @file Secure Storage Utilities
4
+ * @description Encrypted storage wrapper for sensitive data
5
+ */
6
+
7
+ export interface SecureStorageOptions {
8
+ encrypt?: boolean;
9
+ expiry?: number; // TTL in milliseconds
10
+ }
11
+
12
+ /**
13
+ * Secure storage implementation with encryption support
14
+ */
15
+ class SecureStorageImpl {
16
+ private encryptionKey: CryptoKey | null = null;
17
+ private initialized = false;
18
+
19
+ /**
20
+ * Initialize secure storage with encryption
21
+ */
22
+ async init(): Promise<void> {
23
+ if (this.initialized) return;
24
+
25
+ try {
26
+ // Check if Web Crypto API is available
27
+ if (window.crypto && window.crypto.subtle) {
28
+ // Generate or retrieve encryption key
29
+ const keyData = localStorage.getItem('_sec_key');
30
+ if (keyData) {
31
+ try {
32
+ const keyBuffer = this.base64ToArrayBuffer(keyData);
33
+ this.encryptionKey = await window.crypto.subtle.importKey(
34
+ 'raw',
35
+ keyBuffer,
36
+ { name: 'AES-GCM' },
37
+ false,
38
+ ['encrypt', 'decrypt']
39
+ );
40
+ } catch (error) {
41
+ await this.generateNewKey();
42
+ }
43
+ } else {
44
+ await this.generateNewKey();
45
+ }
46
+ }
47
+ this.initialized = true;
48
+ } catch (error) {
49
+ this.initialized = true;
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Store item securely
55
+ */
56
+ async setItem(
57
+ key: string,
58
+ value: string,
59
+ options: SecureStorageOptions = {}
60
+ ): Promise<void> {
61
+ await this.init();
62
+
63
+ const data = {
64
+ value,
65
+ timestamp: Date.now(),
66
+ expiry: options.expiry ? Date.now() + options.expiry : undefined,
67
+ };
68
+
69
+ const serialized = JSON.stringify(data);
70
+
71
+ if (options.encrypt && this.encryptionKey) {
72
+ try {
73
+ const encrypted = await this.encrypt(serialized);
74
+ localStorage.setItem(`_sec_${key}`, encrypted);
75
+ return;
76
+ } catch (error) {
77
+ // Silent fail - store as plain text
78
+ }
79
+ }
80
+
81
+ localStorage.setItem(key, serialized);
82
+ }
83
+
84
+ /**
85
+ * Retrieve item securely
86
+ */
87
+ async getItem(key: string): Promise<string | null> {
88
+ await this.init();
89
+
90
+ // Try encrypted storage first
91
+ const encryptedData = localStorage.getItem(`_sec_${key}`);
92
+ if (encryptedData && this.encryptionKey) {
93
+ try {
94
+ const decrypted = await this.decrypt(encryptedData);
95
+ const parsed = JSON.parse(decrypted);
96
+
97
+ // Check expiry
98
+ if (parsed.expiry && Date.now() > parsed.expiry) {
99
+ await this.removeItem(key);
100
+ return null;
101
+ }
102
+
103
+ return parsed.value;
104
+ } catch (error) {
105
+ // Silent fail - try plain storage
106
+ }
107
+ }
108
+
109
+ // Fallback to plain storage
110
+ const plainData = localStorage.getItem(key);
111
+ if (!plainData) return null;
112
+
113
+ try {
114
+ const parsed = JSON.parse(plainData);
115
+
116
+ // Check expiry
117
+ if (parsed.expiry && Date.now() > parsed.expiry) {
118
+ await this.removeItem(key);
119
+ return null;
120
+ }
121
+
122
+ return parsed.value || plainData;
123
+ } catch (error) {
124
+ // If parsing fails, return as-is (backward compatibility)
125
+ return plainData;
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Remove item
131
+ */
132
+ async removeItem(key: string): Promise<void> {
133
+ localStorage.removeItem(key);
134
+ localStorage.removeItem(`_sec_${key}`);
135
+ }
136
+
137
+ /**
138
+ * Clear all secure storage
139
+ */
140
+ async clear(): Promise<void> {
141
+ const keys = Object.keys(localStorage);
142
+ for (const key of keys) {
143
+ if (key.startsWith('_sec_')) {
144
+ localStorage.removeItem(key);
145
+ }
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Generate new encryption key
151
+ */
152
+ private async generateNewKey(): Promise<void> {
153
+ if (!window.crypto?.subtle) return;
154
+
155
+ try {
156
+ this.encryptionKey = await window.crypto.subtle.generateKey(
157
+ { name: 'AES-GCM', length: 256 },
158
+ true,
159
+ ['encrypt', 'decrypt']
160
+ );
161
+
162
+ // Export and store key
163
+ const exportedKey = await window.crypto.subtle.exportKey('raw', this.encryptionKey);
164
+ const keyData = this.arrayBufferToBase64(exportedKey);
165
+ localStorage.setItem('_sec_key', keyData);
166
+ } catch (error) {
167
+ // Silent fail - encryption not available
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Encrypt data
173
+ */
174
+ private async encrypt(data: string): Promise<string> {
175
+ if (!this.encryptionKey || !window.crypto?.subtle) {
176
+ throw new Error('Encryption not available');
177
+ }
178
+
179
+ const encoder = new TextEncoder();
180
+ const dataBuffer = encoder.encode(data);
181
+ const iv = window.crypto.getRandomValues(new Uint8Array(12));
182
+
183
+ const encrypted = await window.crypto.subtle.encrypt(
184
+ { name: 'AES-GCM', iv },
185
+ this.encryptionKey,
186
+ dataBuffer
187
+ );
188
+
189
+ // Combine IV and encrypted data
190
+ const combined = new Uint8Array(iv.length + encrypted.byteLength);
191
+ combined.set(iv);
192
+ combined.set(new Uint8Array(encrypted), iv.length);
193
+
194
+ return this.arrayBufferToBase64(combined.buffer);
195
+ }
196
+
197
+ /**
198
+ * Decrypt data
199
+ */
200
+ private async decrypt(encryptedData: string): Promise<string> {
201
+ if (!this.encryptionKey || !window.crypto?.subtle) {
202
+ throw new Error('Decryption not available');
203
+ }
204
+
205
+ const combined = this.base64ToArrayBuffer(encryptedData);
206
+ const iv = combined.slice(0, 12);
207
+ const encrypted = combined.slice(12);
208
+
209
+ const decrypted = await window.crypto.subtle.decrypt(
210
+ { name: 'AES-GCM', iv },
211
+ this.encryptionKey,
212
+ encrypted
213
+ );
214
+
215
+ const decoder = new TextDecoder();
216
+ return decoder.decode(decrypted);
217
+ }
218
+
219
+ /**
220
+ * Convert ArrayBuffer to base64
221
+ */
222
+ private arrayBufferToBase64(buffer: ArrayBuffer): string {
223
+ const bytes = new Uint8Array(buffer);
224
+ let binary = '';
225
+ for (let i = 0; i < bytes.byteLength; i++) {
226
+ binary += String.fromCharCode(bytes[i]);
227
+ }
228
+ return btoa(binary);
229
+ }
230
+
231
+ /**
232
+ * Convert base64 to ArrayBuffer
233
+ */
234
+ private base64ToArrayBuffer(base64: string): ArrayBuffer {
235
+ const binary = atob(base64);
236
+ const bytes = new Uint8Array(binary.length);
237
+ for (let i = 0; i < binary.length; i++) {
238
+ bytes[i] = binary.charCodeAt(i);
239
+ }
240
+ return bytes.buffer;
241
+ }
242
+ }
243
+
244
+ export const secureStorage = new SecureStorageImpl();