@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.
- package/CHANGELOG.md +45 -0
- package/cursor-rules/00-pace-core-compliance.mdc +34 -2
- package/dist/{AuthService-BPvc3Ka0.d.ts → AuthService-Cb34EQs3.d.ts} +9 -1
- package/dist/{DataTable-TPTKCX4D.js → DataTable-E7YQZD7D.js} +9 -8
- package/dist/{PublicPageProvider-DC6kCaqf.d.ts → PublicPageProvider-DEMpysFR.d.ts} +45 -67
- package/dist/{UnifiedAuthProvider-CVcTjx-d.d.ts → UnifiedAuthProvider-CKvHP1MK.d.ts} +1 -8
- package/dist/{UnifiedAuthProvider-CH6Z342H.js → UnifiedAuthProvider-QPXO24B4.js} +5 -4
- package/dist/{api-MVVQZLJI.js → api-6LVZTHDS.js} +10 -10
- package/dist/{audit-B5P6FFIR.js → audit-V53FV5AG.js} +2 -2
- package/dist/chunk-36LVWXB2.js +227 -0
- package/dist/chunk-36LVWXB2.js.map +1 -0
- package/dist/{chunk-24UVZUZG.js → chunk-3LPHPB62.js} +129 -387
- package/dist/chunk-3LPHPB62.js.map +1 -0
- package/dist/{chunk-2UOI2FG5.js → chunk-5EC5MEWX.js} +4 -4
- package/dist/{chunk-3XC4CPTD.js → chunk-7JPAB3T5.js} +244 -5727
- package/dist/chunk-7JPAB3T5.js.map +1 -0
- package/dist/{chunk-6J4GEEJR.js → chunk-ATKZM7RX.js} +53 -27
- package/dist/chunk-ATKZM7RX.js.map +1 -0
- package/dist/{chunk-EHMR7VYL.js → chunk-AVMLPIM7.js} +443 -189
- package/dist/chunk-AVMLPIM7.js.map +1 -0
- package/dist/chunk-DGUM43GV.js +11 -0
- package/dist/{chunk-NECFR5MM.js → chunk-I6DAQMWX.js} +575 -647
- package/dist/chunk-I6DAQMWX.js.map +1 -0
- package/dist/{chunk-F2IMUDXZ.js → chunk-M7MPQISP.js} +2 -2
- package/dist/{chunk-XWQCNGTQ.js → chunk-NN6WWZ5U.js} +173 -79
- package/dist/chunk-NN6WWZ5U.js.map +1 -0
- package/dist/{chunk-MMZ7JXPU.js → chunk-OEWDTMG7.js} +13 -21
- package/dist/{chunk-MMZ7JXPU.js.map → chunk-OEWDTMG7.js.map} +1 -1
- package/dist/{chunk-SFZUDBL5.js → chunk-YKRAFF5K.js} +70 -56
- package/dist/chunk-YKRAFF5K.js.map +1 -0
- package/dist/components.d.ts +2 -2
- package/dist/components.js +12 -13
- package/dist/contextValidator-OOPCLPZW.js +9 -0
- package/dist/contextValidator-OOPCLPZW.js.map +1 -0
- package/dist/eslint-rules/pace-core-compliance.cjs +106 -0
- package/dist/hooks.d.ts +2 -2
- package/dist/hooks.js +7 -6
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +7 -7
- package/dist/index.js +21 -16
- package/dist/index.js.map +1 -1
- package/dist/providers.d.ts +3 -3
- package/dist/providers.js +4 -3
- package/dist/rbac/index.d.ts +67 -27
- package/dist/rbac/index.js +15 -8
- package/dist/styles/index.js +1 -1
- package/dist/theming/runtime.js +1 -1
- package/dist/types.js +1 -1
- package/dist/{usePublicRouteParams-1oMokgLF.d.ts → usePublicRouteParams-i3qtoBgg.d.ts} +7 -16
- package/dist/utils.js +5 -7
- package/dist/utils.js.map +1 -1
- package/docs/api/README.md +14 -16
- package/docs/api/modules.md +3796 -2513
- package/docs/components/context-selector.md +126 -0
- package/docs/migration/RBAC_SCOPE_MIGRATION.md +385 -0
- package/docs/pace-mint-fix-auto-selection.md +218 -0
- package/docs/pace-mint-rbac-setup.md +391 -0
- package/docs/rbac/secure-client-protection.md +330 -0
- package/package.json +10 -5
- package/scripts/audit/core/checks/compliance.cjs +72 -0
- package/scripts/audit/core/checks/dependencies.cjs +568 -28
- package/scripts/audit/core/checks/documentation.cjs +68 -3
- package/scripts/audit/core/checks/environment.cjs +2 -14
- package/scripts/audit/core/checks/error-handling.cjs +47 -6
- package/src/components/ContextSelector/ContextSelector.tsx +384 -0
- package/src/components/ContextSelector/index.ts +3 -0
- package/src/components/DataTable/components/RowComponent.tsx +19 -19
- package/src/components/DataTable/components/UnifiedTableBody.tsx +2 -2
- package/src/components/DataTable/hooks/useDataTablePermissions.ts +8 -6
- package/src/components/Dialog/Dialog.tsx +29 -1
- package/src/components/FileDisplay/FileDisplay.tsx +42 -10
- package/src/components/Header/Header.test.tsx +43 -73
- package/src/components/Header/Header.tsx +44 -45
- package/src/components/PaceAppLayout/PaceAppLayout.integration.test.tsx +10 -19
- package/src/components/PaceAppLayout/PaceAppLayout.performance.test.tsx +2 -2
- package/src/components/PaceAppLayout/PaceAppLayout.security.test.tsx +5 -5
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +9 -9
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +157 -36
- package/src/components/PaceAppLayout/README.md +14 -17
- package/src/components/PaceAppLayout/test-setup.tsx +2 -2
- package/src/components/index.ts +5 -5
- package/src/eslint-rules/pace-core-compliance.cjs +106 -0
- package/src/hooks/__tests__/useAppConfig.unit.test.ts +4 -98
- package/src/hooks/useAppConfig.ts +15 -30
- package/src/hooks/useFileDisplay.ts +77 -50
- package/src/index.ts +4 -5
- package/src/providers/services/AuthServiceProvider.tsx +17 -7
- package/src/providers/services/EventServiceProvider.tsx +33 -5
- package/src/providers/services/UnifiedAuthProvider.tsx +90 -134
- package/src/rbac/__tests__/adapters.comprehensive.test.tsx +1 -1
- package/src/rbac/adapters.tsx +2 -2
- package/src/rbac/api.test.ts +59 -51
- package/src/rbac/api.ts +178 -132
- package/src/rbac/components/PagePermissionGuard.tsx +38 -10
- package/src/rbac/hooks/__tests__/useSecureSupabase.test.ts +32 -21
- package/src/rbac/hooks/permissions/useAccessLevel.ts +1 -1
- package/src/rbac/hooks/permissions/useCan.ts +41 -11
- package/src/rbac/hooks/permissions/useHasAllPermissions.ts +1 -1
- package/src/rbac/hooks/permissions/useHasAnyPermission.ts +1 -1
- package/src/rbac/hooks/permissions/useMultiplePermissions.ts +1 -1
- package/src/rbac/hooks/useCan.test.ts +0 -9
- package/src/rbac/hooks/useRBAC.test.ts +1 -5
- package/src/rbac/hooks/useRBAC.ts +36 -37
- package/src/rbac/hooks/useResolvedScope.test.ts +120 -35
- package/src/rbac/hooks/useResolvedScope.ts +35 -40
- package/src/rbac/hooks/useSecureSupabase.ts +7 -7
- package/src/rbac/index.ts +7 -0
- package/src/rbac/secureClient.test.ts +22 -18
- package/src/rbac/secureClient.ts +103 -16
- package/src/rbac/security.ts +0 -17
- package/src/rbac/types.ts +1 -0
- package/src/rbac/utils/__tests__/contextValidator.test.ts +64 -86
- package/src/rbac/utils/clientSecurity.ts +93 -0
- package/src/rbac/utils/contextValidator.ts +77 -168
- package/src/services/AuthService.ts +39 -7
- package/src/services/EventService.ts +285 -56
- package/src/services/OrganisationService.ts +81 -14
- package/src/services/__tests__/EventService.test.ts +1 -2
- package/src/services/base/BaseService.ts +3 -0
- package/src/utils/dynamic/dynamicUtils.ts +7 -4
- package/dist/chunk-24UVZUZG.js.map +0 -1
- package/dist/chunk-3XC4CPTD.js.map +0 -1
- package/dist/chunk-6J4GEEJR.js.map +0 -1
- package/dist/chunk-7D4SUZUM.js +0 -38
- package/dist/chunk-EHMR7VYL.js.map +0 -1
- package/dist/chunk-NECFR5MM.js.map +0 -1
- package/dist/chunk-SFZUDBL5.js.map +0 -1
- package/dist/chunk-XWQCNGTQ.js.map +0 -1
- package/docs/api/classes/ColumnFactory.md +0 -243
- package/docs/api/classes/InvalidScopeError.md +0 -73
- package/docs/api/classes/Logger.md +0 -178
- package/docs/api/classes/MissingUserContextError.md +0 -66
- package/docs/api/classes/OrganisationContextRequiredError.md +0 -66
- package/docs/api/classes/PermissionDeniedError.md +0 -73
- package/docs/api/classes/RBACAuditManager.md +0 -297
- package/docs/api/classes/RBACCache.md +0 -322
- package/docs/api/classes/RBACEngine.md +0 -171
- package/docs/api/classes/RBACError.md +0 -76
- package/docs/api/classes/RBACNotInitializedError.md +0 -66
- package/docs/api/classes/SecureSupabaseClient.md +0 -163
- package/docs/api/classes/StorageUtils.md +0 -328
- package/docs/api/enums/FileCategory.md +0 -184
- package/docs/api/enums/LogLevel.md +0 -54
- package/docs/api/enums/RBACErrorCode.md +0 -228
- package/docs/api/enums/RPCFunction.md +0 -118
- package/docs/api/interfaces/AddressFieldProps.md +0 -241
- package/docs/api/interfaces/AddressFieldRef.md +0 -94
- package/docs/api/interfaces/AggregateConfig.md +0 -43
- package/docs/api/interfaces/AutocompleteOptions.md +0 -75
- package/docs/api/interfaces/AvatarProps.md +0 -128
- package/docs/api/interfaces/BadgeProps.md +0 -34
- package/docs/api/interfaces/ButtonProps.md +0 -56
- package/docs/api/interfaces/CalendarProps.md +0 -73
- package/docs/api/interfaces/CardProps.md +0 -69
- package/docs/api/interfaces/ColorPalette.md +0 -7
- package/docs/api/interfaces/ColorShade.md +0 -66
- package/docs/api/interfaces/ComplianceResult.md +0 -30
- package/docs/api/interfaces/DataAccessRecord.md +0 -96
- package/docs/api/interfaces/DataRecord.md +0 -11
- package/docs/api/interfaces/DataTableAction.md +0 -252
- package/docs/api/interfaces/DataTableColumn.md +0 -504
- package/docs/api/interfaces/DataTableProps.md +0 -625
- package/docs/api/interfaces/DataTableToolbarButton.md +0 -96
- package/docs/api/interfaces/DatabaseComplianceResult.md +0 -85
- package/docs/api/interfaces/DatabaseIssue.md +0 -41
- package/docs/api/interfaces/EmptyStateConfig.md +0 -61
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +0 -235
- package/docs/api/interfaces/ErrorBoundaryProps.md +0 -147
- package/docs/api/interfaces/ErrorBoundaryProviderProps.md +0 -36
- package/docs/api/interfaces/ErrorBoundaryState.md +0 -75
- package/docs/api/interfaces/EventAppRoleData.md +0 -71
- package/docs/api/interfaces/ExportColumn.md +0 -90
- package/docs/api/interfaces/ExportOptions.md +0 -126
- package/docs/api/interfaces/FileDisplayProps.md +0 -249
- package/docs/api/interfaces/FileMetadata.md +0 -129
- package/docs/api/interfaces/FileReference.md +0 -118
- package/docs/api/interfaces/FileSizeLimits.md +0 -7
- package/docs/api/interfaces/FileUploadOptions.md +0 -139
- package/docs/api/interfaces/FileUploadProps.md +0 -296
- package/docs/api/interfaces/FooterProps.md +0 -107
- package/docs/api/interfaces/FormFieldProps.md +0 -166
- package/docs/api/interfaces/FormProps.md +0 -113
- package/docs/api/interfaces/GrantEventAppRoleParams.md +0 -122
- package/docs/api/interfaces/InactivityWarningModalProps.md +0 -115
- package/docs/api/interfaces/InputProps.md +0 -56
- package/docs/api/interfaces/LabelProps.md +0 -107
- package/docs/api/interfaces/LoggerConfig.md +0 -62
- package/docs/api/interfaces/LoginFormProps.md +0 -187
- package/docs/api/interfaces/NavigationAccessRecord.md +0 -107
- package/docs/api/interfaces/NavigationContextType.md +0 -164
- package/docs/api/interfaces/NavigationGuardProps.md +0 -139
- package/docs/api/interfaces/NavigationItem.md +0 -120
- package/docs/api/interfaces/NavigationMenuProps.md +0 -221
- package/docs/api/interfaces/NavigationProviderProps.md +0 -117
- package/docs/api/interfaces/Organisation.md +0 -140
- package/docs/api/interfaces/OrganisationContextType.md +0 -388
- package/docs/api/interfaces/OrganisationMembership.md +0 -140
- package/docs/api/interfaces/OrganisationProviderProps.md +0 -76
- package/docs/api/interfaces/OrganisationSecurityError.md +0 -62
- package/docs/api/interfaces/PaceAppLayoutProps.md +0 -409
- package/docs/api/interfaces/PaceLoginPageProps.md +0 -49
- package/docs/api/interfaces/PageAccessRecord.md +0 -85
- package/docs/api/interfaces/PagePermissionContextType.md +0 -140
- package/docs/api/interfaces/PagePermissionGuardProps.md +0 -153
- package/docs/api/interfaces/PagePermissionProviderProps.md +0 -119
- package/docs/api/interfaces/PaletteData.md +0 -41
- package/docs/api/interfaces/ParsedAddress.md +0 -120
- package/docs/api/interfaces/PermissionEnforcerProps.md +0 -153
- package/docs/api/interfaces/ProgressProps.md +0 -42
- package/docs/api/interfaces/ProtectedRouteProps.md +0 -78
- package/docs/api/interfaces/PublicPageFooterProps.md +0 -112
- package/docs/api/interfaces/PublicPageHeaderProps.md +0 -125
- package/docs/api/interfaces/PublicPageLayoutProps.md +0 -185
- package/docs/api/interfaces/QuickFix.md +0 -52
- package/docs/api/interfaces/RBACAccessValidateParams.md +0 -52
- package/docs/api/interfaces/RBACAccessValidateResult.md +0 -41
- package/docs/api/interfaces/RBACAuditLogParams.md +0 -85
- package/docs/api/interfaces/RBACAuditLogResult.md +0 -52
- package/docs/api/interfaces/RBACConfig.md +0 -133
- package/docs/api/interfaces/RBACContext.md +0 -52
- package/docs/api/interfaces/RBACLogger.md +0 -112
- package/docs/api/interfaces/RBACPageAccessCheckParams.md +0 -74
- package/docs/api/interfaces/RBACPerformanceMetrics.md +0 -138
- package/docs/api/interfaces/RBACPermissionCheckParams.md +0 -74
- package/docs/api/interfaces/RBACPermissionCheckResult.md +0 -52
- package/docs/api/interfaces/RBACPermissionsGetParams.md +0 -63
- package/docs/api/interfaces/RBACPermissionsGetResult.md +0 -63
- package/docs/api/interfaces/RBACResult.md +0 -58
- package/docs/api/interfaces/RBACRoleGrantParams.md +0 -63
- package/docs/api/interfaces/RBACRoleGrantResult.md +0 -52
- package/docs/api/interfaces/RBACRoleRevokeParams.md +0 -63
- package/docs/api/interfaces/RBACRoleRevokeResult.md +0 -52
- package/docs/api/interfaces/RBACRoleValidateParams.md +0 -52
- package/docs/api/interfaces/RBACRoleValidateResult.md +0 -63
- package/docs/api/interfaces/RBACRolesListParams.md +0 -52
- package/docs/api/interfaces/RBACRolesListResult.md +0 -74
- package/docs/api/interfaces/RBACSessionTrackParams.md +0 -74
- package/docs/api/interfaces/RBACSessionTrackResult.md +0 -52
- package/docs/api/interfaces/ResourcePermissions.md +0 -155
- package/docs/api/interfaces/RevokeEventAppRoleParams.md +0 -100
- package/docs/api/interfaces/RoleBasedRouterContextType.md +0 -151
- package/docs/api/interfaces/RoleBasedRouterProps.md +0 -156
- package/docs/api/interfaces/RoleManagementResult.md +0 -52
- package/docs/api/interfaces/RouteAccessRecord.md +0 -107
- package/docs/api/interfaces/RouteConfig.md +0 -134
- package/docs/api/interfaces/RuntimeComplianceResult.md +0 -55
- package/docs/api/interfaces/SecureDataContextType.md +0 -168
- package/docs/api/interfaces/SecureDataProviderProps.md +0 -132
- package/docs/api/interfaces/SessionRestorationLoaderProps.md +0 -34
- package/docs/api/interfaces/SetupIssue.md +0 -41
- package/docs/api/interfaces/StorageConfig.md +0 -41
- package/docs/api/interfaces/StorageFileInfo.md +0 -74
- package/docs/api/interfaces/StorageFileMetadata.md +0 -151
- package/docs/api/interfaces/StorageListOptions.md +0 -99
- package/docs/api/interfaces/StorageListResult.md +0 -41
- package/docs/api/interfaces/StorageUploadOptions.md +0 -101
- package/docs/api/interfaces/StorageUploadResult.md +0 -63
- package/docs/api/interfaces/StorageUrlOptions.md +0 -60
- package/docs/api/interfaces/StyleImport.md +0 -19
- package/docs/api/interfaces/SwitchProps.md +0 -34
- package/docs/api/interfaces/TabsContentProps.md +0 -9
- package/docs/api/interfaces/TabsListProps.md +0 -9
- package/docs/api/interfaces/TabsProps.md +0 -9
- package/docs/api/interfaces/TabsTriggerProps.md +0 -50
- package/docs/api/interfaces/TextareaProps.md +0 -53
- package/docs/api/interfaces/ToastActionElement.md +0 -12
- package/docs/api/interfaces/ToastProps.md +0 -9
- package/docs/api/interfaces/UnifiedAuthContextType.md +0 -823
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +0 -173
- package/docs/api/interfaces/UseFormDialogOptions.md +0 -62
- package/docs/api/interfaces/UseFormDialogReturn.md +0 -117
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +0 -138
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +0 -123
- package/docs/api/interfaces/UsePublicEventLogoOptions.md +0 -87
- package/docs/api/interfaces/UsePublicEventLogoReturn.md +0 -84
- package/docs/api/interfaces/UsePublicEventOptions.md +0 -34
- package/docs/api/interfaces/UsePublicEventReturn.md +0 -71
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +0 -47
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +0 -123
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +0 -97
- package/docs/api/interfaces/UseResolvedScopeOptions.md +0 -47
- package/docs/api/interfaces/UseResolvedScopeReturn.md +0 -47
- package/docs/api/interfaces/UseResourcePermissionsOptions.md +0 -34
- package/docs/api/interfaces/UserEventAccess.md +0 -121
- package/docs/api/interfaces/UserMenuProps.md +0 -88
- package/docs/api/interfaces/UserProfile.md +0 -63
- package/src/components/EventSelector/EventSelector.test.tsx +0 -720
- package/src/components/EventSelector/EventSelector.tsx +0 -423
- package/src/components/EventSelector/index.ts +0 -3
- package/src/components/OrganisationSelector/OrganisationSelector.test.tsx +0 -784
- package/src/components/OrganisationSelector/OrganisationSelector.tsx +0 -327
- package/src/components/OrganisationSelector/index.ts +0 -9
- /package/dist/{DataTable-TPTKCX4D.js.map → DataTable-E7YQZD7D.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-CH6Z342H.js.map → UnifiedAuthProvider-QPXO24B4.js.map} +0 -0
- /package/dist/{api-MVVQZLJI.js.map → api-6LVZTHDS.js.map} +0 -0
- /package/dist/{audit-B5P6FFIR.js.map → audit-V53FV5AG.js.map} +0 -0
- /package/dist/{chunk-2UOI2FG5.js.map → chunk-5EC5MEWX.js.map} +0 -0
- /package/dist/{chunk-7D4SUZUM.js.map → chunk-DGUM43GV.js.map} +0 -0
- /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
|
|
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('
|
|
23
|
-
it('
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
|
39
|
-
expect(result.error).toBeInstanceOf(OrganisationContextRequiredError);
|
|
29
|
+
expect(result).toBe('derived-org');
|
|
40
30
|
});
|
|
31
|
+
});
|
|
41
32
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
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('
|
|
51
|
-
const
|
|
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('
|
|
58
|
-
const
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
const
|
|
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).
|
|
71
|
-
|
|
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('
|
|
78
|
-
const
|
|
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).
|
|
75
|
+
expect(result.error?.message).toContain('either organisation or event context');
|
|
82
76
|
});
|
|
83
77
|
|
|
84
|
-
it('
|
|
85
|
-
const
|
|
86
|
-
const
|
|
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(
|
|
91
|
-
expect(result).
|
|
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('
|
|
99
|
-
const
|
|
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(
|
|
102
|
-
expect(result.
|
|
90
|
+
expect(result.isValid).toBe(true);
|
|
91
|
+
expect(result.resolvedScope?.eventId).toBe('event-1');
|
|
103
92
|
});
|
|
104
93
|
|
|
105
|
-
it('
|
|
94
|
+
it('handles both scope - derives org from event when event provided', async () => {
|
|
106
95
|
const supabase = createSupabaseMock();
|
|
107
|
-
vi.spyOn(ContextValidator, 'deriveOrgFromEvent').
|
|
96
|
+
vi.spyOn(ContextValidator, 'deriveOrgFromEvent').mockResolvedValue('derived-org');
|
|
108
97
|
|
|
109
|
-
const
|
|
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(
|
|
112
|
-
expect(result.
|
|
113
|
-
expect(result.
|
|
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
|
|
124
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
131
|
-
|
|
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('
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
141
|
-
|
|
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
|
-
|
|
146
|
-
expect(
|
|
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
|
-
|
|
26
|
-
|
|
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
|
-
*
|
|
54
|
+
* Derive organisation ID from event ID
|
|
52
55
|
*
|
|
53
|
-
* @param
|
|
54
|
-
* @param
|
|
55
|
-
* @
|
|
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
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
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
|
|
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
|
|
79
|
+
static async resolveScopeForPage(
|
|
138
80
|
scope: Scope,
|
|
139
|
-
|
|
81
|
+
pageScopeType: PageScopeType,
|
|
140
82
|
appName?: string,
|
|
141
83
|
supabase?: SupabaseClient<Database> | null
|
|
142
84
|
): Promise<ContextValidationResult> {
|
|
143
|
-
//
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
|
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:
|
|
114
|
+
resolvedScope: {
|
|
115
|
+
organisationId,
|
|
116
|
+
eventId: scope.eventId,
|
|
117
|
+
appId: scope.appId
|
|
118
|
+
},
|
|
168
119
|
error: null
|
|
169
120
|
};
|
|
170
121
|
}
|
|
171
|
-
|
|
172
|
-
//
|
|
173
|
-
if (
|
|
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
|
-
//
|
|
223
|
-
if (
|
|
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:
|
|
226
|
-
resolvedScope:
|
|
227
|
-
|
|
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:
|
|
233
|
-
resolvedScope:
|
|
234
|
-
error:
|
|
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
|
|