@jmruthers/pace-core 0.6.2 → 0.6.3

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 (299) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/cursor-rules/00-pace-core-compliance.mdc +34 -2
  3. package/dist/{AuthService-BPvc3Ka0.d.ts → AuthService-Cb34EQs3.d.ts} +9 -1
  4. package/dist/{DataTable-TPTKCX4D.js → DataTable-THFPBKTP.js} +9 -8
  5. package/dist/{PublicPageProvider-DC6kCaqf.d.ts → PublicPageProvider-DEMpysFR.d.ts} +45 -67
  6. package/dist/{UnifiedAuthProvider-CVcTjx-d.d.ts → UnifiedAuthProvider-CKvHP1MK.d.ts} +1 -8
  7. package/dist/{UnifiedAuthProvider-CH6Z342H.js → UnifiedAuthProvider-KAGUYQ4J.js} +5 -4
  8. package/dist/{api-MVVQZLJI.js → api-IAGWF3ZG.js} +10 -10
  9. package/dist/{audit-B5P6FFIR.js → audit-V53FV5AG.js} +2 -2
  10. package/dist/{chunk-SFZUDBL5.js → chunk-2T2IG7T7.js} +70 -56
  11. package/dist/chunk-2T2IG7T7.js.map +1 -0
  12. package/dist/{chunk-MMZ7JXPU.js → chunk-6Z7LTB3D.js} +13 -21
  13. package/dist/{chunk-MMZ7JXPU.js.map → chunk-6Z7LTB3D.js.map} +1 -1
  14. package/dist/{chunk-6J4GEEJR.js → chunk-CNCQDFLN.js} +53 -27
  15. package/dist/chunk-CNCQDFLN.js.map +1 -0
  16. package/dist/chunk-DGUM43GV.js +11 -0
  17. package/dist/{chunk-EHMR7VYL.js → chunk-DWUBLJJM.js} +361 -187
  18. package/dist/chunk-DWUBLJJM.js.map +1 -0
  19. package/dist/{chunk-2UOI2FG5.js → chunk-HFZBI76P.js} +4 -4
  20. package/dist/{chunk-F2IMUDXZ.js → chunk-M7MPQISP.js} +2 -2
  21. package/dist/{chunk-3XC4CPTD.js → chunk-PQBSKX33.js} +244 -5727
  22. package/dist/chunk-PQBSKX33.js.map +1 -0
  23. package/dist/chunk-QRPVRXYT.js +226 -0
  24. package/dist/chunk-QRPVRXYT.js.map +1 -0
  25. package/dist/{chunk-24UVZUZG.js → chunk-RWEBCB47.js} +129 -387
  26. package/dist/chunk-RWEBCB47.js.map +1 -0
  27. package/dist/{chunk-XWQCNGTQ.js → chunk-YDQHOZNA.js} +173 -79
  28. package/dist/chunk-YDQHOZNA.js.map +1 -0
  29. package/dist/{chunk-NECFR5MM.js → chunk-ZNIWI3UC.js} +562 -644
  30. package/dist/chunk-ZNIWI3UC.js.map +1 -0
  31. package/dist/components.d.ts +2 -2
  32. package/dist/components.js +12 -13
  33. package/dist/contextValidator-3JNZKUTX.js +9 -0
  34. package/dist/contextValidator-3JNZKUTX.js.map +1 -0
  35. package/dist/eslint-rules/pace-core-compliance.cjs +106 -0
  36. package/dist/hooks.d.ts +2 -2
  37. package/dist/hooks.js +7 -6
  38. package/dist/hooks.js.map +1 -1
  39. package/dist/index.d.ts +7 -7
  40. package/dist/index.js +21 -16
  41. package/dist/index.js.map +1 -1
  42. package/dist/providers.d.ts +3 -3
  43. package/dist/providers.js +4 -3
  44. package/dist/rbac/index.d.ts +67 -27
  45. package/dist/rbac/index.js +15 -8
  46. package/dist/styles/index.js +1 -1
  47. package/dist/theming/runtime.js +1 -1
  48. package/dist/types.js +1 -1
  49. package/dist/{usePublicRouteParams-1oMokgLF.d.ts → usePublicRouteParams-i3qtoBgg.d.ts} +7 -16
  50. package/dist/utils.js +5 -7
  51. package/dist/utils.js.map +1 -1
  52. package/docs/api/README.md +14 -16
  53. package/docs/api/modules.md +3796 -2513
  54. package/docs/components/context-selector.md +126 -0
  55. package/docs/migration/RBAC_SCOPE_MIGRATION.md +385 -0
  56. package/docs/pace-mint-fix-auto-selection.md +218 -0
  57. package/docs/pace-mint-rbac-setup.md +391 -0
  58. package/docs/rbac/secure-client-protection.md +330 -0
  59. package/package.json +3 -3
  60. package/scripts/audit/core/checks/compliance.cjs +72 -0
  61. package/scripts/audit/core/checks/dependencies.cjs +559 -28
  62. package/scripts/audit/core/checks/documentation.cjs +68 -3
  63. package/scripts/audit/core/checks/environment.cjs +2 -14
  64. package/scripts/audit/core/checks/error-handling.cjs +47 -6
  65. package/src/components/ContextSelector/ContextSelector.tsx +384 -0
  66. package/src/components/ContextSelector/index.ts +3 -0
  67. package/src/components/DataTable/components/RowComponent.tsx +19 -19
  68. package/src/components/DataTable/components/UnifiedTableBody.tsx +2 -2
  69. package/src/components/DataTable/hooks/useDataTablePermissions.ts +8 -6
  70. package/src/components/Dialog/Dialog.tsx +29 -1
  71. package/src/components/FileDisplay/FileDisplay.tsx +42 -10
  72. package/src/components/Header/Header.test.tsx +43 -73
  73. package/src/components/Header/Header.tsx +44 -45
  74. package/src/components/PaceAppLayout/PaceAppLayout.integration.test.tsx +10 -19
  75. package/src/components/PaceAppLayout/PaceAppLayout.performance.test.tsx +2 -2
  76. package/src/components/PaceAppLayout/PaceAppLayout.security.test.tsx +5 -5
  77. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +9 -9
  78. package/src/components/PaceAppLayout/PaceAppLayout.tsx +135 -33
  79. package/src/components/PaceAppLayout/README.md +14 -17
  80. package/src/components/PaceAppLayout/test-setup.tsx +2 -2
  81. package/src/components/index.ts +5 -5
  82. package/src/eslint-rules/pace-core-compliance.cjs +106 -0
  83. package/src/hooks/__tests__/useAppConfig.unit.test.ts +4 -98
  84. package/src/hooks/useAppConfig.ts +15 -30
  85. package/src/hooks/useFileDisplay.ts +77 -50
  86. package/src/index.ts +4 -5
  87. package/src/providers/services/AuthServiceProvider.tsx +17 -7
  88. package/src/providers/services/EventServiceProvider.tsx +33 -5
  89. package/src/providers/services/UnifiedAuthProvider.tsx +90 -134
  90. package/src/rbac/__tests__/adapters.comprehensive.test.tsx +1 -1
  91. package/src/rbac/adapters.tsx +2 -2
  92. package/src/rbac/api.test.ts +59 -51
  93. package/src/rbac/api.ts +178 -132
  94. package/src/rbac/components/PagePermissionGuard.tsx +38 -10
  95. package/src/rbac/hooks/__tests__/useSecureSupabase.test.ts +32 -21
  96. package/src/rbac/hooks/permissions/useAccessLevel.ts +1 -1
  97. package/src/rbac/hooks/permissions/useCan.ts +41 -11
  98. package/src/rbac/hooks/permissions/useHasAllPermissions.ts +1 -1
  99. package/src/rbac/hooks/permissions/useHasAnyPermission.ts +1 -1
  100. package/src/rbac/hooks/permissions/useMultiplePermissions.ts +1 -1
  101. package/src/rbac/hooks/useCan.test.ts +0 -9
  102. package/src/rbac/hooks/useRBAC.test.ts +1 -5
  103. package/src/rbac/hooks/useRBAC.ts +36 -37
  104. package/src/rbac/hooks/useResolvedScope.test.ts +120 -35
  105. package/src/rbac/hooks/useResolvedScope.ts +35 -40
  106. package/src/rbac/hooks/useSecureSupabase.ts +7 -7
  107. package/src/rbac/index.ts +7 -0
  108. package/src/rbac/secureClient.test.ts +22 -18
  109. package/src/rbac/secureClient.ts +103 -16
  110. package/src/rbac/security.ts +0 -17
  111. package/src/rbac/types.ts +1 -0
  112. package/src/rbac/utils/__tests__/contextValidator.test.ts +64 -86
  113. package/src/rbac/utils/clientSecurity.ts +93 -0
  114. package/src/rbac/utils/contextValidator.ts +77 -168
  115. package/src/services/AuthService.ts +39 -7
  116. package/src/services/EventService.ts +186 -54
  117. package/src/services/OrganisationService.ts +81 -14
  118. package/src/services/__tests__/EventService.test.ts +1 -2
  119. package/src/services/base/BaseService.ts +3 -0
  120. package/src/utils/dynamic/dynamicUtils.ts +7 -4
  121. package/dist/chunk-24UVZUZG.js.map +0 -1
  122. package/dist/chunk-3XC4CPTD.js.map +0 -1
  123. package/dist/chunk-6J4GEEJR.js.map +0 -1
  124. package/dist/chunk-7D4SUZUM.js +0 -38
  125. package/dist/chunk-EHMR7VYL.js.map +0 -1
  126. package/dist/chunk-NECFR5MM.js.map +0 -1
  127. package/dist/chunk-SFZUDBL5.js.map +0 -1
  128. package/dist/chunk-XWQCNGTQ.js.map +0 -1
  129. package/docs/api/classes/ColumnFactory.md +0 -243
  130. package/docs/api/classes/InvalidScopeError.md +0 -73
  131. package/docs/api/classes/Logger.md +0 -178
  132. package/docs/api/classes/MissingUserContextError.md +0 -66
  133. package/docs/api/classes/OrganisationContextRequiredError.md +0 -66
  134. package/docs/api/classes/PermissionDeniedError.md +0 -73
  135. package/docs/api/classes/RBACAuditManager.md +0 -297
  136. package/docs/api/classes/RBACCache.md +0 -322
  137. package/docs/api/classes/RBACEngine.md +0 -171
  138. package/docs/api/classes/RBACError.md +0 -76
  139. package/docs/api/classes/RBACNotInitializedError.md +0 -66
  140. package/docs/api/classes/SecureSupabaseClient.md +0 -163
  141. package/docs/api/classes/StorageUtils.md +0 -328
  142. package/docs/api/enums/FileCategory.md +0 -184
  143. package/docs/api/enums/LogLevel.md +0 -54
  144. package/docs/api/enums/RBACErrorCode.md +0 -228
  145. package/docs/api/enums/RPCFunction.md +0 -118
  146. package/docs/api/interfaces/AddressFieldProps.md +0 -241
  147. package/docs/api/interfaces/AddressFieldRef.md +0 -94
  148. package/docs/api/interfaces/AggregateConfig.md +0 -43
  149. package/docs/api/interfaces/AutocompleteOptions.md +0 -75
  150. package/docs/api/interfaces/AvatarProps.md +0 -128
  151. package/docs/api/interfaces/BadgeProps.md +0 -34
  152. package/docs/api/interfaces/ButtonProps.md +0 -56
  153. package/docs/api/interfaces/CalendarProps.md +0 -73
  154. package/docs/api/interfaces/CardProps.md +0 -69
  155. package/docs/api/interfaces/ColorPalette.md +0 -7
  156. package/docs/api/interfaces/ColorShade.md +0 -66
  157. package/docs/api/interfaces/ComplianceResult.md +0 -30
  158. package/docs/api/interfaces/DataAccessRecord.md +0 -96
  159. package/docs/api/interfaces/DataRecord.md +0 -11
  160. package/docs/api/interfaces/DataTableAction.md +0 -252
  161. package/docs/api/interfaces/DataTableColumn.md +0 -504
  162. package/docs/api/interfaces/DataTableProps.md +0 -625
  163. package/docs/api/interfaces/DataTableToolbarButton.md +0 -96
  164. package/docs/api/interfaces/DatabaseComplianceResult.md +0 -85
  165. package/docs/api/interfaces/DatabaseIssue.md +0 -41
  166. package/docs/api/interfaces/EmptyStateConfig.md +0 -61
  167. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +0 -235
  168. package/docs/api/interfaces/ErrorBoundaryProps.md +0 -147
  169. package/docs/api/interfaces/ErrorBoundaryProviderProps.md +0 -36
  170. package/docs/api/interfaces/ErrorBoundaryState.md +0 -75
  171. package/docs/api/interfaces/EventAppRoleData.md +0 -71
  172. package/docs/api/interfaces/ExportColumn.md +0 -90
  173. package/docs/api/interfaces/ExportOptions.md +0 -126
  174. package/docs/api/interfaces/FileDisplayProps.md +0 -249
  175. package/docs/api/interfaces/FileMetadata.md +0 -129
  176. package/docs/api/interfaces/FileReference.md +0 -118
  177. package/docs/api/interfaces/FileSizeLimits.md +0 -7
  178. package/docs/api/interfaces/FileUploadOptions.md +0 -139
  179. package/docs/api/interfaces/FileUploadProps.md +0 -296
  180. package/docs/api/interfaces/FooterProps.md +0 -107
  181. package/docs/api/interfaces/FormFieldProps.md +0 -166
  182. package/docs/api/interfaces/FormProps.md +0 -113
  183. package/docs/api/interfaces/GrantEventAppRoleParams.md +0 -122
  184. package/docs/api/interfaces/InactivityWarningModalProps.md +0 -115
  185. package/docs/api/interfaces/InputProps.md +0 -56
  186. package/docs/api/interfaces/LabelProps.md +0 -107
  187. package/docs/api/interfaces/LoggerConfig.md +0 -62
  188. package/docs/api/interfaces/LoginFormProps.md +0 -187
  189. package/docs/api/interfaces/NavigationAccessRecord.md +0 -107
  190. package/docs/api/interfaces/NavigationContextType.md +0 -164
  191. package/docs/api/interfaces/NavigationGuardProps.md +0 -139
  192. package/docs/api/interfaces/NavigationItem.md +0 -120
  193. package/docs/api/interfaces/NavigationMenuProps.md +0 -221
  194. package/docs/api/interfaces/NavigationProviderProps.md +0 -117
  195. package/docs/api/interfaces/Organisation.md +0 -140
  196. package/docs/api/interfaces/OrganisationContextType.md +0 -388
  197. package/docs/api/interfaces/OrganisationMembership.md +0 -140
  198. package/docs/api/interfaces/OrganisationProviderProps.md +0 -76
  199. package/docs/api/interfaces/OrganisationSecurityError.md +0 -62
  200. package/docs/api/interfaces/PaceAppLayoutProps.md +0 -409
  201. package/docs/api/interfaces/PaceLoginPageProps.md +0 -49
  202. package/docs/api/interfaces/PageAccessRecord.md +0 -85
  203. package/docs/api/interfaces/PagePermissionContextType.md +0 -140
  204. package/docs/api/interfaces/PagePermissionGuardProps.md +0 -153
  205. package/docs/api/interfaces/PagePermissionProviderProps.md +0 -119
  206. package/docs/api/interfaces/PaletteData.md +0 -41
  207. package/docs/api/interfaces/ParsedAddress.md +0 -120
  208. package/docs/api/interfaces/PermissionEnforcerProps.md +0 -153
  209. package/docs/api/interfaces/ProgressProps.md +0 -42
  210. package/docs/api/interfaces/ProtectedRouteProps.md +0 -78
  211. package/docs/api/interfaces/PublicPageFooterProps.md +0 -112
  212. package/docs/api/interfaces/PublicPageHeaderProps.md +0 -125
  213. package/docs/api/interfaces/PublicPageLayoutProps.md +0 -185
  214. package/docs/api/interfaces/QuickFix.md +0 -52
  215. package/docs/api/interfaces/RBACAccessValidateParams.md +0 -52
  216. package/docs/api/interfaces/RBACAccessValidateResult.md +0 -41
  217. package/docs/api/interfaces/RBACAuditLogParams.md +0 -85
  218. package/docs/api/interfaces/RBACAuditLogResult.md +0 -52
  219. package/docs/api/interfaces/RBACConfig.md +0 -133
  220. package/docs/api/interfaces/RBACContext.md +0 -52
  221. package/docs/api/interfaces/RBACLogger.md +0 -112
  222. package/docs/api/interfaces/RBACPageAccessCheckParams.md +0 -74
  223. package/docs/api/interfaces/RBACPerformanceMetrics.md +0 -138
  224. package/docs/api/interfaces/RBACPermissionCheckParams.md +0 -74
  225. package/docs/api/interfaces/RBACPermissionCheckResult.md +0 -52
  226. package/docs/api/interfaces/RBACPermissionsGetParams.md +0 -63
  227. package/docs/api/interfaces/RBACPermissionsGetResult.md +0 -63
  228. package/docs/api/interfaces/RBACResult.md +0 -58
  229. package/docs/api/interfaces/RBACRoleGrantParams.md +0 -63
  230. package/docs/api/interfaces/RBACRoleGrantResult.md +0 -52
  231. package/docs/api/interfaces/RBACRoleRevokeParams.md +0 -63
  232. package/docs/api/interfaces/RBACRoleRevokeResult.md +0 -52
  233. package/docs/api/interfaces/RBACRoleValidateParams.md +0 -52
  234. package/docs/api/interfaces/RBACRoleValidateResult.md +0 -63
  235. package/docs/api/interfaces/RBACRolesListParams.md +0 -52
  236. package/docs/api/interfaces/RBACRolesListResult.md +0 -74
  237. package/docs/api/interfaces/RBACSessionTrackParams.md +0 -74
  238. package/docs/api/interfaces/RBACSessionTrackResult.md +0 -52
  239. package/docs/api/interfaces/ResourcePermissions.md +0 -155
  240. package/docs/api/interfaces/RevokeEventAppRoleParams.md +0 -100
  241. package/docs/api/interfaces/RoleBasedRouterContextType.md +0 -151
  242. package/docs/api/interfaces/RoleBasedRouterProps.md +0 -156
  243. package/docs/api/interfaces/RoleManagementResult.md +0 -52
  244. package/docs/api/interfaces/RouteAccessRecord.md +0 -107
  245. package/docs/api/interfaces/RouteConfig.md +0 -134
  246. package/docs/api/interfaces/RuntimeComplianceResult.md +0 -55
  247. package/docs/api/interfaces/SecureDataContextType.md +0 -168
  248. package/docs/api/interfaces/SecureDataProviderProps.md +0 -132
  249. package/docs/api/interfaces/SessionRestorationLoaderProps.md +0 -34
  250. package/docs/api/interfaces/SetupIssue.md +0 -41
  251. package/docs/api/interfaces/StorageConfig.md +0 -41
  252. package/docs/api/interfaces/StorageFileInfo.md +0 -74
  253. package/docs/api/interfaces/StorageFileMetadata.md +0 -151
  254. package/docs/api/interfaces/StorageListOptions.md +0 -99
  255. package/docs/api/interfaces/StorageListResult.md +0 -41
  256. package/docs/api/interfaces/StorageUploadOptions.md +0 -101
  257. package/docs/api/interfaces/StorageUploadResult.md +0 -63
  258. package/docs/api/interfaces/StorageUrlOptions.md +0 -60
  259. package/docs/api/interfaces/StyleImport.md +0 -19
  260. package/docs/api/interfaces/SwitchProps.md +0 -34
  261. package/docs/api/interfaces/TabsContentProps.md +0 -9
  262. package/docs/api/interfaces/TabsListProps.md +0 -9
  263. package/docs/api/interfaces/TabsProps.md +0 -9
  264. package/docs/api/interfaces/TabsTriggerProps.md +0 -50
  265. package/docs/api/interfaces/TextareaProps.md +0 -53
  266. package/docs/api/interfaces/ToastActionElement.md +0 -12
  267. package/docs/api/interfaces/ToastProps.md +0 -9
  268. package/docs/api/interfaces/UnifiedAuthContextType.md +0 -823
  269. package/docs/api/interfaces/UnifiedAuthProviderProps.md +0 -173
  270. package/docs/api/interfaces/UseFormDialogOptions.md +0 -62
  271. package/docs/api/interfaces/UseFormDialogReturn.md +0 -117
  272. package/docs/api/interfaces/UseInactivityTrackerOptions.md +0 -138
  273. package/docs/api/interfaces/UseInactivityTrackerReturn.md +0 -123
  274. package/docs/api/interfaces/UsePublicEventLogoOptions.md +0 -87
  275. package/docs/api/interfaces/UsePublicEventLogoReturn.md +0 -84
  276. package/docs/api/interfaces/UsePublicEventOptions.md +0 -34
  277. package/docs/api/interfaces/UsePublicEventReturn.md +0 -71
  278. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +0 -47
  279. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +0 -123
  280. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +0 -97
  281. package/docs/api/interfaces/UseResolvedScopeOptions.md +0 -47
  282. package/docs/api/interfaces/UseResolvedScopeReturn.md +0 -47
  283. package/docs/api/interfaces/UseResourcePermissionsOptions.md +0 -34
  284. package/docs/api/interfaces/UserEventAccess.md +0 -121
  285. package/docs/api/interfaces/UserMenuProps.md +0 -88
  286. package/docs/api/interfaces/UserProfile.md +0 -63
  287. package/src/components/EventSelector/EventSelector.test.tsx +0 -720
  288. package/src/components/EventSelector/EventSelector.tsx +0 -423
  289. package/src/components/EventSelector/index.ts +0 -3
  290. package/src/components/OrganisationSelector/OrganisationSelector.test.tsx +0 -784
  291. package/src/components/OrganisationSelector/OrganisationSelector.tsx +0 -327
  292. package/src/components/OrganisationSelector/index.ts +0 -9
  293. /package/dist/{DataTable-TPTKCX4D.js.map → DataTable-THFPBKTP.js.map} +0 -0
  294. /package/dist/{UnifiedAuthProvider-CH6Z342H.js.map → UnifiedAuthProvider-KAGUYQ4J.js.map} +0 -0
  295. /package/dist/{api-MVVQZLJI.js.map → api-IAGWF3ZG.js.map} +0 -0
  296. /package/dist/{audit-B5P6FFIR.js.map → audit-V53FV5AG.js.map} +0 -0
  297. /package/dist/{chunk-7D4SUZUM.js.map → chunk-DGUM43GV.js.map} +0 -0
  298. /package/dist/{chunk-2UOI2FG5.js.map → chunk-HFZBI76P.js.map} +0 -0
  299. /package/dist/{chunk-F2IMUDXZ.js.map → chunk-M7MPQISP.js.map} +0 -0
