@jmruthers/pace-core 0.5.74 → 0.5.75

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 (278) hide show
  1. package/dist/{DataTable-2QR5TER5.js → DataTable-HWZQGASI.js} +8 -8
  2. package/dist/{PublicLoadingSpinner-DLpF5bbs.d.ts → PublicLoadingSpinner-BKNBT6b6.d.ts} +2 -2
  3. package/dist/RBACService-C4udt_Zp.d.ts +528 -0
  4. package/dist/{UnifiedAuthProvider-K4NRGXL4.js → UnifiedAuthProvider-3NKDOSOK.js} +6 -4
  5. package/dist/UnifiedAuthProvider-Bj6YCf7c.d.ts +113 -0
  6. package/dist/{chunk-UJMCGBLS.js → chunk-2CHATWBF.js} +5 -7
  7. package/dist/chunk-2CHATWBF.js.map +1 -0
  8. package/dist/{chunk-BKVGJVUR.js → chunk-2DFZ432F.js} +496 -30
  9. package/dist/chunk-2DFZ432F.js.map +1 -0
  10. package/dist/{chunk-LVQ26TCN.js → chunk-33PHABLB.js} +36 -3
  11. package/dist/chunk-33PHABLB.js.map +1 -0
  12. package/dist/chunk-5F3NDPJV.js +232 -0
  13. package/dist/chunk-5F3NDPJV.js.map +1 -0
  14. package/dist/chunk-A4FUBC7B.js +17 -0
  15. package/dist/chunk-A4FUBC7B.js.map +1 -0
  16. package/dist/{chunk-SMJZMKYN.js → chunk-A6HBIY5P.js} +2 -11
  17. package/dist/{chunk-SMJZMKYN.js.map → chunk-A6HBIY5P.js.map} +1 -1
  18. package/dist/{chunk-IHMMNKNA.js → chunk-CY3AHGO4.js} +6256 -1937
  19. package/dist/chunk-CY3AHGO4.js.map +1 -0
  20. package/dist/{chunk-H2TNUICK.js → chunk-DAXLNIDY.js} +47 -49
  21. package/dist/chunk-DAXLNIDY.js.map +1 -0
  22. package/dist/{chunk-VKOCWWVY.js → chunk-L3RV2ALE.js} +1 -6
  23. package/dist/{chunk-VKOCWWVY.js.map → chunk-L3RV2ALE.js.map} +1 -1
  24. package/dist/chunk-LW7MMEAQ.js +59 -0
  25. package/dist/chunk-LW7MMEAQ.js.map +1 -0
  26. package/dist/{chunk-DG5Z55HH.js → chunk-NTNILOBC.js} +7 -9
  27. package/dist/chunk-NTNILOBC.js.map +1 -0
  28. package/dist/chunk-PYUXFQJ3.js +11 -0
  29. package/dist/chunk-PYUXFQJ3.js.map +1 -0
  30. package/dist/chunk-URUTVZ7N.js +27 -0
  31. package/dist/chunk-URUTVZ7N.js.map +1 -0
  32. package/dist/chunk-WN6XJWOS.js +2468 -0
  33. package/dist/chunk-WN6XJWOS.js.map +1 -0
  34. package/dist/{chunk-3SP4P7NS.js → chunk-XLZ7U46Z.js} +59 -1
  35. package/dist/chunk-XLZ7U46Z.js.map +1 -0
  36. package/dist/{chunk-ORSMVXO2.js → chunk-ZTT2AXMX.js} +9 -14
  37. package/dist/chunk-ZTT2AXMX.js.map +1 -0
  38. package/dist/components.d.ts +4 -5
  39. package/dist/components.js +32 -39
  40. package/dist/components.js.map +1 -1
  41. package/dist/hooks.d.ts +3 -3
  42. package/dist/hooks.js +9 -8
  43. package/dist/hooks.js.map +1 -1
  44. package/dist/index.d.ts +156 -10
  45. package/dist/index.js +188 -93
  46. package/dist/index.js.map +1 -1
  47. package/dist/{organisation-t-vvQC3g.d.ts → organisation-BtshODVF.d.ts} +4 -3
  48. package/dist/providers.d.ts +27 -38
  49. package/dist/providers.js +33 -23
  50. package/dist/rbac/index.d.ts +61 -5
  51. package/dist/rbac/index.js +13 -14
  52. package/dist/styles/index.js +2 -2
  53. package/dist/theming/runtime.js +1 -3
  54. package/dist/types.d.ts +3 -3
  55. package/dist/types.js +1 -1
  56. package/dist/types.js.map +1 -1
  57. package/dist/{unified-CMPjE_fv.d.ts → unified-CM7T0aTK.d.ts} +1 -1
  58. package/dist/useInactivityTracker-MRUU55XI.js +10 -0
  59. package/dist/useInactivityTracker-MRUU55XI.js.map +1 -0
  60. package/dist/{usePublicRouteParams-Ua1Vz-HG.d.ts → usePublicRouteParams-B-CumWRc.d.ts} +3 -3
  61. package/dist/utils.js +7 -9
  62. package/dist/utils.js.map +1 -1
  63. package/dist/validation.d.ts +1 -1
  64. package/docs/api/classes/ColumnFactory.md +1 -1
  65. package/docs/api/classes/ErrorBoundary.md +1 -1
  66. package/docs/api/classes/InvalidScopeError.md +1 -1
  67. package/docs/api/classes/MissingUserContextError.md +1 -1
  68. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  69. package/docs/api/classes/PermissionDeniedError.md +1 -1
  70. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  71. package/docs/api/classes/RBACAuditManager.md +1 -1
  72. package/docs/api/classes/RBACCache.md +1 -1
  73. package/docs/api/classes/RBACEngine.md +1 -1
  74. package/docs/api/classes/RBACError.md +1 -1
  75. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  76. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  77. package/docs/api/classes/StorageUtils.md +1 -1
  78. package/docs/api/enums/FileCategory.md +1 -1
  79. package/docs/api/interfaces/AggregateConfig.md +1 -1
  80. package/docs/api/interfaces/ButtonProps.md +3 -3
  81. package/docs/api/interfaces/CardProps.md +2 -2
  82. package/docs/api/interfaces/ColorPalette.md +1 -1
  83. package/docs/api/interfaces/ColorShade.md +1 -1
  84. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  85. package/docs/api/interfaces/DataTableAction.md +1 -1
  86. package/docs/api/interfaces/DataTableColumn.md +1 -1
  87. package/docs/api/interfaces/DataTableProps.md +1 -1
  88. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  89. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  90. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  91. package/docs/api/interfaces/EventLogoProps.md +2 -2
  92. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  93. package/docs/api/interfaces/FileMetadata.md +1 -1
  94. package/docs/api/interfaces/FileReference.md +1 -1
  95. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  96. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  97. package/docs/api/interfaces/FileUploadProps.md +1 -1
  98. package/docs/api/interfaces/FooterProps.md +1 -1
  99. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  100. package/docs/api/interfaces/InputProps.md +2 -2
  101. package/docs/api/interfaces/LabelProps.md +1 -1
  102. package/docs/api/interfaces/LoginFormProps.md +1 -1
  103. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  104. package/docs/api/interfaces/NavigationContextType.md +1 -1
  105. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  106. package/docs/api/interfaces/NavigationItem.md +1 -1
  107. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  108. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  109. package/docs/api/interfaces/Organisation.md +1 -1
  110. package/docs/api/interfaces/OrganisationContextType.md +28 -17
  111. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  112. package/docs/api/interfaces/OrganisationProviderProps.md +2 -2
  113. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  114. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  115. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  116. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  117. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  118. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  119. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  120. package/docs/api/interfaces/PaletteData.md +1 -1
  121. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  122. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  123. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  124. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +2 -2
  125. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  126. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  127. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  128. package/docs/api/interfaces/RBACConfig.md +1 -1
  129. package/docs/api/interfaces/RBACContextType.md +5 -11
  130. package/docs/api/interfaces/RBACLogger.md +1 -1
  131. package/docs/api/interfaces/RBACProviderProps.md +1 -1
  132. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  133. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  134. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  135. package/docs/api/interfaces/RouteConfig.md +1 -1
  136. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  137. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  138. package/docs/api/interfaces/StorageConfig.md +1 -1
  139. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  140. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  141. package/docs/api/interfaces/StorageListOptions.md +1 -1
  142. package/docs/api/interfaces/StorageListResult.md +1 -1
  143. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  144. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  145. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  146. package/docs/api/interfaces/StyleImport.md +1 -1
  147. package/docs/api/interfaces/SwitchProps.md +1 -1
  148. package/docs/api/interfaces/ToastActionElement.md +1 -1
  149. package/docs/api/interfaces/ToastProps.md +1 -1
  150. package/docs/api/interfaces/UnifiedAuthContextType.md +524 -440
  151. package/docs/api/interfaces/UnifiedAuthProviderProps.md +14 -14
  152. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  153. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  154. package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
  155. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  156. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  157. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  158. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  159. package/docs/api/interfaces/UserEventAccess.md +11 -11
  160. package/docs/api/interfaces/UserMenuProps.md +1 -1
  161. package/docs/api/interfaces/UserProfile.md +1 -1
  162. package/docs/api/modules.md +179 -52
  163. package/docs/architecture/services.md +30 -32
  164. package/docs/breaking-changes.md +2 -5
  165. package/docs/migration/service-architecture.md +121 -260
  166. package/docs/rbac/README-rbac-rls-integration.md +48 -38
  167. package/{src/rbac/examples → examples/RBAC}/CompleteRBACExample.tsx +3 -2
  168. package/{src/rbac/examples → examples/RBAC}/EventBasedApp.tsx +5 -4
  169. package/{src/components/examples → examples/RBAC}/PermissionExample.tsx +7 -6
  170. package/examples/RBAC/__tests__/PermissionExample.test.tsx +150 -0
  171. package/examples/RBAC/index.ts +13 -0
  172. package/examples/README.md +37 -0
  173. package/examples/index.ts +22 -0
  174. package/{src/examples → examples/public-pages}/CorrectPublicPageImplementation.tsx +1 -1
  175. package/{src/examples → examples/public-pages}/PublicEventPage.tsx +1 -1
  176. package/{src/examples → examples/public-pages}/PublicPageApp.tsx +1 -1
  177. package/{src/examples → examples/public-pages}/PublicPageUsageExample.tsx +1 -1
  178. package/examples/public-pages/__tests__/PublicPageUsageExample.test.tsx +159 -0
  179. package/examples/public-pages/index.ts +14 -0
  180. package/package.json +22 -18
  181. package/src/__tests__/TEST_GUIDE_CURSOR.md +650 -9
  182. package/src/__tests__/helpers/README.md +255 -0
  183. package/src/__tests__/helpers/index.ts +62 -0
  184. package/src/__tests__/helpers/supabaseMock.ts +27 -3
  185. package/src/__tests__/rbac/PagePermissionGuard.test.tsx +6 -8
  186. package/src/components/DataTable/components/__tests__/COVERAGE_NOTE.md +55 -0
  187. package/src/components/DataTable/core/ColumnManager.ts +10 -0
  188. package/src/components/DataTable/core/__tests__/ColumnFactory.test.ts +254 -0
  189. package/src/components/DataTable/core/__tests__/ColumnManager.test.ts +193 -0
  190. package/src/components/DataTable/examples/__tests__/HierarchicalExample.test.tsx +45 -0
  191. package/src/components/DataTable/examples/__tests__/PerformanceExample.test.tsx +117 -0
  192. package/src/components/Dialog/examples/__tests__/HtmlDialogExample.test.tsx +71 -0
  193. package/src/components/Dialog/examples/__tests__/SimpleHtmlTest.test.tsx +122 -0
  194. package/src/components/EventSelector/EventSelector.tsx +1 -1
  195. package/src/components/Header/Header.test.tsx +35 -1
  196. package/src/components/Header/Header.tsx +3 -1
  197. package/src/components/OrganisationSelector/OrganisationSelector.tsx +3 -3
  198. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.rbac.test.tsx +24 -4
  199. package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +3 -2
  200. package/src/hooks/__tests__/useFocusManagement.unit.test.ts +220 -0
  201. package/src/hooks/__tests__/useIsMobile.unit.test.ts +117 -0
  202. package/src/hooks/__tests__/useKeyboardShortcuts.unit.test.ts +295 -0
  203. package/src/hooks/__tests__/useOrganisationSecurity.unit.test.tsx +29 -19
  204. package/src/hooks/__tests__/useRBAC.unit.test.ts +7 -3
  205. package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +115 -19
  206. package/src/hooks/useEventTheme.test.ts +350 -0
  207. package/src/hooks/useEventTheme.ts +1 -1
  208. package/src/hooks/useEvents.ts +61 -0
  209. package/src/hooks/useOrganisationSecurity.test.ts +4 -4
  210. package/src/hooks/useOrganisationSecurity.ts +2 -2
  211. package/src/hooks/useOrganisations.ts +64 -0
  212. package/src/hooks/useSecureDataAccess.test.ts +9 -5
  213. package/src/hooks/useSecureDataAccess.ts +2 -2
  214. package/src/index.ts +18 -3
  215. package/src/providers/AuthProvider.tsx +8 -292
  216. package/src/providers/EventProvider.tsx +15 -425
  217. package/src/providers/InactivityProvider.tsx +8 -231
  218. package/src/providers/OrganisationProvider.test.simple.tsx +3 -2
  219. package/src/providers/OrganisationProvider.tsx +11 -890
  220. package/src/providers/UnifiedAuthProvider.tsx +8 -320
  221. package/src/providers/__tests__/AuthProvider.test.tsx +18 -17
  222. package/src/providers/__tests__/EventProvider.test.tsx +253 -2
  223. package/src/providers/__tests__/InactivityProvider.test-helper.tsx +65 -0
  224. package/src/providers/__tests__/InactivityProvider.test.tsx +46 -114
  225. package/src/providers/__tests__/OrganisationProvider.test.tsx +313 -3
  226. package/src/providers/__tests__/UnifiedAuthProvider.test.tsx +383 -2
  227. package/src/providers/index.ts +8 -7
  228. package/src/providers/services/EventServiceProvider.tsx +3 -0
  229. package/src/providers/services/UnifiedAuthProvider.tsx +3 -0
  230. package/src/rbac/hooks/usePermissions.test.ts +296 -0
  231. package/src/rbac/hooks/useRBAC.test.ts +9 -5
  232. package/src/rbac/hooks/useRBAC.ts +3 -3
  233. package/src/rbac/providers/__tests__/RBACProvider.integration.test.tsx +688 -0
  234. package/src/rbac/providers/__tests__/RBACProvider.test.tsx +507 -0
  235. package/src/services/AuthService.ts +19 -4
  236. package/src/services/__tests__/AuthService.test.ts +288 -0
  237. package/src/styles/core.css +2 -0
  238. package/src/types/__tests__/guards.test.ts +246 -0
  239. package/src/types/guards.ts +1 -0
  240. package/src/types/organisation.ts +3 -2
  241. package/src/validation/__tests__/sanitization.unit.test.ts +250 -0
  242. package/src/validation/__tests__/schemaUtils.unit.test.ts +451 -0
  243. package/src/validation/__tests__/user.unit.test.ts +440 -0
  244. package/dist/RBACProvider-BO4ilsQB.d.ts +0 -63
  245. package/dist/UnifiedAuthProvider-D02AMXgO.d.ts +0 -103
  246. package/dist/chunk-3SP4P7NS.js.map +0 -1
  247. package/dist/chunk-B5LK25HV.js +0 -953
  248. package/dist/chunk-B5LK25HV.js.map +0 -1
  249. package/dist/chunk-BKVGJVUR.js.map +0 -1
  250. package/dist/chunk-C5Q5LRU5.js +0 -5691
  251. package/dist/chunk-C5Q5LRU5.js.map +0 -1
  252. package/dist/chunk-CDDYJCYU.js +0 -79
  253. package/dist/chunk-CDDYJCYU.js.map +0 -1
  254. package/dist/chunk-DG5Z55HH.js.map +0 -1
  255. package/dist/chunk-H2TNUICK.js.map +0 -1
  256. package/dist/chunk-IHMMNKNA.js.map +0 -1
  257. package/dist/chunk-LVQ26TCN.js.map +0 -1
  258. package/dist/chunk-ORSMVXO2.js.map +0 -1
  259. package/dist/chunk-UJMCGBLS.js.map +0 -1
  260. package/dist/chunk-V6BHACCH.js +0 -17
  261. package/dist/chunk-V6BHACCH.js.map +0 -1
  262. package/dist/rbac/cli/policy-manager.js +0 -278
  263. package/dist/rbac/cli/policy-manager.js.map +0 -1
  264. package/docs/api/interfaces/EventContextType.md +0 -96
  265. package/docs/api/interfaces/EventProviderProps.md +0 -19
  266. package/src/providers/OrganisationProvider.test.tsx +0 -164
  267. package/src/providers/UnifiedAuthProvider.test.tsx +0 -124
  268. package/src/providers/__tests__/AuthProvider.test.tsx.backup +0 -771
  269. package/src/providers/__tests__/EventProvider.test.tsx.backup +0 -824
  270. package/src/providers/__tests__/OrganisationProvider.test.tsx.backup +0 -820
  271. package/src/providers/__tests__/UnifiedAuthProvider.test.tsx.backup +0 -911
  272. package/src/providers/__tests__/UnifiedAuthProvider.test.tsx.backup2 +0 -166
  273. package/src/rbac/cli/__tests__/policy-manager.test.ts +0 -339
  274. package/src/rbac/cli/policy-manager.ts +0 -443
  275. package/dist/{DataTable-2QR5TER5.js.map → DataTable-HWZQGASI.js.map} +0 -0
  276. package/dist/{UnifiedAuthProvider-K4NRGXL4.js.map → UnifiedAuthProvider-3NKDOSOK.js.map} +0 -0
  277. package/dist/{validation-PM_iOaTI.d.ts → validation-D8VcbTzC.d.ts} +2 -2
  278. /package/src/utils/{appNameResolver.test.ts.backup → appNameResolver.test 2.ts} +0 -0
