@jmruthers/pace-core 0.5.186 → 0.5.187
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/dist/{DataTable-Z9NLVJh0.d.ts → DataTable-IVYljGJ6.d.ts} +1 -1
- package/dist/{DataTable-IX2NBUTP.js → DataTable-K3RJRSOX.js} +7 -7
- package/dist/{PublicPageProvider-DIzEzwKl.d.ts → PublicPageProvider-DrLDztHt.d.ts} +211 -106
- package/dist/{UnifiedAuthProvider-A4BCQRJY.js → UnifiedAuthProvider-B76OWOAT.js} +2 -2
- package/dist/{api-BMFCXVQX.js → api-YP7XD5L6.js} +3 -3
- package/dist/{audit-WRS3KJKI.js → audit-B5P6FFIR.js} +2 -2
- package/dist/{chunk-445GEP27.js → chunk-3IC5WCMO.js} +33 -8
- package/dist/chunk-3IC5WCMO.js.map +1 -0
- package/dist/{chunk-OALXJH4Y.js → chunk-3NFNJOO7.js} +8 -8
- package/dist/chunk-3NFNJOO7.js.map +1 -0
- package/dist/{chunk-FSFQFJCU.js → chunk-63FOKYGO.js} +174 -6
- package/dist/chunk-63FOKYGO.js.map +1 -0
- package/dist/{chunk-TC7D3CR3.js → chunk-C4OYJOV4.js} +556 -101
- package/dist/chunk-C4OYJOV4.js.map +1 -0
- package/dist/{chunk-HGPQUCBC.js → chunk-FMTK4XNN.js} +3 -3
- package/dist/{chunk-U6WNSFX5.js → chunk-HEHYGYOX.js} +279 -44
- package/dist/chunk-HEHYGYOX.js.map +1 -0
- package/dist/{chunk-XAUHJD3L.js → chunk-K2JGDXGU.js} +2 -2
- package/dist/{chunk-HDCUMOOI.js → chunk-LBBUPSSC.js} +792 -559
- package/dist/chunk-LBBUPSSC.js.map +1 -0
- package/dist/{chunk-UQWSHFVX.js → chunk-SAUPYVLF.js} +1 -1
- package/dist/{chunk-UQWSHFVX.js.map → chunk-SAUPYVLF.js.map} +1 -1
- package/dist/{chunk-GRIQLQ52.js → chunk-T6ZJVI3A.js} +27 -23
- package/dist/chunk-T6ZJVI3A.js.map +1 -0
- package/dist/{chunk-DAGICKHT.js → chunk-ULX5FYEM.js} +3 -3
- package/dist/{chunk-FXFJRTKI.js → chunk-WK2Y6TGA.js} +3 -3
- package/dist/chunk-WK2Y6TGA.js.map +1 -0
- package/dist/chunk-YHCN776L.js +447 -0
- package/dist/chunk-YHCN776L.js.map +1 -0
- package/dist/components.d.ts +4 -4
- package/dist/components.js +12 -10
- package/dist/components.js.map +1 -1
- package/dist/{file-reference-PRTSLxKx.d.ts → file-reference-D037xOFK.d.ts} +0 -1
- package/dist/hooks.d.ts +221 -6
- package/dist/hooks.js +146 -49
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +24 -9
- package/dist/index.js +62 -28
- package/dist/index.js.map +1 -1
- package/dist/providers.js +1 -1
- package/dist/rbac/index.d.ts +124 -7
- package/dist/rbac/index.js +27 -7
- package/dist/{types-DUyCRSTj.d.ts → types-Bwgl--Xo.d.ts} +162 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/{usePublicRouteParams-D71QLlg4.d.ts → usePublicRouteParams-CTDELQ7H.d.ts} +2 -2
- package/dist/utils.d.ts +213 -3
- package/dist/utils.js +22 -2
- package/dist/utils.js.map +1 -1
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +1 -1
- package/docs/api/classes/Logger.md +1 -1
- package/docs/api/classes/MissingUserContextError.md +1 -1
- package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
- package/docs/api/classes/PermissionDeniedError.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +21 -17
- package/docs/api/classes/RBACCache.md +31 -23
- package/docs/api/classes/RBACEngine.md +5 -5
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +1 -1
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/enums/LogLevel.md +1 -1
- package/docs/api/enums/RBACErrorCode.md +1 -1
- package/docs/api/enums/RPCFunction.md +1 -1
- package/docs/api/interfaces/AddressFieldProps.md +241 -0
- package/docs/api/interfaces/AddressFieldRef.md +94 -0
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/AutocompleteOptions.md +75 -0
- package/docs/api/interfaces/BadgeProps.md +1 -1
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CalendarProps.md +1 -1
- package/docs/api/interfaces/CardProps.md +1 -1
- package/docs/api/interfaces/ColorPalette.md +1 -1
- package/docs/api/interfaces/ColorShade.md +1 -1
- package/docs/api/interfaces/ComplianceResult.md +1 -1
- package/docs/api/interfaces/DataAccessRecord.md +1 -1
- package/docs/api/interfaces/DataRecord.md +1 -1
- package/docs/api/interfaces/DataTableAction.md +1 -1
- package/docs/api/interfaces/DataTableColumn.md +1 -1
- package/docs/api/interfaces/DataTableProps.md +1 -1
- package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
- package/docs/api/interfaces/DatabaseComplianceResult.md +1 -1
- package/docs/api/interfaces/DatabaseIssue.md +1 -1
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/EventAppRoleData.md +1 -1
- package/docs/api/interfaces/ExportColumn.md +1 -1
- package/docs/api/interfaces/ExportOptions.md +1 -1
- package/docs/api/interfaces/FileDisplayProps.md +15 -15
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/FormFieldProps.md +1 -1
- package/docs/api/interfaces/FormProps.md +1 -1
- package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoggerConfig.md +1 -1
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
- package/docs/api/interfaces/NavigationContextType.md +1 -1
- package/docs/api/interfaces/NavigationGuardProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +1 -1
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
- package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
- package/docs/api/interfaces/PageAccessRecord.md +1 -1
- package/docs/api/interfaces/PagePermissionContextType.md +1 -1
- package/docs/api/interfaces/PagePermissionGuardProps.md +11 -11
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/ParsedAddress.md +120 -0
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/ProgressProps.md +1 -1
- package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/QuickFix.md +1 -1
- package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
- package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
- package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
- package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +26 -3
- package/docs/api/interfaces/RBACContext.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +5 -5
- package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
- package/docs/api/interfaces/RBACPerformanceMetrics.md +138 -0
- package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
- package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
- package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
- package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
- package/docs/api/interfaces/RBACResult.md +1 -1
- package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
- package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
- package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
- package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
- package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
- package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
- package/docs/api/interfaces/RBACRolesListParams.md +1 -1
- package/docs/api/interfaces/RBACRolesListResult.md +1 -1
- package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
- package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
- package/docs/api/interfaces/ResourcePermissions.md +1 -1
- package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RoleManagementResult.md +1 -1
- package/docs/api/interfaces/RouteAccessRecord.md +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- package/docs/api/interfaces/RuntimeComplianceResult.md +1 -1
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
- package/docs/api/interfaces/SetupIssue.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/SwitchProps.md +1 -1
- package/docs/api/interfaces/TabsContentProps.md +1 -1
- package/docs/api/interfaces/TabsListProps.md +1 -1
- package/docs/api/interfaces/TabsProps.md +1 -1
- package/docs/api/interfaces/TabsTriggerProps.md +1 -1
- package/docs/api/interfaces/TextareaProps.md +1 -1
- package/docs/api/interfaces/ToastActionElement.md +1 -1
- package/docs/api/interfaces/ToastProps.md +1 -1
- package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
- package/docs/api/interfaces/UseFormDialogOptions.md +1 -1
- package/docs/api/interfaces/UseFormDialogReturn.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
- package/docs/api/interfaces/UserEventAccess.md +1 -1
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +318 -59
- package/docs/best-practices/performance.md +11 -0
- package/docs/implementation-guides/file-upload-storage.md +29 -0
- package/docs/rbac/README.md +2 -1
- package/docs/rbac/api-reference.md +11 -0
- package/docs/rbac/performance.md +320 -0
- package/docs/standards/01-architecture-standard.md +5 -0
- package/docs/standards/05-security-standard.md +12 -0
- package/package.json +1 -1
- package/src/components/AddressField/AddressField.test.tsx +411 -0
- package/src/components/AddressField/AddressField.tsx +323 -0
- package/src/components/AddressField/README.md +336 -0
- package/src/components/AddressField/index.ts +10 -0
- package/src/components/AddressField/types.ts +65 -0
- package/src/components/FileDisplay/FileDisplay.test.tsx +454 -0
- package/src/components/FileDisplay/FileDisplay.tsx +28 -1
- package/src/components/index.ts +2 -0
- package/src/hooks/__tests__/useFileDisplay.unit.test.ts +30 -5
- package/src/hooks/__tests__/useOrganisationSecurity.unit.test.tsx +11 -10
- package/src/hooks/__tests__/usePublicFileDisplay.test.ts +31 -6
- package/src/hooks/index.ts +6 -0
- package/src/hooks/public/usePublicFileDisplay.ts +8 -10
- package/src/hooks/useAddressAutocomplete.test.ts +318 -0
- package/src/hooks/useAddressAutocomplete.ts +268 -0
- package/src/hooks/useFileDisplay.ts +3 -15
- package/src/hooks/useFileReference.test.ts +20 -3
- package/src/hooks/useFileReference.ts +3 -24
- package/src/hooks/useFileUrlCache.ts +246 -0
- package/src/hooks/useInactivityTracker.ts +31 -20
- package/src/hooks/useOrganisationSecurity.test.ts +10 -7
- package/src/hooks/useOrganisationSecurity.ts +3 -3
- package/src/hooks/useQueryCache.ts +315 -0
- package/src/index.ts +2 -0
- package/src/providers/services/EventServiceProvider.tsx +4 -1
- package/src/rbac/api.test.ts +21 -6
- package/src/rbac/api.ts +32 -11
- package/src/rbac/audit-batched.ts +223 -0
- package/src/rbac/audit-enhanced.ts +2 -2
- package/src/rbac/audit.test.ts +6 -5
- package/src/rbac/audit.ts +34 -6
- package/src/rbac/cache-invalidation.ts +63 -12
- package/src/rbac/cache.test.ts +2 -2
- package/src/rbac/cache.ts +61 -14
- package/src/rbac/components/PagePermissionGuard.tsx +19 -10
- package/src/rbac/components/__tests__/PagePermissionGuard.performance.test.tsx +248 -0
- package/src/rbac/config.ts +9 -0
- package/src/rbac/engine.ts +2 -21
- package/src/rbac/hooks/usePermissions.ts +21 -5
- package/src/rbac/index.ts +19 -0
- package/src/rbac/performance.ts +210 -0
- package/src/rbac/request-deduplication.ts +87 -0
- package/src/rbac/utils/deep-equal.ts +93 -0
- package/src/types/file-reference.ts +0 -1
- package/src/utils/file-reference/__tests__/file-reference.test.ts +31 -4
- package/src/utils/file-reference/index.ts +44 -15
- package/src/utils/google-places/googlePlacesUtils.test.ts +403 -0
- package/src/utils/google-places/googlePlacesUtils.ts +475 -0
- package/src/utils/google-places/index.ts +26 -0
- package/src/utils/google-places/loadGoogleMapsScript.ts +207 -0
- package/src/utils/google-places/types.ts +94 -0
- package/src/utils/index.ts +23 -0
- package/src/utils/request-deduplication.ts +165 -0
- package/src/utils/storage/helpers.ts +143 -4
- package/dist/chunk-445GEP27.js.map +0 -1
- package/dist/chunk-FMUCXFII.js +0 -76
- package/dist/chunk-FMUCXFII.js.map +0 -1
- package/dist/chunk-FSFQFJCU.js.map +0 -1
- package/dist/chunk-FXFJRTKI.js.map +0 -1
- package/dist/chunk-GRIQLQ52.js.map +0 -1
- package/dist/chunk-HDCUMOOI.js.map +0 -1
- package/dist/chunk-OALXJH4Y.js.map +0 -1
- package/dist/chunk-TC7D3CR3.js.map +0 -1
- package/dist/chunk-U6WNSFX5.js.map +0 -1
- /package/dist/{DataTable-IX2NBUTP.js.map → DataTable-K3RJRSOX.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-A4BCQRJY.js.map → UnifiedAuthProvider-B76OWOAT.js.map} +0 -0
- /package/dist/{api-BMFCXVQX.js.map → api-YP7XD5L6.js.map} +0 -0
- /package/dist/{audit-WRS3KJKI.js.map → audit-B5P6FFIR.js.map} +0 -0
- /package/dist/{chunk-HGPQUCBC.js.map → chunk-FMTK4XNN.js.map} +0 -0
- /package/dist/{chunk-XAUHJD3L.js.map → chunk-K2JGDXGU.js.map} +0 -0
- /package/dist/{chunk-DAGICKHT.js.map → chunk-ULX5FYEM.js.map} +0 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Batched Audit Manager for RBAC
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module RBAC/AuditBatched
|
|
5
|
+
* @since 2.0.0
|
|
6
|
+
*
|
|
7
|
+
* This module provides batched audit logging to reduce network requests
|
|
8
|
+
* by queuing events and sending them in batches.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { SupabaseClient } from '@supabase/supabase-js';
|
|
12
|
+
import { Database } from '../types/database';
|
|
13
|
+
import { RBACAuditEvent, UUID } from './types';
|
|
14
|
+
import { AuditEventPayload } from './audit';
|
|
15
|
+
import { logger } from '../utils/core/logger';
|
|
16
|
+
|
|
17
|
+
export interface BatchedAuditConfig {
|
|
18
|
+
/** Enable batched audit logging (default: true) */
|
|
19
|
+
enabled: boolean;
|
|
20
|
+
/** Time window in milliseconds to wait before sending batch (default: 100ms) */
|
|
21
|
+
batchWindow: number;
|
|
22
|
+
/** Maximum batch size before forcing send (default: 10) */
|
|
23
|
+
batchSize: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const DEFAULT_CONFIG: BatchedAuditConfig = {
|
|
27
|
+
enabled: true,
|
|
28
|
+
batchWindow: 500, // 500ms - increased for better batching
|
|
29
|
+
batchSize: 20, // Increased from 10 to 20 for better efficiency
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Batched Audit Manager
|
|
34
|
+
*
|
|
35
|
+
* Queues audit events and sends them in batches to reduce network requests.
|
|
36
|
+
*/
|
|
37
|
+
export class BatchedAuditManager {
|
|
38
|
+
private supabase: SupabaseClient<Database>;
|
|
39
|
+
private config: BatchedAuditConfig;
|
|
40
|
+
private eventQueue: Array<Omit<RBACAuditEvent, 'id' | 'created_at'>> = [];
|
|
41
|
+
private flushTimer: ReturnType<typeof setTimeout> | null = null;
|
|
42
|
+
private isFlushing: boolean = false;
|
|
43
|
+
|
|
44
|
+
constructor(supabase: SupabaseClient<Database>, config: Partial<BatchedAuditConfig> = {}) {
|
|
45
|
+
this.supabase = supabase;
|
|
46
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Update configuration
|
|
51
|
+
*/
|
|
52
|
+
updateConfig(config: Partial<BatchedAuditConfig>): void {
|
|
53
|
+
this.config = { ...this.config, ...config };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Queue an audit event for batching
|
|
58
|
+
*
|
|
59
|
+
* @param event - Audit event payload
|
|
60
|
+
*/
|
|
61
|
+
async queueEvent(event: AuditEventPayload): Promise<void> {
|
|
62
|
+
if (!this.config.enabled) {
|
|
63
|
+
// If batching is disabled, send immediately
|
|
64
|
+
await this.sendEventImmediately(event);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Skip audit logging for cached checks (only log on cache miss)
|
|
69
|
+
if ('cache_hit' in event && event.cache_hit === true) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Convert event payload to database format
|
|
74
|
+
const auditEvent = this.convertToAuditEvent(event);
|
|
75
|
+
if (!auditEvent) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Add to queue
|
|
80
|
+
this.eventQueue.push(auditEvent);
|
|
81
|
+
|
|
82
|
+
// Check if we should flush immediately
|
|
83
|
+
if (this.eventQueue.length >= this.config.batchSize) {
|
|
84
|
+
await this.flush();
|
|
85
|
+
} else {
|
|
86
|
+
// Schedule flush after batch window
|
|
87
|
+
this.scheduleFlush();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Convert audit event payload to database format
|
|
93
|
+
*/
|
|
94
|
+
private convertToAuditEvent(event: AuditEventPayload): Omit<RBACAuditEvent, 'id' | 'created_at'> | null {
|
|
95
|
+
// Validate required fields
|
|
96
|
+
if (!event.userId) {
|
|
97
|
+
logger.error('RBAC Audit', 'Cannot queue audit event without userId:', {
|
|
98
|
+
eventType: event.type,
|
|
99
|
+
organisationId: event.organisationId
|
|
100
|
+
});
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Validate pageId: only include in page_id column if it's a valid UUID
|
|
105
|
+
const rawPageId = 'pageId' in event ? event.pageId : undefined;
|
|
106
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
107
|
+
const isValidPageIdUuid = rawPageId && uuidRegex.test(rawPageId);
|
|
108
|
+
const pageIdUuid: UUID | undefined = isValidPageIdUuid ? (rawPageId as UUID) : undefined;
|
|
109
|
+
const pageIdName: string | undefined = rawPageId && !isValidPageIdUuid ? rawPageId : undefined;
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
event_type: event.type,
|
|
113
|
+
user_id: event.userId,
|
|
114
|
+
organisation_id: event.organisationId || null,
|
|
115
|
+
event_id: 'eventId' in event ? event.eventId : undefined,
|
|
116
|
+
app_id: 'appId' in event ? event.appId : undefined,
|
|
117
|
+
page_id: pageIdUuid,
|
|
118
|
+
permission: 'permission' in event ? event.permission : undefined,
|
|
119
|
+
decision: 'decision' in event ? event.decision : undefined,
|
|
120
|
+
source: 'source' in event ? event.source : 'api',
|
|
121
|
+
bypass: 'bypass' in event ? event.bypass : undefined,
|
|
122
|
+
duration_ms: 'duration_ms' in event ? event.duration_ms : undefined,
|
|
123
|
+
metadata: {
|
|
124
|
+
...event.metadata,
|
|
125
|
+
cache_hit: 'cache_hit' in event ? event.cache_hit : undefined,
|
|
126
|
+
cache_source: 'cache_source' in event ? event.cache_source : undefined,
|
|
127
|
+
no_organisation_context: !event.organisationId,
|
|
128
|
+
page_name: pageIdName,
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Schedule a flush after the batch window
|
|
135
|
+
*/
|
|
136
|
+
private scheduleFlush(): void {
|
|
137
|
+
if (this.flushTimer) {
|
|
138
|
+
return; // Already scheduled
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
this.flushTimer = setTimeout(() => {
|
|
142
|
+
this.flushTimer = null;
|
|
143
|
+
this.flush();
|
|
144
|
+
}, this.config.batchWindow);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Flush all queued events
|
|
149
|
+
*/
|
|
150
|
+
async flush(): Promise<void> {
|
|
151
|
+
if (this.isFlushing || this.eventQueue.length === 0) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Clear any pending timer
|
|
156
|
+
if (this.flushTimer) {
|
|
157
|
+
clearTimeout(this.flushTimer);
|
|
158
|
+
this.flushTimer = null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
this.isFlushing = true;
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
// Get all events from queue
|
|
165
|
+
const eventsToSend = [...this.eventQueue];
|
|
166
|
+
this.eventQueue = [];
|
|
167
|
+
|
|
168
|
+
if (eventsToSend.length === 0) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Send batch to database
|
|
173
|
+
const { error } = await (this.supabase as any)
|
|
174
|
+
.from('rbac_audit_events')
|
|
175
|
+
.insert(eventsToSend);
|
|
176
|
+
|
|
177
|
+
if (error) {
|
|
178
|
+
logger.warn('RBAC Audit', 'Failed to insert batched audit events:', {
|
|
179
|
+
error: error.message,
|
|
180
|
+
code: error.code,
|
|
181
|
+
batchSize: eventsToSend.length,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
} catch (error) {
|
|
185
|
+
logger.error('RBAC Audit', 'Unexpected error during batched audit logging:', error);
|
|
186
|
+
} finally {
|
|
187
|
+
this.isFlushing = false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Send event immediately (bypass batching)
|
|
193
|
+
*/
|
|
194
|
+
private async sendEventImmediately(event: AuditEventPayload): Promise<void> {
|
|
195
|
+
const auditEvent = this.convertToAuditEvent(event);
|
|
196
|
+
if (!auditEvent) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
const { error } = await (this.supabase as any)
|
|
202
|
+
.from('rbac_audit_events')
|
|
203
|
+
.insert([auditEvent]);
|
|
204
|
+
|
|
205
|
+
if (error) {
|
|
206
|
+
logger.warn('RBAC Audit', 'Failed to insert audit event:', {
|
|
207
|
+
error: error.message,
|
|
208
|
+
code: error.code,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
} catch (error) {
|
|
212
|
+
logger.error('RBAC Audit', 'Unexpected error during audit logging:', error);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Force flush and wait for completion
|
|
218
|
+
*/
|
|
219
|
+
async forceFlush(): Promise<void> {
|
|
220
|
+
await this.flush();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
@@ -294,7 +294,7 @@ export class EnhancedRBACAuditManager {
|
|
|
294
294
|
async getUserAuditEvents(userId: UUID, limit: number = 100): Promise<RBACAuditEvent[]> {
|
|
295
295
|
const { data, error } = await this.supabase
|
|
296
296
|
.from('rbac_audit_events')
|
|
297
|
-
.select('
|
|
297
|
+
.select('id, event_type, user_id, organisation_id, event_id, app_id, page_id, permission, decision, source, bypass, duration_ms, metadata, created_at')
|
|
298
298
|
.eq('user_id', userId)
|
|
299
299
|
.order('created_at', { ascending: false })
|
|
300
300
|
.limit(limit);
|
|
@@ -338,7 +338,7 @@ export class EnhancedRBACAuditManager {
|
|
|
338
338
|
async getOrganisationAuditEvents(organisationId: UUID, limit: number = 100): Promise<RBACAuditEvent[]> {
|
|
339
339
|
const { data, error } = await this.supabase
|
|
340
340
|
.from('rbac_audit_events')
|
|
341
|
-
.select('
|
|
341
|
+
.select('id, event_type, user_id, organisation_id, event_id, app_id, page_id, permission, decision, source, bypass, duration_ms, metadata, created_at')
|
|
342
342
|
.eq('organisation_id', organisationId)
|
|
343
343
|
.order('created_at', { ascending: false })
|
|
344
344
|
.limit(limit);
|
package/src/rbac/audit.test.ts
CHANGED
|
@@ -60,7 +60,8 @@ describe('RBACAuditManager', () => {
|
|
|
60
60
|
|
|
61
61
|
beforeEach(() => {
|
|
62
62
|
mockSupabase = createMockSupabaseClient();
|
|
63
|
-
|
|
63
|
+
// Disable batching in tests for predictable behavior
|
|
64
|
+
auditManager = new RBACAuditManager(mockSupabase as any, false);
|
|
64
65
|
});
|
|
65
66
|
|
|
66
67
|
afterEach(() => {
|
|
@@ -544,7 +545,7 @@ describe('RBACAuditManager', () => {
|
|
|
544
545
|
source: 'ui' as AuditEventSource,
|
|
545
546
|
bypass: true,
|
|
546
547
|
duration_ms: 250,
|
|
547
|
-
cache_hit:
|
|
548
|
+
cache_hit: false, // Set to false so event is actually logged (cached events are skipped)
|
|
548
549
|
cache_source: 'memory',
|
|
549
550
|
metadata: { test: 'data' }
|
|
550
551
|
};
|
|
@@ -567,7 +568,7 @@ describe('RBACAuditManager', () => {
|
|
|
567
568
|
source: 'ui',
|
|
568
569
|
bypass: true,
|
|
569
570
|
duration_ms: 250,
|
|
570
|
-
cache_hit:
|
|
571
|
+
cache_hit: false,
|
|
571
572
|
cache_source: 'memory',
|
|
572
573
|
metadata: { test: 'data' }
|
|
573
574
|
}),
|
|
@@ -648,7 +649,7 @@ describe('Global Audit Manager', () => {
|
|
|
648
649
|
});
|
|
649
650
|
|
|
650
651
|
it('creates and sets global audit manager', () => {
|
|
651
|
-
const manager = createAuditManager(mockSupabase as any);
|
|
652
|
+
const manager = createAuditManager(mockSupabase as any, false);
|
|
652
653
|
setGlobalAuditManager(manager);
|
|
653
654
|
|
|
654
655
|
const globalManager = getGlobalAuditManager();
|
|
@@ -656,7 +657,7 @@ describe('Global Audit Manager', () => {
|
|
|
656
657
|
});
|
|
657
658
|
|
|
658
659
|
it('emits events through global manager', async () => {
|
|
659
|
-
const manager = createAuditManager(mockSupabase as any);
|
|
660
|
+
const manager = createAuditManager(mockSupabase as any, false);
|
|
660
661
|
setGlobalAuditManager(manager);
|
|
661
662
|
|
|
662
663
|
const event: PermissionCheckAuditEvent = {
|
package/src/rbac/audit.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from './types';
|
|
17
17
|
import type { AuditEventType } from './types/functions';
|
|
18
18
|
import { logger } from '../utils/core/logger';
|
|
19
|
+
import { BatchedAuditManager } from './audit-batched';
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Audit event payload for permission checks
|
|
@@ -111,9 +112,19 @@ export class RBACAuditManager {
|
|
|
111
112
|
private supabase: SupabaseClient<Database>;
|
|
112
113
|
private enabled: boolean = true;
|
|
113
114
|
private fallbackEnabled: boolean = true;
|
|
114
|
-
|
|
115
|
-
|
|
115
|
+
private batchedManager: BatchedAuditManager | null = null;
|
|
116
|
+
private useBatching: boolean = true;
|
|
117
|
+
|
|
118
|
+
constructor(
|
|
119
|
+
supabase: SupabaseClient<Database>,
|
|
120
|
+
useBatching: boolean = true,
|
|
121
|
+
batchConfig?: { batchWindow?: number; batchSize?: number }
|
|
122
|
+
) {
|
|
116
123
|
this.supabase = supabase;
|
|
124
|
+
this.useBatching = useBatching;
|
|
125
|
+
if (useBatching) {
|
|
126
|
+
this.batchedManager = new BatchedAuditManager(supabase, batchConfig);
|
|
127
|
+
}
|
|
117
128
|
}
|
|
118
129
|
|
|
119
130
|
/**
|
|
@@ -154,6 +165,17 @@ export class RBACAuditManager {
|
|
|
154
165
|
return;
|
|
155
166
|
}
|
|
156
167
|
|
|
168
|
+
// Skip audit logging for cached checks (only log on cache miss)
|
|
169
|
+
if ('cache_hit' in event && event.cache_hit === true) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Use batched manager if enabled
|
|
174
|
+
if (this.useBatching && this.batchedManager) {
|
|
175
|
+
await this.batchedManager.queueEvent(event);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
157
179
|
// Validate required fields before attempting to insert
|
|
158
180
|
// MANDATORY: All audit events must have userId
|
|
159
181
|
if (!event.userId) {
|
|
@@ -332,7 +354,7 @@ export class RBACAuditManager {
|
|
|
332
354
|
async getUserAuditEvents(userId: UUID, limit: number = 100): Promise<RBACAuditEvent[]> {
|
|
333
355
|
const { data, error } = await this.supabase
|
|
334
356
|
.from('rbac_audit_events')
|
|
335
|
-
.select('
|
|
357
|
+
.select('id, event_type, user_id, organisation_id, event_id, app_id, page_id, permission, decision, source, bypass, duration_ms, metadata, created_at')
|
|
336
358
|
.eq('user_id', userId)
|
|
337
359
|
.order('created_at', { ascending: false })
|
|
338
360
|
.limit(limit);
|
|
@@ -376,7 +398,7 @@ export class RBACAuditManager {
|
|
|
376
398
|
async getOrganisationAuditEvents(organisationId: UUID, limit: number = 100): Promise<RBACAuditEvent[]> {
|
|
377
399
|
const { data, error } = await this.supabase
|
|
378
400
|
.from('rbac_audit_events')
|
|
379
|
-
.select('
|
|
401
|
+
.select('id, event_type, user_id, organisation_id, event_id, app_id, page_id, permission, decision, source, bypass, duration_ms, metadata, created_at')
|
|
380
402
|
.eq('organisation_id', organisationId)
|
|
381
403
|
.order('created_at', { ascending: false })
|
|
382
404
|
.limit(limit);
|
|
@@ -415,10 +437,16 @@ export class RBACAuditManager {
|
|
|
415
437
|
* Create an audit manager instance
|
|
416
438
|
*
|
|
417
439
|
* @param supabase - Supabase client
|
|
440
|
+
* @param useBatching - Whether to use batched audit logging (default: true)
|
|
441
|
+
* @param batchConfig - Optional batch configuration
|
|
418
442
|
* @returns RBACAuditManager instance
|
|
419
443
|
*/
|
|
420
|
-
export function createAuditManager(
|
|
421
|
-
|
|
444
|
+
export function createAuditManager(
|
|
445
|
+
supabase: SupabaseClient<Database>,
|
|
446
|
+
useBatching: boolean = true,
|
|
447
|
+
batchConfig?: { batchWindow?: number; batchSize?: number }
|
|
448
|
+
): RBACAuditManager {
|
|
449
|
+
return new RBACAuditManager(supabase, useBatching, batchConfig);
|
|
422
450
|
}
|
|
423
451
|
|
|
424
452
|
/**
|
|
@@ -67,6 +67,7 @@ export const INVALIDATION_PATTERNS = {
|
|
|
67
67
|
export class RBACCacheInvalidationManager {
|
|
68
68
|
private supabase: SupabaseClient<Database>;
|
|
69
69
|
private invalidationCallbacks: Set<(pattern: string) => void> = new Set();
|
|
70
|
+
private channels: Array<{ unsubscribe: () => void }> = [];
|
|
70
71
|
|
|
71
72
|
constructor(supabase: SupabaseClient<Database>) {
|
|
72
73
|
this.supabase = supabase;
|
|
@@ -179,6 +180,7 @@ export class RBACCacheInvalidationManager {
|
|
|
179
180
|
|
|
180
181
|
/**
|
|
181
182
|
* Setup realtime subscriptions for automatic cache invalidation
|
|
183
|
+
* Prevents duplicate subscriptions by checking if already set up
|
|
182
184
|
*/
|
|
183
185
|
private setupRealtimeSubscriptions(): void {
|
|
184
186
|
// Check if realtime is available (skip in test environments)
|
|
@@ -186,9 +188,15 @@ export class RBACCacheInvalidationManager {
|
|
|
186
188
|
log.debug('Realtime not available, skipping subscriptions');
|
|
187
189
|
return;
|
|
188
190
|
}
|
|
191
|
+
|
|
192
|
+
// Prevent duplicate subscriptions - if channels already exist, skip setup
|
|
193
|
+
if (this.channels.length > 0) {
|
|
194
|
+
log.debug('Realtime subscriptions already set up, skipping duplicate setup');
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
189
197
|
|
|
190
198
|
// Subscribe to organisation role changes
|
|
191
|
-
this.supabase
|
|
199
|
+
const orgRolesChannel = this.supabase
|
|
192
200
|
.channel('rbac_organisation_roles_changes')
|
|
193
201
|
.on('postgres_changes', {
|
|
194
202
|
event: '*',
|
|
@@ -202,11 +210,12 @@ export class RBACCacheInvalidationManager {
|
|
|
202
210
|
if (user_id) {
|
|
203
211
|
this.invalidateUser(user_id, `organisation_role_${payload.eventType}`);
|
|
204
212
|
}
|
|
205
|
-
})
|
|
206
|
-
|
|
213
|
+
});
|
|
214
|
+
const orgRolesSubscription = orgRolesChannel.subscribe();
|
|
215
|
+
this.channels.push(orgRolesSubscription);
|
|
207
216
|
|
|
208
217
|
// Subscribe to event app role changes
|
|
209
|
-
this.supabase
|
|
218
|
+
const eventAppRolesChannel = this.supabase
|
|
210
219
|
.channel('rbac_event_app_roles_changes')
|
|
211
220
|
.on('postgres_changes', {
|
|
212
221
|
event: '*',
|
|
@@ -226,11 +235,12 @@ export class RBACCacheInvalidationManager {
|
|
|
226
235
|
if (app_id) {
|
|
227
236
|
this.invalidateApp(app_id, `event_app_role_${payload.eventType}`);
|
|
228
237
|
}
|
|
229
|
-
})
|
|
230
|
-
|
|
238
|
+
});
|
|
239
|
+
const eventAppRolesSubscription = eventAppRolesChannel.subscribe();
|
|
240
|
+
this.channels.push(eventAppRolesSubscription);
|
|
231
241
|
|
|
232
242
|
// Subscribe to global role changes
|
|
233
|
-
this.supabase
|
|
243
|
+
const globalRolesChannel = this.supabase
|
|
234
244
|
.channel('rbac_global_roles_changes')
|
|
235
245
|
.on('postgres_changes', {
|
|
236
246
|
event: '*',
|
|
@@ -241,11 +251,12 @@ export class RBACCacheInvalidationManager {
|
|
|
241
251
|
if (user_id) {
|
|
242
252
|
this.invalidateUser(user_id, `global_role_${payload.eventType}`);
|
|
243
253
|
}
|
|
244
|
-
})
|
|
245
|
-
|
|
254
|
+
});
|
|
255
|
+
const globalRolesSubscription = globalRolesChannel.subscribe();
|
|
256
|
+
this.channels.push(globalRolesSubscription);
|
|
246
257
|
|
|
247
258
|
// Subscribe to page permission changes
|
|
248
|
-
this.supabase
|
|
259
|
+
const pagePermissionsChannel = this.supabase
|
|
249
260
|
.channel('rbac_page_permissions_changes')
|
|
250
261
|
.on('postgres_changes', {
|
|
251
262
|
event: '*',
|
|
@@ -261,8 +272,30 @@ export class RBACCacheInvalidationManager {
|
|
|
261
272
|
}
|
|
262
273
|
// Note: We can't easily get user_id from role_id without additional query
|
|
263
274
|
// This is a limitation of the current schema design
|
|
264
|
-
})
|
|
265
|
-
|
|
275
|
+
});
|
|
276
|
+
const pagePermissionsSubscription = pagePermissionsChannel.subscribe();
|
|
277
|
+
this.channels.push(pagePermissionsSubscription);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Cleanup all realtime subscriptions
|
|
282
|
+
* Call this when the manager is no longer needed to prevent memory leaks
|
|
283
|
+
*/
|
|
284
|
+
cleanup(): void {
|
|
285
|
+
// Unsubscribe from all channels
|
|
286
|
+
this.channels.forEach(channel => {
|
|
287
|
+
try {
|
|
288
|
+
if (channel && typeof channel.unsubscribe === 'function') {
|
|
289
|
+
channel.unsubscribe();
|
|
290
|
+
}
|
|
291
|
+
} catch (error) {
|
|
292
|
+
log.warn('Failed to unsubscribe from channel:', error);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
this.channels = [];
|
|
296
|
+
|
|
297
|
+
// Clear all callbacks
|
|
298
|
+
this.invalidationCallbacks.clear();
|
|
266
299
|
}
|
|
267
300
|
|
|
268
301
|
/**
|
|
@@ -302,11 +335,18 @@ let globalCacheInvalidationManager: RBACCacheInvalidationManager | null = null;
|
|
|
302
335
|
|
|
303
336
|
/**
|
|
304
337
|
* Initialize the global cache invalidation manager
|
|
338
|
+
* Ensures only one instance exists per application lifecycle
|
|
305
339
|
*
|
|
306
340
|
* @param supabase - Supabase client
|
|
307
341
|
* @returns Cache invalidation manager instance
|
|
308
342
|
*/
|
|
309
343
|
export function initializeCacheInvalidation(supabase: SupabaseClient<Database>): RBACCacheInvalidationManager {
|
|
344
|
+
// Clean up existing manager if it exists (e.g., when switching Supabase clients)
|
|
345
|
+
if (globalCacheInvalidationManager) {
|
|
346
|
+
log.debug('Cleaning up existing cache invalidation manager before creating new one');
|
|
347
|
+
globalCacheInvalidationManager.cleanup();
|
|
348
|
+
}
|
|
349
|
+
|
|
310
350
|
globalCacheInvalidationManager = new RBACCacheInvalidationManager(supabase);
|
|
311
351
|
return globalCacheInvalidationManager;
|
|
312
352
|
}
|
|
@@ -319,3 +359,14 @@ export function initializeCacheInvalidation(supabase: SupabaseClient<Database>):
|
|
|
319
359
|
export function getCacheInvalidationManager(): RBACCacheInvalidationManager | null {
|
|
320
360
|
return globalCacheInvalidationManager;
|
|
321
361
|
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Cleanup the global cache invalidation manager
|
|
365
|
+
* Call this when the application is shutting down or when switching Supabase clients
|
|
366
|
+
*/
|
|
367
|
+
export function cleanupCacheInvalidation(): void {
|
|
368
|
+
if (globalCacheInvalidationManager) {
|
|
369
|
+
globalCacheInvalidationManager.cleanup();
|
|
370
|
+
globalCacheInvalidationManager = null;
|
|
371
|
+
}
|
|
372
|
+
}
|
package/src/rbac/cache.test.ts
CHANGED
|
@@ -81,8 +81,8 @@ describe('RBACCache', () => {
|
|
|
81
81
|
mockDateNow.mockReturnValue(startTime);
|
|
82
82
|
testCache.set('test-key', 'test-value');
|
|
83
83
|
|
|
84
|
-
// Advance time by
|
|
85
|
-
mockDateNow.mockReturnValue(startTime +
|
|
84
|
+
// Advance time by 120 seconds (default TTL) + 1ms to ensure expiration
|
|
85
|
+
mockDateNow.mockReturnValue(startTime + 120001);
|
|
86
86
|
expect(testCache.get('test-key')).toBeNull();
|
|
87
87
|
|
|
88
88
|
mockDateNow.mockRestore();
|
package/src/rbac/cache.ts
CHANGED
|
@@ -13,32 +13,50 @@ import { CacheEntry, PermissionCacheKey } from './types';
|
|
|
13
13
|
/**
|
|
14
14
|
* In-memory cache for RBAC operations
|
|
15
15
|
*
|
|
16
|
-
* Provides
|
|
16
|
+
* Provides two-tier caching:
|
|
17
|
+
* - Short-term cache: 120 seconds for frequently changing permissions
|
|
18
|
+
* - Session cache: 15 minutes for stable permissions (page-level checks)
|
|
17
19
|
*/
|
|
18
20
|
export class RBACCache {
|
|
19
21
|
private cache = new Map<string, CacheEntry<any>>();
|
|
20
|
-
private
|
|
22
|
+
private sessionCache = new Map<string, CacheEntry<any>>();
|
|
23
|
+
private readonly TTL = 120 * 1000; // 120 seconds (short-term) - increased from 60s
|
|
24
|
+
private readonly SESSION_TTL = 15 * 60 * 1000; // 15 minutes (session-level) - increased from 5min
|
|
21
25
|
private invalidationCallbacks: Set<(pattern: string) => void> = new Set();
|
|
22
26
|
|
|
23
27
|
/**
|
|
24
28
|
* Get a value from the cache
|
|
25
29
|
*
|
|
30
|
+
* Checks both short-term cache and session cache.
|
|
31
|
+
*
|
|
26
32
|
* @param key - Cache key
|
|
33
|
+
* @param useSessionCache - Whether to check session cache (default: true)
|
|
27
34
|
* @returns Cached value or null if not found/expired
|
|
28
35
|
*/
|
|
29
|
-
get<T>(key: string): T | null {
|
|
30
|
-
const
|
|
36
|
+
get<T>(key: string, useSessionCache: boolean = true): T | null {
|
|
37
|
+
const now = Date.now();
|
|
31
38
|
|
|
32
|
-
|
|
33
|
-
|
|
39
|
+
// Check short-term cache first
|
|
40
|
+
const entry = this.cache.get(key);
|
|
41
|
+
if (entry && now <= entry.expires) {
|
|
42
|
+
return entry.data as T;
|
|
34
43
|
}
|
|
35
|
-
|
|
36
|
-
if (Date.now() > entry.expires) {
|
|
44
|
+
if (entry && now > entry.expires) {
|
|
37
45
|
this.cache.delete(key);
|
|
38
|
-
return null;
|
|
39
46
|
}
|
|
40
|
-
|
|
41
|
-
|
|
47
|
+
|
|
48
|
+
// Check session cache if enabled
|
|
49
|
+
if (useSessionCache) {
|
|
50
|
+
const sessionEntry = this.sessionCache.get(key);
|
|
51
|
+
if (sessionEntry && now <= sessionEntry.expires) {
|
|
52
|
+
return sessionEntry.data as T;
|
|
53
|
+
}
|
|
54
|
+
if (sessionEntry && now > sessionEntry.expires) {
|
|
55
|
+
this.sessionCache.delete(key);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return null;
|
|
42
60
|
}
|
|
43
61
|
|
|
44
62
|
/**
|
|
@@ -47,14 +65,27 @@ export class RBACCache {
|
|
|
47
65
|
* @param key - Cache key
|
|
48
66
|
* @param data - Data to cache
|
|
49
67
|
* @param ttl - Time to live in milliseconds (defaults to 60s)
|
|
68
|
+
* @param useSessionCache - Whether to also store in session cache (default: false for page-level checks)
|
|
50
69
|
*/
|
|
51
|
-
set<T>(key: string, data: T, ttl: number = this.TTL): void {
|
|
70
|
+
set<T>(key: string, data: T, ttl: number = this.TTL, useSessionCache: boolean = false): void {
|
|
71
|
+
const now = Date.now();
|
|
52
72
|
// For zero or negative TTL, set expires to current time to make it immediately expired
|
|
53
|
-
const expires = ttl <= 0 ?
|
|
73
|
+
const expires = ttl <= 0 ? now - 1 : now + ttl;
|
|
74
|
+
|
|
75
|
+
// Always store in short-term cache
|
|
54
76
|
this.cache.set(key, {
|
|
55
77
|
data,
|
|
56
78
|
expires,
|
|
57
79
|
});
|
|
80
|
+
|
|
81
|
+
// Optionally store in session cache for page-level permissions
|
|
82
|
+
if (useSessionCache) {
|
|
83
|
+
const sessionExpires = ttl <= 0 ? now - 1 : now + this.SESSION_TTL;
|
|
84
|
+
this.sessionCache.set(key, {
|
|
85
|
+
data,
|
|
86
|
+
expires: sessionExpires,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
58
89
|
}
|
|
59
90
|
|
|
60
91
|
/**
|
|
@@ -64,6 +95,7 @@ export class RBACCache {
|
|
|
64
95
|
*/
|
|
65
96
|
delete(key: string): void {
|
|
66
97
|
this.cache.delete(key);
|
|
98
|
+
this.sessionCache.delete(key);
|
|
67
99
|
}
|
|
68
100
|
|
|
69
101
|
/**
|
|
@@ -81,13 +113,23 @@ export class RBACCache {
|
|
|
81
113
|
const matcher = this.createMatcher(trimmedPattern);
|
|
82
114
|
const keysToDelete: string[] = [];
|
|
83
115
|
|
|
116
|
+
// Invalidate from both caches
|
|
84
117
|
for (const key of this.cache.keys()) {
|
|
85
118
|
if (matcher(key)) {
|
|
86
119
|
keysToDelete.push(key);
|
|
87
120
|
}
|
|
88
121
|
}
|
|
122
|
+
|
|
123
|
+
for (const key of this.sessionCache.keys()) {
|
|
124
|
+
if (matcher(key) && !keysToDelete.includes(key)) {
|
|
125
|
+
keysToDelete.push(key);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
89
128
|
|
|
90
|
-
keysToDelete.forEach(key =>
|
|
129
|
+
keysToDelete.forEach(key => {
|
|
130
|
+
this.cache.delete(key);
|
|
131
|
+
this.sessionCache.delete(key);
|
|
132
|
+
});
|
|
91
133
|
|
|
92
134
|
// Notify invalidation callbacks
|
|
93
135
|
this.invalidationCallbacks.forEach(callback => callback(trimmedPattern));
|
|
@@ -111,6 +153,7 @@ export class RBACCache {
|
|
|
111
153
|
*/
|
|
112
154
|
clear(): void {
|
|
113
155
|
this.cache.clear();
|
|
156
|
+
this.sessionCache.clear();
|
|
114
157
|
}
|
|
115
158
|
|
|
116
159
|
/**
|
|
@@ -118,12 +161,16 @@ export class RBACCache {
|
|
|
118
161
|
*/
|
|
119
162
|
getStats(): {
|
|
120
163
|
size: number;
|
|
164
|
+
sessionSize: number;
|
|
121
165
|
ttl: number;
|
|
166
|
+
sessionTtl: number;
|
|
122
167
|
keys: string[];
|
|
123
168
|
} {
|
|
124
169
|
return {
|
|
125
170
|
size: this.cache.size,
|
|
171
|
+
sessionSize: this.sessionCache.size,
|
|
126
172
|
ttl: this.TTL,
|
|
173
|
+
sessionTtl: this.SESSION_TTL,
|
|
127
174
|
keys: Array.from(this.cache.keys()),
|
|
128
175
|
};
|
|
129
176
|
}
|