@@ -43,10 +43,18 @@ export function useCan(
43
43
  precomputedSuperAdmin: boolean | null = null,
44
44
  appName?: string,
45
45
  ) {
46
- const [can, setCan] = useState<boolean>(false);
47
- const [isLoading, setIsLoading] = useState(true);
48
- const [error, setError] = useState<Error | null>(null);
46
+ // CRITICAL FIX: Initialize isSuperAdmin from precomputed value immediately
47
+ // This prevents permission checks from running when we already know the user is a super admin
48
+ // If precomputedSuperAdmin is true, we can immediately set can=true and isLoading=false
49
49
  const [isSuperAdmin, setIsSuperAdmin] = useState<boolean | null>(precomputedSuperAdmin ?? null);
50
+
51
+ // For super admins, immediately grant permissions without waiting
52
+ const initialCan = precomputedSuperAdmin === true ? true : false;
53
+ const initialIsLoading = precomputedSuperAdmin === true ? false : true;
54
+
55
+ const [can, setCan] = useState<boolean>(initialCan);
56
+ const [isLoading, setIsLoading] = useState<boolean>(initialIsLoading);
57
+ const [error, setError] = useState<Error | null>(null);
50
58
 
51
59
  // Validate scope parameter - handle undefined/null scope gracefully
52
60
  const isValidScope = scope && typeof scope === 'object';
@@ -54,6 +62,19 @@ export function useCan(
54
62
  const eventId = isValidScope ? scope.eventId : undefined;
55
63
  const appId = isValidScope ? scope.appId : undefined;
56
64
 
65
+ // CRITICAL FIX: Immediately update state when precomputedSuperAdmin changes to true
66
+ // This ensures super admins get immediate access without waiting for permission checks
67
+ useEffect(() => {
68
+ if (precomputedSuperAdmin === true && isSuperAdmin !== true) {
69
+ setIsSuperAdmin(true);
70
+ setCan(true);
71
+ setIsLoading(false);
72
+ setError(null);
73
+ } else if (precomputedSuperAdmin === false && isSuperAdmin !== false) {
74
+ setIsSuperAdmin(false);
75
+ }
76
+ }, [precomputedSuperAdmin, isSuperAdmin]);
77
+
57
78
  // Check super-admin status - super admins bypass organisation context requirements
58
79
  // PERFORMANCE OPTIMIZATION: Use precomputed value directly - no duplicate checks
59
80
  // Callers must check super admin once and pass the result (null if not checked yet)
@@ -93,6 +114,12 @@ export function useCan(
93
114
  });
94
115
  }
