create-fluxstack 1.0.13 โ 1.0.14
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/.env.example +29 -29
- package/app/client/README.md +69 -69
- package/app/client/index.html +14 -13
- package/app/client/src/App.tsx +157 -524
- package/app/client/src/components/ErrorBoundary.tsx +107 -0
- package/app/client/src/components/ErrorDisplay.css +365 -0
- package/app/client/src/components/ErrorDisplay.tsx +258 -0
- package/app/client/src/components/FluxStackConfig.tsx +1321 -0
- package/app/client/src/components/HybridLiveCounter.tsx +140 -0
- package/app/client/src/components/LiveClock.tsx +286 -0
- package/app/client/src/components/MainLayout.tsx +390 -0
- package/app/client/src/components/SidebarNavigation.tsx +391 -0
- package/app/client/src/components/StateDemo.tsx +178 -0
- package/app/client/src/components/SystemMonitor.tsx +1038 -0
- package/app/client/src/components/Teste.tsx +104 -0
- package/app/client/src/components/UserProfile.tsx +809 -0
- package/app/client/src/hooks/useAuth.ts +39 -0
- package/app/client/src/hooks/useNotifications.ts +56 -0
- package/app/client/src/lib/eden-api.ts +189 -53
- package/app/client/src/lib/errors.ts +340 -0
- package/app/client/src/lib/hooks/useErrorHandler.ts +258 -0
- package/app/client/src/lib/index.ts +45 -0
- package/app/client/src/main.tsx +3 -2
- package/app/client/src/pages/ApiDocs.tsx +182 -0
- package/app/client/src/pages/Demo.tsx +174 -0
- package/app/client/src/pages/HybridLive.tsx +263 -0
- package/app/client/src/pages/Overview.tsx +155 -0
- package/app/client/src/store/README.md +43 -0
- package/app/client/src/store/index.ts +16 -0
- package/app/client/src/store/slices/uiSlice.ts +151 -0
- package/app/client/src/store/slices/userSlice.ts +161 -0
- package/app/client/src/test/README.md +257 -0
- package/app/client/src/test/setup.ts +70 -0
- package/app/client/src/test/types.ts +12 -0
- package/app/client/src/vite-env.d.ts +1 -1
- package/app/client/tsconfig.app.json +44 -43
- package/app/client/tsconfig.json +7 -7
- package/app/client/tsconfig.node.json +25 -25
- package/app/client/zustand-setup.md +65 -0
- package/app/server/controllers/users.controller.ts +68 -68
- package/app/server/index.ts +9 -1
- package/app/server/live/CounterComponent.ts +191 -0
- package/app/server/live/FluxStackConfig.ts +529 -0
- package/app/server/live/LiveClockComponent.ts +214 -0
- package/app/server/live/SidebarNavigation.ts +156 -0
- package/app/server/live/SystemMonitor.ts +594 -0
- package/app/server/live/SystemMonitorIntegration.ts +151 -0
- package/app/server/live/TesteComponent.ts +87 -0
- package/app/server/live/UserProfileComponent.ts +135 -0
- package/app/server/live/register-components.ts +28 -0
- package/app/server/middleware/auth.ts +136 -0
- package/app/server/middleware/errorHandling.ts +250 -0
- package/app/server/middleware/index.ts +10 -0
- package/app/server/middleware/rateLimit.ts +193 -0
- package/app/server/middleware/requestLogging.ts +215 -0
- package/app/server/middleware/validation.ts +270 -0
- package/app/server/routes/index.ts +14 -2
- package/app/server/routes/upload.ts +92 -0
- package/app/server/routes/users.routes.ts +2 -9
- package/app/server/services/NotificationService.ts +302 -0
- package/app/server/services/UserService.ts +222 -0
- package/app/server/services/index.ts +46 -0
- package/core/cli/commands/plugin-deps.ts +263 -0
- package/core/cli/generators/README.md +339 -0
- package/core/cli/generators/component.ts +770 -0
- package/core/cli/generators/controller.ts +299 -0
- package/core/cli/generators/index.ts +144 -0
- package/core/cli/generators/interactive.ts +228 -0
- package/core/cli/generators/prompts.ts +83 -0
- package/core/cli/generators/route.ts +513 -0
- package/core/cli/generators/service.ts +465 -0
- package/core/cli/generators/template-engine.ts +154 -0
- package/core/cli/generators/types.ts +71 -0
- package/core/cli/generators/utils.ts +192 -0
- package/core/cli/index.ts +69 -0
- package/core/cli/plugin-discovery.ts +16 -85
- package/core/client/fluxstack.ts +17 -0
- package/core/client/hooks/index.ts +7 -0
- package/core/client/hooks/state-validator.ts +130 -0
- package/core/client/hooks/useAuth.ts +49 -0
- package/core/client/hooks/useChunkedUpload.ts +258 -0
- package/core/client/hooks/useHybridLiveComponent.ts +967 -0
- package/core/client/hooks/useWebSocket.ts +373 -0
- package/core/client/index.ts +47 -0
- package/core/client/state/createStore.ts +193 -0
- package/core/client/state/index.ts +15 -0
- package/core/config/env-dynamic.ts +1 -1
- package/core/config/env.ts +2 -1
- package/core/config/runtime-config.ts +3 -3
- package/core/config/schema.ts +84 -49
- package/core/framework/server.ts +30 -0
- package/core/index.ts +25 -0
- package/core/live/ComponentRegistry.ts +399 -0
- package/core/live/types.ts +164 -0
- package/core/plugins/built-in/live-components/commands/create-live-component.ts +1201 -0
- package/core/plugins/built-in/live-components/index.ts +27 -0
- package/core/plugins/built-in/logger/index.ts +1 -1
- package/core/plugins/built-in/monitoring/index.ts +1 -1
- package/core/plugins/built-in/static/index.ts +1 -1
- package/core/plugins/built-in/swagger/index.ts +1 -1
- package/core/plugins/built-in/vite/index.ts +1 -1
- package/core/plugins/dependency-manager.ts +384 -0
- package/core/plugins/index.ts +5 -1
- package/core/plugins/manager.ts +7 -3
- package/core/plugins/registry.ts +88 -10
- package/core/plugins/types.ts +11 -11
- package/core/server/framework.ts +43 -0
- package/core/server/index.ts +11 -1
- package/core/server/live/ComponentRegistry.ts +1017 -0
- package/core/server/live/FileUploadManager.ts +272 -0
- package/core/server/live/LiveComponentPerformanceMonitor.ts +930 -0
- package/core/server/live/SingleConnectionManager.ts +0 -0
- package/core/server/live/StateSignature.ts +644 -0
- package/core/server/live/WebSocketConnectionManager.ts +688 -0
- package/core/server/live/websocket-plugin.ts +435 -0
- package/core/server/middleware/errorHandling.ts +141 -0
- package/core/server/middleware/index.ts +16 -0
- package/core/server/plugins/static-files-plugin.ts +232 -0
- package/core/server/services/BaseService.ts +95 -0
- package/core/server/services/ServiceContainer.ts +144 -0
- package/core/server/services/index.ts +9 -0
- package/core/templates/create-project.ts +46 -2
- package/core/testing/index.ts +10 -0
- package/core/testing/setup.ts +74 -0
- package/core/types/build.ts +38 -14
- package/core/types/types.ts +319 -0
- package/core/utils/env-runtime.ts +7 -0
- package/core/utils/errors/handlers.ts +264 -39
- package/core/utils/errors/index.ts +528 -18
- package/core/utils/errors/middleware.ts +114 -0
- package/core/utils/logger/formatters.ts +222 -0
- package/core/utils/logger/index.ts +167 -48
- package/core/utils/logger/middleware.ts +253 -0
- package/core/utils/logger/performance.ts +384 -0
- package/core/utils/logger/transports.ts +365 -0
- package/create-fluxstack.ts +296 -296
- package/fluxstack.config.ts +17 -1
- package/package-template.json +66 -66
- package/package.json +31 -6
- package/public/README.md +16 -0
- package/vite.config.ts +29 -14
- package/.claude/settings.local.json +0 -74
- package/.github/workflows/ci-build-tests.yml +0 -480
- package/.github/workflows/dependency-management.yml +0 -324
- package/.github/workflows/release-validation.yml +0 -355
- package/.kiro/specs/fluxstack-architecture-optimization/design.md +0 -700
- package/.kiro/specs/fluxstack-architecture-optimization/requirements.md +0 -127
- package/.kiro/specs/fluxstack-architecture-optimization/tasks.md +0 -330
- package/CLAUDE.md +0 -200
- package/Dockerfile +0 -58
- package/Dockerfile.backend +0 -52
- package/Dockerfile.frontend +0 -54
- package/README-Docker.md +0 -85
- package/ai-context/00-QUICK-START.md +0 -86
- package/ai-context/README.md +0 -88
- package/ai-context/development/eden-treaty-guide.md +0 -362
- package/ai-context/development/patterns.md +0 -382
- package/ai-context/development/plugins-guide.md +0 -572
- package/ai-context/examples/crud-complete.md +0 -626
- package/ai-context/project/architecture.md +0 -399
- package/ai-context/project/overview.md +0 -213
- package/ai-context/recent-changes/eden-treaty-refactor.md +0 -281
- package/ai-context/recent-changes/type-inference-fix.md +0 -223
- package/ai-context/reference/environment-vars.md +0 -384
- package/ai-context/reference/troubleshooting.md +0 -407
- package/app/client/src/components/TestPage.tsx +0 -453
- package/bun.lock +0 -1063
- package/bunfig.toml +0 -16
- package/core/__tests__/integration.test.ts +0 -227
- package/core/build/index.ts +0 -186
- package/core/config/__tests__/config-loader.test.ts +0 -554
- package/core/config/__tests__/config-merger.test.ts +0 -657
- package/core/config/__tests__/env-converter.test.ts +0 -372
- package/core/config/__tests__/env-processor.test.ts +0 -431
- package/core/config/__tests__/env.test.ts +0 -452
- package/core/config/__tests__/integration.test.ts +0 -418
- package/core/config/__tests__/loader.test.ts +0 -331
- package/core/config/__tests__/schema.test.ts +0 -129
- package/core/config/__tests__/validator.test.ts +0 -318
- package/core/framework/__tests__/server.test.ts +0 -233
- package/core/plugins/__tests__/built-in.test.ts.disabled +0 -366
- package/core/plugins/__tests__/manager.test.ts +0 -398
- package/core/plugins/__tests__/monitoring.test.ts +0 -401
- package/core/plugins/__tests__/registry.test.ts +0 -335
- package/core/utils/__tests__/errors.test.ts +0 -139
- package/core/utils/__tests__/helpers.test.ts +0 -297
- package/core/utils/__tests__/logger.test.ts +0 -141
- package/create-test-app.ts +0 -156
- package/docker-compose.microservices.yml +0 -75
- package/docker-compose.simple.yml +0 -57
- package/docker-compose.yml +0 -71
- package/eslint.config.js +0 -23
- package/flux-cli.ts +0 -214
- package/nginx-lb.conf +0 -37
- package/publish.sh +0 -63
- package/run-clean.ts +0 -26
- package/run-env-tests.ts +0 -313
- package/tailwind.config.js +0 -34
- package/tests/__mocks__/api.ts +0 -56
- package/tests/fixtures/users.ts +0 -69
- package/tests/integration/api/users.routes.test.ts +0 -221
- package/tests/setup.ts +0 -29
- package/tests/unit/app/client/App-simple.test.tsx +0 -56
- package/tests/unit/app/client/App.test.tsx.skip +0 -237
- package/tests/unit/app/client/eden-api.test.ts +0 -186
- package/tests/unit/app/client/simple.test.tsx +0 -23
- package/tests/unit/app/controllers/users.controller.test.ts +0 -150
- package/tests/unit/core/create-project.test.ts.skip +0 -95
- package/tests/unit/core/framework.test.ts +0 -144
- package/tests/unit/core/plugins/logger.test.ts.skip +0 -268
- package/tests/unit/core/plugins/vite.test.ts.disabled +0 -188
- package/tests/utils/test-helpers.ts +0 -61
- package/vitest.config.ts +0 -50
- package/workspace.json +0 -6
|
File without changes
|
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
// ๐ FluxStack Enhanced State Signature System - Advanced cryptographic validation with key rotation and compression
|
|
2
|
+
|
|
3
|
+
import { createHmac, randomBytes, createCipheriv, createDecipheriv, scrypt } from 'crypto'
|
|
4
|
+
import { promisify } from 'util'
|
|
5
|
+
import { gzip, gunzip } from 'zlib'
|
|
6
|
+
|
|
7
|
+
const scryptAsync = promisify(scrypt)
|
|
8
|
+
const gzipAsync = promisify(gzip)
|
|
9
|
+
const gunzipAsync = promisify(gunzip)
|
|
10
|
+
|
|
11
|
+
export interface SignedState<T = any> {
|
|
12
|
+
data: T
|
|
13
|
+
signature: string
|
|
14
|
+
timestamp: number
|
|
15
|
+
componentId: string
|
|
16
|
+
version: number
|
|
17
|
+
keyId?: string // For key rotation
|
|
18
|
+
compressed?: boolean // For state compression
|
|
19
|
+
encrypted?: boolean // For sensitive data
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface StateValidationResult {
|
|
23
|
+
valid: boolean
|
|
24
|
+
error?: string
|
|
25
|
+
tampered?: boolean
|
|
26
|
+
expired?: boolean
|
|
27
|
+
keyRotated?: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface StateBackup<T = any> {
|
|
31
|
+
componentId: string
|
|
32
|
+
state: T
|
|
33
|
+
timestamp: number
|
|
34
|
+
version: number
|
|
35
|
+
checksum: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface KeyRotationConfig {
|
|
39
|
+
rotationInterval: number // milliseconds
|
|
40
|
+
maxKeyAge: number // milliseconds
|
|
41
|
+
keyRetentionCount: number // number of old keys to keep
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface CompressionConfig {
|
|
45
|
+
enabled: boolean
|
|
46
|
+
threshold: number // bytes - compress if state is larger than this
|
|
47
|
+
level: number // compression level 1-9
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class StateSignature {
|
|
51
|
+
private static instance: StateSignature
|
|
52
|
+
private currentKey: string
|
|
53
|
+
private keyHistory: Map<string, { key: string; createdAt: number }> = new Map()
|
|
54
|
+
private readonly maxAge = 24 * 60 * 60 * 1000 // 24 hours default
|
|
55
|
+
private keyRotationConfig: KeyRotationConfig
|
|
56
|
+
private compressionConfig: CompressionConfig
|
|
57
|
+
private backups = new Map<string, StateBackup[]>() // componentId -> backups
|
|
58
|
+
private migrationFunctions = new Map<string, (state: any) => any>() // version -> migration function
|
|
59
|
+
|
|
60
|
+
constructor(secretKey?: string, options?: {
|
|
61
|
+
keyRotation?: Partial<KeyRotationConfig>
|
|
62
|
+
compression?: Partial<CompressionConfig>
|
|
63
|
+
}) {
|
|
64
|
+
this.currentKey = secretKey || this.generateSecretKey()
|
|
65
|
+
this.keyHistory.set(this.getCurrentKeyId(), {
|
|
66
|
+
key: this.currentKey,
|
|
67
|
+
createdAt: Date.now()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
this.keyRotationConfig = {
|
|
71
|
+
rotationInterval: 7 * 24 * 60 * 60 * 1000, // 7 days
|
|
72
|
+
maxKeyAge: 30 * 24 * 60 * 60 * 1000, // 30 days
|
|
73
|
+
keyRetentionCount: 5,
|
|
74
|
+
...options?.keyRotation
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
this.compressionConfig = {
|
|
78
|
+
enabled: true,
|
|
79
|
+
threshold: 1024, // 1KB
|
|
80
|
+
level: 6,
|
|
81
|
+
...options?.compression
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
this.setupKeyRotation()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
public static getInstance(secretKey?: string, options?: {
|
|
88
|
+
keyRotation?: Partial<KeyRotationConfig>
|
|
89
|
+
compression?: Partial<CompressionConfig>
|
|
90
|
+
}): StateSignature {
|
|
91
|
+
if (!StateSignature.instance) {
|
|
92
|
+
StateSignature.instance = new StateSignature(secretKey, options)
|
|
93
|
+
}
|
|
94
|
+
return StateSignature.instance
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private generateSecretKey(): string {
|
|
98
|
+
return randomBytes(32).toString('hex')
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private getCurrentKeyId(): string {
|
|
102
|
+
return createHmac('sha256', this.currentKey).update('keyid').digest('hex').substring(0, 8)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private setupKeyRotation(): void {
|
|
106
|
+
// Rotate keys periodically
|
|
107
|
+
setInterval(() => {
|
|
108
|
+
this.rotateKey()
|
|
109
|
+
}, this.keyRotationConfig.rotationInterval)
|
|
110
|
+
|
|
111
|
+
// Cleanup old keys
|
|
112
|
+
setInterval(() => {
|
|
113
|
+
this.cleanupOldKeys()
|
|
114
|
+
}, 24 * 60 * 60 * 1000) // Daily cleanup
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private rotateKey(): void {
|
|
118
|
+
const oldKeyId = this.getCurrentKeyId()
|
|
119
|
+
this.currentKey = this.generateSecretKey()
|
|
120
|
+
const newKeyId = this.getCurrentKeyId()
|
|
121
|
+
|
|
122
|
+
this.keyHistory.set(newKeyId, {
|
|
123
|
+
key: this.currentKey,
|
|
124
|
+
createdAt: Date.now()
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
console.log(`๐ Key rotated from ${oldKeyId} to ${newKeyId}`)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private cleanupOldKeys(): void {
|
|
131
|
+
const now = Date.now()
|
|
132
|
+
const keysToDelete: string[] = []
|
|
133
|
+
|
|
134
|
+
for (const [keyId, keyData] of this.keyHistory) {
|
|
135
|
+
const keyAge = now - keyData.createdAt
|
|
136
|
+
if (keyAge > this.keyRotationConfig.maxKeyAge) {
|
|
137
|
+
keysToDelete.push(keyId)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Keep at least the retention count of keys
|
|
142
|
+
const sortedKeys = Array.from(this.keyHistory.entries())
|
|
143
|
+
.sort((a, b) => b[1].createdAt - a[1].createdAt)
|
|
144
|
+
|
|
145
|
+
if (sortedKeys.length > this.keyRotationConfig.keyRetentionCount) {
|
|
146
|
+
const excessKeys = sortedKeys.slice(this.keyRotationConfig.keyRetentionCount)
|
|
147
|
+
for (const [keyId] of excessKeys) {
|
|
148
|
+
keysToDelete.push(keyId)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
for (const keyId of keysToDelete) {
|
|
153
|
+
this.keyHistory.delete(keyId)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (keysToDelete.length > 0) {
|
|
157
|
+
console.log(`๐งน Cleaned up ${keysToDelete.length} old keys`)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private getKeyById(keyId: string): string | null {
|
|
162
|
+
const keyData = this.keyHistory.get(keyId)
|
|
163
|
+
return keyData ? keyData.key : null
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Sign component state with enhanced security, compression, and encryption
|
|
168
|
+
*/
|
|
169
|
+
public async signState<T>(
|
|
170
|
+
componentId: string,
|
|
171
|
+
data: T,
|
|
172
|
+
version: number = 1,
|
|
173
|
+
options?: {
|
|
174
|
+
compress?: boolean
|
|
175
|
+
encrypt?: boolean
|
|
176
|
+
backup?: boolean
|
|
177
|
+
}
|
|
178
|
+
): Promise<SignedState<T>> {
|
|
179
|
+
const timestamp = Date.now()
|
|
180
|
+
const keyId = this.getCurrentKeyId()
|
|
181
|
+
|
|
182
|
+
let processedData = data
|
|
183
|
+
let compressed = false
|
|
184
|
+
let encrypted = false
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
// Serialize data for processing
|
|
188
|
+
const serializedData = JSON.stringify(data)
|
|
189
|
+
|
|
190
|
+
// Compress if enabled and data is large enough
|
|
191
|
+
if (this.compressionConfig.enabled &&
|
|
192
|
+
(options?.compress !== false) &&
|
|
193
|
+
Buffer.byteLength(serializedData, 'utf8') > this.compressionConfig.threshold) {
|
|
194
|
+
|
|
195
|
+
const compressedBuffer = await gzipAsync(Buffer.from(serializedData, 'utf8'))
|
|
196
|
+
processedData = compressedBuffer.toString('base64') as any
|
|
197
|
+
compressed = true
|
|
198
|
+
|
|
199
|
+
console.log(`๐๏ธ State compressed: ${Buffer.byteLength(serializedData, 'utf8')} -> ${compressedBuffer.length} bytes`)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Encrypt sensitive data if requested
|
|
203
|
+
if (options?.encrypt) {
|
|
204
|
+
const encryptedData = await this.encryptData(processedData)
|
|
205
|
+
processedData = encryptedData as any
|
|
206
|
+
encrypted = true
|
|
207
|
+
|
|
208
|
+
console.log('๐ State encrypted for component:', componentId)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Create payload for signing
|
|
212
|
+
const payload = {
|
|
213
|
+
data: processedData,
|
|
214
|
+
componentId,
|
|
215
|
+
timestamp,
|
|
216
|
+
version,
|
|
217
|
+
keyId,
|
|
218
|
+
compressed,
|
|
219
|
+
encrypted
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Generate signature with current key
|
|
223
|
+
const signature = this.createSignature(payload)
|
|
224
|
+
|
|
225
|
+
// Create backup if requested
|
|
226
|
+
if (options?.backup) {
|
|
227
|
+
await this.createStateBackup(componentId, data, version)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
console.log('๐ State signed:', {
|
|
231
|
+
componentId,
|
|
232
|
+
timestamp,
|
|
233
|
+
version,
|
|
234
|
+
keyId,
|
|
235
|
+
compressed,
|
|
236
|
+
encrypted,
|
|
237
|
+
signature: signature.substring(0, 16) + '...'
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
data: processedData,
|
|
242
|
+
signature,
|
|
243
|
+
timestamp,
|
|
244
|
+
componentId,
|
|
245
|
+
version,
|
|
246
|
+
keyId,
|
|
247
|
+
compressed,
|
|
248
|
+
encrypted
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
} catch (error) {
|
|
252
|
+
console.error('โ Failed to sign state:', error)
|
|
253
|
+
throw new Error(`State signing failed: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Validate signed state integrity with enhanced security checks
|
|
259
|
+
*/
|
|
260
|
+
public async validateState<T>(signedState: SignedState<T>, maxAge?: number): Promise<StateValidationResult> {
|
|
261
|
+
const { data, signature, timestamp, componentId, version, keyId, compressed, encrypted } = signedState
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
// Check timestamp (prevent replay attacks)
|
|
265
|
+
const age = Date.now() - timestamp
|
|
266
|
+
const ageLimit = maxAge || this.maxAge
|
|
267
|
+
|
|
268
|
+
if (age > ageLimit) {
|
|
269
|
+
return {
|
|
270
|
+
valid: false,
|
|
271
|
+
error: 'State signature expired',
|
|
272
|
+
expired: true
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Determine which key to use for validation
|
|
277
|
+
let validationKey = this.currentKey
|
|
278
|
+
let keyRotated = false
|
|
279
|
+
|
|
280
|
+
if (keyId) {
|
|
281
|
+
const historicalKey = this.getKeyById(keyId)
|
|
282
|
+
if (historicalKey) {
|
|
283
|
+
validationKey = historicalKey
|
|
284
|
+
keyRotated = keyId !== this.getCurrentKeyId()
|
|
285
|
+
} else {
|
|
286
|
+
return {
|
|
287
|
+
valid: false,
|
|
288
|
+
error: 'Signing key not found or expired',
|
|
289
|
+
keyRotated: true
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Recreate payload for verification
|
|
295
|
+
const payload = {
|
|
296
|
+
data,
|
|
297
|
+
componentId,
|
|
298
|
+
timestamp,
|
|
299
|
+
version,
|
|
300
|
+
keyId,
|
|
301
|
+
compressed,
|
|
302
|
+
encrypted
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Verify signature with appropriate key
|
|
306
|
+
const expectedSignature = this.createSignature(payload, validationKey)
|
|
307
|
+
|
|
308
|
+
if (!this.constantTimeEquals(signature, expectedSignature)) {
|
|
309
|
+
console.warn('โ ๏ธ State signature mismatch:', {
|
|
310
|
+
componentId,
|
|
311
|
+
expected: expectedSignature.substring(0, 16) + '...',
|
|
312
|
+
received: signature.substring(0, 16) + '...'
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
valid: false,
|
|
317
|
+
error: 'State signature invalid - possible tampering',
|
|
318
|
+
tampered: true
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
console.log('โ
State signature valid:', {
|
|
323
|
+
componentId,
|
|
324
|
+
age: `${Math.round(age / 1000)}s`,
|
|
325
|
+
version
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
return { valid: true }
|
|
329
|
+
|
|
330
|
+
} catch (error: any) {
|
|
331
|
+
return {
|
|
332
|
+
valid: false,
|
|
333
|
+
error: `Validation error: ${error.message}`
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Create HMAC signature for payload using specified key
|
|
340
|
+
*/
|
|
341
|
+
private createSignature(payload: any, key?: string): string {
|
|
342
|
+
// Stringify deterministically (sorted keys)
|
|
343
|
+
const normalizedPayload = JSON.stringify(payload, Object.keys(payload).sort())
|
|
344
|
+
|
|
345
|
+
return createHmac('sha256', key || this.currentKey)
|
|
346
|
+
.update(normalizedPayload)
|
|
347
|
+
.digest('hex')
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Encrypt sensitive data
|
|
352
|
+
*/
|
|
353
|
+
private async encryptData<T>(data: T): Promise<string> {
|
|
354
|
+
try {
|
|
355
|
+
const serializedData = JSON.stringify(data)
|
|
356
|
+
const key = await scryptAsync(this.currentKey, 'salt', 32) as Buffer
|
|
357
|
+
const iv = randomBytes(16)
|
|
358
|
+
const cipher = createCipheriv('aes-256-cbc', key, iv)
|
|
359
|
+
|
|
360
|
+
let encrypted = cipher.update(serializedData, 'utf8', 'hex')
|
|
361
|
+
encrypted += cipher.final('hex')
|
|
362
|
+
|
|
363
|
+
return iv.toString('hex') + ':' + encrypted
|
|
364
|
+
} catch (error) {
|
|
365
|
+
throw new Error(`Encryption failed: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Decrypt sensitive data
|
|
371
|
+
*/
|
|
372
|
+
private async decryptData(encryptedData: string, key?: string): Promise<any> {
|
|
373
|
+
try {
|
|
374
|
+
const [ivHex, encrypted] = encryptedData.split(':')
|
|
375
|
+
const iv = Buffer.from(ivHex, 'hex')
|
|
376
|
+
const derivedKey = await scryptAsync(key || this.currentKey, 'salt', 32) as Buffer
|
|
377
|
+
const decipher = createDecipheriv('aes-256-cbc', derivedKey, iv)
|
|
378
|
+
|
|
379
|
+
let decrypted = decipher.update(encrypted, 'hex', 'utf8')
|
|
380
|
+
decrypted += decipher.final('utf8')
|
|
381
|
+
|
|
382
|
+
return JSON.parse(decrypted)
|
|
383
|
+
} catch (error) {
|
|
384
|
+
throw new Error(`Decryption failed: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Decompress state data
|
|
390
|
+
*/
|
|
391
|
+
private async decompressData(compressedData: string): Promise<any> {
|
|
392
|
+
try {
|
|
393
|
+
const compressedBuffer = Buffer.from(compressedData, 'base64')
|
|
394
|
+
const decompressedBuffer = await gunzipAsync(compressedBuffer)
|
|
395
|
+
return JSON.parse(decompressedBuffer.toString('utf8'))
|
|
396
|
+
} catch (error) {
|
|
397
|
+
throw new Error(`Decompression failed: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Create state backup
|
|
403
|
+
*/
|
|
404
|
+
private async createStateBackup<T>(componentId: string, state: T, version: number): Promise<void> {
|
|
405
|
+
try {
|
|
406
|
+
const backup: StateBackup<T> = {
|
|
407
|
+
componentId,
|
|
408
|
+
state,
|
|
409
|
+
timestamp: Date.now(),
|
|
410
|
+
version,
|
|
411
|
+
checksum: createHmac('sha256', this.currentKey).update(JSON.stringify(state)).digest('hex')
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
let backups = this.backups.get(componentId) || []
|
|
415
|
+
backups.push(backup)
|
|
416
|
+
|
|
417
|
+
// Keep only last 10 backups per component
|
|
418
|
+
if (backups.length > 10) {
|
|
419
|
+
backups = backups.slice(-10)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
this.backups.set(componentId, backups)
|
|
423
|
+
|
|
424
|
+
console.log(`๐พ State backup created for component ${componentId} v${version}`)
|
|
425
|
+
} catch (error) {
|
|
426
|
+
console.error(`โ Failed to create backup for component ${componentId}:`, error)
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Constant-time string comparison to prevent timing attacks
|
|
432
|
+
*/
|
|
433
|
+
private constantTimeEquals(a: string, b: string): boolean {
|
|
434
|
+
if (a.length !== b.length) {
|
|
435
|
+
return false
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
let result = 0
|
|
439
|
+
for (let i = 0; i < a.length; i++) {
|
|
440
|
+
result |= a.charCodeAt(i) ^ b.charCodeAt(i)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return result === 0
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Extract and process signed state data (decompression, decryption)
|
|
448
|
+
*/
|
|
449
|
+
public async extractData<T>(signedState: SignedState<T>): Promise<T> {
|
|
450
|
+
let data = signedState.data
|
|
451
|
+
|
|
452
|
+
try {
|
|
453
|
+
// Decrypt if encrypted
|
|
454
|
+
if (signedState.encrypted) {
|
|
455
|
+
const keyToUse = signedState.keyId ? this.getKeyById(signedState.keyId) : this.currentKey
|
|
456
|
+
if (!keyToUse) {
|
|
457
|
+
throw new Error('Decryption key not available')
|
|
458
|
+
}
|
|
459
|
+
data = await this.decryptData(data as string, keyToUse)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Decompress if compressed
|
|
463
|
+
if (signedState.compressed) {
|
|
464
|
+
data = await this.decompressData(data as string)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return data
|
|
468
|
+
} catch (error) {
|
|
469
|
+
console.error('โ Failed to extract state data:', error)
|
|
470
|
+
throw error
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Update signature for new state version with enhanced options
|
|
476
|
+
*/
|
|
477
|
+
public async updateSignature<T>(
|
|
478
|
+
signedState: SignedState<T>,
|
|
479
|
+
newData: T,
|
|
480
|
+
options?: {
|
|
481
|
+
compress?: boolean
|
|
482
|
+
encrypt?: boolean
|
|
483
|
+
backup?: boolean
|
|
484
|
+
}
|
|
485
|
+
): Promise<SignedState<T>> {
|
|
486
|
+
return this.signState(
|
|
487
|
+
signedState.componentId,
|
|
488
|
+
newData,
|
|
489
|
+
signedState.version + 1,
|
|
490
|
+
options
|
|
491
|
+
)
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Register state migration function
|
|
496
|
+
*/
|
|
497
|
+
public registerMigration(fromVersion: string, toVersion: string, migrationFn: (state: any) => any): void {
|
|
498
|
+
const key = `${fromVersion}->${toVersion}`
|
|
499
|
+
this.migrationFunctions.set(key, migrationFn)
|
|
500
|
+
console.log(`๐ Registered migration: ${key}`)
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Migrate state to new version
|
|
505
|
+
*/
|
|
506
|
+
public async migrateState<T>(signedState: SignedState<T>, targetVersion: string): Promise<SignedState<T> | null> {
|
|
507
|
+
const currentVersion = signedState.version.toString()
|
|
508
|
+
const migrationKey = `${currentVersion}->${targetVersion}`
|
|
509
|
+
|
|
510
|
+
const migrationFn = this.migrationFunctions.get(migrationKey)
|
|
511
|
+
if (!migrationFn) {
|
|
512
|
+
console.warn(`โ ๏ธ No migration function found for ${migrationKey}`)
|
|
513
|
+
return null
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
try {
|
|
517
|
+
// Extract current data
|
|
518
|
+
const currentData = await this.extractData(signedState)
|
|
519
|
+
|
|
520
|
+
// Apply migration
|
|
521
|
+
const migratedData = migrationFn(currentData)
|
|
522
|
+
|
|
523
|
+
// Create new signed state
|
|
524
|
+
const newSignedState = await this.signState(
|
|
525
|
+
signedState.componentId,
|
|
526
|
+
migratedData,
|
|
527
|
+
parseInt(targetVersion),
|
|
528
|
+
{
|
|
529
|
+
compress: signedState.compressed,
|
|
530
|
+
encrypt: signedState.encrypted,
|
|
531
|
+
backup: true
|
|
532
|
+
}
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
console.log(`โ
State migrated from v${currentVersion} to v${targetVersion} for component ${signedState.componentId}`)
|
|
536
|
+
return newSignedState
|
|
537
|
+
|
|
538
|
+
} catch (error) {
|
|
539
|
+
console.error(`โ State migration failed for ${migrationKey}:`, error)
|
|
540
|
+
return null
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Recover state from backup
|
|
546
|
+
*/
|
|
547
|
+
public recoverStateFromBackup<T>(componentId: string, version?: number): StateBackup<T> | null {
|
|
548
|
+
const backups = this.backups.get(componentId)
|
|
549
|
+
if (!backups || backups.length === 0) {
|
|
550
|
+
return null
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (version !== undefined) {
|
|
554
|
+
// Find specific version
|
|
555
|
+
return backups.find(backup => backup.version === version) || null
|
|
556
|
+
} else {
|
|
557
|
+
// Return latest backup
|
|
558
|
+
return backups[backups.length - 1] || null
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Get all backups for a component
|
|
564
|
+
*/
|
|
565
|
+
public getComponentBackups(componentId: string): StateBackup[] {
|
|
566
|
+
return this.backups.get(componentId) || []
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Verify backup integrity
|
|
571
|
+
*/
|
|
572
|
+
public verifyBackup<T>(backup: StateBackup<T>): boolean {
|
|
573
|
+
try {
|
|
574
|
+
const expectedChecksum = createHmac('sha256', this.currentKey)
|
|
575
|
+
.update(JSON.stringify(backup.state))
|
|
576
|
+
.digest('hex')
|
|
577
|
+
|
|
578
|
+
return this.constantTimeEquals(backup.checksum, expectedChecksum)
|
|
579
|
+
} catch {
|
|
580
|
+
return false
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Clean up old backups
|
|
586
|
+
*/
|
|
587
|
+
public cleanupBackups(maxAge: number = 7 * 24 * 60 * 60 * 1000): void {
|
|
588
|
+
const now = Date.now()
|
|
589
|
+
let totalCleaned = 0
|
|
590
|
+
|
|
591
|
+
for (const [componentId, backups] of this.backups) {
|
|
592
|
+
const validBackups = backups.filter(backup => {
|
|
593
|
+
const age = now - backup.timestamp
|
|
594
|
+
return age <= maxAge
|
|
595
|
+
})
|
|
596
|
+
|
|
597
|
+
const cleaned = backups.length - validBackups.length
|
|
598
|
+
totalCleaned += cleaned
|
|
599
|
+
|
|
600
|
+
if (validBackups.length === 0) {
|
|
601
|
+
this.backups.delete(componentId)
|
|
602
|
+
} else {
|
|
603
|
+
this.backups.set(componentId, validBackups)
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (totalCleaned > 0) {
|
|
608
|
+
console.log(`๐งน Cleaned up ${totalCleaned} old state backups`)
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Get server's signature info for debugging
|
|
614
|
+
*/
|
|
615
|
+
public getSignatureInfo() {
|
|
616
|
+
return {
|
|
617
|
+
algorithm: 'HMAC-SHA256',
|
|
618
|
+
keyLength: this.currentKey.length,
|
|
619
|
+
maxAge: this.maxAge,
|
|
620
|
+
keyPreview: this.currentKey.substring(0, 8) + '...',
|
|
621
|
+
currentKeyId: this.getCurrentKeyId(),
|
|
622
|
+
keyHistoryCount: this.keyHistory.size,
|
|
623
|
+
compressionEnabled: this.compressionConfig.enabled,
|
|
624
|
+
rotationInterval: this.keyRotationConfig.rotationInterval
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Global instance with enhanced configuration
|
|
630
|
+
export const stateSignature = StateSignature.getInstance(
|
|
631
|
+
process.env.FLUXSTACK_STATE_SECRET || undefined,
|
|
632
|
+
{
|
|
633
|
+
keyRotation: {
|
|
634
|
+
rotationInterval: parseInt(process.env.FLUXSTACK_KEY_ROTATION_INTERVAL || '604800000'), // 7 days
|
|
635
|
+
maxKeyAge: parseInt(process.env.FLUXSTACK_MAX_KEY_AGE || '2592000000'), // 30 days
|
|
636
|
+
keyRetentionCount: parseInt(process.env.FLUXSTACK_KEY_RETENTION_COUNT || '5')
|
|
637
|
+
},
|
|
638
|
+
compression: {
|
|
639
|
+
enabled: process.env.FLUXSTACK_COMPRESSION_ENABLED !== 'false',
|
|
640
|
+
threshold: parseInt(process.env.FLUXSTACK_COMPRESSION_THRESHOLD || '1024'), // 1KB
|
|
641
|
+
level: parseInt(process.env.FLUXSTACK_COMPRESSION_LEVEL || '6')
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
)
|