@jmruthers/pace-core 0.6.2 → 0.6.4

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-E7YQZD7D.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-QPXO24B4.js} +5 -4
  8. package/dist/{api-MVVQZLJI.js → api-6LVZTHDS.js} +10 -10
  9. package/dist/{audit-B5P6FFIR.js → audit-V53FV5AG.js} +2 -2
  10. package/dist/chunk-36LVWXB2.js +227 -0
  11. package/dist/chunk-36LVWXB2.js.map +1 -0
  12. package/dist/{chunk-24UVZUZG.js → chunk-3LPHPB62.js} +129 -387
  13. package/dist/chunk-3LPHPB62.js.map +1 -0
  14. package/dist/{chunk-2UOI2FG5.js → chunk-5EC5MEWX.js} +4 -4
  15. package/dist/{chunk-3XC4CPTD.js → chunk-7JPAB3T5.js} +244 -5727
  16. package/dist/chunk-7JPAB3T5.js.map +1 -0
  17. package/dist/{chunk-6J4GEEJR.js → chunk-ATKZM7RX.js} +53 -27
  18. package/dist/chunk-ATKZM7RX.js.map +1 -0
  19. package/dist/{chunk-EHMR7VYL.js → chunk-AVMLPIM7.js} +443 -189
  20. package/dist/chunk-AVMLPIM7.js.map +1 -0
  21. package/dist/chunk-DGUM43GV.js +11 -0
  22. package/dist/{chunk-NECFR5MM.js → chunk-I6DAQMWX.js} +575 -647
  23. package/dist/chunk-I6DAQMWX.js.map +1 -0
  24. package/dist/{chunk-F2IMUDXZ.js → chunk-M7MPQISP.js} +2 -2
  25. package/dist/{chunk-XWQCNGTQ.js → chunk-NN6WWZ5U.js} +173 -79
  26. package/dist/chunk-NN6WWZ5U.js.map +1 -0
  27. package/dist/{chunk-MMZ7JXPU.js → chunk-OEWDTMG7.js} +13 -21
  28. package/dist/{chunk-MMZ7JXPU.js.map → chunk-OEWDTMG7.js.map} +1 -1
  29. package/dist/{chunk-SFZUDBL5.js → chunk-YKRAFF5K.js} +70 -56
  30. package/dist/chunk-YKRAFF5K.js.map +1 -0
  31. package/dist/components.d.ts +2 -2
  32. package/dist/components.js +12 -13
  33. package/dist/contextValidator-OOPCLPZW.js +9 -0
  34. package/dist/contextValidator-OOPCLPZW.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 +10 -5
  60. package/scripts/audit/core/checks/compliance.cjs +72 -0
  61. package/scripts/audit/core/checks/dependencies.cjs +568 -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 +157 -36
  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 +285 -56
  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-E7YQZD7D.js.map} +0 -0
  294. /package/dist/{UnifiedAuthProvider-CH6Z342H.js.map → UnifiedAuthProvider-QPXO24B4.js.map} +0 -0
  295. /package/dist/{api-MVVQZLJI.js.map → api-6LVZTHDS.js.map} +0 -0
  296. /package/dist/{audit-B5P6FFIR.js.map → audit-V53FV5AG.js.map} +0 -0
  297. /package/dist/{chunk-2UOI2FG5.js.map → chunk-5EC5MEWX.js.map} +0 -0
  298. /package/dist/{chunk-7D4SUZUM.js.map → chunk-DGUM43GV.js.map} +0 -0
  299. /package/dist/{chunk-F2IMUDXZ.js.map → chunk-M7MPQISP.js.map} +0 -0
@@ -1,12 +1,10 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
2
  import type { SupabaseClient } from '@supabase/supabase-js';
3
- import { ContextValidator, type AppConfig } from '../contextValidator';
3
+ import { ContextValidator, type PageScopeType } from '../contextValidator';
4
4
  import { EventContextRequiredError, OrganisationContextRequiredError, type Scope } from '../../types';
5
5
  import type { Database } from '../../../types/database';
6
6
 