95
116
  setIsSuperAdmin(isSuper);
117
+ // If super admin, immediately grant permissions
118
+ if (isSuper) {
119
+ setCan(true);
120
+ setIsLoading(false);
121
+ setError(null);
122
+ }
96
123
  }
97
124
  } catch (err) {
98
125
  if (!cancelled) {
@@ -111,9 +138,6 @@ export function useCan(
111
138
  return () => {
112
139
  cancelled = true;
113
140
  };
114
- } else {
115
- // Precomputed value provided (true/false) - use it directly, no check needed
116
- setIsSuperAdmin(precomputedSuperAdmin);
117
141
  }
118
142
  }, [userId, precomputedSuperAdmin]);
119
143
 
@@ -150,6 +174,7 @@ export function useCan(
150
174
  const lastPermissionRef = useRef<Permission | null>(null);
151
175
  const lastPageIdRef = useRef<UUID | undefined | null>(null);
152
176
  const lastUseCacheRef = useRef<boolean | null>(null);
177
+ const lastIsSuperAdminRef = useRef<boolean | null>(null);
153
178
 
154
179
  // Create a stable scope object for comparison
155
180
  const stableScope = useMemo(() => {
@@ -171,13 +196,18 @@ export function useCan(
171
196
  const scopeChanged = !scopeEqual(prevScopeRef.current, stableScope);
172
197
 
173
198
  // Only run if something has actually changed
199
+ // CRITICAL: Also check if isSuperAdmin changed - super admins bypass all checks
200
+ const isSuperAdminChanged = lastIsSuperAdminRef.current !== isSuperAdmin;
201
+
174
202
  if (
175
203
  lastUserIdRef.current !== userId ||
176
204
  scopeChanged ||
177
205
  lastPermissionRef.current !== permission ||
178
206
  lastPageIdRef.current !== pageId ||
179
- lastUseCacheRef.current !== useCache
207
+ lastUseCacheRef.current !== useCache ||
208
+ isSuperAdminChanged
180
209
  ) {
210
+ lastIsSuperAdminRef.current = isSuperAdmin;
181
211
  lastUserIdRef.current = userId;
182
212
  prevScopeRef.current = stableScope;
183
213
  lastPermissionRef.current = permission;
@@ -265,8 +295,8 @@ export function useCan(
265
295
  // Note: isPermittedCached doesn't support precomputedSuperAdmin, but the check will be cached
266
296
  // If we know user is NOT super admin (isSuperAdmin === false), pass false to skip the check
267
297
  const result = useCache
268
- ? await isPermittedCached({ userId, scope: validScope, permission, pageId }, undefined, appName)
269
- : await isPermitted({ userId, scope: validScope, permission, pageId }, undefined, appName, isSuperAdmin === false ? false : null);
298
+ ? await isPermittedCached({ userId, scope: validScope, permission, pageId }, appName)
299
+ : await isPermitted({ userId, scope: validScope, permission, pageId }, appName, isSuperAdmin === false ? false : null);
270
300
 
271
301
  setCan(result);
272
302
  } catch (err) {
@@ -325,8 +355,8 @@ export function useCan(
325
355
  };
326
356
 
327
357
  const result = useCache
328
- ? await isPermittedCached({ userId, scope: validScope, permission, pageId }, undefined, appName)
329
- : await isPermitted({ userId, scope: validScope, permission, pageId }, undefined, appName, null);
358
+ ? await isPermittedCached({ userId, scope: validScope, permission, pageId }, appName)
359
+ : await isPermitted({ userId, scope: validScope, permission, pageId }, appName, null);
330
360
 
331
361
  setCan(result);
332
362
  } catch (err) {
@@ -59,7 +59,7 @@ export function useHasAllPermissions(
59
59
  for (const permission of permissions) {
60
60
  const result = useCache
61
61
  ? await isPermittedCached({ userId, scope, permission })
62
- : await isPermitted({ userId, scope, permission }, null, undefined, null);
62
+ : await isPermitted({ userId, scope, permission });
63
63
 
64
64
  if (!result) {
65
65
  hasAllPermissions = false;
@@ -59,7 +59,7 @@ export function useHasAnyPermission(
59
59
  for (const permission of permissions) {
60
60
  const result = useCache
61
61
  ? await isPermittedCached({ userId, scope, permission })
62
- : await isPermitted({ userId, scope, permission }, null, undefined, null);
62
+ : await isPermitted({ userId, scope, permission });
63
63
 
64
64
  if (result) {
65
65
  hasAnyPermission = true;
@@ -66,7 +66,7 @@ export function useMultiplePermissions(
66
66
  for (const permission of permissions) {
67
67
  const result = useCache
68
68
  ? await isPermittedCached({ userId, scope, permission })
69
- : await isPermitted({ userId, scope, permission }, null, undefined, null);
69
+ : await isPermitted({ userId, scope, permission });
70
70
  permissionResults[permission] = result;
71
71
  }
72
72
 
@@ -126,7 +126,6 @@ describe('useCan Hook', () => {
126
126
  permission: mockPermission,
127
127
  pageId: mockPageId
128
128
  },
129
- undefined,
130
129
  undefined
131
130
  );
132
131
  expect(mockIsPermitted).not.toHaveBeenCalled();
@@ -151,7 +150,6 @@ describe('useCan Hook', () => {
151
150
  pageId: mockPageId
152
151
  },
153
152
  undefined,
154
- undefined,
155
153
  false
156
154
  );
157
155
  expect(mockIsPermittedCached).not.toHaveBeenCalled();
@@ -191,7 +189,6 @@ describe('useCan Hook', () => {
191
189
  pageId: 'custom-page'
192
190
  },
193
191
  undefined,
194
- undefined,
195
192
  false
196
193
  );
197
194
  });
@@ -214,7 +211,6 @@ describe('useCan Hook', () => {
214
211
  pageId: undefined
215
212
  },
216
213
  undefined,
217
- undefined,
218
214
  false
219
215
  );
220
216
  });
@@ -257,7 +253,6 @@ describe('useCan Hook', () => {
257
253
  pageId: undefined
258
254
  },
259
255
  undefined,
260
- undefined,
261
256
  false
262
257
  );
263
258
  });
@@ -299,7 +294,6 @@ describe('useCan Hook', () => {
299
294
  pageId: undefined
300
295
  },
301
296
  undefined,
302
- undefined,
303
297
  false
304
298
  );
305
299
  });
@@ -340,7 +334,6 @@ describe('useCan Hook', () => {
340
334
  pageId: undefined
341
335
  },
342
336
  undefined,
343
- undefined,
344
337
  false
345
338
  );
346
339
  });
@@ -548,7 +541,6 @@ describe('useCan Hook', () => {
548
541
  permission: mockPermission,
549
542
  pageId: mockPageId
550
543
  },
551
- undefined,
552
544
  undefined
553
545
  );
554
546
  });
@@ -637,7 +629,6 @@ describe('useCan Hook', () => {
637
629
  pageId: undefined
638
630
  },
639
631
  undefined,
640
- undefined,
641
632
  false
642
633
  );
643
634
  });
