create-fluxstack 1.13.0 → 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LLMD/patterns/anti-patterns.md +100 -0
- package/LLMD/reference/routing.md +39 -39
- package/LLMD/resources/live-auth.md +20 -2
- package/LLMD/resources/live-components.md +94 -10
- package/LLMD/resources/live-logging.md +95 -33
- package/LLMD/resources/live-upload.md +59 -8
- package/app/client/index.html +2 -2
- package/app/client/public/favicon.svg +46 -0
- package/app/client/src/App.tsx +2 -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 +138 -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 +23 -21
- 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/live/LiveAdminPanel.ts +1 -0
- package/app/server/live/LiveChat.ts +78 -77
- package/app/server/live/LiveCounter.ts +1 -1
- package/app/server/live/LiveForm.ts +1 -0
- package/app/server/live/LiveLocalCounter.ts +38 -37
- package/app/server/live/LiveProtectedChat.ts +1 -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/config/system/runtime.config.ts +4 -0
- package/core/build/optimizer.ts +235 -235
- package/core/client/components/Live.tsx +17 -11
- package/core/client/components/LiveDebugger.tsx +1324 -0
- package/core/client/hooks/AdaptiveChunkSizer.ts +215 -215
- package/core/client/hooks/useLiveComponent.ts +11 -1
- package/core/client/hooks/useLiveDebugger.ts +392 -0
- package/core/client/index.ts +14 -0
- package/core/plugins/built-in/index.ts +134 -134
- package/core/plugins/built-in/live-components/commands/create-live-component.ts +4 -0
- package/core/plugins/built-in/vite/index.ts +75 -21
- package/core/server/index.ts +15 -15
- package/core/server/live/ComponentRegistry.ts +55 -26
- package/core/server/live/FileUploadManager.ts +188 -24
- package/core/server/live/LiveDebugger.ts +462 -0
- package/core/server/live/LiveLogger.ts +38 -5
- package/core/server/live/LiveRoomManager.ts +17 -1
- package/core/server/live/StateSignature.ts +87 -27
- package/core/server/live/WebSocketConnectionManager.ts +11 -10
- package/core/server/live/auto-generated-components.ts +1 -1
- package/core/server/live/websocket-plugin.ts +233 -8
- package/core/server/plugins/static-files-plugin.ts +179 -69
- package/core/types/build.ts +219 -219
- package/core/types/plugin.ts +107 -107
- package/core/types/types.ts +145 -9
- package/core/utils/logger/startup-banner.ts +82 -82
- package/core/utils/version.ts +6 -6
- package/package.json +1 -1
- package/app/client/src/assets/react.svg +0 -1
|
@@ -277,6 +277,100 @@ if (import.meta.env.DEV) {
|
|
|
277
277
|
}
|
|
278
278
|
```
|
|
279
279
|
|
|
280
|
+
## Live Component Security Anti-Patterns
|
|
281
|
+
|
|
282
|
+
### Never Omit publicActions
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
// ❌ WRONG - No publicActions = ALL remote actions blocked (secure by default)
|
|
286
|
+
export class MyComponent extends LiveComponent<State> {
|
|
287
|
+
static componentName = 'MyComponent'
|
|
288
|
+
static defaultState = { count: 0 }
|
|
289
|
+
|
|
290
|
+
async increment() { this.state.count++ } // Client CANNOT call this!
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ✅ CORRECT - Explicitly whitelist callable methods
|
|
294
|
+
export class MyComponent extends LiveComponent<State> {
|
|
295
|
+
static componentName = 'MyComponent'
|
|
296
|
+
static publicActions = ['increment'] as const // Only increment is callable
|
|
297
|
+
static defaultState = { count: 0 }
|
|
298
|
+
|
|
299
|
+
async increment() { this.state.count++ }
|
|
300
|
+
|
|
301
|
+
// Internal helper - not in publicActions, so not callable from client
|
|
302
|
+
private _recalculate() { /* ... */ }
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
**Why**: Components without `publicActions` deny ALL remote actions. This is secure by default - if you forget, nothing is exposed rather than everything.
|
|
307
|
+
|
|
308
|
+
### Never Include setValue Without Careful Consideration
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
// ❌ DANGEROUS - setValue allows client to set ANY state key
|
|
312
|
+
static publicActions = ['sendMessage', 'setValue'] as const
|
|
313
|
+
// Client can now do: component.setValue({ key: 'isAdmin', value: true })
|
|
314
|
+
|
|
315
|
+
// ✅ CORRECT - Only expose specific, safe actions
|
|
316
|
+
static publicActions = ['sendMessage', 'deleteMessage'] as const
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
**Why**: `setValue` is a generic action that allows the client to modify any state property. Only include it if all state fields are safe for clients to modify.
|
|
320
|
+
|
|
321
|
+
### Never Trust MIME Types Alone for Uploads
|
|
322
|
+
|
|
323
|
+
```typescript
|
|
324
|
+
// ❌ WRONG - Only checking MIME type header (easily spoofed)
|
|
325
|
+
if (file.type === 'image/jpeg') {
|
|
326
|
+
// Accept file - but it could be an EXE with a fake MIME header!
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ✅ CORRECT - Framework validates magic bytes automatically
|
|
330
|
+
// FileUploadManager.validateContentMagicBytes() runs on completeUpload()
|
|
331
|
+
// No manual code needed - the framework handles this
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
**Why**: MIME types come from the client and can be spoofed. The framework validates actual file content (magic bytes) against the claimed type.
|
|
335
|
+
|
|
336
|
+
### Never Store Sensitive Data in State
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
// ❌ WRONG - Token goes to the client via STATE_UPDATE/STATE_DELTA
|
|
340
|
+
export class Chat extends LiveComponent<State> {
|
|
341
|
+
static defaultState = { messages: [], token: '' } // token synced to client!
|
|
342
|
+
static publicActions = ['connect'] as const
|
|
343
|
+
|
|
344
|
+
async connect(payload: { token: string }) {
|
|
345
|
+
this.state.token = payload.token // 💀 Visible in browser DevTools!
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ✅ CORRECT - Use $private for server-only data
|
|
350
|
+
export class Chat extends LiveComponent<State> {
|
|
351
|
+
static defaultState = { messages: [] as string[] }
|
|
352
|
+
static publicActions = ['connect'] as const
|
|
353
|
+
|
|
354
|
+
async connect(payload: { token: string }) {
|
|
355
|
+
this.$private.token = payload.token // 🔒 Never leaves the server
|
|
356
|
+
this.state.messages = await fetch(this.$private.token)
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
**Why**: Everything in `state` is serialized and sent to the client via WebSocket. Use `$private` for tokens, API keys, internal IDs, or any data the client should not see.
|
|
362
|
+
|
|
363
|
+
### Never Ignore Double Extensions
|
|
364
|
+
|
|
365
|
+
```typescript
|
|
366
|
+
// ❌ WRONG - Only checking last extension
|
|
367
|
+
const ext = filename.split('.').pop() // Returns 'jpg' for 'malware.exe.jpg'
|
|
368
|
+
|
|
369
|
+
// ✅ CORRECT - Framework checks all intermediate extensions automatically
|
|
370
|
+
// FileUploadManager blocks files like 'malware.exe.jpg'
|
|
371
|
+
// No manual code needed - handled at framework level
|
|
372
|
+
```
|
|
373
|
+
|
|
280
374
|
## Summary Table
|
|
281
375
|
|
|
282
376
|
| Anti-Pattern | Impact | Solution |
|
|
@@ -288,6 +382,10 @@ if (import.meta.env.DEV) {
|
|
|
288
382
|
| Deep relative imports | Fragile paths | Use aliases |
|
|
289
383
|
| NPM plugins without whitelist | Security risk | Set PLUGINS_ALLOWED |
|
|
290
384
|
| Business logic in routes | Unmaintainable | Use controllers |
|
|
385
|
+
| Missing `publicActions` | All actions blocked | Always define whitelist |
|
|
386
|
+
| Including `setValue` carelessly | Privilege escalation | Use specific actions |
|
|
387
|
+
| Sensitive data in `state` | Data leak to client | Use `$private` instead |
|
|
388
|
+
| Trusting MIME types alone | File disguise attacks | Framework validates magic bytes |
|
|
291
389
|
|
|
292
390
|
## Related
|
|
293
391
|
|
|
@@ -295,3 +393,5 @@ if (import.meta.env.DEV) {
|
|
|
295
393
|
- [Type Safety](./type-safety.md)
|
|
296
394
|
- [Plugin Security](../core/plugin-system.md)
|
|
297
395
|
- [Routes with Eden Treaty](../resources/routes-eden.md)
|
|
396
|
+
- [Live Components](../resources/live-components.md)
|
|
397
|
+
- [Live Upload](../resources/live-upload.md)
|
|
@@ -1,39 +1,39 @@
|
|
|
1
|
-
# Routing (React Router v7)
|
|
2
|
-
|
|
3
|
-
FluxStack uses **React Router v7** via the `react-router` package for web routing.
|
|
4
|
-
|
|
5
|
-
## Where It Lives
|
|
6
|
-
|
|
7
|
-
- Router provider: `app/client/src/main.tsx`
|
|
8
|
-
- Routes and pages: `app/client/src/App.tsx`
|
|
9
|
-
- Pages: `app/client/src/pages/*`
|
|
10
|
-
- Shared layout: `app/client/src/components/AppLayout.tsx`
|
|
11
|
-
|
|
12
|
-
## Why `react-router` (not `react-router-dom`)
|
|
13
|
-
|
|
14
|
-
In v7, the React Router team recommends using the core `react-router` package
|
|
15
|
-
directly for web apps. The `react-router-dom` package remains as a compatibility
|
|
16
|
-
re-export for older apps, but new projects should import from `react-router`.
|
|
17
|
-
|
|
18
|
-
## Example: Adding a New Route
|
|
19
|
-
|
|
20
|
-
1. Create a page in `app/client/src/pages/MyPage.tsx`
|
|
21
|
-
2. Add a route in `app/client/src/App.tsx`:
|
|
22
|
-
|
|
23
|
-
```tsx
|
|
24
|
-
import { MyPage } from './pages/MyPage'
|
|
25
|
-
|
|
26
|
-
<Route path="/my-page" element={<MyPage />} />
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
3. Add a nav link in `app/client/src/components/AppLayout.tsx`
|
|
30
|
-
|
|
31
|
-
## Current Demo Routes
|
|
32
|
-
|
|
33
|
-
- `/` Home
|
|
34
|
-
- `/counter` Live Counter
|
|
35
|
-
- `/form` Live Form
|
|
36
|
-
- `/upload` Live Upload
|
|
37
|
-
- `/chat` Live Chat
|
|
38
|
-
- `/api-test` Eden Treaty API Test
|
|
39
|
-
|
|
1
|
+
# Routing (React Router v7)
|
|
2
|
+
|
|
3
|
+
FluxStack uses **React Router v7** via the `react-router` package for web routing.
|
|
4
|
+
|
|
5
|
+
## Where It Lives
|
|
6
|
+
|
|
7
|
+
- Router provider: `app/client/src/main.tsx`
|
|
8
|
+
- Routes and pages: `app/client/src/App.tsx`
|
|
9
|
+
- Pages: `app/client/src/pages/*`
|
|
10
|
+
- Shared layout: `app/client/src/components/AppLayout.tsx`
|
|
11
|
+
|
|
12
|
+
## Why `react-router` (not `react-router-dom`)
|
|
13
|
+
|
|
14
|
+
In v7, the React Router team recommends using the core `react-router` package
|
|
15
|
+
directly for web apps. The `react-router-dom` package remains as a compatibility
|
|
16
|
+
re-export for older apps, but new projects should import from `react-router`.
|
|
17
|
+
|
|
18
|
+
## Example: Adding a New Route
|
|
19
|
+
|
|
20
|
+
1. Create a page in `app/client/src/pages/MyPage.tsx`
|
|
21
|
+
2. Add a route in `app/client/src/App.tsx`:
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
import { MyPage } from './pages/MyPage'
|
|
25
|
+
|
|
26
|
+
<Route path="/my-page" element={<MyPage />} />
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
3. Add a nav link in `app/client/src/components/AppLayout.tsx`
|
|
30
|
+
|
|
31
|
+
## Current Demo Routes
|
|
32
|
+
|
|
33
|
+
- `/` Home
|
|
34
|
+
- `/counter` Live Counter
|
|
35
|
+
- `/form` Live Form
|
|
36
|
+
- `/upload` Live Upload
|
|
37
|
+
- `/chat` Live Chat
|
|
38
|
+
- `/api-test` Eden Treaty API Test
|
|
39
|
+
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
## Quick Facts
|
|
6
6
|
|
|
7
|
+
- **`publicActions` is the foundation** - Only whitelisted methods can be called remotely
|
|
7
8
|
- Declarative auth configuration via `static auth` and `static actionAuth`
|
|
8
9
|
- Role-based access control (RBAC) with OR logic
|
|
9
10
|
- Permission-based access control with AND logic
|
|
@@ -22,6 +23,7 @@ import type { LiveComponentAuth } from '@core/server/live/auth/types'
|
|
|
22
23
|
|
|
23
24
|
export class ProtectedChat extends LiveComponent<typeof ProtectedChat.defaultState> {
|
|
24
25
|
static componentName = 'ProtectedChat'
|
|
26
|
+
static publicActions = ['sendMessage'] as const // 🔒 REQUIRED
|
|
25
27
|
static defaultState = {
|
|
26
28
|
messages: [] as string[]
|
|
27
29
|
}
|
|
@@ -47,6 +49,7 @@ import type { LiveComponentAuth } from '@core/server/live/auth/types'
|
|
|
47
49
|
|
|
48
50
|
export class AdminPanel extends LiveComponent<typeof AdminPanel.defaultState> {
|
|
49
51
|
static componentName = 'AdminPanel'
|
|
52
|
+
static publicActions = ['deleteUser'] as const // 🔒 REQUIRED
|
|
50
53
|
static defaultState = {
|
|
51
54
|
users: [] as { id: string; name: string; role: string }[]
|
|
52
55
|
}
|
|
@@ -74,6 +77,7 @@ import type { LiveComponentAuth } from '@core/server/live/auth/types'
|
|
|
74
77
|
|
|
75
78
|
export class ContentEditor extends LiveComponent<typeof ContentEditor.defaultState> {
|
|
76
79
|
static componentName = 'ContentEditor'
|
|
80
|
+
static publicActions = ['editContent', 'saveContent'] as const // 🔒 REQUIRED
|
|
77
81
|
static defaultState = {
|
|
78
82
|
content: ''
|
|
79
83
|
}
|
|
@@ -95,6 +99,7 @@ import type { LiveComponentAuth, LiveActionAuthMap } from '@core/server/live/aut
|
|
|
95
99
|
|
|
96
100
|
export class ModerationPanel extends LiveComponent<typeof ModerationPanel.defaultState> {
|
|
97
101
|
static componentName = 'ModerationPanel'
|
|
102
|
+
static publicActions = ['getReports', 'deleteReport', 'banUser'] as const // 🔒 REQUIRED
|
|
98
103
|
static defaultState = {
|
|
99
104
|
reports: [] as any[]
|
|
100
105
|
}
|
|
@@ -104,7 +109,7 @@ export class ModerationPanel extends LiveComponent<typeof ModerationPanel.defaul
|
|
|
104
109
|
required: true
|
|
105
110
|
}
|
|
106
111
|
|
|
107
|
-
// Per-action auth
|
|
112
|
+
// Per-action auth (works together with publicActions)
|
|
108
113
|
static actionAuth: LiveActionAuthMap = {
|
|
109
114
|
deleteReport: { permissions: ['reports.delete'] },
|
|
110
115
|
banUser: { roles: ['admin', 'moderator'] }
|
|
@@ -382,7 +387,7 @@ For testing, a `DevAuthProvider` with simple tokens is available:
|
|
|
382
387
|
│ 1. WebSocket connect → store authContext on ws.data │
|
|
383
388
|
│ 2. AUTH message → liveAuthManager.authenticate() │
|
|
384
389
|
│ 3. COMPONENT_MOUNT → check static auth config │
|
|
385
|
-
│ 4. CALL_ACTION → check
|
|
390
|
+
│ 4. CALL_ACTION → check blocklist → publicActions → actionAuth │
|
|
386
391
|
│ 5. Component has access to this.$auth │
|
|
387
392
|
└─────────────────────────────────────────────────────────────┘
|
|
388
393
|
```
|
|
@@ -416,9 +421,21 @@ interface LiveAuthCredentials {
|
|
|
416
421
|
}
|
|
417
422
|
```
|
|
418
423
|
|
|
424
|
+
## Security Layers (Action Execution Order)
|
|
425
|
+
|
|
426
|
+
When a client calls an action, the server checks in this order:
|
|
427
|
+
|
|
428
|
+
1. **Blocklist** - Internal methods (destroy, setState, emit, etc.) are always blocked
|
|
429
|
+
2. **Private methods** - Methods starting with `_` or `#` are blocked
|
|
430
|
+
3. **publicActions** - Action must be in the whitelist (mandatory, no fallback)
|
|
431
|
+
4. **actionAuth** - Per-action role/permission check (if defined)
|
|
432
|
+
5. **Method exists** - Action must exist on the component instance
|
|
433
|
+
6. **Object.prototype** - Blocks toString, valueOf, hasOwnProperty
|
|
434
|
+
|
|
419
435
|
## Critical Rules
|
|
420
436
|
|
|
421
437
|
**ALWAYS:**
|
|
438
|
+
- Define `static publicActions` listing all client-callable methods (MANDATORY)
|
|
422
439
|
- Define `static auth` for protected components
|
|
423
440
|
- Define `static actionAuth` for protected actions
|
|
424
441
|
- Use `$auth.hasRole()` / `$auth.hasPermission()` in action logic
|
|
@@ -426,6 +443,7 @@ interface LiveAuthCredentials {
|
|
|
426
443
|
- Handle `AUTH_DENIED` errors in client UI
|
|
427
444
|
|
|
428
445
|
**NEVER:**
|
|
446
|
+
- Omit `publicActions` (component will deny ALL remote actions)
|
|
429
447
|
- Store sensitive data in component state
|
|
430
448
|
- Trust client-side auth checks alone (always verify server-side)
|
|
431
449
|
- Expose tokens in error messages
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
- Server-side state management with WebSocket sync
|
|
8
8
|
- **Direct state access** - `this.count++` auto-syncs (v1.13.0)
|
|
9
|
-
-
|
|
9
|
+
- **Mandatory `publicActions`** - Only whitelisted methods are callable from client (secure by default)
|
|
10
|
+
- Automatic state persistence and re-hydration (with anti-replay nonces)
|
|
10
11
|
- Room-based event system for multi-user sync
|
|
11
12
|
- Type-safe client-server communication (FluxStackWebSocket)
|
|
12
13
|
- Built-in connection management and recovery
|
|
@@ -25,7 +26,8 @@ import type { CounterDemo as _Client } from '@client/src/live/CounterDemo'
|
|
|
25
26
|
|
|
26
27
|
export class LiveCounter extends LiveComponent<typeof LiveCounter.defaultState> {
|
|
27
28
|
static componentName = 'LiveCounter'
|
|
28
|
-
static
|
|
29
|
+
static publicActions = ['increment', 'decrement', 'reset'] as const // 🔒 REQUIRED
|
|
30
|
+
// static logging = ['lifecycle', 'messages'] as const // Console logging (optional, prefer DEBUG_LIVE)
|
|
29
31
|
static defaultState = {
|
|
30
32
|
count: 0
|
|
31
33
|
}
|
|
@@ -56,6 +58,7 @@ export class LiveCounter extends LiveComponent<typeof LiveCounter.defaultState>
|
|
|
56
58
|
1. **Direct state access** - `this.count++` instead of `this.state.count++`
|
|
57
59
|
2. **declare keyword** - TypeScript hint for dynamic properties
|
|
58
60
|
3. **Cleaner code** - No need to prefix with `this.state.`
|
|
61
|
+
4. **Mandatory `publicActions`** - Components without it deny ALL remote actions (secure by default)
|
|
59
62
|
|
|
60
63
|
### Key Changes in v1.12.0
|
|
61
64
|
|
|
@@ -72,6 +75,7 @@ import { LiveComponent, type FluxStackWebSocket } from '@core/types/types'
|
|
|
72
75
|
|
|
73
76
|
export class LiveCounter extends LiveComponent<typeof LiveCounter.defaultState> {
|
|
74
77
|
static componentName = 'LiveCounter'
|
|
78
|
+
static publicActions = ['increment'] as const // 🔒 REQUIRED
|
|
75
79
|
static defaultState = {
|
|
76
80
|
count: 0,
|
|
77
81
|
lastUpdatedBy: null as string | null,
|
|
@@ -186,16 +190,86 @@ this.setState(prev => ({
|
|
|
186
190
|
|
|
187
191
|
### setValue (Generic Action)
|
|
188
192
|
|
|
189
|
-
Built-in action to set any state key from the client
|
|
193
|
+
Built-in action to set any state key from the client. **Must be explicitly included in `publicActions` to be callable:**
|
|
190
194
|
|
|
191
195
|
```typescript
|
|
192
|
-
//
|
|
196
|
+
// Server: opt-in to setValue
|
|
197
|
+
static publicActions = ['increment', 'setValue'] as const // Must include 'setValue'
|
|
198
|
+
|
|
199
|
+
// Client can then call:
|
|
193
200
|
await component.setValue({ key: 'count', value: 42 })
|
|
194
201
|
```
|
|
195
202
|
|
|
203
|
+
> **Security note:** `setValue` is powerful - it allows the client to set any state key. Only add it to `publicActions` if you trust the client to modify any state field.
|
|
204
|
+
|
|
205
|
+
### $private — Server-Only State
|
|
206
|
+
|
|
207
|
+
`$private` is a key-value store that lives **exclusively on the server**. It is NEVER synchronized with the client — no `STATE_UPDATE`, no `STATE_DELTA`, not included in `getSerializableState()`.
|
|
208
|
+
|
|
209
|
+
Use it for sensitive data like tokens, API keys, internal IDs, or any server-side bookkeeping:
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
export class Chat extends LiveComponent<typeof Chat.defaultState> {
|
|
213
|
+
static componentName = 'Chat'
|
|
214
|
+
static publicActions = ['connect', 'sendMessage'] as const
|
|
215
|
+
static defaultState = { messages: [] as string[] }
|
|
216
|
+
|
|
217
|
+
async connect(payload: { token: string }) {
|
|
218
|
+
// 🔒 Stays on server — never sent to client
|
|
219
|
+
this.$private.token = payload.token
|
|
220
|
+
this.$private.apiKey = await getApiKey()
|
|
221
|
+
|
|
222
|
+
// ✅ Only UI data goes to state (synced with client)
|
|
223
|
+
this.state.messages = await fetchMessages(this.$private.token)
|
|
224
|
+
return { success: true }
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async sendMessage(payload: { text: string }) {
|
|
228
|
+
// Use $private data for server-side logic
|
|
229
|
+
await postToAPI(this.$private.apiKey, payload.text)
|
|
230
|
+
this.state.messages = [...this.state.messages, payload.text]
|
|
231
|
+
return { success: true }
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
#### Typed $private (optional)
|
|
237
|
+
|
|
238
|
+
Pass a second generic to get full autocomplete and type checking:
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
interface ChatPrivate {
|
|
242
|
+
token: string
|
|
243
|
+
apiKey: string
|
|
244
|
+
retryCount: number
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export class Chat extends LiveComponent<typeof Chat.defaultState, ChatPrivate> {
|
|
248
|
+
static componentName = 'Chat'
|
|
249
|
+
static publicActions = ['connect'] as const
|
|
250
|
+
static defaultState = { messages: [] as string[] }
|
|
251
|
+
|
|
252
|
+
async connect(payload: { token: string }) {
|
|
253
|
+
this.$private.token = payload.token // ✅ autocomplete
|
|
254
|
+
this.$private.retryCount = 0 // ✅ must be number
|
|
255
|
+
this.$private.tokkken = 'x' // ❌ TypeScript error (typo)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
The second generic defaults to `Record<string, any>`, so existing components work without changes.
|
|
261
|
+
|
|
262
|
+
**Key facts:**
|
|
263
|
+
- Starts as an empty `{}` — no static default needed
|
|
264
|
+
- Mutations do NOT trigger any WebSocket messages
|
|
265
|
+
- Cleared automatically on `destroy()`
|
|
266
|
+
- Lost on rehydration (re-populate in your action handlers)
|
|
267
|
+
- Blocked from remote access (`$private` and `_privateState` are in BLOCKED_ACTIONS)
|
|
268
|
+
- Optional `TPrivate` generic for full type safety
|
|
269
|
+
|
|
196
270
|
### getSerializableState
|
|
197
271
|
|
|
198
|
-
Get current state for serialization:
|
|
272
|
+
Get current state for serialization (does NOT include `$private`):
|
|
199
273
|
|
|
200
274
|
```typescript
|
|
201
275
|
const currentState = this.getSerializableState()
|
|
@@ -260,11 +334,13 @@ const counter = Live.use(LiveCounter, {
|
|
|
260
334
|
|
|
261
335
|
## Actions
|
|
262
336
|
|
|
263
|
-
Actions are methods
|
|
337
|
+
Actions are methods callable from the client. **Only methods listed in `publicActions` can be called remotely.** Components without `publicActions` deny ALL remote actions.
|
|
264
338
|
|
|
265
339
|
```typescript
|
|
266
340
|
// Server-side
|
|
267
341
|
export class LiveForm extends LiveComponent<FormState> {
|
|
342
|
+
static publicActions = ['submit', 'validate'] as const // 🔒 REQUIRED
|
|
343
|
+
|
|
268
344
|
async submit() {
|
|
269
345
|
const { name, email } = this.state
|
|
270
346
|
|
|
@@ -429,10 +505,11 @@ On reconnect, components restore previous state:
|
|
|
429
505
|
|
|
430
506
|
1. Client stores signed state in localStorage
|
|
431
507
|
2. On reconnect, sends signed state to server
|
|
432
|
-
3. Server validates signature
|
|
508
|
+
3. Server validates signature (HMAC-SHA256) and **anti-replay nonce**
|
|
433
509
|
4. Component re-hydrated with previous state
|
|
510
|
+
5. State expires after 24 hours (configurable)
|
|
434
511
|
|
|
435
|
-
No manual code needed - automatic.
|
|
512
|
+
No manual code needed - automatic. Each signed state includes a cryptographic nonce that is consumed on validation, preventing replay attacks.
|
|
436
513
|
|
|
437
514
|
## Multi-User Synchronization
|
|
438
515
|
|
|
@@ -530,8 +607,9 @@ app/client/src/live/
|
|
|
530
607
|
|
|
531
608
|
Each server file contains:
|
|
532
609
|
- `static componentName` - Component identifier
|
|
610
|
+
- `static publicActions` - **REQUIRED** whitelist of client-callable methods
|
|
533
611
|
- `static defaultState` - Initial state object
|
|
534
|
-
- `static logging` - Per-component log control (optional, see [Live Logging](./live-logging.md))
|
|
612
|
+
- `static logging` - Per-component console log control (optional, prefer `DEBUG_LIVE=true` for debug panel — see [Live Logging](./live-logging.md))
|
|
535
613
|
- Component class extending `LiveComponent`
|
|
536
614
|
- Client link via `import type { Demo as _Client }`
|
|
537
615
|
|
|
@@ -583,6 +661,7 @@ export class MyComponent extends LiveComponent<State> {
|
|
|
583
661
|
|
|
584
662
|
**ALWAYS:**
|
|
585
663
|
- Define `static componentName` matching class name
|
|
664
|
+
- Define `static publicActions` listing ALL client-callable methods (MANDATORY)
|
|
586
665
|
- Define `static defaultState` inside the class
|
|
587
666
|
- Use `typeof ClassName.defaultState` for type parameter
|
|
588
667
|
- Use `declare` for each state property (TypeScript type hint)
|
|
@@ -592,12 +671,15 @@ export class MyComponent extends LiveComponent<State> {
|
|
|
592
671
|
- Add client link: `import type { Demo as _Client } from '@client/...'`
|
|
593
672
|
|
|
594
673
|
**NEVER:**
|
|
674
|
+
- Omit `static publicActions` (component will deny ALL remote actions)
|
|
595
675
|
- Export separate `defaultState` constant (use static)
|
|
596
676
|
- Create constructor just to call super() (not needed)
|
|
597
677
|
- Forget `static componentName` (breaks minification)
|
|
598
678
|
- Emit room events without subscribing first
|
|
599
679
|
- Store non-serializable data in state
|
|
600
|
-
- Use reserved names for state properties (id, state, ws, room, userId, $room, $rooms, broadcastToRoom, roomType)
|
|
680
|
+
- Use reserved names for state properties (id, state, ws, room, userId, $room, $rooms, $private, broadcastToRoom, roomType)
|
|
681
|
+
- Include `setValue` in `publicActions` unless you trust clients to modify any state key
|
|
682
|
+
- Store sensitive data (tokens, API keys, secrets) in `state` — use `$private` instead
|
|
601
683
|
|
|
602
684
|
**STATE UPDATES (v1.13.0) — all auto-sync via Proxy:**
|
|
603
685
|
```typescript
|
|
@@ -636,6 +718,8 @@ import { liveUploadDefaultState, type LiveUploadState } from '@app/shared'
|
|
|
636
718
|
export const defaultState: LiveUploadState = liveUploadDefaultState
|
|
637
719
|
|
|
638
720
|
export class LiveUpload extends LiveComponent<LiveUploadState> {
|
|
721
|
+
static componentName = 'LiveUpload'
|
|
722
|
+
static publicActions = ['startUpload', 'updateProgress', 'completeUpload', 'failUpload', 'reset'] as const
|
|
639
723
|
static defaultState = defaultState
|
|
640
724
|
|
|
641
725
|
constructor(initialState: Partial<typeof defaultState>, ws: any, options?: { room?: string; userId?: string }) {
|