create-fluxstack 1.12.0 → 1.13.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/resources/live-auth.md +447 -0
- package/LLMD/resources/live-components.md +79 -21
- package/LLMD/resources/live-logging.md +158 -0
- package/LLMD/resources/live-upload.md +1 -1
- package/LLMD/resources/rest-auth.md +290 -0
- package/README.md +520 -340
- package/app/client/src/App.tsx +11 -0
- package/app/client/src/components/AppLayout.tsx +1 -0
- package/app/client/src/live/AuthDemo.tsx +332 -0
- package/app/client/src/live/RoomChatDemo.tsx +24 -105
- 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 +173 -0
- package/app/server/live/LiveCounter.ts +1 -0
- package/app/server/live/LiveLocalCounter.ts +13 -8
- package/app/server/live/LiveProtectedChat.ts +150 -0
- package/app/server/live/LiveRoomChat.ts +45 -203
- 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/session.config.ts +33 -0
- package/core/client/LiveComponentsProvider.tsx +76 -5
- package/core/client/components/Live.tsx +2 -1
- package/core/client/hooks/useLiveComponent.ts +47 -4
- package/core/client/index.ts +2 -1
- package/core/framework/server.ts +36 -4
- package/core/plugins/built-in/live-components/commands/create-live-component.ts +15 -8
- package/core/plugins/built-in/monitoring/index.ts +10 -3
- package/core/plugins/built-in/vite/index.ts +95 -18
- 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/live/ComponentRegistry.ts +79 -24
- package/core/server/live/LiveComponentPerformanceMonitor.ts +9 -8
- package/core/server/live/LiveLogger.ts +111 -0
- package/core/server/live/LiveRoomManager.ts +5 -4
- package/core/server/live/StateSignature.ts +644 -643
- 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 +92 -16
- package/core/templates/create-project.ts +0 -3
- package/core/types/types.ts +133 -13
- package/core/utils/index.ts +17 -17
- package/core/utils/logger/index.ts +5 -2
- package/core/utils/version.ts +1 -1
- 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
|
@@ -5,10 +5,13 @@ 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'
|
|
12
15
|
|
|
13
16
|
// ===== Response Schemas for Live Components Routes =====
|
|
14
17
|
|
|
@@ -138,10 +141,10 @@ export const liveComponentsPlugin: Plugin = {
|
|
|
138
141
|
// Binary messages will be ArrayBuffer/Uint8Array, JSON will be parsed objects
|
|
139
142
|
body: t.Any(),
|
|
140
143
|
|
|
141
|
-
open(ws) {
|
|
144
|
+
async open(ws) {
|
|
142
145
|
const socket = ws as unknown as FluxStackWebSocket
|
|
143
146
|
const connectionId = `ws-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
|
144
|
-
|
|
147
|
+
liveLog('websocket', null, `🔌 Live Components WebSocket connected: ${connectionId}`)
|
|
145
148
|
|
|
146
149
|
// Register connection with enhanced connection manager
|
|
147
150
|
connectionManager.registerConnection(ws as unknown as FluxStackWebSocket, connectionId, 'live-components')
|
|
@@ -164,16 +167,35 @@ export const liveComponentsPlugin: Plugin = {
|
|
|
164
167
|
socket.data.connectedAt = new Date()
|
|
165
168
|
}
|
|
166
169
|
|
|
170
|
+
// 🔒 Try to authenticate from query params (token=xxx)
|
|
171
|
+
try {
|
|
172
|
+
const query = (ws as any).data?.query as Record<string, string> | undefined
|
|
173
|
+
const token = query?.token
|
|
174
|
+
if (token && liveAuthManager.hasProviders()) {
|
|
175
|
+
const authContext = await liveAuthManager.authenticate({ token })
|
|
176
|
+
socket.data.authContext = authContext
|
|
177
|
+
if (authContext.authenticated) {
|
|
178
|
+
socket.data.userId = authContext.user?.id
|
|
179
|
+
liveLog('websocket', null, `🔒 WebSocket authenticated via query: user=${authContext.user?.id}`)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
} catch {
|
|
183
|
+
// Query param auth is optional - continue without auth
|
|
184
|
+
}
|
|
185
|
+
|
|
167
186
|
// Send connection confirmation
|
|
168
187
|
ws.send(JSON.stringify({
|
|
169
188
|
type: 'CONNECTION_ESTABLISHED',
|
|
170
189
|
connectionId,
|
|
171
190
|
timestamp: Date.now(),
|
|
191
|
+
authenticated: socket.data.authContext?.authenticated ?? false,
|
|
192
|
+
userId: socket.data.authContext?.user?.id,
|
|
172
193
|
features: {
|
|
173
194
|
compression: true,
|
|
174
195
|
encryption: true,
|
|
175
196
|
offlineQueue: true,
|
|
176
|
-
loadBalancing: true
|
|
197
|
+
loadBalancing: true,
|
|
198
|
+
auth: liveAuthManager.hasProviders()
|
|
177
199
|
}
|
|
178
200
|
}))
|
|
179
201
|
},
|
|
@@ -201,7 +223,7 @@ export const liveComponentsPlugin: Plugin = {
|
|
|
201
223
|
// Extract binary chunk data
|
|
202
224
|
binaryChunkData = buffer.slice(4 + headerLength)
|
|
203
225
|
|
|
204
|
-
|
|
226
|
+
liveLog('messages', null, `📦 Binary chunk received: ${binaryChunkData.length} bytes for upload ${header.uploadId}`)
|
|
205
227
|
|
|
206
228
|
// Create message with binary data attached
|
|
207
229
|
message = {
|
|
@@ -215,7 +237,7 @@ export const liveComponentsPlugin: Plugin = {
|
|
|
215
237
|
message.timestamp = Date.now()
|
|
216
238
|
}
|
|
217
239
|
|
|
218
|
-
|
|
240
|
+
liveLog('messages', message.componentId || null, `📨 Received message:`, {
|
|
219
241
|
type: message.type,
|
|
220
242
|
componentId: message.componentId,
|
|
221
243
|
action: message.action,
|
|
@@ -243,6 +265,9 @@ export const liveComponentsPlugin: Plugin = {
|
|
|
243
265
|
case 'COMPONENT_PING':
|
|
244
266
|
await handleComponentPing(socket, message)
|
|
245
267
|
break
|
|
268
|
+
case 'AUTH':
|
|
269
|
+
await handleAuth(socket, message)
|
|
270
|
+
break
|
|
246
271
|
case 'FILE_UPLOAD_START':
|
|
247
272
|
await handleFileUploadStart(socket, message as FileUploadStartMessage)
|
|
248
273
|
break
|
|
@@ -286,7 +311,7 @@ export const liveComponentsPlugin: Plugin = {
|
|
|
286
311
|
close(ws) {
|
|
287
312
|
const socket = ws as unknown as FluxStackWebSocket
|
|
288
313
|
const connectionId = socket.data?.connectionId
|
|
289
|
-
|
|
314
|
+
liveLog('websocket', null, `🔌 Live Components WebSocket disconnected: ${connectionId}`)
|
|
290
315
|
|
|
291
316
|
// Cleanup connection in connection manager
|
|
292
317
|
if (connectionId) {
|
|
@@ -514,7 +539,7 @@ async function handleComponentMount(ws: FluxStackWebSocket, message: LiveMessage
|
|
|
514
539
|
}
|
|
515
540
|
|
|
516
541
|
async function handleComponentRehydrate(ws: FluxStackWebSocket, message: LiveMessage) {
|
|
517
|
-
|
|
542
|
+
liveLog('lifecycle', message.componentId, '🔄 Processing component re-hydration request:', {
|
|
518
543
|
componentId: message.componentId,
|
|
519
544
|
payload: message.payload
|
|
520
545
|
})
|
|
@@ -547,7 +572,7 @@ async function handleComponentRehydrate(ws: FluxStackWebSocket, message: LiveMes
|
|
|
547
572
|
timestamp: Date.now()
|
|
548
573
|
}
|
|
549
574
|
|
|
550
|
-
|
|
575
|
+
liveLog('lifecycle', message.componentId, '📤 Sending COMPONENT_REHYDRATED response:', {
|
|
551
576
|
type: response.type,
|
|
552
577
|
success: response.success,
|
|
553
578
|
newComponentId: response.result?.newComponentId,
|
|
@@ -638,9 +663,60 @@ async function handleComponentPing(ws: FluxStackWebSocket, message: LiveMessage)
|
|
|
638
663
|
ws.send(JSON.stringify(response))
|
|
639
664
|
}
|
|
640
665
|
|
|
666
|
+
// ===== Auth Handler =====
|
|
667
|
+
|
|
668
|
+
async function handleAuth(ws: FluxStackWebSocket, message: LiveMessage) {
|
|
669
|
+
liveLog('websocket', null, '🔒 Processing WebSocket authentication request')
|
|
670
|
+
|
|
671
|
+
try {
|
|
672
|
+
const credentials = message.payload || {}
|
|
673
|
+
const providerName = credentials.provider as string | undefined
|
|
674
|
+
|
|
675
|
+
if (!liveAuthManager.hasProviders()) {
|
|
676
|
+
ws.send(JSON.stringify({
|
|
677
|
+
type: 'AUTH_RESPONSE',
|
|
678
|
+
success: false,
|
|
679
|
+
error: 'No auth providers configured',
|
|
680
|
+
requestId: message.requestId,
|
|
681
|
+
timestamp: Date.now()
|
|
682
|
+
}))
|
|
683
|
+
return
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const authContext = await liveAuthManager.authenticate(credentials, providerName)
|
|
687
|
+
|
|
688
|
+
// Store auth context on the WebSocket connection
|
|
689
|
+
ws.data.authContext = authContext
|
|
690
|
+
|
|
691
|
+
if (authContext.authenticated) {
|
|
692
|
+
ws.data.userId = authContext.user?.id
|
|
693
|
+
liveLog('websocket', null, `🔒 WebSocket authenticated: user=${authContext.user?.id}`)
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
ws.send(JSON.stringify({
|
|
697
|
+
type: 'AUTH_RESPONSE',
|
|
698
|
+
success: authContext.authenticated,
|
|
699
|
+
authenticated: authContext.authenticated,
|
|
700
|
+
userId: authContext.user?.id,
|
|
701
|
+
roles: authContext.user?.roles,
|
|
702
|
+
requestId: message.requestId,
|
|
703
|
+
timestamp: Date.now()
|
|
704
|
+
}))
|
|
705
|
+
} catch (error: any) {
|
|
706
|
+
console.error('🔒 WebSocket auth error:', error.message)
|
|
707
|
+
ws.send(JSON.stringify({
|
|
708
|
+
type: 'AUTH_RESPONSE',
|
|
709
|
+
success: false,
|
|
710
|
+
error: error.message,
|
|
711
|
+
requestId: message.requestId,
|
|
712
|
+
timestamp: Date.now()
|
|
713
|
+
}))
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
641
717
|
// File Upload Handler Functions
|
|
642
718
|
async function handleFileUploadStart(ws: FluxStackWebSocket, message: FileUploadStartMessage) {
|
|
643
|
-
|
|
719
|
+
liveLog('messages', message.componentId || null, '📤 Starting file upload:', message.uploadId)
|
|
644
720
|
|
|
645
721
|
const result = await fileUploadManager.startUpload(message)
|
|
646
722
|
|
|
@@ -658,7 +734,7 @@ async function handleFileUploadStart(ws: FluxStackWebSocket, message: FileUpload
|
|
|
658
734
|
}
|
|
659
735
|
|
|
660
736
|
async function handleFileUploadChunk(ws: FluxStackWebSocket, message: FileUploadChunkMessage, binaryData: Buffer | null = null) {
|
|
661
|
-
|
|
737
|
+
liveLog('messages', message.componentId || null, `📦 Receiving chunk ${message.chunkIndex + 1} for upload ${message.uploadId}${binaryData ? ' (binary)' : ' (base64)'}`)
|
|
662
738
|
|
|
663
739
|
const progressResponse = await fileUploadManager.receiveChunk(message, ws, binaryData)
|
|
664
740
|
|
|
@@ -686,7 +762,7 @@ async function handleFileUploadChunk(ws: FluxStackWebSocket, message: FileUpload
|
|
|
686
762
|
}
|
|
687
763
|
|
|
688
764
|
async function handleFileUploadComplete(ws: FluxStackWebSocket, message: FileUploadCompleteMessage) {
|
|
689
|
-
|
|
765
|
+
liveLog('messages', null, '✅ Completing file upload:', message.uploadId)
|
|
690
766
|
|
|
691
767
|
const completeResponse = await fileUploadManager.completeUpload(message)
|
|
692
768
|
|
|
@@ -702,7 +778,7 @@ async function handleFileUploadComplete(ws: FluxStackWebSocket, message: FileUpl
|
|
|
702
778
|
// ===== Room System Handlers =====
|
|
703
779
|
|
|
704
780
|
async function handleRoomJoin(ws: FluxStackWebSocket, message: RoomMessage) {
|
|
705
|
-
|
|
781
|
+
liveLog('rooms', message.componentId, `🚪 Component ${message.componentId} joining room ${message.roomId}`)
|
|
706
782
|
|
|
707
783
|
try {
|
|
708
784
|
const result = liveRoomManager.joinRoom(
|
|
@@ -736,7 +812,7 @@ async function handleRoomJoin(ws: FluxStackWebSocket, message: RoomMessage) {
|
|
|
736
812
|
}
|
|
737
813
|
|
|
738
814
|
async function handleRoomLeave(ws: FluxStackWebSocket, message: RoomMessage) {
|
|
739
|
-
|
|
815
|
+
liveLog('rooms', message.componentId, `🚶 Component ${message.componentId} leaving room ${message.roomId}`)
|
|
740
816
|
|
|
741
817
|
try {
|
|
742
818
|
liveRoomManager.leaveRoom(message.componentId, message.roomId)
|
|
@@ -764,7 +840,7 @@ async function handleRoomLeave(ws: FluxStackWebSocket, message: RoomMessage) {
|
|
|
764
840
|
}
|
|
765
841
|
|
|
766
842
|
async function handleRoomEmit(ws: FluxStackWebSocket, message: RoomMessage) {
|
|
767
|
-
|
|
843
|
+
liveLog('rooms', message.componentId, `📡 Component ${message.componentId} emitting '${message.event}' to room ${message.roomId}`)
|
|
768
844
|
|
|
769
845
|
try {
|
|
770
846
|
const count = liveRoomManager.emitToRoom(
|
|
@@ -774,7 +850,7 @@ async function handleRoomEmit(ws: FluxStackWebSocket, message: RoomMessage) {
|
|
|
774
850
|
message.componentId // Excluir quem enviou
|
|
775
851
|
)
|
|
776
852
|
|
|
777
|
-
|
|
853
|
+
liveLog('rooms', message.componentId, ` → Notified ${count} components`)
|
|
778
854
|
} catch (error: any) {
|
|
779
855
|
ws.send(JSON.stringify({
|
|
780
856
|
type: 'ERROR',
|
|
@@ -787,7 +863,7 @@ async function handleRoomEmit(ws: FluxStackWebSocket, message: RoomMessage) {
|
|
|
787
863
|
}
|
|
788
864
|
|
|
789
865
|
async function handleRoomStateSet(ws: FluxStackWebSocket, message: RoomMessage) {
|
|
790
|
-
|
|
866
|
+
liveLog('rooms', message.componentId, `📝 Component ${message.componentId} updating state in room ${message.roomId}`)
|
|
791
867
|
|
|
792
868
|
try {
|
|
793
869
|
liveRoomManager.setRoomState(
|
|
@@ -160,7 +160,6 @@ export class ProjectCreator {
|
|
|
160
160
|
"@types/react": "^18.2.0",
|
|
161
161
|
"@types/react-dom": "^18.2.0",
|
|
162
162
|
"@types/uuid": "^10.0.0",
|
|
163
|
-
"@types/ws": "^8.18.1",
|
|
164
163
|
"@testing-library/react": "^14.0.0",
|
|
165
164
|
"@testing-library/jest-dom": "^6.1.0",
|
|
166
165
|
"@testing-library/user-event": "^14.5.0",
|
|
@@ -175,14 +174,12 @@ export class ProjectCreator {
|
|
|
175
174
|
"@sinclair/typebox": "^0.34.41",
|
|
176
175
|
"@vitejs/plugin-react": "^4.0.0",
|
|
177
176
|
"chalk": "^5.3.0",
|
|
178
|
-
"chokidar": "^4.0.3",
|
|
179
177
|
"elysia": "latest",
|
|
180
178
|
"react": "^18.2.0",
|
|
181
179
|
"react-dom": "^18.2.0",
|
|
182
180
|
"react-icons": "^5.5.0",
|
|
183
181
|
"uuid": "^13.0.0",
|
|
184
182
|
"vite": "^5.0.0",
|
|
185
|
-
"ws": "^8.18.3",
|
|
186
183
|
"zustand": "^5.0.8"
|
|
187
184
|
}
|
|
188
185
|
}
|
package/core/types/types.ts
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import { roomEvents } from '@core/server/live/RoomEventBus'
|
|
4
4
|
import { liveRoomManager } from '@core/server/live/LiveRoomManager'
|
|
5
|
+
import { ANONYMOUS_CONTEXT } from '@core/server/live/auth/LiveAuthContext'
|
|
6
|
+
import { liveLog, liveWarn } from '@core/server/live/LiveLogger'
|
|
7
|
+
import type { LiveAuthContext, LiveComponentAuth, LiveActionAuthMap } from '@core/server/live/auth/types'
|
|
5
8
|
import type { ServerWebSocket } from 'bun'
|
|
6
9
|
|
|
7
10
|
// ============================================
|
|
@@ -18,6 +21,8 @@ export interface FluxStackWSData {
|
|
|
18
21
|
subscriptions: Set<string>
|
|
19
22
|
connectedAt: Date
|
|
20
23
|
userId?: string
|
|
24
|
+
/** Contexto de autenticação da conexão WebSocket */
|
|
25
|
+
authContext?: LiveAuthContext
|
|
21
26
|
}
|
|
22
27
|
|
|
23
28
|
/**
|
|
@@ -46,9 +51,11 @@ export type FluxStackServerWebSocket = ServerWebSocket<FluxStackWSData>
|
|
|
46
51
|
export interface LiveMessage {
|
|
47
52
|
type: 'COMPONENT_MOUNT' | 'COMPONENT_UNMOUNT' |
|
|
48
53
|
'COMPONENT_REHYDRATE' | 'COMPONENT_ACTION' | 'CALL_ACTION' |
|
|
49
|
-
'ACTION_RESPONSE' | 'PROPERTY_UPDATE' | 'STATE_UPDATE' | 'STATE_REHYDRATED' |
|
|
54
|
+
'ACTION_RESPONSE' | 'PROPERTY_UPDATE' | 'STATE_UPDATE' | 'STATE_DELTA' | 'STATE_REHYDRATED' |
|
|
50
55
|
'ERROR' | 'BROADCAST' | 'FILE_UPLOAD_START' | 'FILE_UPLOAD_CHUNK' | 'FILE_UPLOAD_COMPLETE' |
|
|
51
56
|
'COMPONENT_PING' | 'COMPONENT_PONG' |
|
|
57
|
+
// Auth system message
|
|
58
|
+
'AUTH' |
|
|
52
59
|
// Room system messages
|
|
53
60
|
'ROOM_JOIN' | 'ROOM_LEAVE' | 'ROOM_EMIT' | 'ROOM_STATE_SET' | 'ROOM_STATE_GET'
|
|
54
61
|
componentId: string
|
|
@@ -117,7 +124,9 @@ export interface WebSocketMessage {
|
|
|
117
124
|
}
|
|
118
125
|
|
|
119
126
|
export interface WebSocketResponse {
|
|
120
|
-
type: 'MESSAGE_RESPONSE' | 'CONNECTION_ESTABLISHED' | 'ERROR' | 'BROADCAST' | 'ACTION_RESPONSE' | 'COMPONENT_MOUNTED' | 'COMPONENT_REHYDRATED' | 'STATE_UPDATE' | 'STATE_REHYDRATED' | 'FILE_UPLOAD_PROGRESS' | 'FILE_UPLOAD_COMPLETE' | 'FILE_UPLOAD_ERROR' | 'FILE_UPLOAD_START_RESPONSE' | 'COMPONENT_PONG' |
|
|
127
|
+
type: 'MESSAGE_RESPONSE' | 'CONNECTION_ESTABLISHED' | 'ERROR' | 'BROADCAST' | 'ACTION_RESPONSE' | 'COMPONENT_MOUNTED' | 'COMPONENT_REHYDRATED' | 'STATE_UPDATE' | 'STATE_DELTA' | 'STATE_REHYDRATED' | 'FILE_UPLOAD_PROGRESS' | 'FILE_UPLOAD_COMPLETE' | 'FILE_UPLOAD_ERROR' | 'FILE_UPLOAD_START_RESPONSE' | 'COMPONENT_PONG' |
|
|
128
|
+
// Auth system response
|
|
129
|
+
'AUTH_RESPONSE' |
|
|
121
130
|
// Room system responses
|
|
122
131
|
'ROOM_EVENT' | 'ROOM_STATE' | 'ROOM_SYSTEM' | 'ROOM_JOINED' | 'ROOM_LEFT'
|
|
123
132
|
originalType?: string
|
|
@@ -214,6 +223,48 @@ export abstract class LiveComponent<TState = ComponentState> {
|
|
|
214
223
|
/** Default state - must be defined in subclasses */
|
|
215
224
|
static defaultState: any
|
|
216
225
|
|
|
226
|
+
/**
|
|
227
|
+
* Per-component logging control. Silent by default.
|
|
228
|
+
*
|
|
229
|
+
* @example
|
|
230
|
+
* // Enable all log categories
|
|
231
|
+
* static logging = true
|
|
232
|
+
*
|
|
233
|
+
* // Enable specific categories only
|
|
234
|
+
* static logging = ['lifecycle', 'messages'] as const
|
|
235
|
+
*
|
|
236
|
+
* // Disabled (default — omit or set false)
|
|
237
|
+
* static logging = false
|
|
238
|
+
*
|
|
239
|
+
* Categories: 'lifecycle' | 'messages' | 'state' | 'performance' | 'rooms' | 'websocket'
|
|
240
|
+
*/
|
|
241
|
+
static logging?: boolean | readonly ('lifecycle' | 'messages' | 'state' | 'performance' | 'rooms' | 'websocket')[]
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Configuração de autenticação do componente.
|
|
245
|
+
* Define se auth é obrigatória e quais roles/permissions são necessárias.
|
|
246
|
+
*
|
|
247
|
+
* @example
|
|
248
|
+
* static auth: LiveComponentAuth = {
|
|
249
|
+
* required: true,
|
|
250
|
+
* roles: ['admin', 'moderator'],
|
|
251
|
+
* permissions: ['chat.read'],
|
|
252
|
+
* }
|
|
253
|
+
*/
|
|
254
|
+
static auth?: LiveComponentAuth
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Configuração de autenticação por action.
|
|
258
|
+
* Permite controle granular de permissões por método.
|
|
259
|
+
*
|
|
260
|
+
* @example
|
|
261
|
+
* static actionAuth: LiveActionAuthMap = {
|
|
262
|
+
* deleteMessage: { permissions: ['chat.admin'] },
|
|
263
|
+
* sendMessage: { permissions: ['chat.write'] },
|
|
264
|
+
* }
|
|
265
|
+
*/
|
|
266
|
+
static actionAuth?: LiveActionAuthMap
|
|
267
|
+
|
|
217
268
|
public readonly id: string
|
|
218
269
|
private _state: TState
|
|
219
270
|
public state: TState // Proxy wrapper
|
|
@@ -222,6 +273,9 @@ export abstract class LiveComponent<TState = ComponentState> {
|
|
|
222
273
|
public userId?: string
|
|
223
274
|
public broadcastToRoom: (message: BroadcastMessage) => void = () => {} // Will be injected by registry
|
|
224
275
|
|
|
276
|
+
// Auth context (injected by registry during mount)
|
|
277
|
+
private _authContext: LiveAuthContext = ANONYMOUS_CONTEXT
|
|
278
|
+
|
|
225
279
|
// Room event subscriptions (cleaned up on destroy)
|
|
226
280
|
private roomEventUnsubscribers: (() => void)[] = []
|
|
227
281
|
private joinedRooms: Set<string> = new Set()
|
|
@@ -250,9 +304,38 @@ export abstract class LiveComponent<TState = ComponentState> {
|
|
|
250
304
|
this.joinedRooms.add(this.room)
|
|
251
305
|
liveRoomManager.joinRoom(this.id, this.room, this.ws)
|
|
252
306
|
}
|
|
307
|
+
|
|
308
|
+
// 🔥 Create direct property accessors (this.count instead of this.state.count)
|
|
309
|
+
this.createDirectStateAccessors()
|
|
253
310
|
}
|
|
254
311
|
|
|
255
|
-
// Create
|
|
312
|
+
// Create getters/setters for each state property directly on `this`
|
|
313
|
+
private createDirectStateAccessors() {
|
|
314
|
+
// Properties that should NOT become state accessors
|
|
315
|
+
const forbidden = new Set([
|
|
316
|
+
// Instance properties
|
|
317
|
+
...Object.keys(this),
|
|
318
|
+
// Prototype methods
|
|
319
|
+
...Object.getOwnPropertyNames(Object.getPrototypeOf(this)),
|
|
320
|
+
// Known internal properties
|
|
321
|
+
'state', '_state', 'ws', 'id', 'room', 'userId', 'broadcastToRoom',
|
|
322
|
+
'$room', '$rooms', 'roomType', 'roomHandles', 'joinedRooms', 'roomEventUnsubscribers'
|
|
323
|
+
])
|
|
324
|
+
|
|
325
|
+
// Create accessor for each state key
|
|
326
|
+
for (const key of Object.keys(this._state as object)) {
|
|
327
|
+
if (!forbidden.has(key)) {
|
|
328
|
+
Object.defineProperty(this, key, {
|
|
329
|
+
get: () => (this._state as any)[key],
|
|
330
|
+
set: (value) => { (this.state as any)[key] = value }, // Uses proxy for auto-sync
|
|
331
|
+
enumerable: true,
|
|
332
|
+
configurable: true
|
|
333
|
+
})
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Create a Proxy that auto-emits STATE_DELTA on any mutation
|
|
256
339
|
private createStateProxy(state: TState): TState {
|
|
257
340
|
const self = this
|
|
258
341
|
return new Proxy(state as object, {
|
|
@@ -260,8 +343,8 @@ export abstract class LiveComponent<TState = ComponentState> {
|
|
|
260
343
|
const oldValue = (target as any)[prop]
|
|
261
344
|
if (oldValue !== value) {
|
|
262
345
|
(target as any)[prop] = value
|
|
263
|
-
//
|
|
264
|
-
self.emit('
|
|
346
|
+
// Delta sync - send only the changed property
|
|
347
|
+
self.emit('STATE_DELTA', { delta: { [prop]: value } })
|
|
265
348
|
}
|
|
266
349
|
return true
|
|
267
350
|
},
|
|
@@ -388,11 +471,48 @@ export abstract class LiveComponent<TState = ComponentState> {
|
|
|
388
471
|
return Array.from(this.joinedRooms)
|
|
389
472
|
}
|
|
390
473
|
|
|
391
|
-
//
|
|
474
|
+
// ========================================
|
|
475
|
+
// 🔒 $auth - Contexto de Autenticação
|
|
476
|
+
// ========================================
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Acessa o contexto de autenticação do usuário atual.
|
|
480
|
+
* Disponível após o mount do componente.
|
|
481
|
+
*
|
|
482
|
+
* @example
|
|
483
|
+
* async sendMessage(payload: { text: string }) {
|
|
484
|
+
* if (!this.$auth.authenticated) {
|
|
485
|
+
* throw new Error('Login required')
|
|
486
|
+
* }
|
|
487
|
+
*
|
|
488
|
+
* const userId = this.$auth.user!.id
|
|
489
|
+
* const isAdmin = this.$auth.hasRole('admin')
|
|
490
|
+
* const canDelete = this.$auth.hasPermission('chat.admin')
|
|
491
|
+
* }
|
|
492
|
+
*/
|
|
493
|
+
public get $auth(): LiveAuthContext {
|
|
494
|
+
return this._authContext
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Injeta o contexto de autenticação no componente.
|
|
499
|
+
* Chamado internamente pelo ComponentRegistry durante o mount.
|
|
500
|
+
* @internal
|
|
501
|
+
*/
|
|
502
|
+
public setAuthContext(context: LiveAuthContext): void {
|
|
503
|
+
this._authContext = context
|
|
504
|
+
// Atualiza userId se disponível no auth context
|
|
505
|
+
if (context.authenticated && context.user?.id && !this.userId) {
|
|
506
|
+
this.userId = context.user.id
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// State management (batch update - single emit with delta)
|
|
392
511
|
public setState(updates: Partial<TState> | ((prev: TState) => Partial<TState>)) {
|
|
393
512
|
const newUpdates = typeof updates === 'function' ? updates(this._state) : updates
|
|
394
513
|
Object.assign(this._state as object, newUpdates)
|
|
395
|
-
|
|
514
|
+
// Delta sync - send only the changed properties
|
|
515
|
+
this.emit('STATE_DELTA', { delta: newUpdates })
|
|
396
516
|
}
|
|
397
517
|
|
|
398
518
|
// Generic setValue action - set any state key with type safety
|
|
@@ -444,7 +564,7 @@ export abstract class LiveComponent<TState = ComponentState> {
|
|
|
444
564
|
// Broadcast to all clients in room (via WebSocket)
|
|
445
565
|
protected broadcast(type: string, payload: any, excludeCurrentUser = false) {
|
|
446
566
|
if (!this.room) {
|
|
447
|
-
|
|
567
|
+
liveWarn('rooms', this.id, `⚠️ [${this.id}] Cannot broadcast '${type}' - no room set`)
|
|
448
568
|
return
|
|
449
569
|
}
|
|
450
570
|
|
|
@@ -455,7 +575,7 @@ export abstract class LiveComponent<TState = ComponentState> {
|
|
|
455
575
|
excludeUser: excludeCurrentUser ? this.userId : undefined
|
|
456
576
|
}
|
|
457
577
|
|
|
458
|
-
|
|
578
|
+
liveLog('rooms', this.id, `📤 [${this.id}] Broadcasting '${type}' to room '${this.room}'`)
|
|
459
579
|
|
|
460
580
|
// This will be handled by the registry
|
|
461
581
|
this.broadcastToRoom(message)
|
|
@@ -475,14 +595,14 @@ export abstract class LiveComponent<TState = ComponentState> {
|
|
|
475
595
|
*/
|
|
476
596
|
protected emitRoomEvent(event: string, data: any, notifySelf = false): number {
|
|
477
597
|
if (!this.room) {
|
|
478
|
-
|
|
598
|
+
liveWarn('rooms', this.id, `⚠️ [${this.id}] Cannot emit room event '${event}' - no room set`)
|
|
479
599
|
return 0
|
|
480
600
|
}
|
|
481
601
|
|
|
482
602
|
const excludeId = notifySelf ? undefined : this.id
|
|
483
603
|
const notified = roomEvents.emit(this.roomType, this.room, event, data, excludeId)
|
|
484
604
|
|
|
485
|
-
|
|
605
|
+
liveLog('rooms', this.id, `📡 [${this.id}] Room event '${event}' → ${notified} components`)
|
|
486
606
|
return notified
|
|
487
607
|
}
|
|
488
608
|
|
|
@@ -495,7 +615,7 @@ export abstract class LiveComponent<TState = ComponentState> {
|
|
|
495
615
|
*/
|
|
496
616
|
protected onRoomEvent<T = any>(event: string, handler: (data: T) => void): void {
|
|
497
617
|
if (!this.room) {
|
|
498
|
-
|
|
618
|
+
liveWarn('rooms', this.id, `⚠️ [${this.id}] Cannot subscribe to room event '${event}' - no room set`)
|
|
499
619
|
return
|
|
500
620
|
}
|
|
501
621
|
|
|
@@ -510,7 +630,7 @@ export abstract class LiveComponent<TState = ComponentState> {
|
|
|
510
630
|
// Guardar para cleanup no destroy
|
|
511
631
|
this.roomEventUnsubscribers.push(unsubscribe)
|
|
512
632
|
|
|
513
|
-
|
|
633
|
+
liveLog('rooms', this.id, `👂 [${this.id}] Subscribed to room event '${event}'`)
|
|
514
634
|
}
|
|
515
635
|
|
|
516
636
|
/**
|
package/core/utils/index.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FluxStack Utilities
|
|
3
|
-
* Main exports for utility functions and classes
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
// Logger utilities
|
|
7
|
-
export { logger, log } from "./logger"
|
|
8
|
-
export type { Logger } from "./logger/index"
|
|
9
|
-
|
|
10
|
-
// Error handling
|
|
11
|
-
export * from "./errors"
|
|
12
|
-
|
|
13
|
-
// Monitoring
|
|
14
|
-
export { MetricsCollector } from "./monitoring"
|
|
15
|
-
export type * from "./monitoring"
|
|
16
|
-
|
|
17
|
-
// General helpers
|
|
1
|
+
/**
|
|
2
|
+
* FluxStack Utilities
|
|
3
|
+
* Main exports for utility functions and classes
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Logger utilities
|
|
7
|
+
export { logger, log } from "./logger"
|
|
8
|
+
export type { Logger } from "./logger/index"
|
|
9
|
+
|
|
10
|
+
// Error handling
|
|
11
|
+
export * from "./errors"
|
|
12
|
+
|
|
13
|
+
// Monitoring
|
|
14
|
+
export { MetricsCollector } from "./monitoring"
|
|
15
|
+
export type * from "./monitoring"
|
|
16
|
+
|
|
17
|
+
// General helpers
|
|
18
18
|
export * from "./helpers"
|
|
@@ -127,7 +127,7 @@ export function SECTION(sectionName: string, callback: () => void): void {
|
|
|
127
127
|
/**
|
|
128
128
|
* HTTP request logging with colors and formatting
|
|
129
129
|
*/
|
|
130
|
-
export function request(method: string, path: string, status?: number, duration?: number): void {
|
|
130
|
+
export function request(method: string, path: string, status?: number, duration?: number, ip?: string): void {
|
|
131
131
|
const { file: callerFile } = getCallerInfo()
|
|
132
132
|
const logger = getLoggerForModule(callerFile)
|
|
133
133
|
|
|
@@ -159,12 +159,15 @@ export function request(method: string, path: string, status?: number, duration?
|
|
|
159
159
|
durationStr = ` ${durationColor(`(${duration}ms)`)}`
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
// Format IP address
|
|
163
|
+
const ipStr = ip ? chalk.dim(`[${ip}]`) + ' ' : ''
|
|
164
|
+
|
|
162
165
|
// Build log message
|
|
163
166
|
const methodStr = methodColor(method.padEnd(7))
|
|
164
167
|
const pathStr = chalk.white(path.padEnd(30))
|
|
165
168
|
const statusStr = status ? `→ ${statusColor(status.toString())}` : ''
|
|
166
169
|
|
|
167
|
-
const message = `${methodStr} ${pathStr} ${statusStr}${durationStr}`
|
|
170
|
+
const message = `${ipStr}${methodStr} ${pathStr} ${statusStr}${durationStr}`
|
|
168
171
|
|
|
169
172
|
// Use appropriate log level based on status
|
|
170
173
|
const level = status && status >= 500 ? 'error' : status && status >= 400 ? 'warn' : 'info'
|
package/core/utils/version.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-fluxstack",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.0",
|
|
4
4
|
"description": "⚡ Revolutionary full-stack TypeScript framework with Declarative Config System, Elysia + React + Bun",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"framework",
|
|
@@ -55,7 +55,6 @@
|
|
|
55
55
|
"@types/react-dom": "^19.1.6",
|
|
56
56
|
"@vitest/coverage-v8": "^3.2.4",
|
|
57
57
|
"@vitest/ui": "^3.2.4",
|
|
58
|
-
"concurrently": "^9.2.0",
|
|
59
58
|
"cross-env": "^10.1.0",
|
|
60
59
|
"eslint": "^9.30.1",
|
|
61
60
|
"eslint-plugin-react-hooks": "^5.2.0",
|
|
@@ -73,16 +72,11 @@
|
|
|
73
72
|
"dependencies": {
|
|
74
73
|
"@elysiajs/eden": "^1.3.2",
|
|
75
74
|
"@elysiajs/swagger": "^1.3.1",
|
|
76
|
-
"@types/http-proxy-middleware": "^1.0.0",
|
|
77
|
-
"@types/ws": "^8.18.1",
|
|
78
75
|
"@vitejs/plugin-react": "^4.6.0",
|
|
79
76
|
"chalk": "^5.3.0",
|
|
80
|
-
"chokidar": "^4.0.3",
|
|
81
77
|
"commander": "^12.1.0",
|
|
82
78
|
"elysia": "^1.4.6",
|
|
83
|
-
"http-proxy-middleware": "^3.0.5",
|
|
84
79
|
"lightningcss": "^1.30.1",
|
|
85
|
-
"lucide-react": "^0.544.0",
|
|
86
80
|
"ora": "^8.1.0",
|
|
87
81
|
"react": "^19.1.0",
|
|
88
82
|
"react-dom": "^19.1.0",
|
|
@@ -92,7 +86,6 @@
|
|
|
92
86
|
"vite": "^7.1.7",
|
|
93
87
|
"winston": "^3.18.3",
|
|
94
88
|
"winston-daily-rotate-file": "^5.0.0",
|
|
95
|
-
"ws": "^8.18.3",
|
|
96
89
|
"zustand": "^5.0.8"
|
|
97
90
|
},
|
|
98
91
|
"engines": {
|
|
@@ -8,6 +8,8 @@ import type { FluxStack, PluginContext, RequestContext, ResponseContext } from "
|
|
|
8
8
|
type Plugin = FluxStack.Plugin
|
|
9
9
|
import { Elysia, t } from "elysia"
|
|
10
10
|
import { CryptoAuthService, AuthMiddleware } from "./server"
|
|
11
|
+
import { CryptoAuthLiveProvider } from "./server/CryptoAuthLiveProvider"
|
|
12
|
+
import { liveAuthManager } from "@core/server/live/auth"
|
|
11
13
|
import { makeProtectedRouteCommand } from "./cli/make-protected-route.command"
|
|
12
14
|
|
|
13
15
|
// ✅ Plugin carrega sua própria configuração (da pasta config/ do plugin)
|
|
@@ -88,6 +90,10 @@ export const cryptoAuthPlugin: Plugin = {
|
|
|
88
90
|
;(global as any).cryptoAuthService = authService
|
|
89
91
|
;(global as any).cryptoAuthMiddleware = authMiddleware
|
|
90
92
|
|
|
93
|
+
// 🔒 Register as LiveAuthProvider for Live Components WebSocket auth
|
|
94
|
+
liveAuthManager.register(new CryptoAuthLiveProvider(authService))
|
|
95
|
+
context.logger.info('🔒 Crypto Auth registered as Live Components auth provider')
|
|
96
|
+
|
|
91
97
|
// Store plugin info for table display
|
|
92
98
|
if (!(global as any).__fluxstackPlugins) {
|
|
93
99
|
(global as any).__fluxstackPlugins = []
|