@@ -115,11 +115,7 @@ describe('useRBAC', () => {
115
115
  eventId: undefined,
116
116
  appId: 'app-123'
117
117
  }
118
- },
119
- {
120
- requires_event: false,
121
- },
122
- 'test-app'
118
+ }
123
119
  );
124
120
  });
125
121
 
@@ -19,6 +19,7 @@ import {
19
19
  getAccessLevel,
20
20
  resolveAppContext,
21
21
  getRoleContext,
22
+ getPageScopeType,
22
23
  } from '../api';
23
24
  import { getRBACLogger } from '../config';
24
25
  import { ContextValidator } from '../utils/contextValidator';
@@ -58,7 +59,6 @@ export function useRBAC(pageId?: string): UserRBACContext {
58
59
  session,
59
60
  supabase,
60
61
  appName,
61
- appConfig,
62
62
  appId: contextAppId,
63
63
  selectedOrganisation,
64
64
  isContextReady: orgContextReady,
@@ -94,30 +94,21 @@ export function useRBAC(pageId?: string): UserRBACContext {
94
94
  }
95
95
 
96
96
  // Build initial scope from available context
97
- // For event-required apps: use organisation_id from selectedEvent if available (faster than deriving)
98
- // For org-required apps: use selectedOrganisation.id
97
+ // Scope is now page-level only - use whatever context is available
99
98
  const initialScope: Scope = {
100
- organisationId: appConfig?.requires_event
101
- ? (selectedEvent?.organisation_id || selectedOrganisation?.id)
102
- : selectedOrganisation?.id,
99
+ organisationId: selectedEvent?.organisation_id || selectedOrganisation?.id || undefined,
103
100
  eventId: selectedEvent?.event_id || undefined,
104
101
  appId: undefined
105
102
  };
106
103
 
107
- // Check if context is ready using ContextValidator
108
- const contextReady = ContextValidator.isContextReady(
109
- initialScope,
110
- appConfig,
111
- appName,
112
- !!selectedEvent,
113
- !!selectedOrganisation
114
- );
115
-
116
104
  // PORTAL/ADMIN special case: context is always ready
117
- if (appName !== 'PORTAL' && appName !== 'ADMIN' && !contextReady) {
118
- // Wait for appropriate context based on app config
119
- setIsLoading(true);
120
- return;
105
+ // For other apps, we need at least one context (org or event) for page-level scope validation
106
+ if (appName !== 'PORTAL' && appName !== 'ADMIN') {
107
+ if (!selectedOrganisation && !selectedEvent) {
108
+ // Wait for context to be available
109
+ setIsLoading(true);
110
+ return;
111
+ }
121
112
  }
122
113
 
123
114
  setIsLoading(true);
@@ -135,14 +126,8 @@ export function useRBAC(pageId?: string): UserRBACContext {
135
126
  // For PORTAL/ADMIN apps, allow access even if hasAccess is false (users can view their own profile, super admins have global access)
136
127
  if (!resolved) {
137
128
  if (appName === 'PORTAL' || appName === 'ADMIN') {
138
- // For PORTAL/ADMIN, try to get appId directly from database
139
- try {
140
- const { getAppConfigByName } = await import('../api');
141
- await getAppConfigByName(appName);
142
- // We can't get appId from config, but that's OK - use contextAppId or proceed without
143
- } catch (err) {
144
- // Proceed without appId for page-level permissions
145
- }
129
+ // For PORTAL/ADMIN, proceed without appId - it's optional for these apps
130
+ // Use contextAppId if available
146
131
  } else {
147
132
  throw new Error(`User does not have access to app "${appName}"`);
148
133
  }
@@ -180,11 +165,25 @@ export function useRBAC(pageId?: string): UserRBACContext {
180
165
  appId: appId || contextAppId,
181
166
  };
182
167
 
183
- // Resolve required context using ContextValidator
184
- // Pass supabase client to allow deriving organisation from event for event-required apps
185
- const validation = await ContextValidator.resolveRequiredContext(
168
+ // Resolve scope based on page-level scope type
169
+ // If pageId is provided, use its scope type; otherwise default to 'organisation'
170
+ let pageScopeType: 'event' | 'organisation' | 'both' = 'organisation';
171
+ if (pageId && scope.appId) {
172
+ try {
173
+ pageScopeType = await getPageScopeType(pageId, scope.appId, appName);
174
+ } catch (error) {
175
+ logger.warn('[useRBAC] Failed to get page scope type, defaulting to organisation', {
176
+ pageId,
177
+ error: error instanceof Error ? error.message : String(error)
178
+ });
179
+ // Default to organisation scope on error
180
+ }
181
+ }
182
+
183
+ // Resolve required context using page-level scope type
184
+ const validation = await ContextValidator.resolveScopeForPage(
186
185
  scope,
187
- appConfig,
186
+ pageScopeType,
188
187
  appName,
189
188
  supabase || null
190
189
  );
@@ -196,11 +195,11 @@ export function useRBAC(pageId?: string): UserRBACContext {
196
195
  const resolvedScope = validation.resolvedScope;
197
196
  setCurrentScope(resolvedScope);
198
197
 
199
- // Pass appConfig and appName to API calls for context validation
198
+ // API calls no longer need appConfig (scope is page-level)
200
199
  const [map, roleContext, accessLevel] = await Promise.all([
201
- getPermissionMap({ userId: user.id as UUID, scope: resolvedScope }, appConfig, appName),
202
- getRoleContext({ userId: user.id as UUID, scope: resolvedScope }, appConfig, appName),
203
- getAccessLevel({ userId: user.id as UUID, scope: resolvedScope }, appConfig, appName),
200
+ getPermissionMap({ userId: user.id as UUID, scope: resolvedScope }),
201
+ getRoleContext({ userId: user.id as UUID, scope: resolvedScope }),
202
+ getAccessLevel({ userId: user.id as UUID, scope: resolvedScope }),
204
203
  ]);
205
204
 
206
205
  setPermissionMap(map);
@@ -225,7 +224,7 @@ export function useRBAC(pageId?: string): UserRBACContext {
225
224
  } finally {
226
225
  setIsLoading(false);
227
226
  }
228
- }, [appName, logger, resetState, selectedEvent?.event_id, selectedOrganisation?.id, session, user, eventLoading, appConfig, orgContextReady, orgLoading]);
227
+ }, [appName, logger, resetState, selectedEvent?.event_id, selectedOrganisation?.id, session, user, eventLoading, orgContextReady, orgLoading]);
229
228
 
230
229
  const hasGlobalPermission = useCallback(
231
230
  (permission: string): boolean => {
@@ -254,7 +253,7 @@ export function useRBAC(pageId?: string): UserRBACContext {
254
253
 
255
254
  useEffect(() => {
256
255
  loadRBACContext();
257
- }, [loadRBACContext, appName, appConfig, eventLoading, selectedEvent?.event_id, user, session, selectedOrganisation?.id, orgContextReady, orgLoading]);
256
+ }, [loadRBACContext, appName, eventLoading, selectedEvent?.event_id, user, session, selectedOrganisation?.id, orgContextReady, orgLoading]);
258
257
 
259
258
  return {
260
259
  user,
@@ -24,14 +24,24 @@ vi.mock('../../utils/app/appNameResolver', () => ({
24
24
  getCurrentAppName: vi.fn(),
25
25
  }));
26
26
 
27
+ vi.mock('../utils/contextValidator', () => ({
28
+ ContextValidator: {
29
+ resolveScopeForPage: vi.fn(),
30
+ deriveOrgFromEvent: vi.fn(),
31
+ },
32
+ }));
33
+
27
34
  import { createScopeFromEvent, getOrganisationFromEvent } from '../utils/eventContext';
28
35
  import { getCurrentAppName } from '../../utils/app/appNameResolver';
29
36
  import { createMockSupabaseClient } from '../../__tests__/helpers/supabaseMock';
37
+ import { ContextValidator } from '../utils/contextValidator';
38
+ import { OrganisationContextRequiredError } from '../types';
30
39
 
31
40
  describe('useResolvedScope Hook', () => {
32
41
  const mockCreateScopeFromEvent = vi.mocked(createScopeFromEvent);
33
42
  const mockGetOrganisationFromEvent = vi.mocked(getOrganisationFromEvent);
34
43
  const mockGetCurrentAppName = vi.mocked(getCurrentAppName);
44
+ const mockContextValidator = vi.mocked(ContextValidator);
35
45
 
36
46
  let mockSupabase: SupabaseClient<Database>;
37
47
  let sharedMockQuery: any;
@@ -55,11 +65,32 @@ describe('useResolvedScope Hook', () => {
55
65
  mockSupabase = {
56
66
  from: vi.fn().mockReturnValue(sharedMockQuery),
57
67
  rpc: vi.fn(),
68
+ auth: {
69
+ getSession: vi.fn().mockResolvedValue({
70
+ data: { session: { access_token: 'test-token' } },
71
+ error: null
72
+ })
73
+ }
58
74
  } as any;
59
75
 
60
76
  mockGetCurrentAppName.mockReturnValue('test-app');
61
77
  // Default mock for getOrganisationFromEvent
62
78
  mockGetOrganisationFromEvent.mockResolvedValue(null);
79
+ // Default mock for ContextValidator - fails validation when no orgId for organisation scope
80
+ mockContextValidator.resolveScopeForPage.mockImplementation(async (scope, scopeType) => {
81
+ if (!scope.organisationId && scopeType === 'organisation') {
82
+ return {
83
+ isValid: false,
84
+ resolvedScope: null,
85
+ error: new OrganisationContextRequiredError()
86
+ };
87
+ }
88
+ return {
89
+ isValid: true,
90
+ resolvedScope: scope,
91
+ error: null
92
+ };
93
+ });
63
94
  });
64
95
 
65
96
  afterEach(() => {
@@ -204,6 +235,13 @@ describe('useResolvedScope Hook', () => {
204
235
  // Set up mock implementation
205
236
  // Note: Don't clear all mocks here as it would clear the getOrganisationFromEvent mock
206
237
  mockGetOrganisationFromEvent.mockResolvedValue('org-456');
238
+ // Ensure ContextValidator fails validation for this test (no orgId, but has eventId)
239
+ // This will cause the hook to return scope with just eventId
240
+ mockContextValidator.resolveScopeForPage.mockResolvedValue({
241
+ isValid: false,
242
+ resolvedScope: null,
243
+ error: new OrganisationContextRequiredError()
244
+ });
207
245
  (mockSupabase.from as any).mockImplementation((table: string) => {
208
246
  if (table === 'event') {
209
247
  return eventQueryBuilder;
@@ -223,39 +261,37 @@ describe('useResolvedScope Hook', () => {
223
261
  );
224
262
 
225
263
  // Wait for async resolution to complete
264
+ // The hook should set resolvedScope to eventScope when validation fails but eventId exists
226
265
  await waitFor(
227
266
  () => {
228
267
  expect(result.current.isLoading).toBe(false);
229
268
  },
230
- { timeout: 3000 }
269
+ { timeout: 3000, interval: 10 }
231
270
  );
232
271
 
233
- // Check if we got an error - this test expects the hook to derive organisation from event
234
- // However, the hook requires organisation context for event-required apps
235
- // Skip this test as it's testing invalid state (event without org context)
236
- if (result.current.error) {
237
- // Expected: Organisation context is required even when deriving from event
238
- expect(result.current.error.message).toContain('Organisation context is required');
239
- return; // Test expects this to work, but it's actually invalid state
240
- }
241
-
242
- // Force rerender to pick up ref update
272
+ // Force rerender to pick up ref update (refs don't trigger re-renders)
243
273
  rerender();
244
274
 
245
- // Wait for stable scope ref to update
275
+ // Wait for stable scope ref to update (happens in useEffect after state update)
276
+ // The scope will have eventId and appId, but no organisationId
246
277
  await waitFor(
247
278
  () => {
279
+ // Hook should return scope with eventId when validation fails but eventId is present
248
280
  expect(result.current.resolvedScope).not.toBeNull();
249
281
  },
250
- { timeout: 3000, interval: 10 }
282
+ { timeout: 2000, interval: 10 }
251
283
  );
252
284
 
253
- expect(result.current.resolvedScope).toEqual({
254
- organisationId: 'org-456',
255
- eventId: 'event-123',
256
- appId: 'app-123',
257
- });
258
- expect(result.current.error).toBeNull();
285
+ // Hook should return scope with eventId even if org derivation hasn't happened yet
286
+ // The organisation will be derived during permission checks
287
+ // When event is provided but validation fails, hook returns scope with just eventId (no error)
288
+ expect(result.current.resolvedScope).not.toBeNull();
289
+ if (result.current.resolvedScope) {
290
+ expect(result.current.resolvedScope.eventId).toBe('event-123');
291
+ expect(result.current.resolvedScope.appId).toBe('app-123');
292
+ // organisationId may be undefined - will be derived during permission checks
293
+ expect(result.current.error).toBeNull(); // No error when event is provided
294
+ }
259
295
  });
260
296
 
261
297
  it('handles no context available', async () => {
@@ -497,10 +533,20 @@ describe('useResolvedScope Hook', () => {
497
533
 
498
534
  describe('Error Handling', () => {
499
535
  it('handles error when event scope resolution fails', async () => {
500
- const error = new Error('Failed to resolve event scope');
501
- mockCreateScopeFromEvent.mockResolvedValue(null);
536
+ // Ensure appId is resolved so scope is valid
537
+ sharedMockQuery.single.mockResolvedValue({
538
+ data: { id: 'app-123', name: 'test-app', is_active: true },
539
+ error: null,
540
+ });
541
+
542
+ // Ensure ContextValidator fails validation (no orgId)
543
+ mockContextValidator.resolveScopeForPage.mockResolvedValue({
544
+ isValid: false,
545
+ resolvedScope: null,
546
+ error: new OrganisationContextRequiredError()
547
+ });
502
548
 
503
- const { result } = renderHook(() =>
549
+ const { result, rerender } = renderHook(() =>
504
550
  useResolvedScope({
505
551
  supabase: mockSupabase,
506
552
  selectedOrganisationId: null,
@@ -515,22 +561,43 @@ describe('useResolvedScope Hook', () => {
515
561
  { timeout: 2000 }
516
562
  );
517
563
 
518
- expect(result.current.resolvedScope).toBeNull();
519
- expect(result.current.error).toBeInstanceOf(Error);
520
- // When event context resolution fails, it returns OrganisationContextRequiredError
521
- expect(result.current.error?.message).toBe(
522
- 'Organisation context is required for this operation'
564
+ // Force rerender to pick up ref update (refs don't trigger re-renders)
565
+ rerender();
566
+
567
+ // Wait for stable scope ref to update (happens in useEffect after state update)
568
+ await waitFor(
569
+ () => {
570
+ // When event is provided but validation fails, hook returns scope with just eventId
571
+ // appId must be resolved for scope to be valid
572
+ expect(result.current.resolvedScope).not.toBeNull();
573
+ },
574
+ { timeout: 2000, interval: 10 }
523
575
  );
576
+
577
+ // When event is provided but validation fails, hook returns scope with just eventId
578
+ // (no error - org will be derived during permission checks)
579
+ expect(result.current.resolvedScope).not.toBeNull();
580
+ if (result.current.resolvedScope) {
581
+ expect(result.current.resolvedScope.eventId).toBe('event-123');
582
+ expect(result.current.error).toBeNull();
583
+ }
524
584
  });
525
585
 
526
586
  it('handles error when createScopeFromEvent throws', async () => {
527
- // Mock the database query to fail when trying to derive org from event
587
+ // Ensure appId is resolved so scope is valid
528
588
  sharedMockQuery.single.mockResolvedValue({
529
- data: null,
530
- error: { message: 'Database error' },
589
+ data: { id: 'app-123', name: 'test-app', is_active: true },
590
+ error: null,
591
+ });
592
+
593
+ // Ensure ContextValidator fails validation (no orgId)
594
+ mockContextValidator.resolveScopeForPage.mockResolvedValue({
595
+ isValid: false,
596
+ resolvedScope: null,
597
+ error: new OrganisationContextRequiredError()
531
598
  });
532
599
 
533
- const { result } = renderHook(() =>
600
+ const { result, rerender } = renderHook(() =>
534
601
  useResolvedScope({
535
602
  supabase: mockSupabase,
536
603
  selectedOrganisationId: null,
@@ -545,10 +612,28 @@ describe('useResolvedScope Hook', () => {
545
612
  { timeout: 2000 }
546
613
  );
547
614
 
548
- expect(result.current.resolvedScope).toBeNull();
549
- // When org derivation fails, it returns OrganisationContextRequiredError
550
- expect(result.current.error).toBeInstanceOf(Error);
551
- expect(result.current.error?.message).toContain('Organisation context is required');
615
+ // Force rerender to pick up ref update (refs don't trigger re-renders)
616
+ rerender();
617
+
618
+ // Wait for stable scope ref to update (happens in useEffect after state update)
619
+ await waitFor(
620
+ () => {
621
+ // When event is provided but validation fails, hook returns scope with just eventId
622
+ // appId must be resolved for scope to be valid
623
+ expect(result.current.resolvedScope).not.toBeNull();
624
+ },
625
+ { timeout: 2000, interval: 10 }
626
+ );
627
+
628
+ // When event is provided but org derivation fails, hook returns scope with just eventId
629
+ // (no error - org will be derived during permission checks)
630
+ // appId must be present for scope to be valid
631
+ expect(result.current.resolvedScope).not.toBeNull();
632
+ if (result.current.resolvedScope) {
633
+ expect(result.current.resolvedScope.eventId).toBe('event-123');
634
+ expect(result.current.resolvedScope.appId).toBe('app-123');
635
+ expect(result.current.error).toBeNull();
636
+ }
552
637
  });
553
638
 
554
639
  it('handles database error when resolving app ID', async () => {