create-fluxstack 1.13.0 → 1.14.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 (61) hide show
  1. package/LLMD/patterns/anti-patterns.md +100 -0
  2. package/LLMD/reference/routing.md +39 -39
  3. package/LLMD/resources/live-auth.md +20 -2
  4. package/LLMD/resources/live-components.md +94 -10
  5. package/LLMD/resources/live-logging.md +95 -33
  6. package/LLMD/resources/live-upload.md +59 -8
  7. package/app/client/index.html +2 -2
  8. package/app/client/public/favicon.svg +46 -0
  9. package/app/client/src/App.tsx +2 -1
  10. package/app/client/src/assets/fluxstack-static.svg +46 -0
  11. package/app/client/src/assets/fluxstack.svg +183 -0
  12. package/app/client/src/components/AppLayout.tsx +138 -9
  13. package/app/client/src/components/BackButton.tsx +13 -13
  14. package/app/client/src/components/DemoPage.tsx +4 -4
  15. package/app/client/src/live/AuthDemo.tsx +23 -21
  16. package/app/client/src/live/ChatDemo.tsx +2 -2
  17. package/app/client/src/live/CounterDemo.tsx +12 -12
  18. package/app/client/src/live/FormDemo.tsx +2 -2
  19. package/app/client/src/live/LiveDebuggerPanel.tsx +779 -0
  20. package/app/client/src/live/RoomChatDemo.tsx +24 -16
  21. package/app/client/src/main.tsx +13 -13
  22. package/app/client/src/pages/ApiTestPage.tsx +6 -6
  23. package/app/client/src/pages/HomePage.tsx +80 -52
  24. package/app/server/live/LiveAdminPanel.ts +1 -0
  25. package/app/server/live/LiveChat.ts +78 -77
  26. package/app/server/live/LiveCounter.ts +1 -1
  27. package/app/server/live/LiveForm.ts +1 -0
  28. package/app/server/live/LiveLocalCounter.ts +38 -37
  29. package/app/server/live/LiveProtectedChat.ts +1 -0
  30. package/app/server/live/LiveRoomChat.ts +1 -0
  31. package/app/server/live/LiveUpload.ts +1 -0
  32. package/app/server/live/register-components.ts +19 -19
  33. package/config/system/runtime.config.ts +4 -0
  34. package/core/build/optimizer.ts +235 -235
  35. package/core/client/components/Live.tsx +17 -11
  36. package/core/client/components/LiveDebugger.tsx +1324 -0
  37. package/core/client/hooks/AdaptiveChunkSizer.ts +215 -215
  38. package/core/client/hooks/useLiveComponent.ts +11 -1
  39. package/core/client/hooks/useLiveDebugger.ts +392 -0
  40. package/core/client/index.ts +14 -0
  41. package/core/plugins/built-in/index.ts +134 -134
  42. package/core/plugins/built-in/live-components/commands/create-live-component.ts +4 -0
  43. package/core/plugins/built-in/vite/index.ts +75 -21
  44. package/core/server/index.ts +15 -15
  45. package/core/server/live/ComponentRegistry.ts +55 -26
  46. package/core/server/live/FileUploadManager.ts +188 -24
  47. package/core/server/live/LiveDebugger.ts +462 -0
  48. package/core/server/live/LiveLogger.ts +38 -5
  49. package/core/server/live/LiveRoomManager.ts +17 -1
  50. package/core/server/live/StateSignature.ts +87 -27
  51. package/core/server/live/WebSocketConnectionManager.ts +11 -10
  52. package/core/server/live/auto-generated-components.ts +1 -1
  53. package/core/server/live/websocket-plugin.ts +233 -8
  54. package/core/server/plugins/static-files-plugin.ts +179 -69
  55. package/core/types/build.ts +219 -219
  56. package/core/types/plugin.ts +107 -107
  57. package/core/types/types.ts +145 -9
  58. package/core/utils/logger/startup-banner.ts +82 -82
  59. package/core/utils/version.ts +6 -6
  60. package/package.json +1 -1
  61. package/app/client/src/assets/react.svg +0 -1
