@jmruthers/pace-core 0.5.190 → 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 (249) hide show
  1. package/dist/{DataTable-IVYljGJ6.d.ts → DataTable-Be6dH_dR.d.ts} +1 -1
  2. package/dist/{DataTable-ON3IXISJ.js → DataTable-WKRZD47S.js} +6 -6
  3. package/dist/{PublicPageProvider-C4uxosp6.d.ts → PublicPageProvider-ULXC_u6U.d.ts} +1 -1
  4. package/dist/{UnifiedAuthProvider-X5NXANVI.js → UnifiedAuthProvider-FTSG5XH7.js} +3 -3
  5. package/dist/{api-I6UCQ5S6.js → api-IHKALJZD.js} +2 -2
  6. package/dist/{chunk-J2XXC7R5.js → chunk-6LTQQAT6.js} +77 -111
  7. package/dist/chunk-6LTQQAT6.js.map +1 -0
  8. package/dist/{chunk-STYK4OH2.js → chunk-6TQDD426.js} +10 -10
  9. package/dist/chunk-6TQDD426.js.map +1 -0
  10. package/dist/{chunk-DZWK57KZ.js → chunk-G37KK66H.js} +1 -1
  11. package/dist/{chunk-DZWK57KZ.js.map → chunk-G37KK66H.js.map} +1 -1
  12. package/dist/{chunk-73HSNNOQ.js → chunk-LOMZXPSN.js} +13 -13
  13. package/dist/{chunk-Y4BUBBHD.js → chunk-OETXORNB.js} +3 -3
  14. package/dist/{chunk-RUYZKXOD.js → chunk-ROXMHMY2.js} +5 -3
  15. package/dist/chunk-ROXMHMY2.js.map +1 -0
  16. package/dist/{chunk-SDMHPX3X.js → chunk-ULHIJK66.js} +56 -21
  17. package/dist/{chunk-SDMHPX3X.js.map → chunk-ULHIJK66.js.map} +1 -1
  18. package/dist/{chunk-VVBAW5A5.js → chunk-VKB2CO4Z.js} +46 -35
  19. package/dist/chunk-VKB2CO4Z.js.map +1 -0
  20. package/dist/{chunk-HQVPB5MZ.js → chunk-VRGWKHDB.js} +6 -6
  21. package/dist/{chunk-NIU6J6OX.js → chunk-XNYQOL3Z.js} +16 -16
  22. package/dist/chunk-XNYQOL3Z.js.map +1 -0
  23. package/dist/{chunk-4QYC5L4K.js → chunk-XYXSXPUK.js} +22 -27
  24. package/dist/chunk-XYXSXPUK.js.map +1 -0
  25. package/dist/components.d.ts +3 -3
  26. package/dist/components.js +8 -8
  27. package/dist/{database.generated-DI89OQeI.d.ts → database.generated-CzIvgcPu.d.ts} +165 -201
  28. package/dist/hooks.d.ts +12 -12
  29. package/dist/hooks.js +7 -7
  30. package/dist/index.d.ts +7 -7
  31. package/dist/index.js +18 -23
  32. package/dist/index.js.map +1 -1
  33. package/dist/providers.js +2 -2
  34. package/dist/rbac/index.d.ts +1 -1
  35. package/dist/rbac/index.js +6 -6
  36. package/dist/{types-Bwgl--Xo.d.ts → types-CEpcvwwF.d.ts} +1 -1
  37. package/dist/types.d.ts +2 -2
  38. package/dist/{usePublicRouteParams-DxIDS4bC.d.ts → usePublicRouteParams-TZe0gy-4.d.ts} +1 -1
  39. package/dist/utils.d.ts +8 -8
  40. package/dist/utils.js +2 -2
  41. package/docs/api/classes/ColumnFactory.md +1 -1
  42. package/docs/api/classes/ErrorBoundary.md +1 -1
  43. package/docs/api/classes/InvalidScopeError.md +1 -1
  44. package/docs/api/classes/Logger.md +1 -1
  45. package/docs/api/classes/MissingUserContextError.md +1 -1
  46. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  47. package/docs/api/classes/PermissionDeniedError.md +1 -1
  48. package/docs/api/classes/RBACAuditManager.md +2 -2
  49. package/docs/api/classes/RBACCache.md +1 -1
  50. package/docs/api/classes/RBACEngine.md +2 -2
  51. package/docs/api/classes/RBACError.md +1 -1
  52. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  53. package/docs/api/classes/SecureSupabaseClient.md +5 -5
  54. package/docs/api/classes/StorageUtils.md +1 -1
  55. package/docs/api/enums/FileCategory.md +1 -1
  56. package/docs/api/enums/LogLevel.md +1 -1
  57. package/docs/api/enums/RBACErrorCode.md +1 -1
  58. package/docs/api/enums/RPCFunction.md +1 -1
  59. package/docs/api/interfaces/AddressFieldProps.md +1 -1
  60. package/docs/api/interfaces/AddressFieldRef.md +1 -1
  61. package/docs/api/interfaces/AggregateConfig.md +1 -1
  62. package/docs/api/interfaces/AutocompleteOptions.md +1 -1
  63. package/docs/api/interfaces/AvatarProps.md +1 -1
  64. package/docs/api/interfaces/BadgeProps.md +1 -1
  65. package/docs/api/interfaces/ButtonProps.md +1 -1
  66. package/docs/api/interfaces/CalendarProps.md +1 -1
  67. package/docs/api/interfaces/CardProps.md +1 -1
  68. package/docs/api/interfaces/ColorPalette.md +1 -1
  69. package/docs/api/interfaces/ColorShade.md +1 -1
  70. package/docs/api/interfaces/ComplianceResult.md +1 -1
  71. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  72. package/docs/api/interfaces/DataRecord.md +1 -1
  73. package/docs/api/interfaces/DataTableAction.md +1 -1
  74. package/docs/api/interfaces/DataTableColumn.md +1 -1
  75. package/docs/api/interfaces/DataTableProps.md +1 -1
  76. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  77. package/docs/api/interfaces/DatabaseComplianceResult.md +1 -1
  78. package/docs/api/interfaces/DatabaseIssue.md +1 -1
  79. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  80. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  81. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  82. package/docs/api/interfaces/ExportColumn.md +1 -1
  83. package/docs/api/interfaces/ExportOptions.md +1 -1
  84. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  85. package/docs/api/interfaces/FileMetadata.md +1 -1
  86. package/docs/api/interfaces/FileReference.md +1 -1
  87. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  88. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  89. package/docs/api/interfaces/FileUploadProps.md +1 -1
  90. package/docs/api/interfaces/FooterProps.md +1 -1
  91. package/docs/api/interfaces/FormFieldProps.md +1 -1
  92. package/docs/api/interfaces/FormProps.md +1 -1
  93. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  94. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  95. package/docs/api/interfaces/InputProps.md +1 -1
  96. package/docs/api/interfaces/LabelProps.md +1 -1
  97. package/docs/api/interfaces/LoggerConfig.md +1 -1
  98. package/docs/api/interfaces/LoginFormProps.md +1 -1
  99. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  100. package/docs/api/interfaces/NavigationContextType.md +1 -1
  101. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  102. package/docs/api/interfaces/NavigationItem.md +1 -1
  103. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  104. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  105. package/docs/api/interfaces/Organisation.md +1 -1
  106. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  107. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  108. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  109. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  110. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  111. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  112. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  113. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  114. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  115. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  116. package/docs/api/interfaces/PaletteData.md +1 -1
  117. package/docs/api/interfaces/ParsedAddress.md +2 -2
  118. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  119. package/docs/api/interfaces/ProgressProps.md +1 -1
  120. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  121. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  122. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  123. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  124. package/docs/api/interfaces/QuickFix.md +1 -1
  125. package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
  126. package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
  127. package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
  128. package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
  129. package/docs/api/interfaces/RBACConfig.md +2 -2
  130. package/docs/api/interfaces/RBACContext.md +1 -1
  131. package/docs/api/interfaces/RBACLogger.md +1 -1
  132. package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
  133. package/docs/api/interfaces/RBACPerformanceMetrics.md +1 -1
  134. package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
  135. package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
  136. package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
  137. package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
  138. package/docs/api/interfaces/RBACResult.md +1 -1
  139. package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
  140. package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
  141. package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
  142. package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
  143. package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
  144. package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
  145. package/docs/api/interfaces/RBACRolesListParams.md +1 -1
  146. package/docs/api/interfaces/RBACRolesListResult.md +1 -1
  147. package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
  148. package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
  149. package/docs/api/interfaces/ResourcePermissions.md +1 -1
  150. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  151. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  152. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  153. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  154. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  155. package/docs/api/interfaces/RouteConfig.md +1 -1
  156. package/docs/api/interfaces/RuntimeComplianceResult.md +1 -1
  157. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  158. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  159. package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
  160. package/docs/api/interfaces/SetupIssue.md +1 -1
  161. package/docs/api/interfaces/StorageConfig.md +1 -1
  162. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  163. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  164. package/docs/api/interfaces/StorageListOptions.md +1 -1
  165. package/docs/api/interfaces/StorageListResult.md +1 -1
  166. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  167. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  168. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  169. package/docs/api/interfaces/StyleImport.md +1 -1
  170. package/docs/api/interfaces/SwitchProps.md +1 -1
  171. package/docs/api/interfaces/TabsContentProps.md +1 -1
  172. package/docs/api/interfaces/TabsListProps.md +1 -1
  173. package/docs/api/interfaces/TabsProps.md +1 -1
  174. package/docs/api/interfaces/TabsTriggerProps.md +1 -1
  175. package/docs/api/interfaces/TextareaProps.md +1 -1
  176. package/docs/api/interfaces/ToastActionElement.md +1 -1
  177. package/docs/api/interfaces/ToastProps.md +1 -1
  178. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  179. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  180. package/docs/api/interfaces/UseFormDialogOptions.md +1 -1
  181. package/docs/api/interfaces/UseFormDialogReturn.md +1 -1
  182. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  183. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  184. package/docs/api/interfaces/UsePublicEventLogoOptions.md +2 -2
  185. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  186. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  187. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  188. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +2 -2
  189. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  190. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  191. package/docs/api/interfaces/UseResolvedScopeOptions.md +2 -2
  192. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  193. package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
  194. package/docs/api/interfaces/UserEventAccess.md +1 -1
  195. package/docs/api/interfaces/UserMenuProps.md +1 -1
  196. package/docs/api/interfaces/UserProfile.md +1 -1
  197. package/docs/api/modules.md +16 -16
  198. package/docs/migration/README.md +18 -0
  199. package/docs/migration/database-changes-december-2025.md +767 -0
  200. package/docs/migration/person-scoped-profiles-migration-guide.md +472 -0
  201. package/package.json +1 -1
  202. package/src/__tests__/public-recipe-view.test.ts +10 -10
  203. package/src/__tests__/rls-policies.test.ts +13 -13
  204. package/src/components/AddressField/README.md +6 -6
  205. package/src/components/OrganisationSelector/OrganisationSelector.tsx +35 -15
  206. package/src/components/Select/Select.test.tsx +4 -1
  207. package/src/components/Select/Select.tsx +60 -15
  208. package/src/hooks/__tests__/usePermissionCache.simple.test.ts +192 -0
  209. package/src/hooks/__tests__/usePermissionCache.unit.test.ts +741 -0
  210. package/src/hooks/__tests__/usePublicEvent.simple.test.ts +703 -0
  211. package/src/hooks/__tests__/usePublicEvent.unit.test.ts +581 -0
  212. package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +9 -8
  213. package/src/hooks/public/usePublicEvent.ts +8 -8
  214. package/src/hooks/public/usePublicFileDisplay.ts +2 -2
  215. package/src/hooks/useFileDisplay.ts +8 -9
  216. package/src/hooks/useQueryCache.ts +6 -6
  217. package/src/hooks/useSecureDataAccess.test.ts +8 -8
  218. package/src/hooks/useSecureDataAccess.ts +15 -11
  219. package/src/providers/__tests__/OrganisationProvider.test.tsx +27 -21
  220. package/src/rbac/hooks/useRBAC.simple.test.ts +95 -0
  221. package/src/rbac/utils/__tests__/eventContext.test.ts +2 -2
  222. package/src/rbac/utils/__tests__/eventContext.unit.test.ts +490 -0
  223. package/src/rbac/utils/eventContext.ts +5 -2
  224. package/src/services/AuthService.ts +37 -8
  225. package/src/services/OrganisationService.ts +92 -139
  226. package/src/services/__tests__/OrganisationService.pagination.test.ts +34 -8
  227. package/src/services/__tests__/OrganisationService.test.ts +218 -86
  228. package/src/types/database.generated.ts +166 -201
  229. package/src/types/supabase.ts +2 -2
  230. package/src/utils/__tests__/secureDataAccess.unit.test.ts +3 -2
  231. package/src/utils/file-reference/index.ts +4 -4
  232. package/src/utils/google-places/googlePlacesUtils.ts +1 -1
  233. package/src/utils/google-places/types.ts +1 -1
  234. package/src/utils/request-deduplication.ts +4 -4
  235. package/src/utils/security/secureDataAccess.test.ts +1 -1
  236. package/src/utils/security/secureDataAccess.ts +7 -4
  237. package/src/utils/storage/README.md +1 -1
  238. package/dist/chunk-4QYC5L4K.js.map +0 -1
  239. package/dist/chunk-J2XXC7R5.js.map +0 -1
  240. package/dist/chunk-NIU6J6OX.js.map +0 -1
  241. package/dist/chunk-RUYZKXOD.js.map +0 -1
  242. package/dist/chunk-STYK4OH2.js.map +0 -1
  243. package/dist/chunk-VVBAW5A5.js.map +0 -1
  244. /package/dist/{DataTable-ON3IXISJ.js.map → DataTable-WKRZD47S.js.map} +0 -0
  245. /package/dist/{UnifiedAuthProvider-X5NXANVI.js.map → UnifiedAuthProvider-FTSG5XH7.js.map} +0 -0
  246. /package/dist/{api-I6UCQ5S6.js.map → api-IHKALJZD.js.map} +0 -0
  247. /package/dist/{chunk-73HSNNOQ.js.map → chunk-LOMZXPSN.js.map} +0 -0
  248. /package/dist/{chunk-Y4BUBBHD.js.map → chunk-OETXORNB.js.map} +0 -0
  249. /package/dist/{chunk-HQVPB5MZ.js.map → chunk-VRGWKHDB.js.map} +0 -0
