create-fluxstack 1.14.0 โ†’ 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/LLMD/INDEX.md +4 -3
  2. package/LLMD/resources/live-binary-delta.md +507 -0
  3. package/LLMD/resources/live-components.md +208 -12
  4. package/LLMD/resources/live-rooms.md +731 -333
  5. package/app/client/.live-stubs/LiveAdminPanel.js +5 -0
  6. package/app/client/.live-stubs/LiveCounter.js +9 -0
  7. package/app/client/.live-stubs/LiveForm.js +11 -0
  8. package/app/client/.live-stubs/LiveLocalCounter.js +8 -0
  9. package/app/client/.live-stubs/LivePingPong.js +10 -0
  10. package/app/client/.live-stubs/LiveRoomChat.js +11 -0
  11. package/app/client/.live-stubs/LiveSharedCounter.js +10 -0
  12. package/app/client/.live-stubs/LiveUpload.js +15 -0
  13. package/app/client/src/App.tsx +19 -7
  14. package/app/client/src/components/AppLayout.tsx +18 -10
  15. package/app/client/src/live/PingPongDemo.tsx +199 -0
  16. package/app/client/src/live/RoomChatDemo.tsx +187 -22
  17. package/app/client/src/live/SharedCounterDemo.tsx +142 -0
  18. package/app/server/auth/DevAuthProvider.ts +2 -2
  19. package/app/server/auth/JWTAuthProvider.example.ts +2 -2
  20. package/app/server/index.ts +2 -2
  21. package/app/server/live/LiveAdminPanel.ts +1 -1
  22. package/app/server/live/LivePingPong.ts +61 -0
  23. package/app/server/live/LiveProtectedChat.ts +1 -1
  24. package/app/server/live/LiveRoomChat.ts +106 -38
  25. package/app/server/live/LiveSharedCounter.ts +73 -0
  26. package/app/server/live/rooms/ChatRoom.ts +68 -0
  27. package/app/server/live/rooms/CounterRoom.ts +51 -0
  28. package/app/server/live/rooms/DirectoryRoom.ts +42 -0
  29. package/app/server/live/rooms/PingRoom.ts +40 -0
  30. package/app/server/routes/room.routes.ts +1 -2
  31. package/core/build/live-components-generator.ts +11 -2
  32. package/core/build/vite-plugins.ts +28 -0
  33. package/core/client/hooks/useLiveUpload.ts +3 -4
  34. package/core/client/index.ts +25 -35
  35. package/core/framework/server.ts +1 -1
  36. package/core/server/index.ts +1 -2
  37. package/core/server/live/auto-generated-components.ts +5 -8
  38. package/core/server/live/index.ts +90 -21
  39. package/core/server/live/websocket-plugin.ts +54 -1079
  40. package/core/types/types.ts +76 -1025
  41. package/core/utils/version.ts +1 -1
  42. package/create-fluxstack.ts +1 -1
  43. package/package.json +100 -95
  44. package/plugins/crypto-auth/index.ts +1 -1
  45. package/plugins/crypto-auth/server/CryptoAuthLiveProvider.ts +2 -2
  46. package/tsconfig.json +4 -1
  47. package/vite.config.ts +40 -12
  48. package/app/client/src/live/ChatDemo.tsx +0 -107
  49. package/app/client/src/live/LiveDebuggerPanel.tsx +0 -779
  50. package/app/server/live/LiveChat.ts +0 -78
  51. package/core/client/LiveComponentsProvider.tsx +0 -531
  52. package/core/client/components/Live.tsx +0 -111
  53. package/core/client/components/LiveDebugger.tsx +0 -1324
  54. package/core/client/hooks/AdaptiveChunkSizer.ts +0 -215
  55. package/core/client/hooks/state-validator.ts +0 -130
  56. package/core/client/hooks/useChunkedUpload.ts +0 -359
  57. package/core/client/hooks/useLiveChunkedUpload.ts +0 -87
  58. package/core/client/hooks/useLiveComponent.ts +0 -853
  59. package/core/client/hooks/useLiveDebugger.ts +0 -392
  60. package/core/client/hooks/useRoom.ts +0 -409
  61. package/core/client/hooks/useRoomProxy.ts +0 -382
  62. package/core/server/live/ComponentRegistry.ts +0 -1128
  63. package/core/server/live/FileUploadManager.ts +0 -446
  64. package/core/server/live/LiveComponentPerformanceMonitor.ts +0 -931
  65. package/core/server/live/LiveDebugger.ts +0 -462
  66. package/core/server/live/LiveLogger.ts +0 -144
  67. package/core/server/live/LiveRoomManager.ts +0 -278
  68. package/core/server/live/RoomEventBus.ts +0 -234
  69. package/core/server/live/RoomStateManager.ts +0 -172
  70. package/core/server/live/SingleConnectionManager.ts +0 -0
  71. package/core/server/live/StateSignature.ts +0 -705
  72. package/core/server/live/WebSocketConnectionManager.ts +0 -710
  73. package/core/server/live/auth/LiveAuthContext.ts +0 -71
  74. package/core/server/live/auth/LiveAuthManager.ts +0 -304
  75. package/core/server/live/auth/index.ts +0 -19
  76. package/core/server/live/auth/types.ts +0 -179
