create-fluxstack 1.12.1 → 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.
- package/LLMD/INDEX.md +8 -1
- package/LLMD/agent.md +867 -0
- package/LLMD/config/environment-vars.md +30 -0
- package/LLMD/patterns/anti-patterns.md +100 -0
- package/LLMD/reference/routing.md +39 -39
- package/LLMD/resources/live-auth.md +465 -0
- package/LLMD/resources/live-components.md +168 -26
- package/LLMD/resources/live-logging.md +220 -0
- package/LLMD/resources/live-upload.md +59 -8
- package/LLMD/resources/rest-auth.md +290 -0
- package/README.md +520 -340
- package/app/client/index.html +2 -2
- package/app/client/public/favicon.svg +46 -0
- package/app/client/src/App.tsx +13 -1
- package/app/client/src/assets/fluxstack-static.svg +46 -0
- package/app/client/src/assets/fluxstack.svg +183 -0
- package/app/client/src/components/AppLayout.tsx +139 -9
- package/app/client/src/components/BackButton.tsx +13 -13
- package/app/client/src/components/DemoPage.tsx +4 -4
- package/app/client/src/live/AuthDemo.tsx +334 -0
- package/app/client/src/live/ChatDemo.tsx +2 -2
- package/app/client/src/live/CounterDemo.tsx +12 -12
- package/app/client/src/live/FormDemo.tsx +2 -2
- package/app/client/src/live/LiveDebuggerPanel.tsx +779 -0
- package/app/client/src/live/RoomChatDemo.tsx +24 -16
- package/app/client/src/main.tsx +13 -13
- package/app/client/src/pages/ApiTestPage.tsx +6 -6
- package/app/client/src/pages/HomePage.tsx +80 -52
- package/app/server/auth/AuthManager.ts +213 -0
- package/app/server/auth/DevAuthProvider.ts +66 -0
- package/app/server/auth/HashManager.ts +123 -0
- package/app/server/auth/JWTAuthProvider.example.ts +101 -0
- package/app/server/auth/RateLimiter.ts +106 -0
- package/app/server/auth/contracts.ts +192 -0
- package/app/server/auth/guards/SessionGuard.ts +167 -0
- package/app/server/auth/guards/TokenGuard.ts +202 -0
- package/app/server/auth/index.ts +174 -0
- package/app/server/auth/middleware.ts +163 -0
- package/app/server/auth/providers/InMemoryProvider.ts +162 -0
- package/app/server/auth/sessions/SessionManager.ts +164 -0
- package/app/server/cache/CacheManager.ts +81 -0
- package/app/server/cache/MemoryDriver.ts +112 -0
- package/app/server/cache/contracts.ts +49 -0
- package/app/server/cache/index.ts +42 -0
- package/app/server/index.ts +14 -0
- package/app/server/live/LiveAdminPanel.ts +174 -0
- package/app/server/live/LiveChat.ts +78 -77
- package/app/server/live/LiveCounter.ts +1 -0
- package/app/server/live/LiveForm.ts +1 -0
- package/app/server/live/LiveLocalCounter.ts +38 -32
- package/app/server/live/LiveProtectedChat.ts +151 -0
- package/app/server/live/LiveRoomChat.ts +1 -0
- package/app/server/live/LiveUpload.ts +1 -0
- package/app/server/live/register-components.ts +19 -19
- package/app/server/routes/auth.routes.ts +278 -0
- package/app/server/routes/index.ts +2 -0
- package/config/index.ts +8 -0
- package/config/system/auth.config.ts +49 -0
- package/config/system/runtime.config.ts +4 -0
- package/config/system/session.config.ts +33 -0
- package/core/build/optimizer.ts +235 -235
- package/core/client/LiveComponentsProvider.tsx +76 -5
- package/core/client/components/Live.tsx +17 -10
- package/core/client/components/LiveDebugger.tsx +1324 -0
- package/core/client/hooks/AdaptiveChunkSizer.ts +215 -215
- package/core/client/hooks/useLiveComponent.ts +58 -5
- package/core/client/hooks/useLiveDebugger.ts +392 -0
- package/core/client/index.ts +16 -1
- package/core/framework/server.ts +36 -4
- package/core/plugins/built-in/index.ts +134 -134
- package/core/plugins/built-in/live-components/commands/create-live-component.ts +19 -8
- package/core/plugins/built-in/monitoring/index.ts +10 -3
- package/core/plugins/built-in/vite/index.ts +151 -20
- package/core/plugins/config.ts +5 -4
- package/core/plugins/discovery.ts +11 -2
- package/core/plugins/manager.ts +11 -5
- package/core/plugins/module-resolver.ts +1 -1
- package/core/plugins/registry.ts +53 -25
- package/core/server/index.ts +15 -15
- package/core/server/live/ComponentRegistry.ts +134 -50
- package/core/server/live/FileUploadManager.ts +188 -24
- package/core/server/live/LiveComponentPerformanceMonitor.ts +9 -8
- package/core/server/live/LiveDebugger.ts +462 -0
- package/core/server/live/LiveLogger.ts +144 -0
- package/core/server/live/LiveRoomManager.ts +22 -5
- package/core/server/live/StateSignature.ts +704 -643
- package/core/server/live/WebSocketConnectionManager.ts +11 -10
- package/core/server/live/auth/LiveAuthContext.ts +71 -0
- package/core/server/live/auth/LiveAuthManager.ts +304 -0
- package/core/server/live/auth/index.ts +19 -0
- package/core/server/live/auth/types.ts +179 -0
- package/core/server/live/auto-generated-components.ts +8 -2
- package/core/server/live/index.ts +16 -0
- package/core/server/live/websocket-plugin.ts +323 -22
- package/core/server/plugins/static-files-plugin.ts +179 -69
- package/core/templates/create-project.ts +0 -3
- package/core/types/build.ts +219 -219
- package/core/types/plugin.ts +107 -107
- package/core/types/types.ts +278 -22
- package/core/utils/index.ts +17 -17
- package/core/utils/logger/index.ts +5 -2
- package/core/utils/logger/startup-banner.ts +82 -82
- package/core/utils/version.ts +6 -6
- package/package.json +1 -8
- package/plugins/crypto-auth/index.ts +6 -0
- package/plugins/crypto-auth/server/CryptoAuthLiveProvider.ts +58 -0
- package/plugins/crypto-auth/server/index.ts +24 -21
- package/rest-tests/README.md +57 -0
- package/rest-tests/auth-token.http +113 -0
- package/rest-tests/auth.http +112 -0
- package/rest-tests/rooms-token.http +69 -0
- package/rest-tests/users-token.http +62 -0
- package/.dockerignore +0 -81
- package/Dockerfile +0 -70
- package/LIVE_COMPONENTS_REVIEW.md +0 -781
- package/app/client/src/assets/react.svg +0 -1
|
@@ -1,11 +1,13 @@
|
|
|
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-
|
|
3
|
+
// Generated at: 2026-02-21T20:03:53.569Z
|
|
4
4
|
|
|
5
|
+
import { LiveAdminPanel } from "@app/server/live/LiveAdminPanel"
|
|
5
6
|
import { LiveChat } from "@app/server/live/LiveChat"
|
|
6
7
|
import { LiveCounter } from "@app/server/live/LiveCounter"
|
|
7
8
|
import { LiveForm } from "@app/server/live/LiveForm"
|
|
8
9
|
import { LiveLocalCounter } from "@app/server/live/LiveLocalCounter"
|
|
10
|
+
import { LiveProtectedChat } from "@app/server/live/LiveProtectedChat"
|
|
9
11
|
import { LiveRoomChat } from "@app/server/live/LiveRoomChat"
|
|
10
12
|
import { LiveUpload } from "@app/server/live/LiveUpload"
|
|
11
13
|
import { componentRegistry } from "@core/server/live/ComponentRegistry"
|
|
@@ -14,14 +16,16 @@ import { componentRegistry } from "@core/server/live/ComponentRegistry"
|
|
|
14
16
|
function registerAllComponents() {
|
|
15
17
|
try {
|
|
16
18
|
// Auto-generated component registrations
|
|
19
|
+
componentRegistry.registerComponentClass('LiveAdminPanel', LiveAdminPanel)
|
|
17
20
|
componentRegistry.registerComponentClass('LiveChat', LiveChat)
|
|
18
21
|
componentRegistry.registerComponentClass('LiveCounter', LiveCounter)
|
|
19
22
|
componentRegistry.registerComponentClass('LiveForm', LiveForm)
|
|
20
23
|
componentRegistry.registerComponentClass('LiveLocalCounter', LiveLocalCounter)
|
|
24
|
+
componentRegistry.registerComponentClass('LiveProtectedChat', LiveProtectedChat)
|
|
21
25
|
componentRegistry.registerComponentClass('LiveRoomChat', LiveRoomChat)
|
|
22
26
|
componentRegistry.registerComponentClass('LiveUpload', LiveUpload)
|
|
23
27
|
|
|
24
|
-
console.log('📝 Live components registered successfully! (
|
|
28
|
+
console.log('📝 Live components registered successfully! (8 components)')
|
|
25
29
|
} catch (error) {
|
|
26
30
|
console.warn('⚠️ Error registering components:', error)
|
|
27
31
|
}
|
|
@@ -32,10 +36,12 @@ registerAllComponents()
|
|
|
32
36
|
|
|
33
37
|
// Export all components to ensure they're included in the bundle
|
|
34
38
|
export {
|
|
39
|
+
LiveAdminPanel,
|
|
35
40
|
LiveChat,
|
|
36
41
|
LiveCounter,
|
|
37
42
|
LiveForm,
|
|
38
43
|
LiveLocalCounter,
|
|
44
|
+
LiveProtectedChat,
|
|
39
45
|
LiveRoomChat,
|
|
40
46
|
LiveUpload
|
|
41
47
|
}
|
|
@@ -12,3 +12,19 @@ export { connectionManager } from './WebSocketConnectionManager'
|
|
|
12
12
|
export { fileUploadManager } from './FileUploadManager'
|
|
13
13
|
export { stateSignature } from './StateSignature'
|
|
14
14
|
export { performanceMonitor } from './LiveComponentPerformanceMonitor'
|
|
15
|
+
export { liveLog, liveWarn, registerComponentLogging, unregisterComponentLogging } from './LiveLogger'
|
|
16
|
+
export type { LiveLogCategory, LiveLogConfig } from './LiveLogger'
|
|
17
|
+
|
|
18
|
+
// 🔒 Auth system
|
|
19
|
+
export { liveAuthManager, LiveAuthManager } from './auth/LiveAuthManager'
|
|
20
|
+
export { AuthenticatedContext, AnonymousContext, ANONYMOUS_CONTEXT } from './auth/LiveAuthContext'
|
|
21
|
+
export type {
|
|
22
|
+
LiveAuthProvider,
|
|
23
|
+
LiveAuthCredentials,
|
|
24
|
+
LiveAuthUser,
|
|
25
|
+
LiveAuthContext,
|
|
26
|
+
LiveComponentAuth,
|
|
27
|
+
LiveActionAuth,
|
|
28
|
+
LiveActionAuthMap,
|
|
29
|
+
LiveAuthResult,
|
|
30
|
+
} from './auth/types'
|
|
@@ -5,10 +5,14 @@ import { fileUploadManager } from './FileUploadManager'
|
|
|
5
5
|
import { connectionManager } from './WebSocketConnectionManager'
|
|
6
6
|
import { performanceMonitor } from './LiveComponentPerformanceMonitor'
|
|
7
7
|
import { liveRoomManager, type RoomMessage } from './LiveRoomManager'
|
|
8
|
+
import { liveAuthManager } from './auth/LiveAuthManager'
|
|
9
|
+
import { ANONYMOUS_CONTEXT } from './auth/LiveAuthContext'
|
|
8
10
|
import type { LiveMessage, FileUploadStartMessage, FileUploadChunkMessage, FileUploadCompleteMessage, BinaryChunkHeader, FluxStackWebSocket, FluxStackWSData } from '@core/types/types'
|
|
9
11
|
import type { Plugin, PluginContext } from '@core/index'
|
|
10
12
|
import { t, Elysia } from 'elysia'
|
|
11
13
|
import path from 'path'
|
|
14
|
+
import { liveLog } from './LiveLogger'
|
|
15
|
+
import { liveDebugger } from './LiveDebugger'
|
|
12
16
|
|
|
13
17
|
// ===== Response Schemas for Live Components Routes =====
|
|
14
18
|
|
|
@@ -113,6 +117,39 @@ const LiveAlertResolveSchema = t.Object({
|
|
|
113
117
|
description: 'Result of alert resolution operation'
|
|
114
118
|
})
|
|
115
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
|
+
|
|
116
153
|
export const liveComponentsPlugin: Plugin = {
|
|
117
154
|
name: 'live-components',
|
|
118
155
|
version: '1.0.0',
|
|
@@ -138,10 +175,13 @@ export const liveComponentsPlugin: Plugin = {
|
|
|
138
175
|
// Binary messages will be ArrayBuffer/Uint8Array, JSON will be parsed objects
|
|
139
176
|
body: t.Any(),
|
|
140
177
|
|
|
141
|
-
open(ws) {
|
|
178
|
+
async open(ws) {
|
|
142
179
|
const socket = ws as unknown as FluxStackWebSocket
|
|
143
|
-
const connectionId = `ws-${
|
|
144
|
-
|
|
180
|
+
const connectionId = `ws-${crypto.randomUUID()}`
|
|
181
|
+
liveLog('websocket', null, `🔌 Live Components WebSocket connected: ${connectionId}`)
|
|
182
|
+
|
|
183
|
+
// 🔒 Initialize rate limiter for this connection
|
|
184
|
+
connectionRateLimiters.set(connectionId, new ConnectionRateLimiter())
|
|
145
185
|
|
|
146
186
|
// Register connection with enhanced connection manager
|
|
147
187
|
connectionManager.registerConnection(ws as unknown as FluxStackWebSocket, connectionId, 'live-components')
|
|
@@ -164,16 +204,42 @@ export const liveComponentsPlugin: Plugin = {
|
|
|
164
204
|
socket.data.connectedAt = new Date()
|
|
165
205
|
}
|
|
166
206
|
|
|
207
|
+
// 🔒 Try to authenticate from query params (token=xxx)
|
|
208
|
+
try {
|
|
209
|
+
const query = (ws as any).data?.query as Record<string, string> | undefined
|
|
210
|
+
const token = query?.token
|
|
211
|
+
if (token && liveAuthManager.hasProviders()) {
|
|
212
|
+
const authContext = await liveAuthManager.authenticate({ token })
|
|
213
|
+
socket.data.authContext = authContext
|
|
214
|
+
if (authContext.authenticated) {
|
|
215
|
+
socket.data.userId = authContext.user?.id
|
|
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`)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
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')
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Debug: track connection
|
|
228
|
+
liveDebugger.trackConnection(connectionId)
|
|
229
|
+
|
|
167
230
|
// Send connection confirmation
|
|
168
231
|
ws.send(JSON.stringify({
|
|
169
232
|
type: 'CONNECTION_ESTABLISHED',
|
|
170
233
|
connectionId,
|
|
171
234
|
timestamp: Date.now(),
|
|
235
|
+
authenticated: socket.data.authContext?.authenticated ?? false,
|
|
236
|
+
userId: socket.data.authContext?.user?.id,
|
|
172
237
|
features: {
|
|
173
238
|
compression: true,
|
|
174
239
|
encryption: true,
|
|
175
240
|
offlineQueue: true,
|
|
176
|
-
loadBalancing: true
|
|
241
|
+
loadBalancing: true,
|
|
242
|
+
auth: liveAuthManager.hasProviders()
|
|
177
243
|
}
|
|
178
244
|
}))
|
|
179
245
|
},
|
|
@@ -181,6 +247,20 @@ export const liveComponentsPlugin: Plugin = {
|
|
|
181
247
|
async message(ws: unknown, rawMessage: LiveMessage | ArrayBuffer | Uint8Array) {
|
|
182
248
|
const socket = ws as FluxStackWebSocket
|
|
183
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
|
+
|
|
184
264
|
let message: LiveMessage
|
|
185
265
|
let binaryChunkData: Buffer | null = null
|
|
186
266
|
|
|
@@ -201,7 +281,7 @@ export const liveComponentsPlugin: Plugin = {
|
|
|
201
281
|
// Extract binary chunk data
|
|
202
282
|
binaryChunkData = buffer.slice(4 + headerLength)
|
|
203
283
|
|
|
204
|
-
|
|
284
|
+
liveLog('messages', null, `📦 Binary chunk received: ${binaryChunkData.length} bytes for upload ${header.uploadId}`)
|
|
205
285
|
|
|
206
286
|
// Create message with binary data attached
|
|
207
287
|
message = {
|
|
@@ -215,7 +295,7 @@ export const liveComponentsPlugin: Plugin = {
|
|
|
215
295
|
message.timestamp = Date.now()
|
|
216
296
|
}
|
|
217
297
|
|
|
218
|
-
|
|
298
|
+
liveLog('messages', message.componentId || null, `📨 Received message:`, {
|
|
219
299
|
type: message.type,
|
|
220
300
|
componentId: message.componentId,
|
|
221
301
|
action: message.action,
|
|
@@ -243,6 +323,9 @@ export const liveComponentsPlugin: Plugin = {
|
|
|
243
323
|
case 'COMPONENT_PING':
|
|
244
324
|
await handleComponentPing(socket, message)
|
|
245
325
|
break
|
|
326
|
+
case 'AUTH':
|
|
327
|
+
await handleAuth(socket, message)
|
|
328
|
+
break
|
|
246
329
|
case 'FILE_UPLOAD_START':
|
|
247
330
|
await handleFileUploadStart(socket, message as FileUploadStartMessage)
|
|
248
331
|
break
|
|
@@ -286,7 +369,18 @@ export const liveComponentsPlugin: Plugin = {
|
|
|
286
369
|
close(ws) {
|
|
287
370
|
const socket = ws as unknown as FluxStackWebSocket
|
|
288
371
|
const connectionId = socket.data?.connectionId
|
|
289
|
-
|
|
372
|
+
liveLog('websocket', null, `🔌 Live Components WebSocket disconnected: ${connectionId}`)
|
|
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
|
+
}
|
|
290
384
|
|
|
291
385
|
// Cleanup connection in connection manager
|
|
292
386
|
if (connectionId) {
|
|
@@ -486,6 +580,132 @@ export const liveComponentsPlugin: Plugin = {
|
|
|
486
580
|
response: LiveAlertResolveSchema
|
|
487
581
|
})
|
|
488
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
|
+
|
|
489
709
|
// Register the grouped routes with the main app
|
|
490
710
|
context.app.use(liveRoutes)
|
|
491
711
|
},
|
|
@@ -514,7 +734,7 @@ async function handleComponentMount(ws: FluxStackWebSocket, message: LiveMessage
|
|
|
514
734
|
}
|
|
515
735
|
|
|
516
736
|
async function handleComponentRehydrate(ws: FluxStackWebSocket, message: LiveMessage) {
|
|
517
|
-
|
|
737
|
+
liveLog('lifecycle', message.componentId, '🔄 Processing component re-hydration request:', {
|
|
518
738
|
componentId: message.componentId,
|
|
519
739
|
payload: message.payload
|
|
520
740
|
})
|
|
@@ -547,7 +767,7 @@ async function handleComponentRehydrate(ws: FluxStackWebSocket, message: LiveMes
|
|
|
547
767
|
timestamp: Date.now()
|
|
548
768
|
}
|
|
549
769
|
|
|
550
|
-
|
|
770
|
+
liveLog('lifecycle', message.componentId, '📤 Sending COMPONENT_REHYDRATED response:', {
|
|
551
771
|
type: response.type,
|
|
552
772
|
success: response.success,
|
|
553
773
|
newComponentId: response.result?.newComponentId,
|
|
@@ -638,12 +858,65 @@ async function handleComponentPing(ws: FluxStackWebSocket, message: LiveMessage)
|
|
|
638
858
|
ws.send(JSON.stringify(response))
|
|
639
859
|
}
|
|
640
860
|
|
|
861
|
+
// ===== Auth Handler =====
|
|
862
|
+
|
|
863
|
+
async function handleAuth(ws: FluxStackWebSocket, message: LiveMessage) {
|
|
864
|
+
liveLog('websocket', null, '🔒 Processing WebSocket authentication request')
|
|
865
|
+
|
|
866
|
+
try {
|
|
867
|
+
const credentials = message.payload || {}
|
|
868
|
+
const providerName = credentials.provider as string | undefined
|
|
869
|
+
|
|
870
|
+
if (!liveAuthManager.hasProviders()) {
|
|
871
|
+
ws.send(JSON.stringify({
|
|
872
|
+
type: 'AUTH_RESPONSE',
|
|
873
|
+
success: false,
|
|
874
|
+
error: 'No auth providers configured',
|
|
875
|
+
requestId: message.requestId,
|
|
876
|
+
timestamp: Date.now()
|
|
877
|
+
}))
|
|
878
|
+
return
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
const authContext = await liveAuthManager.authenticate(credentials, providerName)
|
|
882
|
+
|
|
883
|
+
// Store auth context on the WebSocket connection
|
|
884
|
+
ws.data.authContext = authContext
|
|
885
|
+
|
|
886
|
+
if (authContext.authenticated) {
|
|
887
|
+
ws.data.userId = authContext.user?.id
|
|
888
|
+
liveLog('websocket', null, `🔒 WebSocket authenticated: user=${authContext.user?.id}`)
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
ws.send(JSON.stringify({
|
|
892
|
+
type: 'AUTH_RESPONSE',
|
|
893
|
+
success: authContext.authenticated,
|
|
894
|
+
authenticated: authContext.authenticated,
|
|
895
|
+
userId: authContext.user?.id,
|
|
896
|
+
roles: authContext.user?.roles,
|
|
897
|
+
requestId: message.requestId,
|
|
898
|
+
timestamp: Date.now()
|
|
899
|
+
}))
|
|
900
|
+
} catch (error: any) {
|
|
901
|
+
console.error('🔒 WebSocket auth error:', error.message)
|
|
902
|
+
ws.send(JSON.stringify({
|
|
903
|
+
type: 'AUTH_RESPONSE',
|
|
904
|
+
success: false,
|
|
905
|
+
error: error.message,
|
|
906
|
+
requestId: message.requestId,
|
|
907
|
+
timestamp: Date.now()
|
|
908
|
+
}))
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
641
912
|
// File Upload Handler Functions
|
|
642
913
|
async function handleFileUploadStart(ws: FluxStackWebSocket, message: FileUploadStartMessage) {
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
914
|
+
liveLog('messages', message.componentId || null, '📤 Starting file upload:', message.uploadId)
|
|
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
|
+
|
|
647
920
|
const response = {
|
|
648
921
|
type: 'FILE_UPLOAD_START_RESPONSE',
|
|
649
922
|
componentId: message.componentId,
|
|
@@ -653,12 +926,12 @@ async function handleFileUploadStart(ws: FluxStackWebSocket, message: FileUpload
|
|
|
653
926
|
requestId: message.requestId,
|
|
654
927
|
timestamp: Date.now()
|
|
655
928
|
}
|
|
656
|
-
|
|
929
|
+
|
|
657
930
|
ws.send(JSON.stringify(response))
|
|
658
931
|
}
|
|
659
932
|
|
|
660
933
|
async function handleFileUploadChunk(ws: FluxStackWebSocket, message: FileUploadChunkMessage, binaryData: Buffer | null = null) {
|
|
661
|
-
|
|
934
|
+
liveLog('messages', message.componentId || null, `📦 Receiving chunk ${message.chunkIndex + 1} for upload ${message.uploadId}${binaryData ? ' (binary)' : ' (base64)'}`)
|
|
662
935
|
|
|
663
936
|
const progressResponse = await fileUploadManager.receiveChunk(message, ws, binaryData)
|
|
664
937
|
|
|
@@ -686,7 +959,7 @@ async function handleFileUploadChunk(ws: FluxStackWebSocket, message: FileUpload
|
|
|
686
959
|
}
|
|
687
960
|
|
|
688
961
|
async function handleFileUploadComplete(ws: FluxStackWebSocket, message: FileUploadCompleteMessage) {
|
|
689
|
-
|
|
962
|
+
liveLog('messages', null, '✅ Completing file upload:', message.uploadId)
|
|
690
963
|
|
|
691
964
|
const completeResponse = await fileUploadManager.completeUpload(message)
|
|
692
965
|
|
|
@@ -702,14 +975,26 @@ async function handleFileUploadComplete(ws: FluxStackWebSocket, message: FileUpl
|
|
|
702
975
|
// ===== Room System Handlers =====
|
|
703
976
|
|
|
704
977
|
async function handleRoomJoin(ws: FluxStackWebSocket, message: RoomMessage) {
|
|
705
|
-
|
|
978
|
+
liveLog('rooms', message.componentId, `🚪 Component ${message.componentId} joining room ${message.roomId}`)
|
|
706
979
|
|
|
707
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
|
+
|
|
708
993
|
const result = liveRoomManager.joinRoom(
|
|
709
994
|
message.componentId,
|
|
710
995
|
message.roomId,
|
|
711
996
|
ws,
|
|
712
|
-
|
|
997
|
+
undefined // 🔒 Don't allow client to set initial room state - server controls this
|
|
713
998
|
)
|
|
714
999
|
|
|
715
1000
|
const response = {
|
|
@@ -736,7 +1021,7 @@ async function handleRoomJoin(ws: FluxStackWebSocket, message: RoomMessage) {
|
|
|
736
1021
|
}
|
|
737
1022
|
|
|
738
1023
|
async function handleRoomLeave(ws: FluxStackWebSocket, message: RoomMessage) {
|
|
739
|
-
|
|
1024
|
+
liveLog('rooms', message.componentId, `🚶 Component ${message.componentId} leaving room ${message.roomId}`)
|
|
740
1025
|
|
|
741
1026
|
try {
|
|
742
1027
|
liveRoomManager.leaveRoom(message.componentId, message.roomId)
|
|
@@ -764,9 +1049,14 @@ async function handleRoomLeave(ws: FluxStackWebSocket, message: RoomMessage) {
|
|
|
764
1049
|
}
|
|
765
1050
|
|
|
766
1051
|
async function handleRoomEmit(ws: FluxStackWebSocket, message: RoomMessage) {
|
|
767
|
-
|
|
1052
|
+
liveLog('rooms', message.componentId, `📡 Component ${message.componentId} emitting '${message.event}' to room ${message.roomId}`)
|
|
768
1053
|
|
|
769
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
|
+
|
|
770
1060
|
const count = liveRoomManager.emitToRoom(
|
|
771
1061
|
message.roomId,
|
|
772
1062
|
message.event!,
|
|
@@ -774,7 +1064,7 @@ async function handleRoomEmit(ws: FluxStackWebSocket, message: RoomMessage) {
|
|
|
774
1064
|
message.componentId // Excluir quem enviou
|
|
775
1065
|
)
|
|
776
1066
|
|
|
777
|
-
|
|
1067
|
+
liveLog('rooms', message.componentId, ` → Notified ${count} components`)
|
|
778
1068
|
} catch (error: any) {
|
|
779
1069
|
ws.send(JSON.stringify({
|
|
780
1070
|
type: 'ERROR',
|
|
@@ -787,9 +1077,20 @@ async function handleRoomEmit(ws: FluxStackWebSocket, message: RoomMessage) {
|
|
|
787
1077
|
}
|
|
788
1078
|
|
|
789
1079
|
async function handleRoomStateSet(ws: FluxStackWebSocket, message: RoomMessage) {
|
|
790
|
-
|
|
1080
|
+
liveLog('rooms', message.componentId, `📝 Component ${message.componentId} updating state in room ${message.roomId}`)
|
|
791
1081
|
|
|
792
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
|
+
|
|
793
1094
|
liveRoomManager.setRoomState(
|
|
794
1095
|
message.roomId,
|
|
795
1096
|
message.data ?? {},
|