@@ -18,6 +18,7 @@ export interface SignedState<T = any> {
18
18
  keyId?: string // For key rotation
19
19
  compressed?: boolean // For state compression
20
20
  encrypted?: boolean // For sensitive data
21
+ nonce?: string // 🔒 Anti-replay: unique per signed state
21
22
  }
22
23
 
23
24
  export interface StateValidationResult {
@@ -26,6 +27,7 @@ export interface StateValidationResult {
26
27
  tampered?: boolean
27
28
  expired?: boolean
28
29
  keyRotated?: boolean
30
+ replayed?: boolean // 🔒 Anti-replay: nonce was already consumed
29
31
  }
30
32
 
31
33
  export interface StateBackup<T = any> {
@@ -57,6 +59,10 @@ export class StateSignature {
57
59
  private compressionConfig: CompressionConfig
58
60
  private backups = new Map<string, StateBackup[]>() // componentId -> backups
59
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
60
66
 
61
67
  constructor(secretKey?: string, options?: {
62
68
  keyRotation?: Partial<KeyRotationConfig>
@@ -108,10 +114,11 @@ export class StateSignature {
108
114
  setInterval(() => {
109
115
  this.rotateKey()
110
116
  }, this.keyRotationConfig.rotationInterval)
111
-
112
- // Cleanup old keys
117
+
118
+ // Cleanup old keys and expired nonces
113
119
  setInterval(() => {
114
120
  this.cleanupOldKeys()
121
+ this.cleanupExpiredNonces()
115
122
  }, 24 * 60 * 60 * 1000) // Daily cleanup
116
123
  }
117
124
 
@@ -159,6 +166,26 @@ export class StateSignature {
159
166
  }
160
167
  }
161
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
+
162
189
  private getKeyById(keyId: string): string | null {
163
190
  const keyData = this.keyHistory.get(keyId)
164
191
  return keyData ? keyData.key : null
@@ -179,37 +206,38 @@ export class StateSignature {
179
206
  ): Promise<SignedState<T>> {
180
207
  const timestamp = Date.now()
181
208
  const keyId = this.getCurrentKeyId()
182
-
209
+ const nonce = randomBytes(16).toString('hex') // 🔒 Anti-replay nonce
210
+
183
211
  let processedData = data
184
212
  let compressed = false
185
213
  let encrypted = false
186
-
214
+
187
215
  try {
188
216
  // Serialize data for processing
189
217
  const serializedData = JSON.stringify(data)
190
-
218
+
191
219
  // Compress if enabled and data is large enough
192
- if (this.compressionConfig.enabled &&
220
+ if (this.compressionConfig.enabled &&
193
221
  (options?.compress !== false) &&
194
222
  Buffer.byteLength(serializedData, 'utf8') > this.compressionConfig.threshold) {
195
-
223
+
196
224
  const compressedBuffer = await gzipAsync(Buffer.from(serializedData, 'utf8'))
197
225
  processedData = compressedBuffer.toString('base64') as any
198
226
  compressed = true
199
-
227
+
200
228
  liveLog('state', componentId, `🗜️ State compressed: ${Buffer.byteLength(serializedData, 'utf8')} -> ${compressedBuffer.length} bytes`)
201
229
  }
202
-
230
+
203
231
  // Encrypt sensitive data if requested
204
232
  if (options?.encrypt) {
205
233
  const encryptedData = await this.encryptData(processedData)
206
234
  processedData = encryptedData as any
207
235
  encrypted = true
208
-
236
+
209
237
  liveLog('state', componentId, `🔒 State encrypted for component: ${componentId}`)
210
238
  }
211
-
212
- // Create payload for signing
239
+
240
+ // Create payload for signing (includes nonce for anti-replay)
213
241
  const payload = {
214
242
  data: processedData,
215
243
  componentId,
@@ -217,9 +245,10 @@ export class StateSignature {
217
245
  version,
218
246
  keyId,
219
247
  compressed,
220
- encrypted
248
+ encrypted,
249
+ nonce
221
250
  }
222
-
251
+
223
252
  // Generate signature with current key
224
253
  const signature = this.createSignature(payload)
225
254
 
@@ -235,6 +264,7 @@ export class StateSignature {
235
264
  keyId,
236
265
  compressed,
237
266
  encrypted,
267
+ nonce: nonce.substring(0, 8) + '...',
238
268
  signature: signature.substring(0, 16) + '...'
239
269
  })
240
270
 
@@ -246,7 +276,8 @@ export class StateSignature {
246
276
  version,
247
277
  keyId,
248
278
  compressed,
249
- encrypted
279
+ encrypted,
280
+ nonce
250
281
  }
251
282
 
252
283
  } catch (error) {
@@ -258,14 +289,19 @@ export class StateSignature {
258
289
  /**
259
290
  * Validate signed state integrity with enhanced security checks
260
291
  */
261
- public async validateState<T>(signedState: SignedState<T>, maxAge?: number): Promise<StateValidationResult> {
262
- const { data, signature, timestamp, componentId, version, keyId, compressed, encrypted } = signedState
263
-
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
+
264
300
  try {
265
301
  // Check timestamp (prevent replay attacks)
266
302
  const age = Date.now() - timestamp
267
303
  const ageLimit = maxAge || this.maxAge
268
-
304
+
269
305
  if (age > ageLimit) {
270
306
  return {
271
307
  valid: false,
@@ -274,10 +310,23 @@ export class StateSignature {
274
310
  }
275
311
  }
276
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
+
277
326
  // Determine which key to use for validation
278
327
  let validationKey = this.currentKey
279
328
  let keyRotated = false
280
-
329
+
281
330
  if (keyId) {
282
331
  const historicalKey = this.getKeyById(keyId)
283
332
  if (historicalKey) {
@@ -292,27 +341,30 @@ export class StateSignature {
292
341
  }
293
342
  }
294
343
 
295
- // Recreate payload for verification
296
- const payload = {
344
+ // Recreate payload for verification (must include nonce if present)
345
+ const payload: Record<string, unknown> = {
297
346
  data,
298
347
  componentId,
299
348
  timestamp,
300
349
  version,
301
350
  keyId,
302
351
  compressed,
303
- encrypted
352
+ encrypted,
353
+ }
354
+ if (nonce !== undefined) {
355
+ payload.nonce = nonce
304
356
  }
305
357
 
306
358
  // Verify signature with appropriate key
307
359
  const expectedSignature = this.createSignature(payload, validationKey)
308
-
360
+
309
361
  if (!this.constantTimeEquals(signature, expectedSignature)) {
310
362
  liveWarn('state', componentId, '⚠️ State signature mismatch:', {
311
363
  componentId,
312
364
  expected: expectedSignature.substring(0, 16) + '...',
313
365
  received: signature.substring(0, 16) + '...'
314
366
  })
315
-
367
+
316
368
  return {
317
369
  valid: false,
318
370
  error: 'State signature invalid - possible tampering',
@@ -320,10 +372,17 @@ export class StateSignature {
320
372
  }
321
373
  }
322
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
+
323
381
  liveLog('state', componentId, '✅ State signature valid:', {
324
382
  componentId,
325
383
  age: `${Math.round(age / 1000)}s`,
326
- version
384
+ version,
385
+ nonceConsumed: !!(nonce && consumeNonce)
327
386
  })
328
387
 
329
388
  return { valid: true }
@@ -622,7 +681,8 @@ export class StateSignature {
622
681
  currentKeyId: this.getCurrentKeyId(),
623
682
  keyHistoryCount: this.keyHistory.size,
624
683
  compressionEnabled: this.compressionConfig.enabled,
625
- rotationInterval: this.keyRotationConfig.rotationInterval
684
+ rotationInterval: this.keyRotationConfig.rotationInterval,
685
+ activeNonces: this.consumedNonces.size // 🔒 Anti-replay tracking
626
686
  }
627
687
  }
628
688
  }
@@ -3,6 +3,7 @@
3
3
 
4
4
  import { EventEmitter } from 'events'
5
5
  import type { FluxStackWebSocket } from '@core/types/types'
6
+ import { liveLog, liveWarn } from './LiveLogger'
6
7
 
7
8
  export interface ConnectionConfig {
8
9
  maxConnections: number
@@ -123,7 +124,7 @@ export class WebSocketConnectionManager extends EventEmitter {
123
124
  // Setup connection event handlers
124
125
  this.setupConnectionHandlers(ws, connectionId)
125
126
 
126
- console.log(`🔌 Connection registered: ${connectionId} (Pool: ${poolId || 'default'})`)
127
+ liveLog('websocket', null, `🔌 Connection registered: ${connectionId} (Pool: ${poolId || 'default'})`)
127
128
  this.emit('connectionRegistered', { connectionId, poolId })
128
129
  }
129
130
 
@@ -193,7 +194,7 @@ export class WebSocketConnectionManager extends EventEmitter {
193
194
  }
194
195
 
195
196
  this.connectionPools.get(poolId)!.add(connectionId)
196
- console.log(`🏊 Connection ${connectionId} added to pool ${poolId}`)
197
+ liveLog('websocket', null, `🏊 Connection ${connectionId} added to pool ${poolId}`)
197
198
  }
198
199
 
199
200
  /**
@@ -331,7 +332,7 @@ export class WebSocketConnectionManager extends EventEmitter {
331
332
  queue.splice(insertIndex, 0, queuedMessage)
332
333
  }
333
334
 
334
- console.log(`📬 Message queued for ${connectionId}: ${queuedMessage.id}`)
335
+ liveLog('messages', null, `📬 Message queued for ${connectionId}: ${queuedMessage.id}`)
335
336
  return true
336
337
  }
337
338
 
@@ -361,10 +362,10 @@ export class WebSocketConnectionManager extends EventEmitter {
361
362
  // Re-queue for retry
362
363
  queue.push(queuedMessage)
363
364
  } else {
364
- console.warn(`❌ Message ${queuedMessage.id} exceeded max retries`)
365
+ liveWarn('messages', null, `❌ Message ${queuedMessage.id} exceeded max retries`)
365
366
  }
366
367
  } else {
367
- console.log(`✅ Queued message delivered: ${queuedMessage.id}`)
368
+ liveLog('messages', null, `✅ Queued message delivered: ${queuedMessage.id}`)
368
369
  }
369
370
  } catch (error) {
370
371
  console.error(`❌ Error processing queued message ${queuedMessage.id}:`, error)
@@ -445,7 +446,7 @@ export class WebSocketConnectionManager extends EventEmitter {
445
446
  * Handle connection close
446
447
  */
447
448
  private handleConnectionClose(connectionId: string): void {
448
- console.log(`🔌 Connection closed: ${connectionId}`)
449
+ liveLog('websocket', null, `🔌 Connection closed: ${connectionId}`)
449
450
 
450
451
  // Update metrics
451
452
  const metrics = this.connectionMetrics.get(connectionId)
@@ -573,7 +574,7 @@ export class WebSocketConnectionManager extends EventEmitter {
573
574
  * Handle unhealthy connection
574
575
  */
575
576
  private async handleUnhealthyConnection(connectionId: string): Promise<void> {
576
- console.warn(`⚠️ Handling unhealthy connection: ${connectionId}`)
577
+ liveWarn('websocket', null, `⚠️ Handling unhealthy connection: ${connectionId}`)
577
578
 
578
579
  const ws = this.connections.get(connectionId)
579
580
  if (ws) {
@@ -606,7 +607,7 @@ export class WebSocketConnectionManager extends EventEmitter {
606
607
  }
607
608
  }
608
609
 
609
- console.log(`🧹 Connection cleaned up: ${connectionId}`)
610
+ liveLog('websocket', null, `🧹 Connection cleaned up: ${connectionId}`)
610
611
  }
611
612
 
612
613
  /**
@@ -679,7 +680,7 @@ export class WebSocketConnectionManager extends EventEmitter {
679
680
  * Shutdown connection manager
680
681
  */
681
682
  shutdown(): void {
682
- console.log('🔌 Shutting down WebSocket Connection Manager...')
683
+ liveLog('websocket', null, '🔌 Shutting down WebSocket Connection Manager...')
683
684
 
684
685
  // Clear intervals
685
686
  if (this.healthCheckInterval) {
@@ -701,7 +702,7 @@ export class WebSocketConnectionManager extends EventEmitter {
701
702
  this.connectionPools.clear()
702
703
  this.messageQueues.clear()
703
704
 
704
- console.log('✅ WebSocket Connection Manager shutdown complete')
705
+ liveLog('websocket', null, '✅ WebSocket Connection Manager shutdown complete')
705
706
  }
706
707
  }
707
708
 
@@ -1,6 +1,6 @@
1
1
  // 🔥 Auto-generated Live Components Registration
2
2
  // This file is automatically generated during build time - DO NOT EDIT MANUALLY
3
- // Generated at: 2026-02-17T00:55:07.182Z
3
+ // Generated at: 2026-02-21T20:03:53.569Z
4
4
 
5
5
  import { LiveAdminPanel } from "@app/server/live/LiveAdminPanel"
6
6
  import { LiveChat } from "@app/server/live/LiveChat"
@@ -12,6 +12,7 @@ import type { Plugin, PluginContext } from '@core/index'
12
12
  import { t, Elysia } from 'elysia'
13
13
  import path from 'path'
14
14
  import { liveLog } from './LiveLogger'
15
+ import { liveDebugger } from './LiveDebugger'
15
16
 
16
17
  // ===== Response Schemas for Live Components Routes =====
17
18
 
@@ -116,6 +117,39 @@ const LiveAlertResolveSchema = t.Object({
116
117
  description: 'Result of alert resolution operation'
117
118
  })
118
119
 
120
+ // 🔒 Per-connection rate limiter to prevent WebSocket message flooding
121
+ class ConnectionRateLimiter {
122
+ private tokens: number
123
+ private lastRefill: number
124
+ private readonly maxTokens: number
125
+ private readonly refillRate: number // tokens per second
126
+
127
+ constructor(maxTokens = 100, refillRate = 50) {
128
+ this.maxTokens = maxTokens
129
+ this.tokens = maxTokens
130
+ this.refillRate = refillRate
131
+ this.lastRefill = Date.now()
132
+ }
133
+
134
+ tryConsume(count = 1): boolean {
135
+ this.refill()
136
+ if (this.tokens >= count) {
137
+ this.tokens -= count
138
+ return true
139
+ }
140
+ return false
141
+ }
142
+
143
+ private refill(): void {
144
+ const now = Date.now()
145
+ const elapsed = (now - this.lastRefill) / 1000
146
+ this.tokens = Math.min(this.maxTokens, this.tokens + elapsed * this.refillRate)
147
+ this.lastRefill = now
148
+ }
149
+ }
150
+
151
+ const connectionRateLimiters = new Map<string, ConnectionRateLimiter>()
152
+
119
153
  export const liveComponentsPlugin: Plugin = {
120
154
  name: 'live-components',
121
155
  version: '1.0.0',
@@ -143,9 +177,12 @@ export const liveComponentsPlugin: Plugin = {
143
177
 
144
178
  async open(ws) {
145
179
  const socket = ws as unknown as FluxStackWebSocket
146
- const connectionId = `ws-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
180
+ const connectionId = `ws-${crypto.randomUUID()}`
147
181
  liveLog('websocket', null, `🔌 Live Components WebSocket connected: ${connectionId}`)
148
182
 
183
+ // 🔒 Initialize rate limiter for this connection
184
+ connectionRateLimiters.set(connectionId, new ConnectionRateLimiter())
185
+
149
186
  // Register connection with enhanced connection manager
150
187
  connectionManager.registerConnection(ws as unknown as FluxStackWebSocket, connectionId, 'live-components')
151
188
 
@@ -177,12 +214,19 @@ export const liveComponentsPlugin: Plugin = {
177
214
  if (authContext.authenticated) {
178
215
  socket.data.userId = authContext.user?.id
179
216
  liveLog('websocket', null, `🔒 WebSocket authenticated via query: user=${authContext.user?.id}`)
217
+ } else {
218
+ // 🔒 Log failed auth attempts (token was provided but auth failed)
219
+ liveLog('websocket', null, `🔒 WebSocket authentication failed via query token`)
180
220
  }
181
221
  }
182
- } catch {
183
- // Query param auth is optional - continue without auth
222
+ } catch (authError) {
223
+ // 🔒 Log auth errors instead of silently ignoring them
224
+ console.warn('🔒 WebSocket query auth error:', authError instanceof Error ? authError.message : 'Unknown error')
184
225
  }
185
226
 
227
+ // Debug: track connection
228
+ liveDebugger.trackConnection(connectionId)
229
+
186
230
  // Send connection confirmation
187
231
  ws.send(JSON.stringify({
188
232
  type: 'CONNECTION_ESTABLISHED',
@@ -203,6 +247,20 @@ export const liveComponentsPlugin: Plugin = {
203
247
  async message(ws: unknown, rawMessage: LiveMessage | ArrayBuffer | Uint8Array) {
204
248
  const socket = ws as FluxStackWebSocket
205
249
  try {
250
+ // 🔒 Rate limiting: reject messages if connection exceeds rate limit
251
+ const connId = socket.data?.connectionId
252
+ if (connId) {
253
+ const limiter = connectionRateLimiters.get(connId)
254
+ if (limiter && !limiter.tryConsume()) {
255
+ socket.send(JSON.stringify({
256
+ type: 'ERROR',
257
+ error: 'Rate limit exceeded. Please slow down.',
258
+ timestamp: Date.now()
259
+ }))
260
+ return
261
+ }
262
+ }
263
+
206
264
  let message: LiveMessage
207
265
  let binaryChunkData: Buffer | null = null
208
266
 
@@ -313,6 +371,17 @@ export const liveComponentsPlugin: Plugin = {
313
371
  const connectionId = socket.data?.connectionId
314
372
  liveLog('websocket', null, `🔌 Live Components WebSocket disconnected: ${connectionId}`)
315
373
 
374
+ // Debug: track disconnection
375
+ const componentCount = socket.data?.components?.size ?? 0
376
+ if (connectionId) {
377
+ liveDebugger.trackDisconnection(connectionId, componentCount)
378
+ }
379
+
380
+ // 🔒 Cleanup rate limiter
381
+ if (connectionId) {
382
+ connectionRateLimiters.delete(connectionId)
383
+ }
384
+
316
385
  // Cleanup connection in connection manager
317
386
  if (connectionId) {
318
387
  connectionManager.cleanupConnection(connectionId)
@@ -511,6 +580,132 @@ export const liveComponentsPlugin: Plugin = {
511
580
  response: LiveAlertResolveSchema
512
581
  })
513
582
 
583
+ // ===== Live Component Debugger Routes =====
584
+
585
+ // Debug WebSocket - streams debug events in real-time
586
+ .ws('/debug/ws', {
587
+ body: t.Any(),
588
+
589
+ open(ws) {
590
+ const socket = ws as unknown as FluxStackWebSocket
591
+ liveLog('websocket', null, '🔍 Debug client connected')
592
+ liveDebugger.registerDebugClient(socket)
593
+ },
594
+
595
+ message() {
596
+ // Debug clients are read-only, no incoming messages to handle
597
+ },
598
+
599
+ close(ws) {
600
+ const socket = ws as unknown as FluxStackWebSocket
601
+ liveLog('websocket', null, '🔍 Debug client disconnected')
602
+ liveDebugger.unregisterDebugClient(socket)
603
+ }
604
+ })
605
+
606
+ // Debug snapshot - current state of all components
607
+ .get('/debug/snapshot', () => {
608
+ return {
609
+ success: true,
610
+ snapshot: liveDebugger.getSnapshot(),
611
+ timestamp: new Date().toISOString()
612
+ }
613
+ }, {
614
+ detail: {
615
+ summary: 'Debug Snapshot',
616
+ description: 'Returns current state of all active Live Components for debugging',
617
+ tags: ['Live Components', 'Debug']
618
+ }
619
+ })
620
+
621
+ // Debug events - recent event history
622
+ .get('/debug/events', ({ query }) => {
623
+ const filter: { componentId?: string; type?: any; limit?: number } = {}
624
+ if (query.componentId) filter.componentId = query.componentId as string
625
+ if (query.type) filter.type = query.type
626
+ if (query.limit) filter.limit = parseInt(query.limit as string, 10)
627
+
628
+ return {
629
+ success: true,
630
+ events: liveDebugger.getEvents(filter),
631
+ timestamp: new Date().toISOString()
632
+ }
633
+ }, {
634
+ detail: {
635
+ summary: 'Debug Events',
636
+ description: 'Returns recent debug events, optionally filtered by component or type',
637
+ tags: ['Live Components', 'Debug']
638
+ },
639
+ query: t.Object({
640
+ componentId: t.Optional(t.String()),
641
+ type: t.Optional(t.String()),
642
+ limit: t.Optional(t.String())
643
+ })
644
+ })
645
+
646
+ // Debug toggle - enable/disable debugger at runtime
647
+ .post('/debug/toggle', ({ body }) => {
648
+ const enabled = (body as any)?.enabled
649
+ if (typeof enabled === 'boolean') {
650
+ liveDebugger.enabled = enabled
651
+ } else {
652
+ liveDebugger.enabled = !liveDebugger.enabled
653
+ }
654
+ return {
655
+ success: true,
656
+ enabled: liveDebugger.enabled,
657
+ timestamp: new Date().toISOString()
658
+ }
659
+ }, {
660
+ detail: {
661
+ summary: 'Toggle Debugger',
662
+ description: 'Enable or disable the Live Component debugger at runtime',
663
+ tags: ['Live Components', 'Debug']
664
+ }
665
+ })
666
+
667
+ // Debug component state - get specific component state
668
+ .get('/debug/components/:componentId', ({ params }) => {
669
+ const snapshot = liveDebugger.getComponentState(params.componentId)
670
+ if (!snapshot) {
671
+ return { success: false, error: 'Component not found' }
672
+ }
673
+ return {
674
+ success: true,
675
+ component: snapshot,
676
+ events: liveDebugger.getEvents({
677
+ componentId: params.componentId,
678
+ limit: 50
679
+ }),
680
+ timestamp: new Date().toISOString()
681
+ }
682
+ }, {
683
+ detail: {
684
+ summary: 'Debug Component State',
685
+ description: 'Returns current state and recent events for a specific component',
686
+ tags: ['Live Components', 'Debug']
687
+ },
688
+ params: t.Object({
689
+ componentId: t.String()
690
+ })
691
+ })
692
+
693
+ // Clear debug events
694
+ .post('/debug/clear', () => {
695
+ liveDebugger.clearEvents()
696
+ return {
697
+ success: true,
698
+ message: 'Debug events cleared',
699
+ timestamp: new Date().toISOString()
700
+ }
701
+ }, {
702
+ detail: {
703
+ summary: 'Clear Debug Events',
704
+ description: 'Clears the debug event history buffer',
705
+ tags: ['Live Components', 'Debug']
706
+ }
707
+ })
708
+
514
709
  // Register the grouped routes with the main app
515
710
  context.app.use(liveRoutes)
516
711
  },
@@ -717,9 +912,11 @@ async function handleAuth(ws: FluxStackWebSocket, message: LiveMessage) {
717
912
  // File Upload Handler Functions
718
913
  async function handleFileUploadStart(ws: FluxStackWebSocket, message: FileUploadStartMessage) {
719
914
  liveLog('messages', message.componentId || null, '📤 Starting file upload:', message.uploadId)
720
-
721
- const result = await fileUploadManager.startUpload(message)
722
-
915
+
916
+ // 🔒 Pass userId for per-user upload quota enforcement
917
+ const userId = ws.data?.userId || ws.data?.authContext?.user?.id
918
+ const result = await fileUploadManager.startUpload(message, userId)
919
+
723
920
  const response = {
724
921
  type: 'FILE_UPLOAD_START_RESPONSE',
725
922
  componentId: message.componentId,
@@ -729,7 +926,7 @@ async function handleFileUploadStart(ws: FluxStackWebSocket, message: FileUpload
729
926
  requestId: message.requestId,
730
927
  timestamp: Date.now()
731
928
  }
732
-
929
+
733
930
  ws.send(JSON.stringify(response))
734
931
  }
735
932
 
@@ -781,11 +978,23 @@ async function handleRoomJoin(ws: FluxStackWebSocket, message: RoomMessage) {
781
978
  liveLog('rooms', message.componentId, `🚪 Component ${message.componentId} joining room ${message.roomId}`)
782
979
 
783
980
  try {
981
+ // 🔒 Validate room name format (alphanumeric, hyphens, underscores, max 64 chars)
982
+ if (!message.roomId || !/^[a-zA-Z0-9_:.-]{1,64}$/.test(message.roomId)) {
983
+ throw new Error('Invalid room name. Must be 1-64 alphanumeric characters, hyphens, underscores, dots, or colons.')
984
+ }
985
+
986
+ // 🔒 Room authorization check
987
+ const authContext = ws.data?.authContext || ANONYMOUS_CONTEXT
988
+ const authResult = await liveAuthManager.authorizeRoom(authContext, message.roomId)
989
+ if (!authResult.allowed) {
990
+ throw new Error(`Room access denied: ${authResult.reason}`)
991
+ }
992
+
784
993
  const result = liveRoomManager.joinRoom(
785
994
  message.componentId,
786
995
  message.roomId,
787
996
  ws,
788
- message.data?.initialState
997
+ undefined // 🔒 Don't allow client to set initial room state - server controls this
789
998
  )
790
999
 
791
1000
  const response = {
@@ -843,6 +1052,11 @@ async function handleRoomEmit(ws: FluxStackWebSocket, message: RoomMessage) {
843
1052
  liveLog('rooms', message.componentId, `📡 Component ${message.componentId} emitting '${message.event}' to room ${message.roomId}`)
844
1053
 
845
1054
  try {
1055
+ // 🔒 Validate room name
1056
+ if (!message.roomId || !/^[a-zA-Z0-9_:.-]{1,64}$/.test(message.roomId)) {
1057
+ throw new Error('Invalid room name')
1058
+ }
1059
+
846
1060
  const count = liveRoomManager.emitToRoom(
847
1061
  message.roomId,
848
1062
  message.event!,
@@ -866,6 +1080,17 @@ async function handleRoomStateSet(ws: FluxStackWebSocket, message: RoomMessage)
866
1080
  liveLog('rooms', message.componentId, `📝 Component ${message.componentId} updating state in room ${message.roomId}`)
867
1081
 
868
1082
  try {
1083
+ // 🔒 Validate room name
1084
+ if (!message.roomId || !/^[a-zA-Z0-9_:.-]{1,64}$/.test(message.roomId)) {
1085
+ throw new Error('Invalid room name')
1086
+ }
1087
+
1088
+ // 🔒 Validate state size (max 1MB per update to prevent memory attacks)
1089
+ const stateStr = JSON.stringify(message.data ?? {})
1090
+ if (stateStr.length > 1024 * 1024) {
1091
+ throw new Error('Room state update too large (max 1MB)')
1092
+ }
1093
+
869
1094
  liveRoomManager.setRoomState(
870
1095
  message.roomId,
871
1096
  message.data ?? {},