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
package/core/types/types.ts
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
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
|
+
|
|
8
|
+
// ===== Debug Instrumentation (injectable to avoid client-side import) =====
|
|
9
|
+
// The real debugger is injected by ComponentRegistry at server startup.
|
|
10
|
+
// This avoids importing server-only LiveDebugger.ts from this shared types file.
|
|
11
|
+
interface LiveDebuggerInterface {
|
|
12
|
+
trackStateChange(componentId: string, delta: Record<string, unknown>, fullState: Record<string, unknown>, source?: string): void
|
|
13
|
+
trackActionCall(componentId: string, action: string, payload: unknown): void
|
|
14
|
+
trackActionResult(componentId: string, action: string, result: unknown, duration: number): void
|
|
15
|
+
trackActionError(componentId: string, action: string, error: string, duration: number): void
|
|
16
|
+
trackRoomEmit(componentId: string, roomId: string, event: string, data: unknown): void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let _liveDebugger: LiveDebuggerInterface | null = null
|
|
20
|
+
|
|
21
|
+
/** @internal Called by ComponentRegistry to inject the debugger instance */
|
|
22
|
+
export function _setLiveDebugger(dbg: LiveDebuggerInterface): void {
|
|
23
|
+
_liveDebugger = dbg
|
|
24
|
+
}
|
|
25
|
+
import type { LiveAuthContext, LiveComponentAuth, LiveActionAuthMap } from '@core/server/live/auth/types'
|
|
5
26
|
import type { ServerWebSocket } from 'bun'
|
|
6
27
|
|
|
7
28
|
// ============================================
|
|
@@ -18,6 +39,8 @@ export interface FluxStackWSData {
|
|
|
18
39
|
subscriptions: Set<string>
|
|
19
40
|
connectedAt: Date
|
|
20
41
|
userId?: string
|
|
42
|
+
/** Contexto de autenticação da conexão WebSocket */
|
|
43
|
+
authContext?: LiveAuthContext
|
|
21
44
|
}
|
|
22
45
|
|
|
23
46
|
/**
|
|
@@ -46,9 +69,11 @@ export type FluxStackServerWebSocket = ServerWebSocket<FluxStackWSData>
|
|
|
46
69
|
export interface LiveMessage {
|
|
47
70
|
type: 'COMPONENT_MOUNT' | 'COMPONENT_UNMOUNT' |
|
|
48
71
|
'COMPONENT_REHYDRATE' | 'COMPONENT_ACTION' | 'CALL_ACTION' |
|
|
49
|
-
'ACTION_RESPONSE' | 'PROPERTY_UPDATE' | 'STATE_UPDATE' | 'STATE_REHYDRATED' |
|
|
72
|
+
'ACTION_RESPONSE' | 'PROPERTY_UPDATE' | 'STATE_UPDATE' | 'STATE_DELTA' | 'STATE_REHYDRATED' |
|
|
50
73
|
'ERROR' | 'BROADCAST' | 'FILE_UPLOAD_START' | 'FILE_UPLOAD_CHUNK' | 'FILE_UPLOAD_COMPLETE' |
|
|
51
74
|
'COMPONENT_PING' | 'COMPONENT_PONG' |
|
|
75
|
+
// Auth system message
|
|
76
|
+
'AUTH' |
|
|
52
77
|
// Room system messages
|
|
53
78
|
'ROOM_JOIN' | 'ROOM_LEAVE' | 'ROOM_EMIT' | 'ROOM_STATE_SET' | 'ROOM_STATE_GET'
|
|
54
79
|
componentId: string
|
|
@@ -117,7 +142,9 @@ export interface WebSocketMessage {
|
|
|
117
142
|
}
|
|
118
143
|
|
|
119
144
|
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' |
|
|
145
|
+
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' |
|
|
146
|
+
// Auth system response
|
|
147
|
+
'AUTH_RESPONSE' |
|
|
121
148
|
// Room system responses
|
|
122
149
|
'ROOM_EVENT' | 'ROOM_STATE' | 'ROOM_SYSTEM' | 'ROOM_JOINED' | 'ROOM_LEFT'
|
|
123
150
|
originalType?: string
|
|
@@ -208,12 +235,54 @@ export interface ServerRoomProxy<TState = any, TEvents extends Record<string, an
|
|
|
208
235
|
setState: (updates: Partial<TState>) => void
|
|
209
236
|
}
|
|
210
237
|
|
|
211
|
-
export abstract class LiveComponent<TState = ComponentState> {
|
|
238
|
+
export abstract class LiveComponent<TState = ComponentState, TPrivate extends Record<string, any> = Record<string, any>> {
|
|
212
239
|
/** Component name for registry lookup - must be defined in subclasses */
|
|
213
240
|
static componentName: string
|
|
214
241
|
/** Default state - must be defined in subclasses */
|
|
215
242
|
static defaultState: any
|
|
216
243
|
|
|
244
|
+
/**
|
|
245
|
+
* Per-component logging control. Silent by default.
|
|
246
|
+
*
|
|
247
|
+
* @example
|
|
248
|
+
* // Enable all log categories
|
|
249
|
+
* static logging = true
|
|
250
|
+
*
|
|
251
|
+
* // Enable specific categories only
|
|
252
|
+
* static logging = ['lifecycle', 'messages'] as const
|
|
253
|
+
*
|
|
254
|
+
* // Disabled (default — omit or set false)
|
|
255
|
+
* static logging = false
|
|
256
|
+
*
|
|
257
|
+
* Categories: 'lifecycle' | 'messages' | 'state' | 'performance' | 'rooms' | 'websocket'
|
|
258
|
+
*/
|
|
259
|
+
static logging?: boolean | readonly ('lifecycle' | 'messages' | 'state' | 'performance' | 'rooms' | 'websocket')[]
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Configuração de autenticação do componente.
|
|
263
|
+
* Define se auth é obrigatória e quais roles/permissions são necessárias.
|
|
264
|
+
*
|
|
265
|
+
* @example
|
|
266
|
+
* static auth: LiveComponentAuth = {
|
|
267
|
+
* required: true,
|
|
268
|
+
* roles: ['admin', 'moderator'],
|
|
269
|
+
* permissions: ['chat.read'],
|
|
270
|
+
* }
|
|
271
|
+
*/
|
|
272
|
+
static auth?: LiveComponentAuth
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Configuração de autenticação por action.
|
|
276
|
+
* Permite controle granular de permissões por método.
|
|
277
|
+
*
|
|
278
|
+
* @example
|
|
279
|
+
* static actionAuth: LiveActionAuthMap = {
|
|
280
|
+
* deleteMessage: { permissions: ['chat.admin'] },
|
|
281
|
+
* sendMessage: { permissions: ['chat.write'] },
|
|
282
|
+
* }
|
|
283
|
+
*/
|
|
284
|
+
static actionAuth?: LiveActionAuthMap
|
|
285
|
+
|
|
217
286
|
public readonly id: string
|
|
218
287
|
private _state: TState
|
|
219
288
|
public state: TState // Proxy wrapper
|
|
@@ -222,6 +291,12 @@ export abstract class LiveComponent<TState = ComponentState> {
|
|
|
222
291
|
public userId?: string
|
|
223
292
|
public broadcastToRoom: (message: BroadcastMessage) => void = () => {} // Will be injected by registry
|
|
224
293
|
|
|
294
|
+
// 🔒 Server-only private state (NEVER sent to client)
|
|
295
|
+
private _privateState: TPrivate = {} as TPrivate
|
|
296
|
+
|
|
297
|
+
// Auth context (injected by registry during mount)
|
|
298
|
+
private _authContext: LiveAuthContext = ANONYMOUS_CONTEXT
|
|
299
|
+
|
|
225
300
|
// Room event subscriptions (cleaned up on destroy)
|
|
226
301
|
private roomEventUnsubscribers: (() => void)[] = []
|
|
227
302
|
private joinedRooms: Set<string> = new Set()
|
|
@@ -250,9 +325,39 @@ export abstract class LiveComponent<TState = ComponentState> {
|
|
|
250
325
|
this.joinedRooms.add(this.room)
|
|
251
326
|
liveRoomManager.joinRoom(this.id, this.room, this.ws)
|
|
252
327
|
}
|
|
328
|
+
|
|
329
|
+
// 🔥 Create direct property accessors (this.count instead of this.state.count)
|
|
330
|
+
this.createDirectStateAccessors()
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Create getters/setters for each state property directly on `this`
|
|
334
|
+
private createDirectStateAccessors() {
|
|
335
|
+
// Properties that should NOT become state accessors
|
|
336
|
+
const forbidden = new Set([
|
|
337
|
+
// Instance properties
|
|
338
|
+
...Object.keys(this),
|
|
339
|
+
// Prototype methods
|
|
340
|
+
...Object.getOwnPropertyNames(Object.getPrototypeOf(this)),
|
|
341
|
+
// Known internal properties
|
|
342
|
+
'state', '_state', 'ws', 'id', 'room', 'userId', 'broadcastToRoom',
|
|
343
|
+
'$private', '_privateState',
|
|
344
|
+
'$room', '$rooms', 'roomType', 'roomHandles', 'joinedRooms', 'roomEventUnsubscribers'
|
|
345
|
+
])
|
|
346
|
+
|
|
347
|
+
// Create accessor for each state key
|
|
348
|
+
for (const key of Object.keys(this._state as object)) {
|
|
349
|
+
if (!forbidden.has(key)) {
|
|
350
|
+
Object.defineProperty(this, key, {
|
|
351
|
+
get: () => (this._state as any)[key],
|
|
352
|
+
set: (value) => { (this.state as any)[key] = value }, // Uses proxy for auto-sync
|
|
353
|
+
enumerable: true,
|
|
354
|
+
configurable: true
|
|
355
|
+
})
|
|
356
|
+
}
|
|
357
|
+
}
|
|
253
358
|
}
|
|
254
359
|
|
|
255
|
-
// Create a Proxy that auto-emits
|
|
360
|
+
// Create a Proxy that auto-emits STATE_DELTA on any mutation
|
|
256
361
|
private createStateProxy(state: TState): TState {
|
|
257
362
|
const self = this
|
|
258
363
|
return new Proxy(state as object, {
|
|
@@ -260,8 +365,15 @@ export abstract class LiveComponent<TState = ComponentState> {
|
|
|
260
365
|
const oldValue = (target as any)[prop]
|
|
261
366
|
if (oldValue !== value) {
|
|
262
367
|
(target as any)[prop] = value
|
|
263
|
-
//
|
|
264
|
-
self.emit('
|
|
368
|
+
// Delta sync - send only the changed property
|
|
369
|
+
self.emit('STATE_DELTA', { delta: { [prop]: value } })
|
|
370
|
+
// Debug: track proxy mutation
|
|
371
|
+
_liveDebugger?.trackStateChange(
|
|
372
|
+
self.id,
|
|
373
|
+
{ [prop]: value } as Record<string, unknown>,
|
|
374
|
+
target as Record<string, unknown>,
|
|
375
|
+
'proxy'
|
|
376
|
+
)
|
|
265
377
|
}
|
|
266
378
|
return true
|
|
267
379
|
},
|
|
@@ -271,6 +383,33 @@ export abstract class LiveComponent<TState = ComponentState> {
|
|
|
271
383
|
}) as TState
|
|
272
384
|
}
|
|
273
385
|
|
|
386
|
+
// ========================================
|
|
387
|
+
// 🔒 $private - Server-Only State
|
|
388
|
+
// ========================================
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Server-only state that is NEVER synchronized with the client.
|
|
392
|
+
* Use this for sensitive data like tokens, API keys, internal IDs, etc.
|
|
393
|
+
*
|
|
394
|
+
* Unlike `this.state`, mutations to `$private` do NOT trigger
|
|
395
|
+
* STATE_DELTA or STATE_UPDATE messages.
|
|
396
|
+
*
|
|
397
|
+
* ⚠️ Private state is lost on rehydration (since it's never sent to client).
|
|
398
|
+
* Re-populate it in your action handlers as needed.
|
|
399
|
+
*
|
|
400
|
+
* @example
|
|
401
|
+
* async connect(payload: { token: string }) {
|
|
402
|
+
* this.$private.token = payload.token
|
|
403
|
+
* this.$private.apiKey = await getKey()
|
|
404
|
+
*
|
|
405
|
+
* // Only UI-relevant data goes to state (synced with client)
|
|
406
|
+
* this.state.messages = await fetchMessages(this.$private.token)
|
|
407
|
+
* }
|
|
408
|
+
*/
|
|
409
|
+
public get $private(): TPrivate {
|
|
410
|
+
return this._privateState
|
|
411
|
+
}
|
|
412
|
+
|
|
274
413
|
// ========================================
|
|
275
414
|
// 🔥 $room - Sistema de Salas Unificado
|
|
276
415
|
// ========================================
|
|
@@ -388,11 +527,55 @@ export abstract class LiveComponent<TState = ComponentState> {
|
|
|
388
527
|
return Array.from(this.joinedRooms)
|
|
389
528
|
}
|
|
390
529
|
|
|
391
|
-
//
|
|
530
|
+
// ========================================
|
|
531
|
+
// 🔒 $auth - Contexto de Autenticação
|
|
532
|
+
// ========================================
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Acessa o contexto de autenticação do usuário atual.
|
|
536
|
+
* Disponível após o mount do componente.
|
|
537
|
+
*
|
|
538
|
+
* @example
|
|
539
|
+
* async sendMessage(payload: { text: string }) {
|
|
540
|
+
* if (!this.$auth.authenticated) {
|
|
541
|
+
* throw new Error('Login required')
|
|
542
|
+
* }
|
|
543
|
+
*
|
|
544
|
+
* const userId = this.$auth.user!.id
|
|
545
|
+
* const isAdmin = this.$auth.hasRole('admin')
|
|
546
|
+
* const canDelete = this.$auth.hasPermission('chat.admin')
|
|
547
|
+
* }
|
|
548
|
+
*/
|
|
549
|
+
public get $auth(): LiveAuthContext {
|
|
550
|
+
return this._authContext
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Injeta o contexto de autenticação no componente.
|
|
555
|
+
* Chamado internamente pelo ComponentRegistry durante o mount.
|
|
556
|
+
* @internal
|
|
557
|
+
*/
|
|
558
|
+
public setAuthContext(context: LiveAuthContext): void {
|
|
559
|
+
this._authContext = context
|
|
560
|
+
// Atualiza userId se disponível no auth context
|
|
561
|
+
if (context.authenticated && context.user?.id && !this.userId) {
|
|
562
|
+
this.userId = context.user.id
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// State management (batch update - single emit with delta)
|
|
392
567
|
public setState(updates: Partial<TState> | ((prev: TState) => Partial<TState>)) {
|
|
393
568
|
const newUpdates = typeof updates === 'function' ? updates(this._state) : updates
|
|
394
569
|
Object.assign(this._state as object, newUpdates)
|
|
395
|
-
|
|
570
|
+
// Delta sync - send only the changed properties
|
|
571
|
+
this.emit('STATE_DELTA', { delta: newUpdates })
|
|
572
|
+
// Debug: track state change
|
|
573
|
+
_liveDebugger?.trackStateChange(
|
|
574
|
+
this.id,
|
|
575
|
+
newUpdates as Record<string, unknown>,
|
|
576
|
+
this._state as Record<string, unknown>,
|
|
577
|
+
'setState'
|
|
578
|
+
)
|
|
396
579
|
}
|
|
397
580
|
|
|
398
581
|
// Generic setValue action - set any state key with type safety
|
|
@@ -403,23 +586,86 @@ export abstract class LiveComponent<TState = ComponentState> {
|
|
|
403
586
|
return { success: true, key, value }
|
|
404
587
|
}
|
|
405
588
|
|
|
406
|
-
|
|
589
|
+
/**
|
|
590
|
+
* 🔒 REQUIRED: List of methods that are explicitly callable from the client.
|
|
591
|
+
* ONLY these methods can be called via CALL_ACTION.
|
|
592
|
+
* Components without publicActions will deny ALL remote actions (secure by default).
|
|
593
|
+
*
|
|
594
|
+
* @example
|
|
595
|
+
* static publicActions = ['sendMessage', 'deleteMessage', 'join'] as const
|
|
596
|
+
*/
|
|
597
|
+
static publicActions?: readonly string[]
|
|
598
|
+
|
|
599
|
+
// Internal methods that must NEVER be callable from the client
|
|
600
|
+
private static readonly BLOCKED_ACTIONS: ReadonlySet<string> = new Set([
|
|
601
|
+
// Lifecycle & internal
|
|
602
|
+
'constructor', 'destroy', 'executeAction', 'getSerializableState',
|
|
603
|
+
// State management internals
|
|
604
|
+
'setState', 'emit', 'broadcast', 'broadcastToRoom',
|
|
605
|
+
'createStateProxy', 'createDirectStateAccessors', 'generateId',
|
|
606
|
+
// Auth internals
|
|
607
|
+
'setAuthContext', '$auth',
|
|
608
|
+
// Private state internals
|
|
609
|
+
'$private', '_privateState',
|
|
610
|
+
// Room internals
|
|
611
|
+
'$room', '$rooms', 'subscribeToRoom', 'unsubscribeFromRoom',
|
|
612
|
+
'emitRoomEvent', 'onRoomEvent', 'emitRoomEventWithState',
|
|
613
|
+
])
|
|
614
|
+
|
|
615
|
+
// Execute action safely with security validation
|
|
407
616
|
public async executeAction(action: string, payload: any): Promise<any> {
|
|
617
|
+
const actionStart = Date.now()
|
|
408
618
|
try {
|
|
409
|
-
//
|
|
619
|
+
// 🔒 Security: Block internal/protected methods from being called remotely
|
|
620
|
+
if ((LiveComponent.BLOCKED_ACTIONS as Set<string>).has(action)) {
|
|
621
|
+
throw new Error(`Action '${action}' is not callable`)
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// 🔒 Security: Block private methods (prefixed with _ or #)
|
|
625
|
+
if (action.startsWith('_') || action.startsWith('#')) {
|
|
626
|
+
throw new Error(`Action '${action}' is not callable`)
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// 🔒 Security: publicActions whitelist is MANDATORY
|
|
630
|
+
// Components without publicActions deny ALL remote actions (secure by default)
|
|
631
|
+
const componentClass = this.constructor as typeof LiveComponent
|
|
632
|
+
const publicActions = componentClass.publicActions
|
|
633
|
+
if (!publicActions) {
|
|
634
|
+
console.warn(`🔒 [SECURITY] Component '${componentClass.componentName || componentClass.name}' has no publicActions defined. All remote actions are blocked. Define static publicActions to allow specific actions.`)
|
|
635
|
+
throw new Error(`Action '${action}' is not callable - component has no publicActions defined`)
|
|
636
|
+
}
|
|
637
|
+
if (!publicActions.includes(action)) {
|
|
638
|
+
throw new Error(`Action '${action}' is not callable`)
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Check if method exists on the instance
|
|
410
642
|
const method = (this as any)[action]
|
|
411
643
|
if (typeof method !== 'function') {
|
|
412
644
|
throw new Error(`Action '${action}' not found on component`)
|
|
413
645
|
}
|
|
414
646
|
|
|
647
|
+
// 🔒 Security: Block inherited Object.prototype methods
|
|
648
|
+
if (Object.prototype.hasOwnProperty.call(Object.prototype, action)) {
|
|
649
|
+
throw new Error(`Action '${action}' is not callable`)
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Debug: track action call
|
|
653
|
+
_liveDebugger?.trackActionCall(this.id, action, payload)
|
|
654
|
+
|
|
415
655
|
// Execute method
|
|
416
656
|
const result = await method.call(this, payload)
|
|
657
|
+
|
|
658
|
+
// Debug: track action result
|
|
659
|
+
_liveDebugger?.trackActionResult(this.id, action, result, Date.now() - actionStart)
|
|
660
|
+
|
|
417
661
|
return result
|
|
418
662
|
} catch (error: any) {
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
663
|
+
// Debug: track action error
|
|
664
|
+
_liveDebugger?.trackActionError(this.id, action, error.message, Date.now() - actionStart)
|
|
665
|
+
|
|
666
|
+
this.emit('ERROR', {
|
|
667
|
+
action,
|
|
668
|
+
error: error.message
|
|
423
669
|
})
|
|
424
670
|
throw error
|
|
425
671
|
}
|
|
@@ -444,7 +690,7 @@ export abstract class LiveComponent<TState = ComponentState> {
|
|
|
444
690
|
// Broadcast to all clients in room (via WebSocket)
|
|
445
691
|
protected broadcast(type: string, payload: any, excludeCurrentUser = false) {
|
|
446
692
|
if (!this.room) {
|
|
447
|
-
|
|
693
|
+
liveWarn('rooms', this.id, `⚠️ [${this.id}] Cannot broadcast '${type}' - no room set`)
|
|
448
694
|
return
|
|
449
695
|
}
|
|
450
696
|
|
|
@@ -455,7 +701,7 @@ export abstract class LiveComponent<TState = ComponentState> {
|
|
|
455
701
|
excludeUser: excludeCurrentUser ? this.userId : undefined
|
|
456
702
|
}
|
|
457
703
|
|
|
458
|
-
|
|
704
|
+
liveLog('rooms', this.id, `📤 [${this.id}] Broadcasting '${type}' to room '${this.room}'`)
|
|
459
705
|
|
|
460
706
|
// This will be handled by the registry
|
|
461
707
|
this.broadcastToRoom(message)
|
|
@@ -475,14 +721,18 @@ export abstract class LiveComponent<TState = ComponentState> {
|
|
|
475
721
|
*/
|
|
476
722
|
protected emitRoomEvent(event: string, data: any, notifySelf = false): number {
|
|
477
723
|
if (!this.room) {
|
|
478
|
-
|
|
724
|
+
liveWarn('rooms', this.id, `⚠️ [${this.id}] Cannot emit room event '${event}' - no room set`)
|
|
479
725
|
return 0
|
|
480
726
|
}
|
|
481
727
|
|
|
482
728
|
const excludeId = notifySelf ? undefined : this.id
|
|
483
729
|
const notified = roomEvents.emit(this.roomType, this.room, event, data, excludeId)
|
|
484
730
|
|
|
485
|
-
|
|
731
|
+
liveLog('rooms', this.id, `📡 [${this.id}] Room event '${event}' → ${notified} components`)
|
|
732
|
+
|
|
733
|
+
// Debug: track room emit
|
|
734
|
+
_liveDebugger?.trackRoomEmit(this.id, this.room, event, data)
|
|
735
|
+
|
|
486
736
|
return notified
|
|
487
737
|
}
|
|
488
738
|
|
|
@@ -495,7 +745,7 @@ export abstract class LiveComponent<TState = ComponentState> {
|
|
|
495
745
|
*/
|
|
496
746
|
protected onRoomEvent<T = any>(event: string, handler: (data: T) => void): void {
|
|
497
747
|
if (!this.room) {
|
|
498
|
-
|
|
748
|
+
liveWarn('rooms', this.id, `⚠️ [${this.id}] Cannot subscribe to room event '${event}' - no room set`)
|
|
499
749
|
return
|
|
500
750
|
}
|
|
501
751
|
|
|
@@ -510,7 +760,7 @@ export abstract class LiveComponent<TState = ComponentState> {
|
|
|
510
760
|
// Guardar para cleanup no destroy
|
|
511
761
|
this.roomEventUnsubscribers.push(unsubscribe)
|
|
512
762
|
|
|
513
|
-
|
|
763
|
+
liveLog('rooms', this.id, `👂 [${this.id}] Subscribed to room event '${event}'`)
|
|
514
764
|
}
|
|
515
765
|
|
|
516
766
|
/**
|
|
@@ -545,9 +795,9 @@ export abstract class LiveComponent<TState = ComponentState> {
|
|
|
545
795
|
// Registry will handle the actual unsubscription
|
|
546
796
|
}
|
|
547
797
|
|
|
548
|
-
// Generate unique ID
|
|
798
|
+
// Generate unique ID using cryptographically secure randomness
|
|
549
799
|
private generateId(): string {
|
|
550
|
-
return `live-${
|
|
800
|
+
return `live-${crypto.randomUUID()}`
|
|
551
801
|
}
|
|
552
802
|
|
|
553
803
|
// Cleanup when component is destroyed
|
|
@@ -564,6 +814,7 @@ export abstract class LiveComponent<TState = ComponentState> {
|
|
|
564
814
|
}
|
|
565
815
|
this.joinedRooms.clear()
|
|
566
816
|
this.roomHandles.clear()
|
|
817
|
+
this._privateState = {} as TPrivate
|
|
567
818
|
|
|
568
819
|
this.unsubscribeFromRoom()
|
|
569
820
|
// Override in subclasses for custom cleanup
|
|
@@ -638,6 +889,11 @@ export type ActionReturn<
|
|
|
638
889
|
*/
|
|
639
890
|
export type InferComponentState<T extends LiveComponent<any>> = T extends LiveComponent<infer S> ? S : never
|
|
640
891
|
|
|
892
|
+
/**
|
|
893
|
+
* Get the private state type from a LiveComponent class
|
|
894
|
+
*/
|
|
895
|
+
export type InferPrivateState<T extends LiveComponent<any, any>> = T extends LiveComponent<any, infer P> ? P : never
|
|
896
|
+
|
|
641
897
|
/**
|
|
642
898
|
* Type-safe call signature for a component
|
|
643
899
|
* Provides autocomplete for action names and validates payload types
|
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'
|