@@ -352,5 +352,293 @@ describe('AuthService', () => {
352
352
  await authService.signIn('test@example.com', 'password');
353
353
  expect(authService.getError()).toBeNull();
354
354
  });
355
+
356
+ it('should handle exceptions during sign in', async () => {
357
+ const exceptionError = new Error('Unexpected exception');
358
+ mockSupabase.auth.signInWithPassword.mockRejectedValue(exceptionError);
359
+
360
+ const result = await authService.signIn('test@example.com', 'password');
361
+
362
+ expect(result.error).toBeInstanceOf(AuthError);
363
+ expect(result.error?.message).toBe('Unexpected exception');
364
+ });
365
+
366
+ it('should handle exceptions during sign up', async () => {
367
+ const exceptionError = new Error('Unexpected exception');
368
+ mockSupabase.auth.signUp.mockRejectedValue(exceptionError);
369
+
370
+ const result = await authService.signUp('test@example.com', 'password');
371
+
372
+ expect(result.error).toBeInstanceOf(AuthError);
373
+ expect(result.error?.message).toBe('Unexpected exception');
374
+ });
375
+
376
+ it('should handle exceptions during sign out', async () => {
377
+ const exceptionError = new Error('Unexpected exception');
378
+ mockSupabase.auth.signOut.mockRejectedValue(exceptionError);
379
+
380
+ const result = await authService.signOut();
381
+
382
+ expect(result.error).toBeInstanceOf(AuthError);
383
+ expect(result.error?.message).toBe('Unexpected exception');
384
+ });
385
+
386
+ it('should handle exceptions during password reset', async () => {
387
+ const exceptionError = new Error('Unexpected exception');
388
+ mockSupabase.auth.resetPasswordForEmail.mockRejectedValue(exceptionError);
389
+
390
+ const result = await authService.resetPassword('test@example.com');
391
+
392
+ expect(result.error).toBeInstanceOf(AuthError);
393
+ });
394
+
395
+ it('should handle missing Supabase client on sign up', async () => {
396
+ const serviceWithoutClient = new AuthService(null as any);
397
+
398
+ const result = await serviceWithoutClient.signUp('test@example.com', 'password');
399
+
400
+ expect(result.error).toBeInstanceOf(AuthError);
401
+ expect(result.error?.message).toBe('Supabase client not available');
402
+ });
403
+
404
+ it('should handle missing Supabase client on sign out', async () => {
405
+ const serviceWithoutClient = new AuthService(null as any);
406
+
407
+ const result = await serviceWithoutClient.signOut();
408
+
409
+ expect(result.error).toBeInstanceOf(AuthError);
410
+ expect(result.error?.message).toBe('Supabase client not available');
411
+ });
412
+
413
+ it('should handle missing Supabase client on password reset', async () => {
414
+ const serviceWithoutClient = new AuthService(null as any);
415
+
416
+ const result = await serviceWithoutClient.resetPassword('test@example.com');
417
+
418
+ expect(result.error).toBeInstanceOf(AuthError);
419
+ expect(result.error?.message).toBe('Supabase client not available');
420
+ });
421
+
422
+ it('should handle missing Supabase client on update password', async () => {
423
+ const serviceWithoutClient = new AuthService(null as any);
424
+
425
+ const result = await serviceWithoutClient.updatePassword('newpassword');
426
+
427
+ expect(result.error).toBeInstanceOf(AuthError);
428
+ expect(result.error?.message).toBe('Supabase client not available');
429
+ });
430
+
431
+ it('should handle missing Supabase client on session refresh', async () => {
432
+ const serviceWithoutClient = new AuthService(null as any);
433
+
434
+ const result = await serviceWithoutClient.refreshSession();
435
+
436
+ expect(result.error).toBeInstanceOf(AuthError);
437
+ expect(result.error?.message).toBe('Supabase client not available');
438
+ });
439
+ });
440
+
441
+ describe('Session Management Edge Cases', () => {
442
+ it('should handle session refresh with null data', async () => {
443
+ mockSupabase.auth.refreshSession.mockResolvedValue({
444
+ data: { session: null, user: null },
445
+ error: null
446
+ });
447
+
448
+ const result = await authService.refreshSession();
449
+
450
+ expect(result.user).toBeNull();
451
+ expect(result.session).toBeNull();
452
+ expect(result.error).toBeNull();
453
+ expect(authService.getUser()).toBeNull();
454
+ expect(authService.getSession()).toBeNull();
455
+ });
456
+
457
+ it('should handle session refresh with user but no session by clearing both', async () => {
458
+ const mockUser = { id: '1', email: 'test@example.com' };
459
+
460
+ mockSupabase.auth.refreshSession.mockResolvedValue({
461
+ data: { session: null, user: mockUser },
462
+ error: null
463
+ });
464
+
465
+ const result = await authService.refreshSession();
466
+
467
+ // If there's no session, we should clear user state too
468
+ // This is intentional - without a valid session, we can't trust the user object
469
+ expect(result.user).toBeNull();
470
+ expect(result.session).toBeNull();
471
+ expect(authService.getUser()).toBeNull();
472
+ expect(authService.getSession()).toBeNull();
473
+ });
474
+
475
+ it('should handle refresh session errors by clearing state', async () => {
476
+ const mockError = new AuthError('Session expired');
477
+
478
+ mockSupabase.auth.refreshSession.mockResolvedValue({
479
+ data: { session: null, user: null },
480
+ error: mockError
481
+ });
482
+
483
+ const result = await authService.refreshSession();
484
+
485
+ expect(result.error).toEqual(mockError);
486
+ expect(authService.getUser()).toBeNull();
487
+ expect(authService.getSession()).toBeNull();
488
+ expect(authService.isAuthenticated()).toBe(false);
489
+ });
490
+ });
491
+
492
+ describe('Initialization Edge Cases', () => {
493
+ it('should handle initialization without Supabase client', async () => {
494
+ const serviceWithoutClient = new AuthService(null as any);
495
+ await serviceWithoutClient.initialize();
496
+
497
+ expect(mockSupabase.auth.getSession).not.toHaveBeenCalled();
498
+ expect(mockSupabase.auth.onAuthStateChange).not.toHaveBeenCalled();
499
+ });
500
+
501
+ it('should handle initialization with getSession error', async () => {
502
+ const sessionError = new AuthError('Session error');
503
+ mockSupabase.auth.getSession.mockResolvedValue({
504
+ data: { session: null },
505
+ error: sessionError
506
+ });
507
+ mockSupabase.auth.getUser.mockResolvedValue({
508
+ data: { user: null },
509
+ error: null
510
+ });
511
+ mockSupabase.auth.onAuthStateChange.mockReturnValue({
512
+ data: { subscription: { unsubscribe: vi.fn() } }
513
+ });
514
+
515
+ await authService.initialize();
516
+
517
+ expect(mockSupabase.auth.getSession).toHaveBeenCalled();
518
+ expect(mockSupabase.auth.getUser).toHaveBeenCalled();
519
+ });
520
+
521
+ it('should handle initialization with getUser error', async () => {
522
+ const userError = new AuthError('User error');
523
+ mockSupabase.auth.getSession.mockResolvedValue({
524
+ data: { session: null },
525
+ error: null
526
+ });
527
+ mockSupabase.auth.getUser.mockResolvedValue({
528
+ data: { user: null },
529
+ error: userError
530
+ });
531
+ mockSupabase.auth.onAuthStateChange.mockReturnValue({
532
+ data: { subscription: { unsubscribe: vi.fn() } }
533
+ });
534
+
535
+ await authService.initialize();
536
+
537
+ expect(mockSupabase.auth.getUser).toHaveBeenCalled();
538
+ });
539
+
540
+ it('should restore session from storage during initialization', async () => {
541
+ const mockUser = { id: '1', email: 'test@example.com' };
542
+ const mockSession = { access_token: 'token', user: mockUser };
543
+
544
+ mockSupabase.auth.getSession.mockResolvedValue({
545
+ data: { session: mockSession },
546
+ error: null
547
+ });
548
+ mockSupabase.auth.onAuthStateChange.mockReturnValue({
549
+ data: { subscription: { unsubscribe: vi.fn() } }
550
+ });
551
+
552
+ await authService.initialize();
553
+
554
+ expect(authService.getUser()).toEqual(mockUser);
555
+ expect(authService.getSession()).toEqual(mockSession);
556
+ expect(authService.isAuthenticated()).toBe(true);
557
+ });
558
+
559
+ it('should handle auth state change unsubscribe gracefully', () => {
560
+ const unsubscribeFn = vi.fn();
561
+ mockSupabase.auth.onAuthStateChange.mockReturnValue({
562
+ data: { subscription: { unsubscribe: unsubscribeFn } }
563
+ });
564
+
565
+ authService.cleanup();
566
+
567
+ // Should not throw error
568
+ expect(unsubscribeFn).not.toHaveBeenCalled();
569
+ });
570
+
571
+ it('should handle auth state change events correctly', async () => {
572
+ const mockUser = { id: '1', email: 'test@example.com' };
573
+ const mockSession = { access_token: 'token', user: mockUser };
574
+
575
+ let authStateCallback: any;
576
+ mockSupabase.auth.onAuthStateChange.mockImplementation((callback) => {
577
+ authStateCallback = callback;
578
+ return { data: { subscription: { unsubscribe: vi.fn() } } };
579
+ });
580
+
581
+ await authService.initialize();
582
+
583
+ // Simulate SIGNED_IN event
584
+ if (authStateCallback) {
585
+ authStateCallback('SIGNED_IN', mockSession);
586
+ expect(authService.getUser()).toEqual(mockUser);
587
+ expect(authService.getSession()).toEqual(mockSession);
588
+ }
589
+
590
+ // Simulate SIGNED_OUT event
591
+ if (authStateCallback) {
592
+ authStateCallback('SIGNED_OUT', null);
593
+ expect(authService.getUser()).toBeNull();
594
+ expect(authService.getSession()).toBeNull();
595
+ }
596
+
597
+ // Simulate TOKEN_REFRESHED event
598
+ const refreshedSession = { ...mockSession, access_token: 'new_token' };
599
+ if (authStateCallback) {
600
+ authStateCallback('TOKEN_REFRESHED', refreshedSession);
601
+ expect(authService.getSession()).toEqual(refreshedSession);
602
+ }
603
+
604
+ // Simulate INITIAL_SESSION event
605
+ const initialSession = { access_token: 'initial_token', user: mockUser };
606
+ if (authStateCallback) {
607
+ authStateCallback('INITIAL_SESSION', initialSession);
608
+ expect(authService.getSession()).toEqual(initialSession);
609
+ }
610
+ });
611
+
612
+ it('should handle errors in auth state change callback', async () => {
613
+ const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
614
+
615
+ mockSupabase.auth.onAuthStateChange.mockImplementation((callback) => {
616
+ // Simulate error in callback
617
+ try {
618
+ callback('INITIAL_SESSION', null);
619
+ } catch (error) {
620
+ // Error is expected to be caught and logged
621
+ }
622
+ return { data: { subscription: { unsubscribe: vi.fn() } } };
623
+ });
624
+
625
+ await authService.initialize();
626
+
627
+ expect(consoleWarnSpy).not.toHaveBeenCalled();
628
+ consoleWarnSpy.mockRestore();
629
+ });
630
+ });
631
+
632
+ describe('Get Session with Null User', () => {
633
+ it('should handle session with null user object', () => {
634
+ const mockSessionWithNullUser = { access_token: 'token', user: null };
635
+
636
+ // This would be set via internal state
637
+ (authService as any).session = mockSessionWithNullUser;
638
+
639
+ expect(authService.getSession()).toEqual(mockSessionWithNullUser);
640
+ expect(authService.getUser()).toBeNull();
641
+ expect(authService.isAuthenticated()).toBe(false);
642
+ });
355
643
  });
