create-fluxstack 1.13.0 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/LLMD/patterns/anti-patterns.md +100 -0
  2. package/LLMD/reference/routing.md +39 -39
  3. package/LLMD/resources/live-auth.md +20 -2
  4. package/LLMD/resources/live-components.md +94 -10
  5. package/LLMD/resources/live-logging.md +95 -33
  6. package/LLMD/resources/live-upload.md +59 -8
  7. package/app/client/index.html +2 -2
  8. package/app/client/public/favicon.svg +46 -0
  9. package/app/client/src/App.tsx +2 -1
  10. package/app/client/src/assets/fluxstack-static.svg +46 -0
  11. package/app/client/src/assets/fluxstack.svg +183 -0
  12. package/app/client/src/components/AppLayout.tsx +138 -9
  13. package/app/client/src/components/BackButton.tsx +13 -13
  14. package/app/client/src/components/DemoPage.tsx +4 -4
  15. package/app/client/src/live/AuthDemo.tsx +23 -21
  16. package/app/client/src/live/ChatDemo.tsx +2 -2
  17. package/app/client/src/live/CounterDemo.tsx +12 -12
  18. package/app/client/src/live/FormDemo.tsx +2 -2
  19. package/app/client/src/live/LiveDebuggerPanel.tsx +779 -0
  20. package/app/client/src/live/RoomChatDemo.tsx +24 -16
  21. package/app/client/src/main.tsx +13 -13
  22. package/app/client/src/pages/ApiTestPage.tsx +6 -6
  23. package/app/client/src/pages/HomePage.tsx +80 -52
  24. package/app/server/live/LiveAdminPanel.ts +1 -0
  25. package/app/server/live/LiveChat.ts +78 -77
  26. package/app/server/live/LiveCounter.ts +1 -1
  27. package/app/server/live/LiveForm.ts +1 -0
  28. package/app/server/live/LiveLocalCounter.ts +38 -37
  29. package/app/server/live/LiveProtectedChat.ts +1 -0
  30. package/app/server/live/LiveRoomChat.ts +1 -0
  31. package/app/server/live/LiveUpload.ts +1 -0
  32. package/app/server/live/register-components.ts +19 -19
  33. package/config/system/runtime.config.ts +4 -0
  34. package/core/build/optimizer.ts +235 -235
  35. package/core/client/components/Live.tsx +17 -11
  36. package/core/client/components/LiveDebugger.tsx +1324 -0
  37. package/core/client/hooks/AdaptiveChunkSizer.ts +215 -215
  38. package/core/client/hooks/useLiveComponent.ts +11 -1
  39. package/core/client/hooks/useLiveDebugger.ts +392 -0
  40. package/core/client/index.ts +14 -0
  41. package/core/plugins/built-in/index.ts +134 -134
  42. package/core/plugins/built-in/live-components/commands/create-live-component.ts +4 -0
  43. package/core/plugins/built-in/vite/index.ts +75 -21
  44. package/core/server/index.ts +15 -15
  45. package/core/server/live/ComponentRegistry.ts +55 -26
  46. package/core/server/live/FileUploadManager.ts +188 -24
  47. package/core/server/live/LiveDebugger.ts +462 -0
  48. package/core/server/live/LiveLogger.ts +38 -5
  49. package/core/server/live/LiveRoomManager.ts +17 -1
  50. package/core/server/live/StateSignature.ts +87 -27
  51. package/core/server/live/WebSocketConnectionManager.ts +11 -10
  52. package/core/server/live/auto-generated-components.ts +1 -1
  53. package/core/server/live/websocket-plugin.ts +233 -8
  54. package/core/server/plugins/static-files-plugin.ts +179 -69
  55. package/core/types/build.ts +219 -219
  56. package/core/types/plugin.ts +107 -107
  57. package/core/types/types.ts +145 -9
  58. package/core/utils/logger/startup-banner.ts +82 -82
  59. package/core/utils/version.ts +6 -6
  60. package/package.json +1 -1
  61. package/app/client/src/assets/react.svg +0 -1
@@ -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 static actionAuth config
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
- - Automatic state persistence and re-hydration
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 logging = ['lifecycle', 'messages'] as const // Per-component logging (optional)
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
- // Client can call this without defining a custom action
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 called from the client:
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 }) {