7
7
  describe('ContextValidator', () => {
8
- const eventRequiredConfig: AppConfig = { requires_event: true };
9
- const orgRequiredConfig: AppConfig = { requires_event: false };
10
8
  const scopeWithOrg: Scope = { organisationId: 'org-1', eventId: 'event-1', appId: 'app-1' };
11
9
 
12
10
  const createSupabaseMock = () => ({}) as SupabaseClient<Database>;
@@ -19,132 +17,112 @@ describe('ContextValidator', () => {
19
17
  vi.clearAllMocks();
20
18
  });
21
19
 
22
- describe('validateScope', () => {
23
- it('treats PORTAL and ADMIN apps as always valid', async () => {
24
- const baseScope: Scope = { appId: 'portal-app' };
25
- const result = await ContextValidator.validateScope(baseScope, eventRequiredConfig, 'PORTAL');
26
-
27
- expect(result).toEqual({
28
- isValid: true,
29
- resolvedScope: { organisationId: undefined, eventId: undefined, appId: 'portal-app' },
30
- error: null
31
- });
32
- });
20
+ describe('deriveOrgFromEvent', () => {
21
+ it('derives organisation ID from event ID', async () => {
22
+ const supabase = createSupabaseMock();
23
+ // Mock the underlying function
24
+ const { getOrganisationFromEvent } = await import('../eventContext');
25
+ vi.spyOn(await import('../eventContext'), 'getOrganisationFromEvent').mockResolvedValue('derived-org' as any);
33
26
 
34
- it('requires organisation context when no app config exists', async () => {
35
- const baseScope: Scope = { eventId: 'event-1' };
36
- const result = await ContextValidator.validateScope(baseScope, null);
27
+ const result = await ContextValidator.deriveOrgFromEvent(supabase, 'event-1');
37
28
 
38
- expect(result.isValid).toBe(false);
39
- expect(result.error).toBeInstanceOf(OrganisationContextRequiredError);
29
+ expect(result).toBe('derived-org');
40
30
  });
31
+ });
41
32
 
42
- it('requires event context for event-based apps', async () => {
43
- const baseScope: Scope = { organisationId: 'org-1' };
44
- const result = await ContextValidator.validateScope(baseScope, eventRequiredConfig);
33
+ describe('resolveScopeForPage', () => {
34
+ it('handles event scope - requires event context', async () => {
35
+ const scope: Scope = { organisationId: 'org-1' };
36
+ const result = await ContextValidator.resolveScopeForPage(scope, 'event');
45
37
 
46
38
  expect(result.isValid).toBe(false);
47
39
  expect(result.error).toBeInstanceOf(EventContextRequiredError);
48
40
  });
49
41
 
50
- it('accepts scopes that satisfy event-based requirements', async () => {
51
- const result = await ContextValidator.validateScope({ eventId: 'event-1' }, eventRequiredConfig);
42
+ it('handles event scope - derives org from event when event provided', async () => {
43
+ const supabase = createSupabaseMock();
44
+ vi.spyOn(ContextValidator, 'deriveOrgFromEvent').mockResolvedValue('derived-org');
45
+
46
+ const scope: Scope = { eventId: 'event-1', appId: 'app-1' };
47
+ const result = await ContextValidator.resolveScopeForPage(scope, 'event', undefined, supabase);
52
48
 
53
49
  expect(result.isValid).toBe(true);
50
+ expect(result.resolvedScope?.organisationId).toBe('derived-org');
54
51
  expect(result.resolvedScope?.eventId).toBe('event-1');
55
52
  });
56
53
 
57
- it('enforces organisation requirements for org-based apps', async () => {
58
- const result = await ContextValidator.validateScope({ eventId: 'event-1' }, orgRequiredConfig);
54
+ it('handles organisation scope - requires organisation context', async () => {
55
+ const scope: Scope = { eventId: 'event-1' };
56
+ const result = await ContextValidator.resolveScopeForPage(scope, 'organisation');
59
57
 
60
58
  expect(result.isValid).toBe(false);
61
59
  expect(result.error).toBeInstanceOf(OrganisationContextRequiredError);
62
60
  });
63
- });
64
61
 
65
- describe('resolveRequiredContext', () => {
66
- it('passes through scope for optional contexts', async () => {
67
- const baseScope: Scope = { appId: 'admin-app' };
68
- const result = await ContextValidator.resolveRequiredContext(baseScope, eventRequiredConfig, 'ADMIN');
62
+ it('handles organisation scope - accepts organisation context', async () => {
63
+ const scope: Scope = { organisationId: 'org-1', eventId: 'event-1', appId: 'app-1' };
64
+ const result = await ContextValidator.resolveScopeForPage(scope, 'organisation');
69
65
 
70
- expect(result).toEqual({
71
- isValid: true,
72
- resolvedScope: { organisationId: undefined, eventId: undefined, appId: 'admin-app' },
73
- error: null
74
- });
66
+ expect(result.isValid).toBe(true);
67
+ expect(result.resolvedScope).toEqual(scope);
75
68
  });
76
69
 
77
- it('returns organisation error when no app config and org is missing', async () => {
78
- const result = await ContextValidator.resolveRequiredContext({ eventId: 'event-1' }, null);
70
+ it('handles both scope - requires at least one context', async () => {
71
+ const scope: Scope = { appId: 'app-1' };
72
+ const result = await ContextValidator.resolveScopeForPage(scope, 'both');
79
73
 
80
74
  expect(result.isValid).toBe(false);
81
- expect(result.error).toBeInstanceOf(OrganisationContextRequiredError);
75
+ expect(result.error?.message).toContain('either organisation or event context');
82
76
  });
83
77
 
84
- it('derives organisation id for event-required apps when missing', async () => {
85
- const supabase = createSupabaseMock();
86
- const deriveSpy = vi.spyOn(ContextValidator, 'deriveOrgFromEvent').mockResolvedValue('derived-org');
87
-
88
- const result = await ContextValidator.resolveRequiredContext({ eventId: 'event-1' }, eventRequiredConfig, undefined, supabase);
78
+ it('handles both scope - accepts organisation context', async () => {
79
+ const scope: Scope = { organisationId: 'org-1', appId: 'app-1' };
80
+ const result = await ContextValidator.resolveScopeForPage(scope, 'both');
89
81
 
90
- expect(deriveSpy).toHaveBeenCalledWith(supabase, 'event-1');
91
- expect(result).toEqual({
92
- isValid: true,
93
- resolvedScope: { organisationId: 'derived-org', eventId: 'event-1', appId: undefined },
94
- error: null
95
- });
82
+ expect(result.isValid).toBe(true);
83
+ expect(result.resolvedScope?.organisationId).toBe('org-1');
96
84
  });
97
85
 
98
- it('surfaces derivation failures when supabase is unavailable', async () => {
99
- const result = await ContextValidator.resolveRequiredContext({ eventId: 'event-1' }, eventRequiredConfig, undefined, null);
86
+ it('handles both scope - accepts event context', async () => {
87
+ const scope: Scope = { eventId: 'event-1', appId: 'app-1' };
88
+ const result = await ContextValidator.resolveScopeForPage(scope, 'both');
100
89
 
101
- expect(result.isValid).toBe(false);
102
- expect(result.error?.message).toContain('supabase client not available');
90
+ expect(result.isValid).toBe(true);
91
+ expect(result.resolvedScope?.eventId).toBe('event-1');
103
92
  });
104
93
 
105
- it('returns derivation error messages when fetch fails', async () => {
94
+ it('handles both scope - derives org from event when event provided', async () => {
106
95
  const supabase = createSupabaseMock();
107
- vi.spyOn(ContextValidator, 'deriveOrgFromEvent').mockRejectedValue(new Error('network down'));
96
+ vi.spyOn(ContextValidator, 'deriveOrgFromEvent').mockResolvedValue('derived-org');
108
97
 
109
- const result = await ContextValidator.resolveRequiredContext({ eventId: 'event-1' }, eventRequiredConfig, undefined, supabase);
98
+ const scope: Scope = { eventId: 'event-1', appId: 'app-1' };
99
+ const result = await ContextValidator.resolveScopeForPage(scope, 'both', undefined, supabase);
110
100
 
111
- expect(result.isValid).toBe(false);
112
- expect(result.error).toBeInstanceOf(Error);
113
- expect(result.error?.message).toContain('network down');
114
- });
115
-
116
- it('enforces organisation requirement for org-based apps', async () => {
117
- const result = await ContextValidator.resolveRequiredContext({ eventId: 'event-1' }, orgRequiredConfig);
118
-
119
- expect(result.isValid).toBe(false);
120
- expect(result.error).toBeInstanceOf(OrganisationContextRequiredError);
101
+ expect(result.isValid).toBe(true);
102
+ expect(result.resolvedScope?.organisationId).toBe('derived-org');
103
+ expect(result.resolvedScope?.eventId).toBe('event-1');
121
104
  });
122
105
 
123
- it('returns valid result when org context is present', async () => {
124
- const result = await ContextValidator.resolveRequiredContext(scopeWithOrg, orgRequiredConfig);
106
+ it('handles event scope - returns error when org derivation fails', async () => {
107
+ const supabase = createSupabaseMock();
108
+ vi.spyOn(ContextValidator, 'deriveOrgFromEvent').mockResolvedValue(null);
125
109
 
126
- expect(result).toEqual({ isValid: true, resolvedScope: scopeWithOrg, error: null });
127
- });
128
- });
110
+ const scope: Scope = { eventId: 'event-1', appId: 'app-1' };
111
+ const result = await ContextValidator.resolveScopeForPage(scope, 'event', undefined, supabase);
129
112
 
130
- describe('isContextReady', () => {
131
- it('is always ready for PORTAL/ADMIN apps', () => {
132
- expect(ContextValidator.isContextReady({}, eventRequiredConfig, 'PORTAL')).toBe(true);
113
+ expect(result.isValid).toBe(false);
114
+ expect(result.error?.message).toContain('Could not resolve organisation from event context');
133
115
  });
134
116
 
135
- it('checks event selection for event-required apps', () => {
136
- expect(ContextValidator.isContextReady({}, eventRequiredConfig, undefined, true)).toBe(true);
137
- expect(ContextValidator.isContextReady({}, eventRequiredConfig, undefined, false)).toBe(false);
138
- });
117
+ it('handles event scope - returns error when org derivation throws', async () => {
118
+ const supabase = createSupabaseMock();
119
+ vi.spyOn(ContextValidator, 'deriveOrgFromEvent').mockRejectedValue(new Error('network error'));
139
120
 
140
- it('checks organisation selection for org-required apps', () => {
141
- expect(ContextValidator.isContextReady({}, orgRequiredConfig, undefined, false, true)).toBe(true);
142
- expect(ContextValidator.isContextReady({}, orgRequiredConfig, undefined, false, false)).toBe(false);
143
- });
121
+ const scope: Scope = { eventId: 'event-1', appId: 'app-1' };
122
+ const result = await ContextValidator.resolveScopeForPage(scope, 'event', undefined, supabase);
144
123
 
145
- it('defaults to organisation requirement when no config', () => {
146
- expect(ContextValidator.isContextReady({}, null, undefined, false, true)).toBe(true);
147
- expect(ContextValidator.isContextReady({}, null, undefined, false, false)).toBe(false);
124
+ expect(result.isValid).toBe(false);
125
+ expect(result.error?.message).toContain('network error');
148
126
  });
149
127
  });
150
128
  });
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Client Security Detection Utilities
3
+ * @package @jmruthers/pace-core
4
+ * @module RBAC/Utils/ClientSecurity
5
+ * @since 1.0.0
6
+ *
7
+ * Utilities to detect and warn about insecure Supabase client usage.
8
+ */
9
+
10
+ import type { SupabaseClient } from '@supabase/supabase-js';
11
+ import { Database } from '../../types/database';
12
+
13
+ /**
14
+ * Symbol to mark secure clients
15
+ * This is attached to clients created by SecureSupabaseClient
16
+ */
17
+ export const SECURE_CLIENT_SYMBOL = Symbol('pace-core-secure-client');
18
+
19
+ /**
20
+ * Check if a Supabase client is a secure client (created via useSecureSupabase or createSecureClient)
21
+ *
22
+ * @param client - The Supabase client to check
23
+ * @returns true if the client is secure, false otherwise
24
+ *
25
+ * @example
26
+ * ```tsx
27
+ * import { isSecureClient } from '@jmruthers/pace-core/rbac/utils/clientSecurity';
28
+ *
29
+ * const supabase = useSecureSupabase();
30
+ * if (isSecureClient(supabase)) {
31
+ * // Client is secure, safe to use
32
+ * }
33
+ * ```
34
+ */
35
+ export function isSecureClient(client: SupabaseClient<Database> | null | undefined): boolean {
36
+ if (!client) return false;
37
+
38
+ // Check for the secure client symbol
39
+ return (client as any)[SECURE_CLIENT_SYMBOL] === true;
40
+ }
41
+
42
+ /**
43
+ * Warn about insecure client usage in development
44
+ *
45
+ * @param client - The client being used
46
+ * @param context - Context about where the client is being used (for better error messages)
47
+ *
48
+ * @example
49
+ * ```tsx
50
+ * import { warnIfInsecureClient } from '@jmruthers/pace-core/rbac/utils/clientSecurity';
51
+ *
52
+ * const supabase = createClient(...); // Wrong!
53
+ * warnIfInsecureClient(supabase, 'MyComponent');
54
+ * ```
55
+ */
56
+ export function warnIfInsecureClient(
57
+ client: SupabaseClient<Database> | null | undefined,
58
+ context?: string
59
+ ): void {
60
+ // Only warn in development
61
+ if (typeof process !== 'undefined' && process.env.NODE_ENV === 'production') {
62
+ return;
63
+ }
64
+
65
+ if (!client) return;
66
+
67
+ if (!isSecureClient(client)) {
68
+ const contextMsg = context ? ` in ${context}` : '';
69
+ console.warn(
70
+ `[pace-core Security Warning] Non-secure Supabase client detected${contextMsg}.\n` +
71
+ `You are using a Supabase client created with createClient() instead of useSecureSupabase().\n` +
72
+ `This bypasses organisation context enforcement and RLS policies, which can lead to:\n` +
73
+ `- Cross-organisation data access\n` +
74
+ `- Security vulnerabilities\n` +
75
+ `- Data leakage between organisations\n\n` +
76
+ `Fix: Replace with:\n` +
77
+ ` import { useSecureSupabase } from '@jmruthers/pace-core/rbac';\n` +
78
+ ` const supabase = useSecureSupabase();\n\n` +
79
+ `See: https://github.com/jmruthers/pace-core/blob/main/packages/core/docs/rbac/getting-started.md`
80
+ );
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Mark a client as secure (internal use only)
86
+ * This is called by SecureSupabaseClient to mark clients as secure
87
+ *
88
+ * @internal
89
+ */
90
+ export function markClientAsSecure(client: SupabaseClient<Database>): void {
91
+ (client as any)[SECURE_CLIENT_SYMBOL] = true;
92
+ }
93
+
@@ -22,9 +22,11 @@ import { createLogger } from '../../utils/core/logger';
22
22
 
23
23
  const log = createLogger('ContextValidator');
24
24
 
25
- export interface AppConfig {
26
- requires_event: boolean;
27
- }
25
+ /**
26
+ * Page scope type - determines what context is required for a page
27
+ * This is the single source of truth for page scoping.
28
+ */
29
+ export type PageScopeType = 'event' | 'organisation' | 'both';
28
30
 
29
31
  /**
30
32
  * Check if an app allows optional contexts (both organisation and event optional)
@@ -47,130 +49,79 @@ export interface ContextValidationResult {
47
49
  * Validates and resolves RBAC scope based on app configuration requirements.
48
50
  */
49
51
  export class ContextValidator {
52
+
50
53
  /**
51
- * Validate scope against app requirements
54
+ * Derive organisation ID from event ID
52
55
  *
53
- * @param scope - Current scope
54
- * @param appConfig - App configuration (requires_event flag)
55
- * @param appName - App name (for PORTAL/ADMIN special case)
56
- * @returns Validation result with resolved scope
56
+ * @param supabase - Supabase client
57
+ * @param eventId - Event ID
58
+ * @returns Organisation ID or null
57
59
  */
58
- static async validateScope(
59
- scope: Scope,
60
- appConfig: AppConfig | null,
61
- appName?: string
62
- ): Promise<ContextValidationResult> {
63
- // PORTAL/ADMIN special case: both contexts optional
64
- if (allowsOptionalContexts(appName)) {
65
- return {
66
- isValid: true,
67
- resolvedScope: {
68
- organisationId: scope.organisationId,
69
- eventId: scope.eventId,
70
- appId: scope.appId
71
- },
72
- error: null
73
- };
74
- }
75
-
76
- // If no app config, default to requiring org context
77
- if (!appConfig) {
78
- if (!scope.organisationId) {
79
- return {
80
- isValid: false,
81
- resolvedScope: null,
82
- error: new OrganisationContextRequiredError()
83
- };
84
- }
85
- return {
86
- isValid: true,
87
- resolvedScope: scope,
88
- error: null
89
- };
90
- }
91
-
92
- // Event-required apps: must have eventId, derive org from event
93
- if (appConfig.requires_event) {
94
- if (!scope.eventId) {
95
- return {
96
- isValid: false,
97
- resolvedScope: null,
98
- error: new EventContextRequiredError()
99
- };
100
- }
101
-
102
- // If org is not provided, we'll need to derive it from event
103
- // But for validation, we just check that eventId exists
104
- // The actual derivation happens in resolveRequiredContext
105
- return {
106
- isValid: true,
107
- resolvedScope: scope,
108
- error: null
109
- };
110
- }
111
-
112
- // Org-required apps: must have organisationId, eventId optional
113
- if (!scope.organisationId) {
114
- return {
115
- isValid: false,
116
- resolvedScope: null,
117
- error: new OrganisationContextRequiredError()
118
- };
119
- }
120
-
121
- return {
122
- isValid: true,
123
- resolvedScope: scope,
124
- error: null
125
- };
60
+ static async deriveOrgFromEvent(
61
+ supabase: SupabaseClient<Database>,
62
+ eventId: string
63
+ ): Promise<UUID | null> {
64
+ return getOrganisationFromEvent(supabase, eventId);
126
65
  }
127
66
 
128
67
  /**
129
- * Resolve required context and derive missing values
68
+ * Resolve scope based on page-level scope_type
69
+ *
70
+ * This method handles page-level scoping. All pages have explicit scope_type set.
71
+ * Used for hybrid apps like pace-mint that have both event and organisation pages.
130
72
  *
131
73
  * @param scope - Current scope
132
- * @param appConfig - App configuration
74
+ * @param pageScopeType - Page scope type ('event', 'organisation', or 'both')
133
75
  * @param appName - App name (for PORTAL/ADMIN special case)
134
76
  * @param supabase - Supabase client (for deriving org from event)
135
77
  * @returns Resolved scope with all required context
136
78
  */
137
- static async resolveRequiredContext(
79
+ static async resolveScopeForPage(
138
80
  scope: Scope,
139
- appConfig: AppConfig | null,
81
+ pageScopeType: PageScopeType,
140
82
  appName?: string,
141
83
  supabase?: SupabaseClient<Database> | null
142
84
  ): Promise<ContextValidationResult> {
143
- // PORTAL/ADMIN special case: both contexts optional
144
- if (allowsOptionalContexts(appName)) {
145
- return {
146
- isValid: true,
147
- resolvedScope: {
148
- organisationId: scope.organisationId,
149
- eventId: scope.eventId,
150
- appId: scope.appId
151
- },
152
- error: null
153
- };
154
- }
155
-
156
- // If no app config, default to requiring org context
157
- if (!appConfig) {
158
- if (!scope.organisationId) {
85
+ // Use page-level scope (single source of truth)
86
+ const effectiveScopeType = pageScopeType;
87
+
88
+ // Handle 'both' scope - requires both contexts available, but can use either
89
+ if (effectiveScopeType === 'both') {
90
+ // For 'both' pages, we need at least one context (org or event)
91
+ // Both will be checked during permission evaluation
92
+ if (!scope.organisationId && !scope.eventId) {
159
93
  return {
160
94
  isValid: false,
161
95
  resolvedScope: null,
162
- error: new OrganisationContextRequiredError()
96
+ error: new Error('Page requires either organisation or event context')
163
97
  };
164
98
  }
99
+
100
+ // Derive org from event if event is provided but org is not
101
+ let organisationId = scope.organisationId;
102
+ if (!organisationId && scope.eventId && supabase) {
103
+ try {
104
+ const derivedOrgId = await this.deriveOrgFromEvent(supabase, scope.eventId);
105
+ organisationId = derivedOrgId || undefined;
106
+ } catch (error) {
107
+ log.warn('Failed to derive org from event for both-scope page:', error);
108
+ // Continue without org - permission check will handle it
109
+ }
110
+ }
111
+
165
112
  return {
166
113
  isValid: true,
167
- resolvedScope: scope,
114
+ resolvedScope: {
115
+ organisationId,
116
+ eventId: scope.eventId,
117
+ appId: scope.appId
118
+ },
168
119
  error: null
169
120
  };
170
121
  }
171
-
172
- // Event-required apps: must have eventId, derive org from event
173
- if (appConfig.requires_event) {
122
+
123
+ // Handle 'event' scope - requires event context
124
+ if (effectiveScopeType === 'event') {
174
125
  if (!scope.eventId) {
175
126
  return {
176
127
  isValid: false,
@@ -178,7 +129,7 @@ export class ContextValidator {
178
129
  error: new EventContextRequiredError()
179
130
  };
180
131
  }
181
-
132
+
182
133
  // Derive organisationId from event if not provided
183
134
  let organisationId: UUID | undefined = scope.organisationId;
184
135
  if (!organisationId && supabase && scope.eventId) {
@@ -200,14 +151,8 @@ export class ContextValidator {
200
151
  error: error instanceof Error ? error : new Error('Failed to derive organisation from event')
201
152
  };
202
153
  }
203
- } else if (!organisationId) {
204
- return {
205
- isValid: false,
206
- resolvedScope: null,
207
- error: new Error('Event context requires organisationId but it could not be derived (supabase client not available)')
208
- };
209
154
  }
210
-
155
+
211
156
  return {
212
157
  isValid: true,
213
158
  resolvedScope: {
@@ -218,71 +163,35 @@ export class ContextValidator {
218
163
  error: null
219
164
  };
220
165
  }
221
-
222
- // Org-required apps: must have organisationId, eventId optional
223
- if (!scope.organisationId) {
166
+
167
+ // Handle 'organisation' scope - requires organisation context
168
+ if (effectiveScopeType === 'organisation') {
169
+ if (!scope.organisationId) {
170
+ return {
171
+ isValid: false,
172
+ resolvedScope: null,
173
+ error: new OrganisationContextRequiredError()
174
+ };
175
+ }
176
+
224
177
  return {
225
- isValid: false,
226
- resolvedScope: null,
227
- error: new OrganisationContextRequiredError()
178
+ isValid: true,
179
+ resolvedScope: {
180
+ organisationId: scope.organisationId,
181
+ eventId: scope.eventId, // Event is optional for org-scoped pages
182
+ appId: scope.appId
183
+ },
184
+ error: null
228
185
  };
229
186
  }
230
-
187
+
188
+ // Fallback (should not happen)
231
189
  return {
232
- isValid: true,
233
- resolvedScope: scope,
234
- error: null
190
+ isValid: false,
191
+ resolvedScope: null,
192
+ error: new Error('Invalid scope type')
235
193
  };
236
194
  }
237
195
 
238
- /**
239
- * Derive organisation ID from event ID
240
- *
241
- * @param supabase - Supabase client
242
- * @param eventId - Event ID
243
- * @returns Organisation ID or null
244
- */
245
- static async deriveOrgFromEvent(
246
- supabase: SupabaseClient<Database>,
247
- eventId: string
248
- ): Promise<UUID | null> {
249
- return getOrganisationFromEvent(supabase, eventId);
250
- }
251
-
252
- /**
253
- * Check if context is ready for permission checks
254
- *
255
- * @param scope - Current scope
256
- * @param appConfig - App configuration
257
- * @param appName - App name
258
- * @param hasSelectedEvent - Whether event is selected
259
- * @param hasSelectedOrganisation - Whether organisation is selected
260
- * @returns True if context is ready
261
- */
262
- static isContextReady(
263
- scope: Scope,
264
- appConfig: AppConfig | null,
265
- appName?: string,
266
- hasSelectedEvent?: boolean,
267
- hasSelectedOrganisation?: boolean
268
- ): boolean {
269
- // PORTAL/ADMIN special case: context is always ready
270
- if (allowsOptionalContexts(appName)) {
271
- return true;
272
- }
273
-
274
- // If no app config, default to requiring org context
275
- if (!appConfig) {
276
- return !!hasSelectedOrganisation || !!scope.organisationId;
277
- }
278
-
279
- // Event-required apps: need event context
280
- if (appConfig.requires_event) {
281
- return !!hasSelectedEvent || !!scope.eventId;
282
- }
283
-
284
- // Org-required apps: need org context
285
- return !!hasSelectedOrganisation || !!scope.organisationId;
286
- }
287
196
  }
288
197