@@ -1,705 +0,0 @@
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
- import { liveLog, liveWarn } from './LiveLogger'
7
-
8
- const scryptAsync = promisify(scrypt)
9
- const gzipAsync = promisify(gzip)
10
- const gunzipAsync = promisify(gunzip)
11
-
12
- export interface SignedState<T = any> {
13
- data: T
14
- signature: string
15
- timestamp: number
16
- componentId: string
17
- version: number
18
- keyId?: string // For key rotation
19
- compressed?: boolean // For state compression
20
- encrypted?: boolean // For sensitive data
21
- nonce?: string // ๐Ÿ”’ Anti-replay: unique per signed state
22
- }
23
-
24
- export interface StateValidationResult {
25
- valid: boolean
26
- error?: string
27
- tampered?: boolean
28
- expired?: boolean
29
- keyRotated?: boolean
30
- replayed?: boolean // ๐Ÿ”’ Anti-replay: nonce was already consumed
31
- }
32
-
33
- export interface StateBackup<T = any> {
34
- componentId: string
35
- state: T
36
- timestamp: number
37
- version: number
38
- checksum: string
39
- }
40
-
41
- export interface KeyRotationConfig {
42
- rotationInterval: number // milliseconds
43
- maxKeyAge: number // milliseconds
44
- keyRetentionCount: number // number of old keys to keep
45
- }
46
-
47
- export interface CompressionConfig {
48
- enabled: boolean
49
- threshold: number // bytes - compress if state is larger than this
50
- level: number // compression level 1-9
51
- }
52
-
53
- export class StateSignature {
54
- private static instance: StateSignature
55
- private currentKey: string
56
- private keyHistory: Map<string, { key: string; createdAt: number }> = new Map()
57
- private readonly maxAge = 24 * 60 * 60 * 1000 // 24 hours default
58
- private keyRotationConfig: KeyRotationConfig
59
- private compressionConfig: CompressionConfig
60
- private backups = new Map<string, StateBackup[]>() // componentId -> backups
61
- private migrationFunctions = new Map<string, (state: any) => any>() // version -> migration function
62
- // ๐Ÿ”’ Anti-replay: track consumed nonces to prevent state replay attacks
63
- private consumedNonces = new Set<string>()
64
- private readonly nonceMaxAge = 24 * 60 * 60 * 1000 // Nonces expire with the state (24h)
65
- private nonceTimestamps = new Map<string, number>() // nonce -> timestamp for cleanup
66
-
67
- constructor(secretKey?: string, options?: {
68
- keyRotation?: Partial<KeyRotationConfig>
69
- compression?: Partial<CompressionConfig>
70
- }) {
71
- this.currentKey = secretKey || this.generateSecretKey()
72
- this.keyHistory.set(this.getCurrentKeyId(), {
73
- key: this.currentKey,
74
- createdAt: Date.now()
75
- })
76
-
77
- this.keyRotationConfig = {
78
- rotationInterval: 7 * 24 * 60 * 60 * 1000, // 7 days
79
- maxKeyAge: 30 * 24 * 60 * 60 * 1000, // 30 days
80
- keyRetentionCount: 5,
81
- ...options?.keyRotation
82
- }
83
-
84
- this.compressionConfig = {
85
- enabled: true,
86
- threshold: 1024, // 1KB
87
- level: 6,
88
- ...options?.compression
89
- }
90
-
91
- this.setupKeyRotation()
92
- }
93
-
94
- public static getInstance(secretKey?: string, options?: {
95
- keyRotation?: Partial<KeyRotationConfig>
96
- compression?: Partial<CompressionConfig>
97
- }): StateSignature {
98
- if (!StateSignature.instance) {
99
- StateSignature.instance = new StateSignature(secretKey, options)
100
- }
101
- return StateSignature.instance
102
- }
103
-
104
- private generateSecretKey(): string {
105
- return randomBytes(32).toString('hex')
106
- }
107
-
108
- private getCurrentKeyId(): string {
109
- return createHmac('sha256', this.currentKey).update('keyid').digest('hex').substring(0, 8)
110
- }
111
-
112
- private setupKeyRotation(): void {
113
- // Rotate keys periodically
114
- setInterval(() => {
115
- this.rotateKey()
116
- }, this.keyRotationConfig.rotationInterval)
117
-
118
- // Cleanup old keys and expired nonces
119
- setInterval(() => {
120
- this.cleanupOldKeys()
121
- this.cleanupExpiredNonces()
122
- }, 24 * 60 * 60 * 1000) // Daily cleanup
123
- }
124
-
125
- private rotateKey(): void {
126
- const oldKeyId = this.getCurrentKeyId()
127
- this.currentKey = this.generateSecretKey()
128
- const newKeyId = this.getCurrentKeyId()
129
-
130
- this.keyHistory.set(newKeyId, {
131
- key: this.currentKey,
132
- createdAt: Date.now()
133
- })
134
-
135
- liveLog('state', null, `๐Ÿ”„ Key rotated from ${oldKeyId} to ${newKeyId}`)
136
- }
137
-
138
- private cleanupOldKeys(): void {
139
- const now = Date.now()
140
- const keysToDelete: string[] = []
141
-
142
- for (const [keyId, keyData] of this.keyHistory) {
143
- const keyAge = now - keyData.createdAt
144
- if (keyAge > this.keyRotationConfig.maxKeyAge) {
145
- keysToDelete.push(keyId)
146
- }
147
- }
148
-
149
- // Keep at least the retention count of keys
150
- const sortedKeys = Array.from(this.keyHistory.entries())
151
- .sort((a, b) => b[1].createdAt - a[1].createdAt)
152
-
153
- if (sortedKeys.length > this.keyRotationConfig.keyRetentionCount) {
154
- const excessKeys = sortedKeys.slice(this.keyRotationConfig.keyRetentionCount)
155
- for (const [keyId] of excessKeys) {
156
- keysToDelete.push(keyId)
157
- }
158
- }
159
-
160
- for (const keyId of keysToDelete) {
161
- this.keyHistory.delete(keyId)
162
- }
163
-
164
- if (keysToDelete.length > 0) {
165
- liveLog('state', null, `๐Ÿงน Cleaned up ${keysToDelete.length} old keys`)
166
- }
167
- }
168
-
169
- /**
170
- * ๐Ÿ”’ Remove expired nonces to prevent unbounded memory growth
171
- */
172
- private cleanupExpiredNonces(): void {
173
- const now = Date.now()
174
- let cleaned = 0
175
-
176
- for (const [nonce, timestamp] of this.nonceTimestamps) {
177
- if (now - timestamp > this.nonceMaxAge) {
178
- this.consumedNonces.delete(nonce)
179
- this.nonceTimestamps.delete(nonce)
180
- cleaned++
181
- }
182
- }
183
-
184
- if (cleaned > 0) {
185
- liveLog('state', null, `๐Ÿงน Cleaned up ${cleaned} expired nonces (${this.consumedNonces.size} active)`)
186
- }
187
- }
188
-
189
- private getKeyById(keyId: string): string | null {
190
- const keyData = this.keyHistory.get(keyId)
191
- return keyData ? keyData.key : null
192
- }
193
-
194
- /**
195
- * Sign component state with enhanced security, compression, and encryption
196
- */
197
- public async signState<T>(
198
- componentId: string,
199
- data: T,
200
- version: number = 1,
201
- options?: {
202
- compress?: boolean
203
- encrypt?: boolean
204
- backup?: boolean
205
- }
206
- ): Promise<SignedState<T>> {
207
- const timestamp = Date.now()
208
- const keyId = this.getCurrentKeyId()
209
- const nonce = randomBytes(16).toString('hex') // ๐Ÿ”’ Anti-replay nonce
210
-
211
- let processedData = data
212
- let compressed = false
213
- let encrypted = false
214
-
215
- try {
216
- // Serialize data for processing
217
- const serializedData = JSON.stringify(data)
218
-
219
- // Compress if enabled and data is large enough
220
- if (this.compressionConfig.enabled &&
221
- (options?.compress !== false) &&
222
- Buffer.byteLength(serializedData, 'utf8') > this.compressionConfig.threshold) {
223
-
224
- const compressedBuffer = await gzipAsync(Buffer.from(serializedData, 'utf8'))
225
- processedData = compressedBuffer.toString('base64') as any
226
- compressed = true
227
-
228
- liveLog('state', componentId, `๐Ÿ—œ๏ธ State compressed: ${Buffer.byteLength(serializedData, 'utf8')} -> ${compressedBuffer.length} bytes`)
229
- }
230
-
231
- // Encrypt sensitive data if requested
232
- if (options?.encrypt) {
233
- const encryptedData = await this.encryptData(processedData)
234
- processedData = encryptedData as any
235
- encrypted = true
236
-
237
- liveLog('state', componentId, `๐Ÿ”’ State encrypted for component: ${componentId}`)
238
- }
239
-
240
- // Create payload for signing (includes nonce for anti-replay)
241
- const payload = {
242
- data: processedData,
243
- componentId,
244
- timestamp,
245
- version,
246
- keyId,
247
- compressed,
248
- encrypted,
249
- nonce
250
- }
251
-
252
- // Generate signature with current key
253
- const signature = this.createSignature(payload)
254
-
255
- // Create backup if requested
256
- if (options?.backup) {
257
- await this.createStateBackup(componentId, data, version)
258
- }
259
-
260
- liveLog('state', componentId, '๐Ÿ” State signed:', {
261
- componentId,
262
- timestamp,
263
- version,
264
- keyId,
265
- compressed,
266
- encrypted,
267
- nonce: nonce.substring(0, 8) + '...',
268
- signature: signature.substring(0, 16) + '...'
269
- })
270
-
271
- return {
272
- data: processedData,
273
- signature,
274
- timestamp,
275
- componentId,
276
- version,
277
- keyId,
278
- compressed,
279
- encrypted,
280
- nonce
281
- }
282
-
283
- } catch (error) {
284
- console.error('โŒ Failed to sign state:', error)
285
- throw new Error(`State signing failed: ${error instanceof Error ? error.message : 'Unknown error'}`)
286
- }
287
- }
288
-
289
- /**
290
- * Validate signed state integrity with enhanced security checks
291
- */
292
- /**
293
- * Validate signed state integrity with enhanced security checks.
294
- * @param consumeNonce If true (default), the nonce is consumed and the same signed state cannot be reused.
295
- * Set to false for read-only validation without consuming the nonce.
296
- */
297
- public async validateState<T>(signedState: SignedState<T>, maxAge?: number, consumeNonce = true): Promise<StateValidationResult> {
298
- const { data, signature, timestamp, componentId, version, keyId, compressed, encrypted, nonce } = signedState
299
-
300
- try {
301
- // Check timestamp (prevent replay attacks)
302
- const age = Date.now() - timestamp
303
- const ageLimit = maxAge || this.maxAge
304
-
305
- if (age > ageLimit) {
306
- return {
307
- valid: false,
308
- error: 'State signature expired',
309
- expired: true
310
- }
311
- }
312
-
313
- // ๐Ÿ”’ Anti-replay: check if this nonce was already consumed
314
- if (nonce && consumeNonce && this.consumedNonces.has(nonce)) {
315
- liveWarn('state', componentId, 'โš ๏ธ Replay attack detected - nonce already consumed:', {
316
- componentId,
317
- nonce: nonce.substring(0, 8) + '...'
318
- })
319
- return {
320
- valid: false,
321
- error: 'State already consumed - replay attack detected',
322
- replayed: true
323
- }
324
- }
325
-
326
- // Determine which key to use for validation
327
- let validationKey = this.currentKey
328
- let keyRotated = false
329
-
330
- if (keyId) {
331
- const historicalKey = this.getKeyById(keyId)
332
- if (historicalKey) {
333
- validationKey = historicalKey
334
- keyRotated = keyId !== this.getCurrentKeyId()
335
- } else {
336
- return {
337
- valid: false,
338
- error: 'Signing key not found or expired',
339
- keyRotated: true
340
- }
341
- }
342
- }
343
-
344
- // Recreate payload for verification (must include nonce if present)
345
- const payload: Record<string, unknown> = {
346
- data,
347
- componentId,
348
- timestamp,
349
- version,
350
- keyId,
351
- compressed,
352
- encrypted,
353
- }
354
- if (nonce !== undefined) {
355
- payload.nonce = nonce
356
- }
357
-
358
- // Verify signature with appropriate key
359
- const expectedSignature = this.createSignature(payload, validationKey)
360
-
361
- if (!this.constantTimeEquals(signature, expectedSignature)) {
362
- liveWarn('state', componentId, 'โš ๏ธ State signature mismatch:', {
363
- componentId,
364
- expected: expectedSignature.substring(0, 16) + '...',
365
- received: signature.substring(0, 16) + '...'
366
- })
367
-
368
- return {
369
- valid: false,
370
- error: 'State signature invalid - possible tampering',
371
- tampered: true
372
- }
373
- }
374
-
375
- // ๐Ÿ”’ Anti-replay: consume the nonce so it cannot be reused
376
- if (nonce && consumeNonce) {
377
- this.consumedNonces.add(nonce)
378
- this.nonceTimestamps.set(nonce, Date.now())
379
- }
380
-
381
- liveLog('state', componentId, 'โœ… State signature valid:', {
382
- componentId,
383
- age: `${Math.round(age / 1000)}s`,
384
- version,
385
- nonceConsumed: !!(nonce && consumeNonce)
386
- })
387
-
388
- return { valid: true }
389
-
390
- } catch (error: any) {
391
- return {
392
- valid: false,
393
- error: `Validation error: ${error.message}`
394
- }
395
- }
396
- }
397
-
398
- /**
399
- * Create HMAC signature for payload using specified key
400
- */
401
- private createSignature(payload: any, key?: string): string {
402
- // Stringify deterministically (sorted keys)
403
- const normalizedPayload = JSON.stringify(payload, Object.keys(payload).sort())
404
-
405
- return createHmac('sha256', key || this.currentKey)
406
- .update(normalizedPayload)
407
- .digest('hex')
408
- }
409
-
410
- /**
411
- * Encrypt sensitive data
412
- */
413
- private async encryptData<T>(data: T): Promise<string> {
414
- try {
415
- const serializedData = JSON.stringify(data)
416
- const key = await scryptAsync(this.currentKey, 'salt', 32) as Buffer
417
- const iv = randomBytes(16)
418
- const cipher = createCipheriv('aes-256-cbc', key, iv)
419
-
420
- let encrypted = cipher.update(serializedData, 'utf8', 'hex')
421
- encrypted += cipher.final('hex')
422
-
423
- return iv.toString('hex') + ':' + encrypted
424
- } catch (error) {
425
- throw new Error(`Encryption failed: ${error instanceof Error ? error.message : 'Unknown error'}`)
426
- }
427
- }
428
-
429
- /**
430
- * Decrypt sensitive data
431
- */
432
- private async decryptData(encryptedData: string, key?: string): Promise<any> {
433
- try {
434
- const [ivHex, encrypted] = encryptedData.split(':')
435
- const iv = Buffer.from(ivHex, 'hex')
436
- const derivedKey = await scryptAsync(key || this.currentKey, 'salt', 32) as Buffer
437
- const decipher = createDecipheriv('aes-256-cbc', derivedKey, iv)
438
-
439
- let decrypted = decipher.update(encrypted, 'hex', 'utf8')
440
- decrypted += decipher.final('utf8')
441
-
442
- return JSON.parse(decrypted)
443
- } catch (error) {
444
- throw new Error(`Decryption failed: ${error instanceof Error ? error.message : 'Unknown error'}`)
445
- }
446
- }
447
-
448
- /**
449
- * Decompress state data
450
- */
451
- private async decompressData(compressedData: string): Promise<any> {
452
- try {
453
- const compressedBuffer = Buffer.from(compressedData, 'base64')
454
- const decompressedBuffer = await gunzipAsync(compressedBuffer)
455
- return JSON.parse(decompressedBuffer.toString('utf8'))
456
- } catch (error) {
457
- throw new Error(`Decompression failed: ${error instanceof Error ? error.message : 'Unknown error'}`)
458
- }
459
- }
460
-
461
- /**
462
- * Create state backup
463
- */
464
- private async createStateBackup<T>(componentId: string, state: T, version: number): Promise<void> {
465
- try {
466
- const backup: StateBackup<T> = {
467
- componentId,
468
- state,
469
- timestamp: Date.now(),
470
- version,
471
- checksum: createHmac('sha256', this.currentKey).update(JSON.stringify(state)).digest('hex')
472
- }
473
-
474
- let backups = this.backups.get(componentId) || []
475
- backups.push(backup)
476
-
477
- // Keep only last 10 backups per component
478
- if (backups.length > 10) {
479
- backups = backups.slice(-10)
480
- }
481
-
482
- this.backups.set(componentId, backups)
483
-
484
- liveLog('state', componentId, `๐Ÿ’พ State backup created for component ${componentId} v${version}`)
485
- } catch (error) {
486
- console.error(`โŒ Failed to create backup for component ${componentId}:`, error)
487
- }
488
- }
489
-
490
- /**
491
- * Constant-time string comparison to prevent timing attacks
492
- */
493
- private constantTimeEquals(a: string, b: string): boolean {
494
- if (a.length !== b.length) {
495
- return false
496
- }
497
-
498
- let result = 0
499
- for (let i = 0; i < a.length; i++) {
500
- result |= a.charCodeAt(i) ^ b.charCodeAt(i)
501
- }
502
-
503
- return result === 0
504
- }
505
-
506
- /**
507
- * Extract and process signed state data (decompression, decryption)
508
- */
509
- public async extractData<T>(signedState: SignedState<T>): Promise<T> {
510
- let data = signedState.data
511
-
512
- try {
513
- // Decrypt if encrypted
514
- if (signedState.encrypted) {
515
- const keyToUse = signedState.keyId ? this.getKeyById(signedState.keyId) : this.currentKey
516
- if (!keyToUse) {
517
- throw new Error('Decryption key not available')
518
- }
519
- data = await this.decryptData(data as string, keyToUse)
520
- }
521
-
522
- // Decompress if compressed
523
- if (signedState.compressed) {
524
- data = await this.decompressData(data as string)
525
- }
526
-
527
- return data
528
- } catch (error) {
529
- console.error('โŒ Failed to extract state data:', error)
530
- throw error
531
- }
532
- }
533
-
534
- /**
535
- * Update signature for new state version with enhanced options
536
- */
537
- public async updateSignature<T>(
538
- signedState: SignedState<T>,
539
- newData: T,
540
- options?: {
541
- compress?: boolean
542
- encrypt?: boolean
543
- backup?: boolean
544
- }
545
- ): Promise<SignedState<T>> {
546
- return this.signState(
547
- signedState.componentId,
548
- newData,
549
- signedState.version + 1,
550
- options
551
- )
552
- }
553
-
554
- /**
555
- * Register state migration function
556
- */
557
- public registerMigration(fromVersion: string, toVersion: string, migrationFn: (state: any) => any): void {
558
- const key = `${fromVersion}->${toVersion}`
559
- this.migrationFunctions.set(key, migrationFn)
560
- liveLog('state', null, `๐Ÿ“‹ Registered migration: ${key}`)
561
- }
562
-
563
- /**
564
- * Migrate state to new version
565
- */
566
- public async migrateState<T>(signedState: SignedState<T>, targetVersion: string): Promise<SignedState<T> | null> {
567
- const currentVersion = signedState.version.toString()
568
- const migrationKey = `${currentVersion}->${targetVersion}`
569
-
570
- const migrationFn = this.migrationFunctions.get(migrationKey)
571
- if (!migrationFn) {
572
- liveWarn('state', null, `โš ๏ธ No migration function found for ${migrationKey}`)
573
- return null
574
- }
575
-
576
- try {
577
- // Extract current data
578
- const currentData = await this.extractData(signedState)
579
-
580
- // Apply migration
581
- const migratedData = migrationFn(currentData)
582
-
583
- // Create new signed state
584
- const newSignedState = await this.signState(
585
- signedState.componentId,
586
- migratedData,
587
- parseInt(targetVersion),
588
- {
589
- compress: signedState.compressed,
590
- encrypt: signedState.encrypted,
591
- backup: true
592
- }
593
- )
594
-
595
- liveLog('state', signedState.componentId, `โœ… State migrated from v${currentVersion} to v${targetVersion} for component ${signedState.componentId}`)
596
- return newSignedState
597
-
598
- } catch (error) {
599
- console.error(`โŒ State migration failed for ${migrationKey}:`, error)
600
- return null
601
- }
602
- }
603
-
604
- /**
605
- * Recover state from backup
606
- */
607
- public recoverStateFromBackup<T>(componentId: string, version?: number): StateBackup<T> | null {
608
- const backups = this.backups.get(componentId)
609
- if (!backups || backups.length === 0) {
610
- return null
611
- }
612
-
613
- if (version !== undefined) {
614
- // Find specific version
615
- return backups.find(backup => backup.version === version) || null
616
- } else {
617
- // Return latest backup
618
- return backups[backups.length - 1] || null
619
- }
620
- }
621
-
622
- /**
623
- * Get all backups for a component
624
- */
625
- public getComponentBackups(componentId: string): StateBackup[] {
626
- return this.backups.get(componentId) || []
627
- }
628
-
629
- /**
630
- * Verify backup integrity
631
- */
632
- public verifyBackup<T>(backup: StateBackup<T>): boolean {
633
- try {
634
- const expectedChecksum = createHmac('sha256', this.currentKey)
635
- .update(JSON.stringify(backup.state))
636
- .digest('hex')
637
-
638
- return this.constantTimeEquals(backup.checksum, expectedChecksum)
639
- } catch {
640
- return false
641
- }
642
- }
643
-
644
- /**
645
- * Clean up old backups
646
- */
647
- public cleanupBackups(maxAge: number = 7 * 24 * 60 * 60 * 1000): void {
648
- const now = Date.now()
649
- let totalCleaned = 0
650
-
651
- for (const [componentId, backups] of this.backups) {
652
- const validBackups = backups.filter(backup => {
653
- const age = now - backup.timestamp
654
- return age <= maxAge
655
- })
656
-
657
- const cleaned = backups.length - validBackups.length
658
- totalCleaned += cleaned
659
-
660
- if (validBackups.length === 0) {
661
- this.backups.delete(componentId)
662
- } else {
663
- this.backups.set(componentId, validBackups)
664
- }
665
- }
666
-
667
- if (totalCleaned > 0) {
668
- liveLog('state', null, `๐Ÿงน Cleaned up ${totalCleaned} old state backups`)
669
- }
670
- }
671
-
672
- /**
673
- * Get server's signature info for debugging
674
- */
675
- public getSignatureInfo() {
676
- return {
677
- algorithm: 'HMAC-SHA256',
678
- keyLength: this.currentKey.length,
679
- maxAge: this.maxAge,
680
- keyPreview: this.currentKey.substring(0, 8) + '...',
681
- currentKeyId: this.getCurrentKeyId(),
682
- keyHistoryCount: this.keyHistory.size,
683
- compressionEnabled: this.compressionConfig.enabled,
684
- rotationInterval: this.keyRotationConfig.rotationInterval,
685
- activeNonces: this.consumedNonces.size // ๐Ÿ”’ Anti-replay tracking
686
- }
687
- }
688
- }
689
-
690
- // Global instance with enhanced configuration
691
- export const stateSignature = StateSignature.getInstance(
692
- process.env.FLUXSTACK_STATE_SECRET || undefined,
693
- {
694
- keyRotation: {
695
- rotationInterval: parseInt(process.env.FLUXSTACK_KEY_ROTATION_INTERVAL || '604800000'), // 7 days
696
- maxKeyAge: parseInt(process.env.FLUXSTACK_MAX_KEY_AGE || '2592000000'), // 30 days
697
- keyRetentionCount: parseInt(process.env.FLUXSTACK_KEY_RETENTION_COUNT || '5')
698
- },
699
- compression: {
700
- enabled: process.env.FLUXSTACK_COMPRESSION_ENABLED !== 'false',
701
- threshold: parseInt(process.env.FLUXSTACK_COMPRESSION_THRESHOLD || '1024'), // 1KB
702
- level: parseInt(process.env.FLUXSTACK_COMPRESSION_LEVEL || '6')
703
- }
704
- }
705
- )