@@ -0,0 +1,490 @@
1
+ /**
2
+ * @file Event Context Utilities Tests
3
+ * @package @jmruthers/pace-core
4
+ * @module RBAC/EventContext/Tests
5
+ * @since 1.0.0
6
+ *
7
+ * Comprehensive tests for event context utilities in the RBAC system.
8
+ */
9
+
10
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
11
+ import { SupabaseClient } from '@supabase/supabase-js';
12
+ import { Database } from '../../../types/database';
13
+ import { UUID, Scope } from '../../types';
14
+ import {
15
+ getOrganisationFromEvent,
16
+ createScopeFromEvent,
17
+ isEventBasedScope,
18
+ isValidEventBasedScope,
19
+ clearAllOrgDerivationCache,
20
+ clearOrgDerivationCache
21
+ } from '../eventContext';
22
+
23
+ // Mock Supabase client
24
+ const createMockSupabaseClient = () => {
25
+ const mockQuery = {
26
+ select: vi.fn().mockReturnThis(),
27
+ eq: vi.fn().mockReturnThis(),
28
+ single: vi.fn()
29
+ };
30
+
31
+ const fromMock = vi.fn().mockReturnValue(mockQuery);
32
+
33
+ return {
34
+ from: fromMock,
35
+ query: mockQuery
36
+ } as unknown as SupabaseClient<Database>;
37
+ };
38
+
39
+ describe('Event Context Utilities', () => {
40
+ let mockSupabase: SupabaseClient<Database>;
41
+ let mockQuery: any;
42
+
43
+ beforeEach(() => {
44
+ // Clear cache before each test
45
+ clearAllOrgDerivationCache();
46
+
47
+ mockSupabase = createMockSupabaseClient();
48
+ // Reset mockQuery to get a fresh query builder for each test
49
+ mockQuery = {
50
+ select: vi.fn().mockReturnThis(),
51
+ eq: vi.fn().mockReturnThis(),
52
+ single: vi.fn()
53
+ };
54
+ (mockSupabase.from as any).mockReturnValue(mockQuery);
55
+ });
56
+
57
+ afterEach(() => {
58
+ vi.clearAllMocks();
59
+ // Clear cache after each test
60
+ clearAllOrgDerivationCache();
61
+ });
62
+
63
+ describe('getOrganisationFromEvent', () => {
64
+ it('should return organisation ID when event exists', async () => {
65
+ const eventId = 'event-123';
66
+ const organisationId = 'org-456';
67
+
68
+ mockQuery.single.mockResolvedValue({
69
+ data: { organisation_id: organisationId },
70
+ error: null
71
+ });
72
+
73
+ const result = await getOrganisationFromEvent(mockSupabase, eventId);
74
+
75
+ expect(result).toBe(organisationId);
76
+ expect(mockSupabase.from).toHaveBeenCalledWith('core_events');
77
+ expect(mockQuery.select).toHaveBeenCalledWith('organisation_id');
78
+ expect(mockQuery.eq).toHaveBeenCalledWith('event_id', eventId);
79
+ expect(mockQuery.single).toHaveBeenCalled();
80
+ });
81
+
82
+ it('should return null when event does not exist', async () => {
83
+ const eventId = 'nonexistent-event';
84
+
85
+ mockQuery.single.mockResolvedValue({
86
+ data: null,
87
+ error: { message: 'Event not found' }
88
+ });
89
+
90
+ const result = await getOrganisationFromEvent(mockSupabase, eventId);
91
+
92
+ expect(result).toBeNull();
93
+ });
94
+
95
+ it('should return null when data is null', async () => {
96
+ const eventId = 'event-123';
97
+
98
+ // Clear cache for this specific test
99
+ clearOrgDerivationCache(eventId);
100
+
101
+ mockQuery.single.mockResolvedValue({
102
+ data: null,
103
+ error: null
104
+ });
105
+
106
+ const result = await getOrganisationFromEvent(mockSupabase, eventId);
107
+
108
+ expect(result).toBeNull();
109
+ });
110
+
111
+ it('should handle database errors gracefully', async () => {
112
+ const eventId = 'event-123';
113
+ const dbError = new Error('Database connection failed');
114
+
115
+ // Clear cache for this specific test
116
+ clearOrgDerivationCache(eventId);
117
+
118
+ mockQuery.single.mockRejectedValue(dbError);
119
+
120
+ await expect(getOrganisationFromEvent(mockSupabase, eventId))
121
+ .rejects.toThrow('Database connection failed');
122
+ });
123
+
124
+ it('should handle empty organisation_id', async () => {
125
+ const eventId = 'event-123';
126
+
127
+ // Clear cache for this specific test
128
+ clearOrgDerivationCache(eventId);
129
+
130
+ mockQuery.single.mockResolvedValue({
131
+ data: { organisation_id: null },
132
+ error: null
133
+ });
134
+
135
+ const result = await getOrganisationFromEvent(mockSupabase, eventId);
136
+
137
+ expect(result).toBeNull();
138
+ });
139
+ });
140
+
141
+ describe('createScopeFromEvent', () => {
142
+ it('should create complete scope when event exists', async () => {
143
+ const eventId = 'event-123';
144
+ const organisationId = 'org-456';
145
+ const appId = 'app-789';
146
+
147
+ mockQuery.single.mockResolvedValue({
148
+ data: { organisation_id: organisationId },
149
+ error: null
150
+ });
151
+
152
+ const result = await createScopeFromEvent(mockSupabase, eventId, appId);
153
+
154
+ expect(result).toEqual({
155
+ organisationId,
156
+ eventId,
157
+ appId
158
+ });
159
+ });
160
+
161
+ it('should create scope without appId when not provided', async () => {
162
+ const eventId = 'event-123';
163
+ const organisationId = 'org-456';
164
+
165
+ mockQuery.single.mockResolvedValue({
166
+ data: { organisation_id: organisationId },
167
+ error: null
168
+ });
169
+
170
+ const result = await createScopeFromEvent(mockSupabase, eventId);
171
+
172
+ expect(result).toEqual({
173
+ organisationId,
174
+ eventId,
175
+ appId: undefined
176
+ });
177
+ });
178
+
179
+ it('should return null when event does not exist', async () => {
180
+ const eventId = 'nonexistent-event';
181
+
182
+ mockQuery.single.mockResolvedValue({
183
+ data: null,
184
+ error: { message: 'Event not found' }
185
+ });
186
+
187
+ const result = await createScopeFromEvent(mockSupabase, eventId);
188
+
189
+ expect(result).toBeNull();
190
+ });
191
+
192
+ it('should return null when organisation lookup fails', async () => {
193
+ const eventId = 'event-123';
194
+
195
+ // Clear cache for this specific test
196
+ clearOrgDerivationCache(eventId);
197
+
198
+ mockQuery.single.mockResolvedValue({
199
+ data: { organisation_id: null },
200
+ error: null
201
+ });
202
+
203
+ const result = await createScopeFromEvent(mockSupabase, eventId);
204
+
205
+ expect(result).toBeNull();
206
+ });
207
+
208
+ it('should handle database errors gracefully', async () => {
209
+ const eventId = 'event-123';
210
+ const dbError = new Error('Database connection failed');
211
+
212
+ // Clear cache for this specific test
213
+ clearOrgDerivationCache(eventId);
214
+
215
+ mockQuery.single.mockRejectedValue(dbError);
216
+
217
+ await expect(createScopeFromEvent(mockSupabase, eventId))
218
+ .rejects.toThrow('Database connection failed');
219
+ });
220
+ });
221
+
222
+ describe('isEventBasedScope', () => {
223
+ it('should return true for event-based scope (no organisationId, has eventId)', () => {
224
+ const scope: Scope = {
225
+ eventId: 'event-123',
226
+ appId: 'app-456'
227
+ };
228
+
229
+ expect(isEventBasedScope(scope)).toBe(true);
230
+ });
231
+
232
+ it('should return false when organisationId is present', () => {
233
+ const scope: Scope = {
234
+ organisationId: 'org-123',
235
+ eventId: 'event-123',
236
+ appId: 'app-456'
237
+ };
238
+
239
+ expect(isEventBasedScope(scope)).toBe(false);
240
+ });
241
+
242
+ it('should return false when eventId is missing', () => {
243
+ const scope: Scope = {
244
+ appId: 'app-456'
245
+ };
246
+
247
+ expect(isEventBasedScope(scope)).toBe(false);
248
+ });
249
+
250
+ it('should return false when both organisationId and eventId are missing', () => {
251
+ const scope: Scope = {
252
+ appId: 'app-456'
253
+ };
254
+
255
+ expect(isEventBasedScope(scope)).toBe(false);
256
+ });
257
+
258
+ it('should return false when eventId is null', () => {
259
+ const scope: Scope = {
260
+ eventId: null,
261
+ appId: 'app-456'
262
+ };
263
+
264
+ expect(isEventBasedScope(scope)).toBe(false);
265
+ });
266
+
267
+ it('should return false when eventId is undefined', () => {
268
+ const scope: Scope = {
269
+ eventId: undefined,
270
+ appId: 'app-456'
271
+ };
272
+
273
+ expect(isEventBasedScope(scope)).toBe(false);
274
+ });
275
+ });
276
+
277
+ describe('isValidEventBasedScope', () => {
278
+ it('should return true for valid event-based scope', () => {
279
+ const scope: Scope = {
280
+ eventId: 'event-123',
281
+ appId: 'app-456'
282
+ };
283
+
284
+ expect(isValidEventBasedScope(scope)).toBe(true);
285
+ });
286
+
287
+ it('should return false when eventId is missing', () => {
288
+ const scope: Scope = {
289
+ appId: 'app-456'
290
+ };
291
+
292
+ expect(isValidEventBasedScope(scope)).toBe(false);
293
+ });
294
+
295
+ it('should return false when eventId is null', () => {
296
+ const scope: Scope = {
297
+ eventId: null,
298
+ appId: 'app-456'
299
+ };
300
+
301
+ expect(isValidEventBasedScope(scope)).toBe(false);
302
+ });
303
+
304
+ it('should return false when eventId is undefined', () => {
305
+ const scope: Scope = {
306
+ eventId: undefined,
307
+ appId: 'app-456'
308
+ };
309
+
310
+ expect(isValidEventBasedScope(scope)).toBe(false);
311
+ });
312
+
313
+ it('should return false when organisationId is present (not event-based)', () => {
314
+ const scope: Scope = {
315
+ organisationId: 'org-123',
316
+ eventId: 'event-123',
317
+ appId: 'app-456'
318
+ };
319
+
320
+ expect(isValidEventBasedScope(scope)).toBe(false);
321
+ });
322
+
323
+ it('should return true for event-based scope without appId', () => {
324
+ const scope: Scope = {
325
+ eventId: 'event-123'
326
+ };
327
+
328
+ expect(isValidEventBasedScope(scope)).toBe(true);
329
+ });
330
+ });
331
+
332
+ describe('Edge Cases and Error Handling', () => {
333
+ it('should handle malformed event IDs', async () => {
334
+ const malformedEventId = '';
335
+
336
+ mockQuery.single.mockResolvedValue({
337
+ data: null,
338
+ error: { message: 'Invalid event ID' }
339
+ });
340
+
341
+ const result = await getOrganisationFromEvent(mockSupabase, malformedEventId);
342
+
343
+ expect(result).toBeNull();
344
+ });
345
+
346
+ it('should handle very long event IDs', async () => {
347
+ const longEventId = 'a'.repeat(1000);
348
+ const organisationId = 'org-456';
349
+
350
+ mockQuery.single.mockResolvedValue({
351
+ data: { organisation_id: organisationId },
352
+ error: null
353
+ });
354
+
355
+ const result = await getOrganisationFromEvent(mockSupabase, longEventId);
356
+
357
+ expect(result).toBe(organisationId);
358
+ expect(mockQuery.eq).toHaveBeenCalledWith('event_id', longEventId);
359
+ });
360
+
361
+ it('should handle special characters in event IDs', async () => {
362
+ const specialEventId = 'event-123!@#$%^&*()';
363
+ const organisationId = 'org-456';
364
+
365
+ mockQuery.single.mockResolvedValue({
366
+ data: { organisation_id: organisationId },
367
+ error: null
368
+ });
369
+
370
+ const result = await getOrganisationFromEvent(mockSupabase, specialEventId);
371
+
372
+ expect(result).toBe(organisationId);
373
+ expect(mockQuery.eq).toHaveBeenCalledWith('event_id', specialEventId);
374
+ });
375
+
376
+ it('should handle concurrent calls to getOrganisationFromEvent', async () => {
377
+ const eventId1 = 'event-123';
378
+ const eventId2 = 'event-456';
379
+ const organisationId1 = 'org-123';
380
+ const organisationId2 = 'org-456';
381
+
382
+ // Clear cache for these specific events
383
+ clearOrgDerivationCache(eventId1);
384
+ clearOrgDerivationCache(eventId2);
385
+
386
+ // Create separate query builders for concurrent calls
387
+ const mockQuery1 = {
388
+ select: vi.fn().mockReturnThis(),
389
+ eq: vi.fn().mockReturnThis(),
390
+ single: vi.fn().mockResolvedValue({
391
+ data: { organisation_id: organisationId1 },
392
+ error: null
393
+ })
394
+ };
395
+ const mockQuery2 = {
396
+ select: vi.fn().mockReturnThis(),
397
+ eq: vi.fn().mockReturnThis(),
398
+ single: vi.fn().mockResolvedValue({
399
+ data: { organisation_id: organisationId2 },
400
+ error: null
401
+ })
402
+ };
403
+
404
+ (mockSupabase.from as any)
405
+ .mockReturnValueOnce(mockQuery1)
406
+ .mockReturnValueOnce(mockQuery2);
407
+
408
+ const [result1, result2] = await Promise.all([
409
+ getOrganisationFromEvent(mockSupabase, eventId1),
410
+ getOrganisationFromEvent(mockSupabase, eventId2)
411
+ ]);
412
+
413
+ expect(result1).toBe(organisationId1);
414
+ expect(result2).toBe(organisationId2);
415
+ });
416
+
417
+ it('should handle concurrent calls to createScopeFromEvent', async () => {
418
+ const eventId1 = 'event-123';
419
+ const eventId2 = 'event-456';
420
+ const organisationId1 = 'org-123';
421
+ const organisationId2 = 'org-456';
422
+ const appId1 = 'app-123';
423
+ const appId2 = 'app-456';
424
+
425
+ // Clear cache for these specific events
426
+ clearOrgDerivationCache(eventId1);
427
+ clearOrgDerivationCache(eventId2);
428
+
429
+ // Create separate query builders for concurrent calls
430
+ const mockQuery1 = {
431
+ select: vi.fn().mockReturnThis(),
432
+ eq: vi.fn().mockReturnThis(),
433
+ single: vi.fn().mockResolvedValue({
434
+ data: { organisation_id: organisationId1 },
435
+ error: null
436
+ })
437
+ };
438
+ const mockQuery2 = {
439
+ select: vi.fn().mockReturnThis(),
440
+ eq: vi.fn().mockReturnThis(),
441
+ single: vi.fn().mockResolvedValue({
442
+ data: { organisation_id: organisationId2 },
443
+ error: null
444
+ })
445
+ };
446
+
447
+ (mockSupabase.from as any)
448
+ .mockReturnValueOnce(mockQuery1)
449
+ .mockReturnValueOnce(mockQuery2);
450
+
451
+ const [result1, result2] = await Promise.all([
452
+ createScopeFromEvent(mockSupabase, eventId1, appId1),
453
+ createScopeFromEvent(mockSupabase, eventId2, appId2)
454
+ ]);
455
+
456
+ expect(result1).toEqual({
457
+ organisationId: organisationId1,
458
+ eventId: eventId1,
459
+ appId: appId1
460
+ });
461
+ expect(result2).toEqual({
462
+ organisationId: organisationId2,
463
+ eventId: eventId2,
464
+ appId: appId2
465
+ });
466
+ });
467
+ });
468
+
469
+ describe('Type Safety', () => {
470
+ it('should handle UUID types correctly', () => {
471
+ const validUUID = '123e4567-e89b-12d3-a456-426614174000';
472
+ const scope: Scope = {
473
+ eventId: validUUID,
474
+ appId: validUUID
475
+ };
476
+
477
+ expect(isValidEventBasedScope(scope)).toBe(true);
478
+ });
479
+
480
+ it('should handle string types correctly', () => {
481
+ const stringId = 'event-123';
482
+ const scope: Scope = {
483
+ eventId: stringId,
484
+ appId: 'app-456'
485
+ };
486
+
487
+ expect(isValidEventBasedScope(scope)).toBe(true);
488
+ });
489
+ });
490
+ });
@@ -56,7 +56,7 @@ export async function getOrganisationFromEvent(
56
56
 
57
57
  // Query database
58
58
  const { data, error } = await supabase
59
- .from('event')
59
+ .from('core_events')
60
60
  .select('organisation_id')
61
61
  .eq('event_id', eventId)
62
62
  .single() as { data: { organisation_id: string } | null; error: any };
@@ -65,8 +65,11 @@ export async function getOrganisationFromEvent(
65
65
 
66
66
  if (error || !data) {
67
67
  organisationId = null;
68
- } else {
68
+ } else if (data.organisation_id) {
69
69
  organisationId = data.organisation_id;
70
+ } else {
71
+ // organisation_id is null or undefined
72
+ organisationId = null;
70
73
  }
71
74
 
72
75
  // Cache the result (with size limit to prevent memory leaks)
@@ -299,7 +299,15 @@ export class AuthService extends BaseService implements IAuthService {
299
299
  // Lifecycle methods
300
300
  async initialize(): Promise<void> {
301
301
  await super.initialize();
302
+ // Set loading to true before starting session restoration
303
+ // This ensures ProtectedRoute shows loading state while session is being restored
304
+ this.authLoading = true;
305
+ this.notify();
306
+
307
+ // Setup auth state listener first - this will receive INITIAL_SESSION event
302
308
  await this.setupAuthStateListener();
309
+
310
+ // Then restore session - this will trigger INITIAL_SESSION event if session exists
303
311
  await this.restoreSession();
304
312
  }
305
313
 
@@ -431,17 +439,25 @@ export class AuthService extends BaseService implements IAuthService {
431
439
  this.sessionRestorationState.restorationError ||
432
440
  (hasTimeoutError && session)) {
433
441
  this.finishSessionRestoration();
434
- return;
442
+ }
443
+ } else {
444
+ // No session in INITIAL_SESSION event - user is not authenticated
445
+ // Finish restoration to clear loading state
446
+ if (this.sessionRestorationState.isRestoring) {
447
+ this.finishSessionRestoration();
435
448
  }
436
449
  }
437
450
 
438
- if (this.sessionRestorationState.isRestoring) {
439
- this.finishSessionRestoration();
440
- return;
441
- }
451
+ // CRITICAL: Set loading to false AFTER handling INITIAL_SESSION
452
+ // This ensures ProtectedRoute waits for session restoration to complete
453
+ // before checking authentication state
454
+ this.authLoading = false;
455
+ this.notify();
456
+ return; // Return early to avoid setting loading to false again below
442
457
  }
443
458
 
444
- // Always set loading to false after any auth state change
459
+ // For other events (SIGNED_IN, SIGNED_OUT, TOKEN_REFRESHED), set loading to false
460
+ // INITIAL_SESSION is handled above and returns early
445
461
  this.authLoading = false;
446
462
  this.notify();
447
463
  } catch (error) {
@@ -519,8 +535,21 @@ export class AuthService extends BaseService implements IAuthService {
519
535
  this.authError = null;
520
536
  }
521
537
 
522
- // Finish successfully even if earlier calls reported an error, to avoid noisy warnings in benign cases
523
- this.finishSessionRestoration();
538
+ // CRITICAL FIX: Don't finish restoration here - wait for INITIAL_SESSION event
539
+ // The INITIAL_SESSION event should fire when the auth state listener is set up
540
+ // However, if it doesn't fire within a short delay (e.g., edge case), we'll finish it
541
+ // This ensures ProtectedRoute waits for the event before checking auth state
542
+
543
+ // Set a short fallback timeout to finish restoration if INITIAL_SESSION doesn't fire
544
+ // This handles edge cases where the event might be delayed or not fire
545
+ setTimeout(() => {
546
+ // Only finish if restoration is still in progress and INITIAL_SESSION hasn't fired
547
+ // The INITIAL_SESSION handler will have set restorationComplete to true if it fired
548
+ if (this.sessionRestorationState.isRestoring && !this.sessionRestorationState.restorationComplete) {
549
+ logger.debug('AuthService', 'INITIAL_SESSION event did not fire, finishing restoration');
550
+ this.finishSessionRestoration();
551
+ }
552
+ }, 100); // 100ms fallback - INITIAL_SESSION should fire immediately when listener is set up
524
553
  } catch (error) {
525
554
  const restorationError = error instanceof Error
526
555
  ? error