356
644
  });
@@ -104,6 +104,7 @@
104
104
  font-weight: 600;
105
105
  line-height: 1.4;
106
106
  color: var(--color-main-700);
107
+ letter-spacing: -0.05rem;
107
108
  }
108
109
 
109
110
  h5 {
@@ -111,6 +112,7 @@
111
112
  font-weight: 500;
112
113
  line-height: 1.4;
113
114
  color: var(--color-main-800);
115
+ letter-spacing: -0.025rem;
114
116
  }
115
117
 
116
118
  h6 {
@@ -0,0 +1,246 @@
1
+ /**
2
+ * @file Type Guard Tests
3
+ * @package @jmruthers/pace-core
4
+ * @module Types/Guards
5
+ * @since 0.4.0
6
+ *
7
+ * Comprehensive tests for type guard functions.
8
+ */
9
+
10
+ import { describe, it, expect } from 'vitest';
11
+ import { isAuthErrorCode, isUser, isSession } from '../guards';
12
+ import { AuthErrorCode } from '../unified';
13
+
14
+ describe('Type Guards', () => {
15
+ describe('isAuthErrorCode', () => {
16
+ it('should return true for valid auth error codes', () => {
17
+ expect(isAuthErrorCode('INVALID_CREDENTIALS')).toBe(true);
18
+ expect(isAuthErrorCode('USER_NOT_FOUND')).toBe(true);
19
+ expect(isAuthErrorCode('EMAIL_NOT_CONFIRMED')).toBe(true);
20
+ expect(isAuthErrorCode('PASSWORD_TOO_WEAK')).toBe(true);
21
+ expect(isAuthErrorCode('SESSION_EXPIRED')).toBe(true);
22
+ });
23
+
24
+ it('should return false for invalid auth error codes', () => {
25
+ expect(isAuthErrorCode('INVALID_CODE')).toBe(false);
26
+ expect(isAuthErrorCode('unknown_error')).toBe(false);
27
+ expect(isAuthErrorCode('')).toBe(false);
28
+ });
29
+
30
+ it('should return false for non-string values', () => {
31
+ expect(isAuthErrorCode(null as any)).toBe(false);
32
+ expect(isAuthErrorCode(undefined as any)).toBe(false);
33
+ expect(isAuthErrorCode(123 as any)).toBe(false);
34
+ expect(isAuthErrorCode({} as any)).toBe(false);
35
+ expect(isAuthErrorCode([] as any)).toBe(false);
36
+ });
37
+
38
+ it('should return true for all enum values', () => {
39
+ Object.values(AuthErrorCode).forEach(code => {
40
+ expect(isAuthErrorCode(code)).toBe(true);
41
+ });
42
+ });
43
+ });
44
+
45
+ describe('isUser', () => {
46
+ it('should return true for valid user objects', () => {
47
+ const validUser = {
48
+ id: 'user-123',
49
+ created_at: '2024-01-01T00:00:00Z',
50
+ email: 'test@example.com'
51
+ };
52
+
53
+ expect(isUser(validUser)).toBe(true);
54
+ });
55
+
56
+ it('should return false for null or undefined', () => {
57
+ expect(isUser(null)).toBe(false);
58
+ expect(isUser(undefined)).toBe(false);
59
+ });
60
+
61
+ it('should return false for non-object types', () => {
62
+ expect(isUser('string')).toBe(false);
63
+ expect(isUser(123)).toBe(false);
64
+ expect(isUser(true)).toBe(false);
65
+ expect(isUser([])).toBe(false);
66
+ });
67
+
68
+ it('should return false for objects missing id', () => {
69
+ expect(isUser({ created_at: '2024-01-01T00:00:00Z' })).toBe(false);
70
+ expect(isUser({ email: 'test@example.com' })).toBe(false);
71
+ });
72
+
73
+ it('should return false for objects with non-string id', () => {
74
+ expect(isUser({ id: 123, created_at: '2024-01-01T00:00:00Z' })).toBe(false);
75
+ expect(isUser({ id: null, created_at: '2024-01-01T00:00:00Z' })).toBe(false);
76
+ expect(isUser({ id: undefined, created_at: '2024-01-01T00:00:00Z' })).toBe(false);
77
+ });
78
+
79
+ it('should return false for objects missing created_at', () => {
80
+ expect(isUser({ id: 'user-123' })).toBe(false);
81
+ expect(isUser({ id: 'user-123', email: 'test@example.com' })).toBe(false);
82
+ });
83
+
84
+ it('should return false for objects with non-string created_at', () => {
85
+ expect(isUser({ id: 'user-123', created_at: 123 })).toBe(false);
86
+ expect(isUser({ id: 'user-123', created_at: null })).toBe(false);
87
+ expect(isUser({ id: 'user-123', created_at: undefined })).toBe(false);
88
+ });
89
+
90
+ it('should return true for users with additional properties', () => {
91
+ const userWithExtraProps = {
92
+ id: 'user-123',
93
+ created_at: '2024-01-01T00:00:00Z',
94
+ email: 'test@example.com',
95
+ metadata: { role: 'admin' },
96
+ extraField: 'value'
97
+ };
98
+
99
+ expect(isUser(userWithExtraProps)).toBe(true);
100
+ });
101
+
102
+ it('should return true for empty string id (edge case)', () => {
103
+ const userWithEmptyId = {
104
+ id: '',
105
+ created_at: '2024-01-01T00:00:00Z'
106
+ };
107
+
108
+ expect(isUser(userWithEmptyId)).toBe(true);
109
+ });
110
+ });
111
+
112
+ describe('isSession', () => {
113
+ it('should return true for valid session objects', () => {
114
+ const validSession = {
115
+ access_token: 'token-123',
116
+ user: {
117
+ id: 'user-123',
118
+ created_at: '2024-01-01T00:00:00Z'
119
+ }
120
+ };
121
+
122
+ expect(isSession(validSession)).toBe(true);
123
+ });
124
+
125
+ it('should return false for null or undefined', () => {
126
+ expect(isSession(null)).toBe(false);
127
+ expect(isSession(undefined)).toBe(false);
128
+ });
129
+
130
+ it('should return false for non-object types', () => {
131
+ expect(isSession('string')).toBe(false);
132
+ expect(isSession(123)).toBe(false);
133
+ expect(isSession(true)).toBe(false);
134
+ expect(isSession([])).toBe(false);
135
+ });
136
+
137
+ it('should return false for objects missing access_token', () => {
138
+ expect(isSession({ user: { id: 'user-123', created_at: '2024-01-01T00:00:00Z' } })).toBe(false);
139
+ });
140
+
141
+ it('should return false for objects with non-string access_token', () => {
142
+ expect(isSession({
143
+ access_token: 123,
144
+ user: { id: 'user-123', created_at: '2024-01-01T00:00:00Z' }
145
+ })).toBe(false);
146
+
147
+ expect(isSession({
148
+ access_token: null,
149
+ user: { id: 'user-123', created_at: '2024-01-01T00:00:00Z' }
150
+ })).toBe(false);
151
+
152
+ expect(isSession({
153
+ access_token: undefined,
154
+ user: { id: 'user-123', created_at: '2024-01-01T00:00:00Z' }
155
+ })).toBe(false);
156
+ });
157
+
158
+ it('should return false for objects missing user', () => {
159
+ expect(isSession({ access_token: 'token-123' })).toBe(false);
160
+ });
161
+
162
+ it('should return false for objects with non-object user', () => {
163
+ expect(isSession({
164
+ access_token: 'token-123',
165
+ user: 'not an object'
166
+ })).toBe(false);
167
+
168
+ expect(isSession({
169
+ access_token: 'token-123',
170
+ user: null
171
+ })).toBe(false);
172
+
173
+ expect(isSession({
174
+ access_token: 'token-123',
175
+ user: undefined
176
+ })).toBe(false);
177
+ });
178
+
179
+ it('should return true for sessions with additional properties', () => {
180
+ const sessionWithExtraProps = {
181
+ access_token: 'token-123',
182
+ refresh_token: 'refresh-123',
183
+ user: {
184
+ id: 'user-123',
185
+ created_at: '2024-01-01T00:00:00Z'
186
+ },
187
+ expires_at: 1234567890,
188
+ expires_in: 3600
189
+ };
190
+
191
+ expect(isSession(sessionWithExtraProps)).toBe(true);
192
+ });
193
+
194
+ it('should return true for empty string access_token (edge case)', () => {
195
+ const sessionWithEmptyToken = {
196
+ access_token: '',
197
+ user: {
198
+ id: 'user-123',
199
+ created_at: '2024-01-01T00:00:00Z'
200
+ }
201
+ };
202
+
203
+ expect(isSession(sessionWithEmptyToken)).toBe(true);
204
+ });
205
+ });
206
+
207
+ describe('Edge Cases', () => {
208
+ it('should handle deeply nested objects correctly', () => {
209
+ const deeplyNested = {
210
+ id: 'user-123',
211
+ created_at: '2024-01-01T00:00:00Z',
212
+ nested: {
213
+ deeply: {
214
+ nested: {
215
+ property: 'value'
216
+ }
217
+ }
218
+ }
219
+ };
220
+
221
+ expect(isUser(deeplyNested)).toBe(true);
222
+ });
223
+
224
+ it('should handle special object types', () => {
225
+ const dateObj = new Date();
226
+ expect(isUser(dateObj)).toBe(false);
227
+ expect(isSession(dateObj)).toBe(false);
228
+
229
+ const arrayObj = [];
230
+ expect(isUser(arrayObj)).toBe(false);
231
+ expect(isSession(arrayObj)).toBe(false);
232
+
233
+ const functionObj = () => {};
234
+ expect(isUser(functionObj)).toBe(false);
235
+ expect(isSession(functionObj)).toBe(false);
236
+ });
237
+
238
+ it('should handle prototype pollution attempts', () => {
239
+ const malicious = Object.create(null);
240
+ (malicious as any).__proto__ = { id: 'user-123', created_at: '2024-01-01T00:00:00Z' };
241
+
242
+ expect(isUser(malicious)).toBe(false);
243
+ });
244
+ });
245
+ });
246
+
@@ -25,6 +25,7 @@ export function isSession(obj: unknown): obj is Session {
25
25
  candidate != null &&
26
26
  typeof candidate === 'object' &&
27
27
  typeof candidate.access_token === 'string' &&
28
+ candidate.user != null &&
28
29
  typeof candidate.user === 'object'
29
30
  );
30
31
  }
@@ -103,7 +103,7 @@ export interface OrganisationSecurityError extends Error {
103
103
 
104
104
  export interface OrganisationContextType {
105
105
  // Current organisation context
106
- selectedOrganisation: Organisation;
106
+ selectedOrganisation: Organisation | null;
107
107
  organisations: Organisation[];
108
108
  userMemberships: OrganisationMembership[];
109
109
 
@@ -111,9 +111,10 @@ export interface OrganisationContextType {
111
111
  isLoading: boolean;
112
112
  error: Error | null;
113
113
  hasValidOrganisationContext: boolean;
114
+ isContextReady: boolean;
114
115
 
115
116
  // Organisation management
116
- setSelectedOrganisation: (org: Organisation) => void;
117
+ setSelectedOrganisation: (org: Organisation | null) => void;
117
118
  switchOrganisation: (orgId: string) => Promise<void>;
118
119
 
119
120
  // Security helpers