@jmruthers/pace-core 0.5.136 → 0.5.137
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-CYOHOX3O.js → DataTable-6M4L6BI2.js} +10 -9
- package/dist/{EventLogo-801uofbR.d.ts → EventLogo-rFL_kRjk.d.ts} +73 -1
- package/dist/{UnifiedAuthProvider-5E5TUNMS.js → UnifiedAuthProvider-XIQQ7LVU.js} +4 -5
- package/dist/{chunk-YLKIDTUK.js → chunk-22WKWKRX.js} +4 -4
- package/dist/{chunk-TVYPTYOY.js → chunk-4C7EXCAR.js} +60 -24
- package/dist/chunk-4C7EXCAR.js.map +1 -0
- package/dist/{chunk-2TWNJ46Y.js → chunk-6LAAY47Q.js} +2 -2
- package/dist/{chunk-444EZN6N.js → chunk-7QCC6MCP.js} +88 -1
- package/dist/chunk-7QCC6MCP.js.map +1 -0
- package/dist/{chunk-FHWWBIHA.js → chunk-BCIBECNB.js} +5 -5
- package/dist/chunk-BJPBT3CU.js +21 -0
- package/dist/chunk-BJPBT3CU.js.map +1 -0
- package/dist/{chunk-L6PGMCMD.js → chunk-BLCXZEYF.js} +3 -3
- package/dist/{chunk-HJGGOMQ6.js → chunk-HAWZXGR2.js} +147 -103
- package/dist/chunk-HAWZXGR2.js.map +1 -0
- package/dist/{chunk-XARJS7CD.js → chunk-INQLMHPF.js} +2 -2
- package/dist/chunk-JISYG63F.js +70 -0
- package/dist/chunk-JISYG63F.js.map +1 -0
- package/dist/{chunk-NOHEVYVX.js → chunk-KYRHUBIU.js} +417 -319
- package/dist/chunk-KYRHUBIU.js.map +1 -0
- package/dist/{chunk-SL2YQDR6.js → chunk-MA6EPSGZ.js} +2 -2
- package/dist/{chunk-5DPZ5EAT.js → chunk-OWAG3GSU.js} +1 -3
- package/dist/{chunk-LTV3XIJJ.js → chunk-T6JN6LH6.js} +4 -4
- package/dist/{chunk-4MT5BGGL.js → chunk-YCWDTTUK.js} +4 -6
- package/dist/{chunk-4MT5BGGL.js.map → chunk-YCWDTTUK.js.map} +1 -1
- package/dist/components.d.ts +1 -1
- package/dist/components.js +12 -11
- package/dist/components.js.map +1 -1
- package/dist/hooks.js +8 -9
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +15 -14
- package/dist/index.js.map +1 -1
- package/dist/providers.js +3 -4
- package/dist/rbac/index.js +8 -9
- package/dist/schema-DTDZQe2u.d.ts +28 -0
- package/dist/types.d.ts +152 -3
- package/dist/types.js +51 -16
- package/dist/types.js.map +1 -1
- package/dist/utils.d.ts +89 -4
- package/dist/utils.js +214 -96
- package/dist/utils.js.map +1 -1
- package/dist/validation.d.ts +1 -343
- package/dist/validation.js +3 -100
- 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/MissingUserContextError.md +1 -1
- package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
- package/docs/api/classes/PermissionDeniedError.md +1 -1
- package/docs/api/classes/PublicErrorBoundary.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +1 -1
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +1 -1
- 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/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/BadgeProps.md +27 -0
- package/docs/api/interfaces/ButtonProps.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/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/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/EventAppRoleData.md +1 -1
- package/docs/api/interfaces/EventLogoProps.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 +1 -1
- 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/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/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 +1 -1
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
- package/docs/api/interfaces/PublicLoadingSpinnerProps.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/RBACConfig.md +1 -1
- package/docs/api/interfaces/RBACLogger.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/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/SessionRestorationLoaderProps.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/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/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.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/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 +79 -10
- package/docs/architecture/README.md +0 -1
- package/docs/styles/README.md +0 -2
- package/examples/RBAC/CompleteRBACExample.tsx +324 -0
- package/examples/RBAC/EventBasedApp.tsx +239 -0
- package/examples/RBAC/PermissionExample.tsx +151 -0
- package/examples/RBAC/index.ts +13 -0
- package/examples/public-pages/CorrectPublicPageImplementation.tsx +301 -0
- package/examples/public-pages/PublicEventPage.tsx +274 -0
- package/examples/public-pages/PublicPageApp.tsx +308 -0
- package/examples/public-pages/PublicPageUsageExample.tsx +216 -0
- package/examples/public-pages/index.ts +14 -0
- package/package.json +1 -10
- package/src/__tests__/TEST_STANDARD.md +92 -0
- package/src/components/Badge/Badge.test.tsx +314 -0
- package/src/components/Badge/Badge.tsx +304 -0
- package/src/components/Badge/index.ts +3 -0
- package/src/components/DataTable/__tests__/DataTableCore.test-setup.ts +217 -0
- package/src/components/DataTable/__tests__/styles.test.ts +1 -1
- package/src/components/DataTable/components/ColumnFilter.tsx +8 -4
- package/src/components/DataTable/components/DataTableBody.tsx +461 -0
- package/src/components/DataTable/components/DraggableColumnHeader.tsx +144 -0
- package/src/components/DataTable/components/FilterRow.tsx +9 -3
- package/src/components/DataTable/components/PaginationControls.tsx +1 -0
- package/src/components/DataTable/components/VirtualizedDataTable.tsx +513 -0
- package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +14 -68
- package/src/components/DataTable/components/__tests__/ColumnFilter.test.tsx +62 -0
- package/src/components/DataTable/components/__tests__/FilterRow.test.tsx +43 -0
- package/src/components/DataTable/core/ActionManager.ts +235 -0
- package/src/components/DataTable/core/ColumnManager.ts +205 -0
- package/src/components/DataTable/core/DataManager.ts +188 -0
- package/src/components/DataTable/core/DataTableContext.tsx +181 -0
- package/src/components/DataTable/core/LocalDataAdapter.ts +273 -0
- package/src/components/DataTable/core/PluginRegistry.ts +229 -0
- package/src/components/DataTable/core/StateManager.ts +311 -0
- package/src/components/DataTable/core/interfaces.ts +338 -0
- package/src/components/DataTable/styles.ts +27 -6
- package/src/components/DataTable/utils/__tests__/columnUtils.test.ts +94 -0
- package/src/components/DataTable/utils/columnUtils.ts +40 -0
- package/src/components/DataTable/utils/debugTools.ts +609 -0
- package/src/components/DataTable/utils/index.ts +1 -0
- package/src/components/Dialog/README.md +804 -0
- package/src/components/Dialog/utils/__tests__/safeHtml.unit.test.ts +611 -0
- package/src/components/Dialog/utils/safeHtml.ts +185 -0
- package/src/components/Footer/Footer.test.tsx +1 -1
- package/src/components/Form/Form.test.tsx +1 -1
- package/src/components/Form/FormErrorSummary.tsx +113 -0
- package/src/components/Form/FormFieldset.tsx +127 -0
- package/src/components/Form/FormLiveRegion.tsx +198 -0
- package/src/components/LoginForm/LoginForm.test.tsx +1 -1
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +76 -10
- package/src/components/PaceLoginPage/PaceLoginPage.tsx +1 -1
- package/src/components/PasswordReset/PasswordResetForm.test.tsx +597 -0
- package/src/components/PasswordReset/PasswordResetForm.tsx +201 -0
- package/src/components/PublicLayout/PublicPageDebugger.tsx +104 -0
- package/src/components/PublicLayout/PublicPageDiagnostic.tsx +162 -0
- package/src/components/PublicLayout/__tests__/PublicPageFooter.test.tsx +1 -1
- package/src/components/Select/Select.test.tsx +1 -1
- package/src/components/Select/Select.tsx +20 -8
- package/src/components/Table/__tests__/Table.test.tsx +1 -1
- package/src/components/index.ts +3 -0
- package/src/hooks/__tests__/useFileUrl.unit.test.ts +83 -85
- package/src/index.ts +4 -0
- package/src/styles/core.css +3 -0
- package/src/utils/appConfig.ts +47 -0
- package/src/utils/appIdResolver.test.ts +499 -0
- package/src/utils/appIdResolver.ts +130 -0
- package/src/utils/appNameResolver.simple.test.ts +212 -0
- package/src/utils/appNameResolver.test.ts +121 -0
- package/src/utils/appNameResolver.ts +191 -0
- package/src/utils/audit.ts +127 -0
- package/src/utils/auth-utils.ts +96 -0
- package/src/utils/bundleAnalysis.ts +129 -0
- package/src/utils/cn.ts +7 -0
- package/src/utils/debugLogger.ts +67 -0
- package/src/utils/deviceFingerprint.ts +215 -0
- package/src/utils/dynamicUtils.ts +105 -0
- package/src/utils/file-reference.test.ts +788 -0
- package/src/utils/file-reference.ts +519 -0
- package/src/utils/formatDate.test.ts +237 -0
- package/src/utils/formatting.ts +133 -0
- package/src/utils/index.ts +7 -0
- package/src/utils/lazyLoad.tsx +44 -0
- package/src/utils/logger.ts +179 -0
- package/src/utils/organisationContext.test.ts +322 -0
- package/src/utils/organisationContext.ts +153 -0
- package/src/utils/performanceBenchmark.ts +64 -0
- package/src/utils/performanceBudgets.ts +110 -0
- package/src/utils/permissionTypes.ts +37 -0
- package/src/utils/permissionUtils.test.ts +393 -0
- package/src/utils/permissionUtils.ts +34 -0
- package/src/utils/sanitization.ts +264 -0
- package/src/utils/schemaUtils.ts +37 -0
- package/src/utils/secureDataAccess.test.ts +711 -0
- package/src/utils/secureDataAccess.ts +377 -0
- package/src/utils/secureErrors.ts +79 -0
- package/src/utils/secureStorage.ts +244 -0
- package/src/utils/security.ts +156 -0
- package/src/utils/securityMonitor.ts +45 -0
- package/src/utils/sessionTracking.ts +126 -0
- package/src/utils/validation.ts +111 -0
- package/src/utils/validationUtils.ts +120 -0
- package/src/validation/index.ts +2 -2
- package/dist/chunk-444EZN6N.js.map +0 -1
- package/dist/chunk-APIBCTL2.js +0 -670
- package/dist/chunk-APIBCTL2.js.map +0 -1
- package/dist/chunk-HJGGOMQ6.js.map +0 -1
- package/dist/chunk-K2WWTH7O.js +0 -94
- package/dist/chunk-K2WWTH7O.js.map +0 -1
- package/dist/chunk-LMC26NLJ.js +0 -84
- package/dist/chunk-LMC26NLJ.js.map +0 -1
- package/dist/chunk-NOHEVYVX.js.map +0 -1
- package/dist/chunk-TVYPTYOY.js.map +0 -1
- package/dist/validation-8npbysjg.d.ts +0 -177
- /package/dist/{DataTable-CYOHOX3O.js.map → DataTable-6M4L6BI2.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-5E5TUNMS.js.map → UnifiedAuthProvider-XIQQ7LVU.js.map} +0 -0
- /package/dist/{chunk-YLKIDTUK.js.map → chunk-22WKWKRX.js.map} +0 -0
- /package/dist/{chunk-2TWNJ46Y.js.map → chunk-6LAAY47Q.js.map} +0 -0
- /package/dist/{chunk-FHWWBIHA.js.map → chunk-BCIBECNB.js.map} +0 -0
- /package/dist/{chunk-L6PGMCMD.js.map → chunk-BLCXZEYF.js.map} +0 -0
- /package/dist/{chunk-XARJS7CD.js.map → chunk-INQLMHPF.js.map} +0 -0
- /package/dist/{chunk-SL2YQDR6.js.map → chunk-MA6EPSGZ.js.map} +0 -0
- /package/dist/{chunk-5DPZ5EAT.js.map → chunk-OWAG3GSU.js.map} +0 -0
- /package/dist/{chunk-LTV3XIJJ.js.map → chunk-T6JN6LH6.js.map} +0 -0
- /package/examples/{components → components 2}/DataTable/HierarchicalActionsExample.tsx +0 -0
- /package/examples/{components → components 2}/DataTable/HierarchicalExample.tsx +0 -0
- /package/examples/{components → components 2}/DataTable/InitialPageSizeExample.tsx +0 -0
- /package/examples/{components → components 2}/DataTable/PerformanceExample.tsx +0 -0
- /package/examples/{components → components 2}/DataTable/index.ts +0 -0
- /package/examples/{components → components 2}/Dialog/BasicHtmlTest.tsx +0 -0
- /package/examples/{components → components 2}/Dialog/DebugHtmlExample.tsx +0 -0
- /package/examples/{components → components 2}/Dialog/HtmlDialogExample.tsx +0 -0
- /package/examples/{components → components 2}/Dialog/ScrollableDialogExample.tsx +0 -0
- /package/examples/{components → components 2}/Dialog/SimpleHtmlTest.tsx +0 -0
- /package/examples/{components → components 2}/Dialog/SmartDialogExample.tsx +0 -0
- /package/examples/{components → components 2}/Dialog/index.ts +0 -0
- /package/examples/{components → components 2}/index.ts +0 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
|
|
2
|
+
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
3
|
+
|
|
4
|
+
export interface SecurityEvent {
|
|
5
|
+
type: string;
|
|
6
|
+
timestamp: Date;
|
|
7
|
+
userId?: string;
|
|
8
|
+
details: Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function logSecurityEvent(event: SecurityEvent): void {
|
|
12
|
+
// In production, this should log to your security monitoring system
|
|
13
|
+
// For now, we'll log to console.warn for testing purposes
|
|
14
|
+
console.warn('[SECURITY EVENT]', {
|
|
15
|
+
...event,
|
|
16
|
+
timestamp: event.timestamp.toISOString()
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function validateUserSession(userId: string, sessionToken?: string): Promise<boolean> {
|
|
21
|
+
// Mock implementation - replace with actual session validation
|
|
22
|
+
if (!userId || typeof userId !== 'string') {
|
|
23
|
+
logSecurityEvent({
|
|
24
|
+
type: 'invalid_session_validation',
|
|
25
|
+
timestamp: new Date(),
|
|
26
|
+
details: { reason: 'Invalid userId provided' }
|
|
27
|
+
});
|
|
28
|
+
return Promise.resolve(false);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (sessionToken && sessionToken.length < 10) {
|
|
32
|
+
logSecurityEvent({
|
|
33
|
+
type: 'suspicious_session_token',
|
|
34
|
+
timestamp: new Date(),
|
|
35
|
+
userId,
|
|
36
|
+
details: { reason: 'Session token too short' }
|
|
37
|
+
});
|
|
38
|
+
return Promise.resolve(false);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return Promise.resolve(true);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createSecureSession(
|
|
45
|
+
_supabaseClient: SupabaseClient,
|
|
46
|
+
sessionData: { userId: string; deviceFingerprint?: string }
|
|
47
|
+
): Promise<string> {
|
|
48
|
+
// Mock implementation - in production, create actual secure session
|
|
49
|
+
const sessionId = `sess_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
50
|
+
|
|
51
|
+
logSecurityEvent({
|
|
52
|
+
type: 'session_created',
|
|
53
|
+
timestamp: new Date(),
|
|
54
|
+
userId: sessionData.userId,
|
|
55
|
+
details: {
|
|
56
|
+
sessionId,
|
|
57
|
+
hasDeviceFingerprint: !!sessionData.deviceFingerprint
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return Promise.resolve(sessionId);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function invalidateSession(
|
|
65
|
+
_supabaseClient: SupabaseClient,
|
|
66
|
+
sessionId: string
|
|
67
|
+
): Promise<void> {
|
|
68
|
+
// Mock implementation - in production, invalidate actual session
|
|
69
|
+
logSecurityEvent({
|
|
70
|
+
type: 'session_invalidated',
|
|
71
|
+
timestamp: new Date(),
|
|
72
|
+
details: { sessionId }
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return Promise.resolve();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function getSecurityHeaders(): Record<string, string> {
|
|
79
|
+
return {
|
|
80
|
+
'X-Content-Type-Options': 'nosniff',
|
|
81
|
+
'X-Frame-Options': 'DENY',
|
|
82
|
+
'X-XSS-Protection': '1; mode=block',
|
|
83
|
+
'Referrer-Policy': 'strict-origin-when-cross-origin',
|
|
84
|
+
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains'
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function validateSecurityHeaders(headers: Record<string, string>): boolean {
|
|
89
|
+
const requiredHeaders = ['X-Content-Type-Options', 'X-Frame-Options'];
|
|
90
|
+
const missingHeaders = requiredHeaders.filter(header => !headers[header]);
|
|
91
|
+
|
|
92
|
+
if (missingHeaders.length > 0) {
|
|
93
|
+
logSecurityEvent({
|
|
94
|
+
type: 'missing_security_headers',
|
|
95
|
+
timestamp: new Date(),
|
|
96
|
+
details: { missingHeaders }
|
|
97
|
+
});
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function generateDeviceFingerprint(): string {
|
|
105
|
+
// Basic device fingerprinting - in production, use more sophisticated methods
|
|
106
|
+
const canvas = document.createElement('canvas');
|
|
107
|
+
const ctx = canvas.getContext('2d');
|
|
108
|
+
if (ctx) {
|
|
109
|
+
ctx.textBaseline = 'top';
|
|
110
|
+
ctx.font = '14px Arial';
|
|
111
|
+
ctx.fillText('Device fingerprint', 2, 2);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const fingerprint = [
|
|
115
|
+
navigator.userAgent,
|
|
116
|
+
navigator.language,
|
|
117
|
+
screen.width + 'x' + screen.height,
|
|
118
|
+
new Date().getTimezoneOffset(),
|
|
119
|
+
canvas.toDataURL()
|
|
120
|
+
].join('|');
|
|
121
|
+
|
|
122
|
+
// Simple hash function for demonstration
|
|
123
|
+
let hash = 0;
|
|
124
|
+
for (let i = 0; i < fingerprint.length; i++) {
|
|
125
|
+
const char = fingerprint.charCodeAt(i);
|
|
126
|
+
hash = ((hash << 5) - hash) + char;
|
|
127
|
+
hash = hash & hash; // Convert to 32-bit integer
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return Math.abs(hash).toString(16);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function validateDeviceFingerprint(fingerprint: string, expectedFingerprint?: string): boolean {
|
|
134
|
+
if (!fingerprint || typeof fingerprint !== 'string') {
|
|
135
|
+
logSecurityEvent({
|
|
136
|
+
type: 'invalid_device_fingerprint',
|
|
137
|
+
timestamp: new Date(),
|
|
138
|
+
details: { reason: 'Invalid fingerprint format' }
|
|
139
|
+
});
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (expectedFingerprint && fingerprint !== expectedFingerprint) {
|
|
144
|
+
logSecurityEvent({
|
|
145
|
+
type: 'device_fingerprint_mismatch',
|
|
146
|
+
timestamp: new Date(),
|
|
147
|
+
details: {
|
|
148
|
+
provided: fingerprint,
|
|
149
|
+
expected: expectedFingerprint
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
|
|
2
|
+
interface SecurityEvent {
|
|
3
|
+
id?: string;
|
|
4
|
+
action: string;
|
|
5
|
+
details: Record<string, any>;
|
|
6
|
+
timestamp?: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface SecurityAlert {
|
|
10
|
+
id: string;
|
|
11
|
+
type: string;
|
|
12
|
+
message: string;
|
|
13
|
+
timestamp: Date;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
class SecurityMonitor {
|
|
17
|
+
private events: SecurityEvent[] = [];
|
|
18
|
+
|
|
19
|
+
logEvent(event: SecurityEvent) {
|
|
20
|
+
const eventWithId = {
|
|
21
|
+
...event,
|
|
22
|
+
id: Math.random().toString(36).substr(2, 9),
|
|
23
|
+
timestamp: Date.now()
|
|
24
|
+
};
|
|
25
|
+
this.events.push(eventWithId);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
getEvents(): SecurityEvent[] {
|
|
29
|
+
return [...this.events];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
clearEvents() {
|
|
33
|
+
this.events = [];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
createAlert(alert: Omit<SecurityAlert, 'id' | 'timestamp'>): SecurityAlert {
|
|
37
|
+
return {
|
|
38
|
+
...alert,
|
|
39
|
+
id: Math.random().toString(36).substr(2, 9),
|
|
40
|
+
timestamp: new Date()
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const securityMonitor = new SecurityMonitor();
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
2
|
+
|
|
3
|
+
// Define the tracking parameters locally since old RBAC types are removed
|
|
4
|
+
interface TrackUserSessionParams {
|
|
5
|
+
p_session_type: 'event_switch' | 'session_expired';
|
|
6
|
+
p_event_id?: string;
|
|
7
|
+
p_app_id?: string;
|
|
8
|
+
ip_address?: string;
|
|
9
|
+
user_agent?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Hook for manual session tracking (event switches and session expiration).
|
|
14
|
+
*
|
|
15
|
+
* Note: Login and logout tracking is automatically handled by UnifiedAuthProvider.
|
|
16
|
+
* You should only use this hook for tracking event switches or session expirations.
|
|
17
|
+
*
|
|
18
|
+
* @param supabaseClient - Supabase client instance
|
|
19
|
+
* @param appName - Optional application name for tracking
|
|
20
|
+
* @returns Object containing tracking functions for event switches and session expiration
|
|
21
|
+
*/
|
|
22
|
+
export function useSessionTracking(supabaseClient: SupabaseClient, appName?: string) {
|
|
23
|
+
// Resolve app name to app_id
|
|
24
|
+
const resolveAppId = async (): Promise<string | undefined> => {
|
|
25
|
+
if (!appName) return undefined;
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const { data, error } = await supabaseClient
|
|
29
|
+
.from('rbac_apps')
|
|
30
|
+
.select('id')
|
|
31
|
+
.eq('name', appName)
|
|
32
|
+
.eq('is_active', true)
|
|
33
|
+
.single();
|
|
34
|
+
|
|
35
|
+
if (error || !data) {
|
|
36
|
+
console.warn('App not found or inactive:', appName);
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return data.id;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error('Failed to resolve app ID:', error);
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Track an event switch
|
|
48
|
+
* @param eventId - ID of the event being switched to
|
|
49
|
+
*/
|
|
50
|
+
const trackEventSwitch = async (eventId: string) => {
|
|
51
|
+
try {
|
|
52
|
+
const { data: { user } } = await supabaseClient.auth.getUser();
|
|
53
|
+
if (!user) {
|
|
54
|
+
console.warn('No authenticated user found for session tracking');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const appId = await resolveAppId();
|
|
59
|
+
|
|
60
|
+
const params: TrackUserSessionParams = {
|
|
61
|
+
p_session_type: 'event_switch',
|
|
62
|
+
p_event_id: eventId,
|
|
63
|
+
p_app_id: appId
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const { error } = await supabaseClient.rpc('rbac_session_track', {
|
|
67
|
+
p_user_id: user?.id,
|
|
68
|
+
p_session_type: params.p_session_type,
|
|
69
|
+
p_event_id: params.p_event_id,
|
|
70
|
+
p_app_id: params.p_app_id,
|
|
71
|
+
p_ip_address: params.ip_address,
|
|
72
|
+
p_user_agent: params.user_agent
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
if (error) {
|
|
76
|
+
console.error('Failed to track event switch session:', error);
|
|
77
|
+
} else {
|
|
78
|
+
console.log('Event switch session tracked successfully');
|
|
79
|
+
}
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error('Failed to track event switch:', error);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Track a session expiration
|
|
87
|
+
*/
|
|
88
|
+
const trackSessionExpired = async () => {
|
|
89
|
+
try {
|
|
90
|
+
const { data: { user } } = await supabaseClient.auth.getUser();
|
|
91
|
+
if (!user) {
|
|
92
|
+
console.warn('No authenticated user found for session tracking');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const appId = await resolveAppId();
|
|
97
|
+
|
|
98
|
+
const params: TrackUserSessionParams = {
|
|
99
|
+
p_session_type: 'session_expired',
|
|
100
|
+
p_app_id: appId
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const { error } = await supabaseClient.rpc('rbac_session_track', {
|
|
104
|
+
p_user_id: user?.id,
|
|
105
|
+
p_session_type: params.p_session_type,
|
|
106
|
+
p_event_id: params.p_event_id,
|
|
107
|
+
p_app_id: params.p_app_id,
|
|
108
|
+
p_ip_address: params.ip_address,
|
|
109
|
+
p_user_agent: params.user_agent
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (error) {
|
|
113
|
+
console.error('Failed to track session expiration:', error);
|
|
114
|
+
} else {
|
|
115
|
+
console.log('Session expiration tracked successfully');
|
|
116
|
+
}
|
|
117
|
+
} catch (error) {
|
|
118
|
+
console.error('Failed to track session expiration:', error);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
trackEventSwitch,
|
|
124
|
+
trackSessionExpired
|
|
125
|
+
};
|
|
126
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Internal utilities for validation module
|
|
3
|
+
* @internal This file contains implementation details that should not be used directly
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Utility functions for validating data in the application
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Check if a string is a valid email
|
|
12
|
+
*/
|
|
13
|
+
export function isValidEmail(email: string): boolean {
|
|
14
|
+
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
15
|
+
return emailPattern.test(email);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Check if a string is empty (either null, undefined, or just whitespace)
|
|
20
|
+
*/
|
|
21
|
+
export function isEmpty(value: string | null | undefined): boolean {
|
|
22
|
+
return value === null || value === undefined || value.trim() === '';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Check if a password meets minimum requirements
|
|
27
|
+
*/
|
|
28
|
+
export function isStrongPassword(password: string): boolean {
|
|
29
|
+
// Minimum 8 characters, at least one uppercase, one lowercase, one number
|
|
30
|
+
const passwordPattern = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/;
|
|
31
|
+
return passwordPattern.test(password);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Check if a URL is valid
|
|
36
|
+
*/
|
|
37
|
+
export function isValidUrl(url: string): boolean {
|
|
38
|
+
try {
|
|
39
|
+
new URL(url);
|
|
40
|
+
return true;
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Check if a date string is valid
|
|
48
|
+
*/
|
|
49
|
+
export function isValidDate(dateStr: string): boolean {
|
|
50
|
+
const date = new Date(dateStr);
|
|
51
|
+
return !isNaN(date.getTime());
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Check if a value is within a range
|
|
56
|
+
*/
|
|
57
|
+
export function isWithinRange(value: number, min: number, max: number): boolean {
|
|
58
|
+
return value >= min && value <= max;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Check if a value matches a specific pattern
|
|
63
|
+
*/
|
|
64
|
+
export function matchesPattern(value: string, pattern: RegExp): boolean {
|
|
65
|
+
return pattern.test(value);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Utility function to deep merge objects for schema combination
|
|
70
|
+
* @internal
|
|
71
|
+
*/
|
|
72
|
+
export function deepMerge<T extends Record<string, unknown>>(
|
|
73
|
+
target: T,
|
|
74
|
+
source: Record<string, unknown>
|
|
75
|
+
): T {
|
|
76
|
+
const output = { ...target };
|
|
77
|
+
|
|
78
|
+
if (isObject(target) && isObject(source)) {
|
|
79
|
+
Object.keys(source).forEach(key => {
|
|
80
|
+
if (isObject(source[key])) {
|
|
81
|
+
if (!(key in target)) {
|
|
82
|
+
Object.assign(output, { [key]: source[key] });
|
|
83
|
+
} else {
|
|
84
|
+
// Use a type assertion to safely handle the indexing
|
|
85
|
+
const targetKey = key as keyof typeof target;
|
|
86
|
+
const targetValue = target[targetKey];
|
|
87
|
+
|
|
88
|
+
if (isObject(targetValue)) {
|
|
89
|
+
// Safe cast using type assertion
|
|
90
|
+
output[targetKey] = deepMerge(
|
|
91
|
+
targetValue as Record<string, unknown>,
|
|
92
|
+
source[key] as Record<string, unknown>
|
|
93
|
+
) as unknown as T[keyof T];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
Object.assign(output, { [key]: source[key] });
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return output as T;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Type guard to check if a value is a plain object
|
|
107
|
+
* @internal
|
|
108
|
+
*/
|
|
109
|
+
export function isObject(item: unknown): item is Record<string, unknown> {
|
|
110
|
+
return item !== null && typeof item === 'object' && !Array.isArray(item);
|
|
111
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* @file Validation utilities
|
|
4
|
+
*
|
|
5
|
+
* Shared validation utilities with enhanced security
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import { sanitizeUserInput, sanitizeFormData, type SanitizationOptions } from './sanitization';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Validates user input against a schema with automatic sanitization
|
|
13
|
+
*/
|
|
14
|
+
export function validateUserInput<T>(
|
|
15
|
+
schema: z.ZodSchema<T>,
|
|
16
|
+
data: unknown,
|
|
17
|
+
sanitizationRules?: Record<string, SanitizationOptions>
|
|
18
|
+
): { success: boolean; data?: T; error?: string } {
|
|
19
|
+
return sanitizeFormData(data, schema, sanitizationRules);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Sanitizes user input by removing potentially dangerous characters
|
|
24
|
+
* @deprecated Use sanitizeUserInput from lib/sanitization instead
|
|
25
|
+
*/
|
|
26
|
+
export function sanitizeUserInput_deprecated(input: string): string {
|
|
27
|
+
// Log deprecation warning
|
|
28
|
+
console.warn('sanitizeUserInput is deprecated. Use sanitizeUserInput from lib/sanitization instead.');
|
|
29
|
+
return sanitizeUserInput(input);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Enhanced email validation with sanitization
|
|
34
|
+
*/
|
|
35
|
+
export const emailSchema = z.string()
|
|
36
|
+
.transform(email => email.toLowerCase().trim())
|
|
37
|
+
.pipe(z.string().min(1, 'Email is required').email('Invalid email format').max(254, 'Email too long'));
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Enhanced password validation
|
|
41
|
+
*/
|
|
42
|
+
export const passwordSchema = z.string()
|
|
43
|
+
.min(8, 'Password must be at least 8 characters')
|
|
44
|
+
.max(128, 'Password too long')
|
|
45
|
+
.regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
|
|
46
|
+
.regex(/[a-z]/, 'Password must contain at least one lowercase letter')
|
|
47
|
+
.regex(/[0-9]/, 'Password must contain at least one number')
|
|
48
|
+
.regex(/[^A-Za-z0-9]/, 'Password must contain at least one special character');
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Username validation with sanitization
|
|
52
|
+
*/
|
|
53
|
+
export const usernameSchema = z.string()
|
|
54
|
+
.transform(username => username.toLowerCase().trim())
|
|
55
|
+
.pipe(z.string().min(3, 'Username must be at least 3 characters').max(30, 'Username too long').regex(/^[a-zA-Z0-9_-]+$/, 'Username can only contain letters, numbers, hyphens, and underscores'));
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Name validation with sanitization
|
|
59
|
+
*/
|
|
60
|
+
export const nameSchema = z.string()
|
|
61
|
+
.min(1, 'Name is required')
|
|
62
|
+
.max(100, 'Name too long')
|
|
63
|
+
.refine(name => {
|
|
64
|
+
// Check for XSS attempts and other invalid patterns
|
|
65
|
+
const dangerousPatterns = [
|
|
66
|
+
/<script/i,
|
|
67
|
+
/<img/i,
|
|
68
|
+
/on\w+\s*=/i,
|
|
69
|
+
/javascript:/i,
|
|
70
|
+
/data:/i,
|
|
71
|
+
/vbscript:/i
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
return !dangerousPatterns.some(pattern => pattern.test(name));
|
|
75
|
+
}, 'Name contains invalid characters')
|
|
76
|
+
.transform(name => sanitizeUserInput(name, {
|
|
77
|
+
allowHtml: false,
|
|
78
|
+
maxLength: 100,
|
|
79
|
+
trim: true
|
|
80
|
+
}));
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Phone number validation with sanitization
|
|
84
|
+
*/
|
|
85
|
+
export const phoneSchema = z.string()
|
|
86
|
+
.min(10, 'Phone number must be at least 10 digits')
|
|
87
|
+
.max(20, 'Phone number too long')
|
|
88
|
+
.regex(/^[\+]?[0-9\s\-\(\)\.]+$/, 'Invalid phone number format')
|
|
89
|
+
.refine(phone => {
|
|
90
|
+
// Remove all non-digit characters and check length
|
|
91
|
+
const digitsOnly = phone.replace(/\D/g, '');
|
|
92
|
+
return digitsOnly.length >= 10 && digitsOnly.length <= 15;
|
|
93
|
+
}, 'Phone number must be between 10 and 15 digits');
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* URL validation with sanitization
|
|
97
|
+
*/
|
|
98
|
+
export const urlSchema = z.string()
|
|
99
|
+
.min(1, 'URL is required')
|
|
100
|
+
.max(2048, 'URL too long')
|
|
101
|
+
.refine(url => {
|
|
102
|
+
try {
|
|
103
|
+
const parsed = new URL(url);
|
|
104
|
+
return ['http:', 'https:'].includes(parsed.protocol);
|
|
105
|
+
} catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}, 'Invalid URL format')
|
|
109
|
+
.refine(url => {
|
|
110
|
+
// Additional security checks
|
|
111
|
+
const dangerousPatterns = [
|
|
112
|
+
/javascript:/i,
|
|
113
|
+
/data:/i,
|
|
114
|
+
/vbscript:/i,
|
|
115
|
+
/file:/i,
|
|
116
|
+
/mailto:/i
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
return !dangerousPatterns.some(pattern => pattern.test(url));
|
|
120
|
+
}, 'URL contains invalid protocol');
|
package/src/validation/index.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* @module Validation
|
|
5
5
|
* @since 0.1.0
|
|
6
6
|
*
|
|
7
|
-
* Re-
|
|
8
|
-
* This
|
|
7
|
+
* Re-export validation utilities from utils/validation for convenience.
|
|
8
|
+
* This provides a top-level validation entry point.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
export * from '../utils/validation';
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/utils/security/secureStorage.ts"],"sourcesContent":["\n/**\n * @file Secure Storage Utilities\n * @description Encrypted storage wrapper for sensitive data\n */\n\nexport interface SecureStorageOptions {\n encrypt?: boolean;\n expiry?: number; // TTL in milliseconds\n}\n\n/**\n * Secure storage implementation with encryption support\n */\nclass SecureStorageImpl {\n private encryptionKey: CryptoKey | null = null;\n private initialized = false;\n\n /**\n * Initialize secure storage with encryption\n */\n async init(): Promise<void> {\n if (this.initialized) return;\n\n try {\n // Check if Web Crypto API is available\n if (window.crypto && window.crypto.subtle) {\n // Generate or retrieve encryption key\n const keyData = localStorage.getItem('_sec_key');\n if (keyData) {\n try {\n const keyBuffer = this.base64ToArrayBuffer(keyData);\n this.encryptionKey = await window.crypto.subtle.importKey(\n 'raw',\n keyBuffer,\n { name: 'AES-GCM' },\n false,\n ['encrypt', 'decrypt']\n );\n } catch (error) {\n await this.generateNewKey();\n }\n } else {\n await this.generateNewKey();\n }\n }\n this.initialized = true;\n } catch (error) {\n this.initialized = true;\n }\n }\n\n /**\n * Store item securely\n */\n async setItem(\n key: string,\n value: string,\n options: SecureStorageOptions = {}\n ): Promise<void> {\n await this.init();\n\n const data = {\n value,\n timestamp: Date.now(),\n expiry: options.expiry ? Date.now() + options.expiry : undefined,\n };\n\n const serialized = JSON.stringify(data);\n \n if (options.encrypt && this.encryptionKey) {\n try {\n const encrypted = await this.encrypt(serialized);\n localStorage.setItem(`_sec_${key}`, encrypted);\n return;\n } catch (error) {\n // Silent fail - store as plain text\n }\n }\n\n localStorage.setItem(key, serialized);\n }\n\n /**\n * Retrieve item securely\n */\n async getItem(key: string): Promise<string | null> {\n await this.init();\n\n // Try encrypted storage first\n const encryptedData = localStorage.getItem(`_sec_${key}`);\n if (encryptedData && this.encryptionKey) {\n try {\n const decrypted = await this.decrypt(encryptedData);\n const parsed = JSON.parse(decrypted);\n \n // Check expiry\n if (parsed.expiry && Date.now() > parsed.expiry) {\n await this.removeItem(key);\n return null;\n }\n \n return parsed.value;\n } catch (error) {\n // Silent fail - try plain storage\n }\n }\n\n // Fallback to plain storage\n const plainData = localStorage.getItem(key);\n if (!plainData) return null;\n\n try {\n const parsed = JSON.parse(plainData);\n \n // Check expiry\n if (parsed.expiry && Date.now() > parsed.expiry) {\n await this.removeItem(key);\n return null;\n }\n \n return parsed.value || plainData;\n } catch (error) {\n // If parsing fails, return as-is (backward compatibility)\n return plainData;\n }\n }\n\n /**\n * Remove item\n */\n async removeItem(key: string): Promise<void> {\n localStorage.removeItem(key);\n localStorage.removeItem(`_sec_${key}`);\n }\n\n /**\n * Clear all secure storage\n */\n async clear(): Promise<void> {\n const keys = Object.keys(localStorage);\n for (const key of keys) {\n if (key.startsWith('_sec_')) {\n localStorage.removeItem(key);\n }\n }\n }\n\n /**\n * Generate new encryption key\n */\n private async generateNewKey(): Promise<void> {\n if (!window.crypto?.subtle) return;\n\n try {\n this.encryptionKey = await window.crypto.subtle.generateKey(\n { name: 'AES-GCM', length: 256 },\n true,\n ['encrypt', 'decrypt']\n );\n\n // Export and store key\n const exportedKey = await window.crypto.subtle.exportKey('raw', this.encryptionKey);\n const keyData = this.arrayBufferToBase64(exportedKey);\n localStorage.setItem('_sec_key', keyData);\n } catch (error) {\n // Silent fail - encryption not available\n }\n }\n\n /**\n * Encrypt data\n */\n private async encrypt(data: string): Promise<string> {\n if (!this.encryptionKey || !window.crypto?.subtle) {\n throw new Error('Encryption not available');\n }\n\n const encoder = new TextEncoder();\n const dataBuffer = encoder.encode(data);\n const iv = window.crypto.getRandomValues(new Uint8Array(12));\n\n const encrypted = await window.crypto.subtle.encrypt(\n { name: 'AES-GCM', iv },\n this.encryptionKey,\n dataBuffer\n );\n\n // Combine IV and encrypted data\n const combined = new Uint8Array(iv.length + encrypted.byteLength);\n combined.set(iv);\n combined.set(new Uint8Array(encrypted), iv.length);\n\n return this.arrayBufferToBase64(combined.buffer);\n }\n\n /**\n * Decrypt data\n */\n private async decrypt(encryptedData: string): Promise<string> {\n if (!this.encryptionKey || !window.crypto?.subtle) {\n throw new Error('Decryption not available');\n }\n\n const combined = this.base64ToArrayBuffer(encryptedData);\n const iv = combined.slice(0, 12);\n const encrypted = combined.slice(12);\n\n const decrypted = await window.crypto.subtle.decrypt(\n { name: 'AES-GCM', iv },\n this.encryptionKey,\n encrypted\n );\n\n const decoder = new TextDecoder();\n return decoder.decode(decrypted);\n }\n\n /**\n * Convert ArrayBuffer to base64\n */\n private arrayBufferToBase64(buffer: ArrayBuffer): string {\n const bytes = new Uint8Array(buffer);\n let binary = '';\n for (let i = 0; i < bytes.byteLength; i++) {\n binary += String.fromCharCode(bytes[i]);\n }\n return btoa(binary);\n }\n\n /**\n * Convert base64 to ArrayBuffer\n */\n private base64ToArrayBuffer(base64: string): ArrayBuffer {\n const binary = atob(base64);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n return bytes.buffer;\n }\n}\n\nexport const secureStorage = new SecureStorageImpl();\n"],"mappings":"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","names":[]}
|