create-fluxstack 1.17.1 → 1.18.1

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 (93) hide show
  1. package/LLMD/resources/live-auth.md +462 -465
  2. package/app/client/.live-stubs/LiveAdminPanel.js +15 -0
  3. package/app/client/.live-stubs/LiveCounter.js +9 -0
  4. package/app/client/.live-stubs/LiveForm.js +11 -0
  5. package/app/client/.live-stubs/LiveLocalCounter.js +8 -0
  6. package/app/client/.live-stubs/LivePingPong.js +10 -0
  7. package/app/client/.live-stubs/LiveRoomChat.js +11 -0
  8. package/app/client/.live-stubs/LiveSharedCounter.js +10 -0
  9. package/app/client/.live-stubs/LiveUpload.js +15 -0
  10. package/app/client/src/App.tsx +45 -3
  11. package/app/client/src/components/AppLayout.tsx +10 -1
  12. package/app/client/src/components/ErrorBoundary.tsx +117 -0
  13. package/app/client/src/components/LiveErrorBoundary.tsx +87 -0
  14. package/app/client/src/components/LiveUploadWidget.tsx +10 -14
  15. package/app/client/src/lib/eden-api.ts +6 -0
  16. package/app/client/src/lib/plugin-hooks.ts +82 -0
  17. package/app/client/src/live/AuthDemo.tsx +0 -1
  18. package/app/client/src/live/FormDemo.tsx +1 -1
  19. package/app/client/src/live/PingPongDemo.tsx +4 -1
  20. package/app/client/src/live/RoomChatDemo.tsx +90 -50
  21. package/app/client/src/live/SharedCounterDemo.tsx +5 -0
  22. package/app/server/auth/AuthManager.ts +24 -0
  23. package/app/server/auth/contracts.ts +12 -1
  24. package/app/server/auth/errors.ts +84 -0
  25. package/app/server/auth/guards/TokenGuard.ts +5 -2
  26. package/app/server/auth/index.ts +1 -1
  27. package/app/server/auth/providers/InMemoryProvider.ts +1 -1
  28. package/app/server/index.ts +3 -4
  29. package/app/server/live/LiveAdminPanel.ts +8 -8
  30. package/app/server/live/LiveForm.ts +1 -1
  31. package/app/server/live/LiveProtectedChat.ts +5 -5
  32. package/app/server/live/LiveRoomChat.ts +50 -28
  33. package/app/server/live/LiveUpload.ts +17 -3
  34. package/app/server/live/auto-generated-components.ts +26 -0
  35. package/app/server/live/rooms/ChatRoom.ts +22 -2
  36. package/app/server/routes/auth.routes.ts +29 -20
  37. package/app/server/routes/index.ts +29 -10
  38. package/app/server/routes/room.routes.ts +6 -6
  39. package/config/index.ts +3 -3
  40. package/config/system/app.config.ts +1 -1
  41. package/config/system/auth.config.ts +1 -1
  42. package/config/system/build.config.ts +8 -6
  43. package/config/system/client.config.ts +6 -4
  44. package/config/system/database.config.ts +1 -1
  45. package/config/system/logger.config.ts +1 -1
  46. package/config/system/monitoring.config.ts +6 -4
  47. package/config/system/plugins.config.ts +1 -1
  48. package/config/system/runtime.config.ts +1 -1
  49. package/config/system/server.config.ts +1 -1
  50. package/config/system/services.config.ts +1 -1
  51. package/config/system/session.config.ts +3 -3
  52. package/config/system/system.config.ts +1 -1
  53. package/core/build/vite-plugins.ts +3 -2
  54. package/core/cli/generators/plugin.ts +1 -1
  55. package/core/config/index.ts +8 -1
  56. package/core/framework/server.ts +19 -3
  57. package/core/index.ts +1 -1
  58. package/core/plugins/index.ts +1 -1
  59. package/core/plugins/manager.ts +5 -1
  60. package/core/plugins/types.ts +17 -1
  61. package/core/server/index.ts +5 -2
  62. package/core/server/live/index.ts +8 -71
  63. package/core/server/plugin-client-hooks.ts +97 -0
  64. package/core/types/types.ts +1 -0
  65. package/core/utils/version.ts +1 -1
  66. package/create-fluxstack.ts +1 -1
  67. package/package.json +107 -104
  68. package/src/client/components/ui/StatusBadge.tsx +23 -0
  69. package/core/utils/config-schema.ts +0 -480
  70. package/core/utils/env.ts +0 -305
  71. package/plugins/crypto-auth/README.md +0 -788
  72. package/plugins/crypto-auth/ai-context.md +0 -1282
  73. package/plugins/crypto-auth/cli/make-protected-route.command.ts +0 -383
  74. package/plugins/crypto-auth/client/CryptoAuthClient.ts +0 -302
  75. package/plugins/crypto-auth/client/components/AuthProvider.tsx +0 -131
  76. package/plugins/crypto-auth/client/components/LoginButton.tsx +0 -138
  77. package/plugins/crypto-auth/client/components/ProtectedRoute.tsx +0 -89
  78. package/plugins/crypto-auth/client/components/index.ts +0 -12
  79. package/plugins/crypto-auth/client/index.ts +0 -12
  80. package/plugins/crypto-auth/config/index.ts +0 -34
  81. package/plugins/crypto-auth/index.ts +0 -173
  82. package/plugins/crypto-auth/package.json +0 -66
  83. package/plugins/crypto-auth/server/AuthMiddleware.ts +0 -181
  84. package/plugins/crypto-auth/server/CryptoAuthLiveProvider.ts +0 -58
  85. package/plugins/crypto-auth/server/CryptoAuthService.ts +0 -186
  86. package/plugins/crypto-auth/server/index.ts +0 -25
  87. package/plugins/crypto-auth/server/middlewares/cryptoAuthAdmin.ts +0 -66
  88. package/plugins/crypto-auth/server/middlewares/cryptoAuthOptional.ts +0 -26
  89. package/plugins/crypto-auth/server/middlewares/cryptoAuthPermissions.ts +0 -77
  90. package/plugins/crypto-auth/server/middlewares/cryptoAuthRequired.ts +0 -45
  91. package/plugins/crypto-auth/server/middlewares/helpers.ts +0 -155
  92. package/plugins/crypto-auth/server/middlewares/index.ts +0 -22
  93. package/plugins/crypto-auth/server/middlewares.ts +0 -19
@@ -1,465 +1,462 @@
1
- # Live Components Authentication
2
-
3
- **Version:** 1.14.0 | **Updated:** 2025-02-12
4
-
5
- ## Quick Facts
6
-
7
- - **`publicActions` is the foundation** - Only whitelisted methods can be called remotely
8
- - Declarative auth configuration via `static auth` and `static actionAuth`
9
- - Role-based access control (RBAC) with OR logic
10
- - Permission-based access control with AND logic
11
- - Auto re-mount when authentication changes
12
- - Pluggable auth providers (JWT, Crypto, Custom)
13
- - `$auth` helper available in component actions
14
-
15
- ## Server-Side: Protected Components
16
-
17
- ### Basic Protection (Auth Required)
18
-
19
- ```typescript
20
- // app/server/live/ProtectedChat.ts
21
- import { LiveComponent } from '@core/types/types'
22
- import type { LiveComponentAuth } from '@core/server/live/auth/types'
23
-
24
- export class ProtectedChat extends LiveComponent<typeof ProtectedChat.defaultState> {
25
- static componentName = 'ProtectedChat'
26
- static publicActions = ['sendMessage'] as const // 🔒 REQUIRED
27
- static defaultState = {
28
- messages: [] as string[]
29
- }
30
-
31
- // Auth required to mount this component
32
- static auth: LiveComponentAuth = {
33
- required: true
34
- }
35
-
36
- async sendMessage(payload: { text: string }) {
37
- // Only authenticated users can call this
38
- return { success: true }
39
- }
40
- }
41
- ```
42
-
43
- ### Role-Based Protection
44
-
45
- ```typescript
46
- // app/server/live/AdminPanel.ts
47
- import { LiveComponent } from '@core/types/types'
48
- import type { LiveComponentAuth } from '@core/server/live/auth/types'
49
-
50
- export class AdminPanel extends LiveComponent<typeof AdminPanel.defaultState> {
51
- static componentName = 'AdminPanel'
52
- static publicActions = ['deleteUser'] as const // 🔒 REQUIRED
53
- static defaultState = {
54
- users: [] as { id: string; name: string; role: string }[]
55
- }
56
-
57
- // Requires auth + admin OR moderator role (OR logic)
58
- static auth: LiveComponentAuth = {
59
- required: true,
60
- roles: ['admin', 'moderator']
61
- }
62
-
63
- async deleteUser(payload: { userId: string }) {
64
- // User info available via $auth
65
- console.log(`User ${this.$auth.user?.id} deleting ${payload.userId}`)
66
- return { success: true }
67
- }
68
- }
69
- ```
70
-
71
- ### Permission-Based Protection
72
-
73
- ```typescript
74
- // app/server/live/ContentEditor.ts
75
- import { LiveComponent } from '@core/types/types'
76
- import type { LiveComponentAuth } from '@core/server/live/auth/types'
77
-
78
- export class ContentEditor extends LiveComponent<typeof ContentEditor.defaultState> {
79
- static componentName = 'ContentEditor'
80
- static publicActions = ['editContent', 'saveContent'] as const // 🔒 REQUIRED
81
- static defaultState = {
82
- content: ''
83
- }
84
-
85
- // Requires ALL permissions (AND logic)
86
- static auth: LiveComponentAuth = {
87
- required: true,
88
- permissions: ['content.read', 'content.write']
89
- }
90
- }
91
- ```
92
-
93
- ### Per-Action Protection
94
-
95
- ```typescript
96
- // app/server/live/ModerationPanel.ts
97
- import { LiveComponent } from '@core/types/types'
98
- import type { LiveComponentAuth, LiveActionAuthMap } from '@core/server/live/auth/types'
99
-
100
- export class ModerationPanel extends LiveComponent<typeof ModerationPanel.defaultState> {
101
- static componentName = 'ModerationPanel'
102
- static publicActions = ['getReports', 'deleteReport', 'banUser'] as const // 🔒 REQUIRED
103
- static defaultState = {
104
- reports: [] as any[]
105
- }
106
-
107
- // Component-level: any authenticated user
108
- static auth: LiveComponentAuth = {
109
- required: true
110
- }
111
-
112
- // Per-action auth (works together with publicActions)
113
- static actionAuth: LiveActionAuthMap = {
114
- deleteReport: { permissions: ['reports.delete'] },
115
- banUser: { roles: ['admin', 'moderator'] }
116
- }
117
-
118
- // Anyone authenticated can view
119
- async getReports() {
120
- return { reports: this.state.reports }
121
- }
122
-
123
- // Requires reports.delete permission
124
- async deleteReport(payload: { reportId: string }) {
125
- return { success: true }
126
- }
127
-
128
- // Requires admin OR moderator role
129
- async banUser(payload: { userId: string }) {
130
- return { success: true }
131
- }
132
- }
133
- ```
134
-
135
- ## Using $auth in Actions
136
-
137
- The `$auth` helper provides access to the authenticated user context:
138
-
139
- ```typescript
140
- export class MyComponent extends LiveComponent<State> {
141
- async myAction() {
142
- // Check if authenticated
143
- if (!this.$auth.authenticated) {
144
- throw new Error('Not authenticated')
145
- }
146
-
147
- // Get user info
148
- const userId = this.$auth.user?.id
149
- const userName = this.$auth.user?.name
150
-
151
- // Check roles
152
- if (this.$auth.hasRole('admin')) {
153
- // Admin-only logic
154
- }
155
-
156
- if (this.$auth.hasAnyRole(['admin', 'moderator'])) {
157
- // Admin OR moderator logic
158
- }
159
-
160
- // Check permissions
161
- if (this.$auth.hasPermission('users.delete')) {
162
- // Has specific permission
163
- }
164
-
165
- if (this.$auth.hasAllPermissions(['users.read', 'users.write'])) {
166
- // Has ALL permissions
167
- }
168
-
169
- return { userId }
170
- }
171
- }
172
- ```
173
-
174
- ### $auth API
175
-
176
- ```typescript
177
- interface LiveAuthContext {
178
- readonly authenticated: boolean
179
- readonly user?: {
180
- id: string
181
- roles?: string[]
182
- permissions?: string[]
183
- [key: string]: unknown // Custom fields
184
- }
185
- readonly token?: string
186
- readonly authenticatedAt?: number
187
-
188
- hasRole(role: string): boolean
189
- hasAnyRole(roles: string[]): boolean
190
- hasAllRoles(roles: string[]): boolean
191
- hasPermission(permission: string): boolean
192
- hasAnyPermission(permissions: string[]): boolean
193
- hasAllPermissions(permissions: string[]): boolean
194
- }
195
- ```
196
-
197
- ## Client-Side: Authentication
198
-
199
- ### Authenticate on Connection
200
-
201
- Pass auth credentials when the WebSocket connects:
202
-
203
- ```typescript
204
- // app/client/src/App.tsx
205
- import { LiveComponentsProvider } from '@/core/client'
206
-
207
- function App() {
208
- const token = localStorage.getItem('auth_token')
209
-
210
- return (
211
- <LiveComponentsProvider
212
- auth={{ token }} // Sent as query param on connect
213
- autoConnect={true}
214
- >
215
- <AppContent />
216
- </LiveComponentsProvider>
217
- )
218
- }
219
- ```
220
-
221
- ### Dynamic Authentication
222
-
223
- Authenticate after connection via `useLiveComponents`:
224
-
225
- ```typescript
226
- import { useLiveComponents } from '@/core/client'
227
-
228
- function LoginForm() {
229
- const { authenticated, authenticate } = useLiveComponents()
230
- const [token, setToken] = useState('')
231
-
232
- const handleLogin = async () => {
233
- const success = await authenticate({ token })
234
- if (success) {
235
- // Components with auth errors will auto re-mount
236
- console.log('Authenticated!')
237
- }
238
- }
239
-
240
- return (
241
- <div>
242
- <p>Status: {authenticated ? 'Logged in' : 'Not logged in'}</p>
243
- <input value={token} onChange={e => setToken(e.target.value)} />
244
- <button onClick={handleLogin}>Login</button>
245
- </div>
246
- )
247
- }
248
- ```
249
-
250
- ### Checking Auth Status in Components
251
-
252
- ```typescript
253
- import { Live } from '@/core/client'
254
- import { AdminPanel } from '@server/live/AdminPanel'
255
-
256
- function AdminSection() {
257
- const panel = Live.use(AdminPanel)
258
-
259
- // Check if authenticated on WebSocket level
260
- if (!panel.$authenticated) {
261
- return <p>Please log in</p>
262
- }
263
-
264
- // Check for auth errors
265
- if (panel.$error?.includes('AUTH_DENIED')) {
266
- return <p>Access denied: {panel.$error}</p>
267
- }
268
-
269
- return <div>{/* Admin content */}</div>
270
- }
271
- ```
272
-
273
- ### Auto Re-mount on Auth Change
274
-
275
- When authentication changes from `false` to `true`, components that failed with `AUTH_DENIED` automatically retry mounting:
276
-
277
- ```typescript
278
- // No manual code needed!
279
- // 1. User tries to mount AdminPanel → AUTH_DENIED
280
- // 2. User calls authenticate({ token: 'admin-token' })
281
- // 3. AdminPanel automatically re-mounts with auth context
282
- ```
283
-
284
- ## Auth Providers
285
-
286
- ### Creating a Custom Provider
287
-
288
- ```typescript
289
- // app/server/auth/MyAuthProvider.ts
290
- import type {
291
- LiveAuthProvider,
292
- LiveAuthCredentials,
293
- LiveAuthContext
294
- } from '@core/server/live/auth/types'
295
- import { AuthenticatedContext } from '@core/server/live/auth/LiveAuthContext'
296
-
297
- export class MyAuthProvider implements LiveAuthProvider {
298
- readonly name = 'my-auth'
299
-
300
- async authenticate(credentials: LiveAuthCredentials): Promise<LiveAuthContext | null> {
301
- const token = credentials.token as string
302
- if (!token) return null
303
-
304
- // Validate token (JWT decode, database lookup, etc.)
305
- const user = await validateToken(token)
306
- if (!user) return null
307
-
308
- return new AuthenticatedContext(
309
- {
310
- id: user.id,
311
- name: user.name,
312
- roles: user.roles,
313
- permissions: user.permissions
314
- },
315
- token
316
- )
317
- }
318
-
319
- // Optional: Custom action authorization
320
- async authorizeAction(
321
- context: LiveAuthContext,
322
- componentName: string,
323
- action: string
324
- ): Promise<boolean> {
325
- // Custom logic (rate limiting, business rules, etc.)
326
- return true
327
- }
328
-
329
- // Optional: Custom room authorization
330
- async authorizeRoom(
331
- context: LiveAuthContext,
332
- roomId: string
333
- ): Promise<boolean> {
334
- // Example: VIP rooms require premium role
335
- if (roomId.startsWith('vip-') && !context.hasRole('premium')) {
336
- return false
337
- }
338
- return true
339
- }
340
- }
341
- ```
342
-
343
- ### Registering Providers
344
-
345
- ```typescript
346
- // app/server/index.ts
347
- import { liveAuthManager } from '@core/server/live/auth'
348
- import { MyAuthProvider } from './auth/MyAuthProvider'
349
-
350
- // Register provider
351
- liveAuthManager.register(new MyAuthProvider())
352
-
353
- // Optional: Set as default (first registered is default)
354
- liveAuthManager.setDefault('my-auth')
355
- ```
356
-
357
- ### Built-in DevAuthProvider (Development)
358
-
359
- For testing, a `DevAuthProvider` with simple tokens is available:
360
-
361
- ```typescript
362
- // Tokens available in development:
363
- // - 'admin-token' → role: admin, all permissions
364
- // - 'user-token' → role: user, basic permissions
365
- // - 'mod-token' → role: moderator
366
-
367
- // Already registered in dev mode automatically
368
- ```
369
-
370
- ## Auth Flow Diagram
371
-
372
- ```
373
- ┌─────────────────────────────────────────────────────────────┐
374
- │ CLIENT │
375
- ├─────────────────────────────────────────────────────────────┤
376
- 1. Connect WebSocket (optional: ?token=xxx) │
377
- │ 2. authenticate({ token }) → AUTH message │
378
- │ 3. Live.use(ProtectedComponent) → COMPONENT_MOUNT │
379
- │ 4. If AUTH_DENIED, wait for auth change │
380
- │ 5. Auth changes → auto re-mount │
381
- └─────────────────────────────────────────────────────────────┘
382
-
383
-
384
- ┌─────────────────────────────────────────────────────────────┐
385
- │ SERVER │
386
- ├─────────────────────────────────────────────────────────────┤
387
- │ 1. WebSocket connect → store authContext on ws.data │
388
- │ 2. AUTH message → liveAuthManager.authenticate() │
389
- │ 3. COMPONENT_MOUNT → check static auth config │
390
- │ 4. CALL_ACTION → check blocklist → publicActions → actionAuth │
391
- │ 5. Component has access to this.$auth │
392
- └─────────────────────────────────────────────────────────────┘
393
- ```
394
-
395
- ## Configuration Types
396
-
397
- ```typescript
398
- // Component-level auth
399
- interface LiveComponentAuth {
400
- required?: boolean // Must be authenticated to mount
401
- roles?: string[] // Required roles (OR logic - any role)
402
- permissions?: string[] // Required permissions (AND logic - all)
403
- }
404
-
405
- // Action-level auth
406
- interface LiveActionAuth {
407
- roles?: string[] // Required roles (OR logic)
408
- permissions?: string[] // Required permissions (AND logic)
409
- }
410
-
411
- type LiveActionAuthMap = Record<string, LiveActionAuth>
412
-
413
- // Credentials from client
414
- interface LiveAuthCredentials {
415
- token?: string
416
- publicKey?: string // For crypto auth
417
- signature?: string // For crypto auth
418
- timestamp?: number
419
- nonce?: string
420
- [key: string]: unknown // Custom fields
421
- }
422
- ```
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
-
435
- ## Critical Rules
436
-
437
- **ALWAYS:**
438
- - Define `static publicActions` listing all client-callable methods (MANDATORY)
439
- - Define `static auth` for protected components
440
- - Define `static actionAuth` for protected actions
441
- - Use `$auth.hasRole()` / `$auth.hasPermission()` in action logic
442
- - Register auth providers before server starts
443
- - Handle `AUTH_DENIED` errors in client UI
444
-
445
- **NEVER:**
446
- - Omit `publicActions` (component will deny ALL remote actions)
447
- - Store sensitive data in component state
448
- - Trust client-side auth checks alone (always verify server-side)
449
- - Expose tokens in error messages
450
- - Skip auth on actions that modify data
451
-
452
- **AUTH LOGIC:**
453
- ```typescript
454
- // Roles: OR logic (any role grants access)
455
- roles: ['admin', 'moderator'] // admin OR moderator
456
-
457
- // Permissions: AND logic (all permissions required)
458
- permissions: ['users.read', 'users.write'] // BOTH required
459
- ```
460
-
461
- ## Related
462
-
463
- - [Live Components](./live-components.md) - Base component documentation
464
- - [Live Rooms](./live-rooms.md) - Room-based communication
465
- - [Plugin System](../core/plugin-system.md) - Auth as plugin
1
+ # Live Components Authentication
2
+
3
+ **Version:** 1.16.0 | **Updated:** 2026-03-25
4
+
5
+ ## Quick Facts
6
+
7
+ - **`publicActions` is the foundation** - Only whitelisted methods can be called remotely
8
+ - Declarative auth via `static auth` and `static actionAuth`
9
+ - Custom `authorize()` function for any logic (DB checks, plans, feature flags)
10
+ - Role-based (OR logic) and permission-based (AND logic) access control
11
+ - `$auth.session` generic session data (user, bot, device, service — dev defines)
12
+ - Token always sent inside WebSocket (never in URL)
13
+ - Auto re-mount when authentication changes
14
+ - `$auth` available on frontend with session data from server
15
+
16
+ ## Auth Approaches
17
+
18
+ Two ways to handle auth — use either or both:
19
+
20
+ ### 1. Provider + `authenticate()` (framework-managed)
21
+
22
+ Global auth for the WebSocket connection. All components share the same `$auth`.
23
+
24
+ ```typescript
25
+ // Server: create provider
26
+ class JWTProvider implements LiveAuthProvider {
27
+ readonly name = 'jwt'
28
+ async authenticate(credentials: LiveAuthCredentials) {
29
+ const decoded = jwt.verify(credentials.token, SECRET)
30
+ return new AuthenticatedContext({
31
+ id: decoded.sub,
32
+ name: decoded.name,
33
+ plan: decoded.plan,
34
+ roles: decoded.roles,
35
+ permissions: decoded.permissions,
36
+ })
37
+ }
38
+ }
39
+
40
+ // Server: register
41
+ liveAuthManager.register(new JWTProvider())
42
+
43
+ // Client: login
44
+ const { authenticate } = useLiveComponents()
45
+ await authenticate({ token: jwtToken })
46
+ ```
47
+
48
+ ### 2. Action + `$private` (component-managed)
49
+
50
+ Auth inside the component itself. No provider needed.
51
+
52
+ ```typescript
53
+ // Server
54
+ export class LiveChat extends LiveComponent<
55
+ { messages: Message[] }, // state: client reads/writes
56
+ { loggedIn: boolean; odId: string } // $private: invisible to client
57
+ > {
58
+ static publicActions = ['login', 'sendMessage'] as const
59
+
60
+ async login(payload: { email: string; password: string }) {
61
+ const user = await db.findByEmail(payload.email)
62
+ if (!user) return { success: false }
63
+ this.$private.loggedIn = true
64
+ this.$private.odId = user.id
65
+ return { success: true, name: user.name }
66
+ }
67
+
68
+ async sendMessage(payload: { text: string }) {
69
+ if (!this.$private.loggedIn) throw new Error('Not authenticated')
70
+ // ...
71
+ }
72
+ }
73
+
74
+ // Client
75
+ const chat = Live.use(LiveChat)
76
+ const result = await chat.login({ email, password })
77
+ ```
78
+
79
+ ## Server-Side: Protected Components
80
+
81
+ ### Basic Protection
82
+
83
+ ```typescript
84
+ export class ProtectedChat extends LiveComponent<State> {
85
+ static componentName = 'ProtectedChat'
86
+ static publicActions = ['sendMessage'] as const
87
+ static defaultState = { messages: [] as string[] }
88
+
89
+ static auth = { required: true }
90
+
91
+ async sendMessage(payload: { text: string }) {
92
+ const userId = this.$auth.session?.id
93
+ return { success: true }
94
+ }
95
+ }
96
+ ```
97
+
98
+ ### Role-Based Protection
99
+
100
+ ```typescript
101
+ export class AdminPanel extends LiveComponent<State> {
102
+ static componentName = 'AdminPanel'
103
+ static publicActions = ['deleteUser'] as const
104
+ static defaultState = { users: [] }
105
+
106
+ // OR logic: admin OR moderator
107
+ static auth = { required: true, roles: ['admin', 'moderator'] }
108
+
109
+ async deleteUser(payload: { userId: string }) {
110
+ console.log(`${this.$auth.session?.id} deleting ${payload.userId}`)
111
+ return { success: true }
112
+ }
113
+ }
114
+ ```
115
+
116
+ ### Permission-Based Protection
117
+
118
+ ```typescript
119
+ export class ContentEditor extends LiveComponent<State> {
120
+ static componentName = 'ContentEditor'
121
+ static publicActions = ['editContent'] as const
122
+ static defaultState = { content: '' }
123
+
124
+ // AND logic: ALL permissions required
125
+ static auth = { required: true, permissions: ['content.read', 'content.write'] }
126
+ }
127
+ ```
128
+
129
+ ### Custom `authorize()` Function
130
+
131
+ For any logic beyond roles/permissions — DB lookups, plan checks, feature flags, etc.
132
+
133
+ ```typescript
134
+ export class ProDashboard extends LiveComponent<State> {
135
+ static componentName = 'ProDashboard'
136
+ static publicActions = ['getData'] as const
137
+
138
+ static auth = {
139
+ required: true,
140
+ // Runs AFTER declarative checks (required, roles, permissions)
141
+ authorize: async (auth) => {
142
+ const plan = await db.getUserPlan(auth.session?.id)
143
+ if (plan !== 'pro') {
144
+ return { allowed: false, reason: 'Pro plan required' }
145
+ }
146
+ return true
147
+ }
148
+ }
149
+ }
150
+ ```
151
+
152
+ ### Per-Action Protection
153
+
154
+ ```typescript
155
+ export class ModerationPanel extends LiveComponent<State> {
156
+ static componentName = 'ModerationPanel'
157
+ static publicActions = ['getReports', 'deleteReport', 'banUser'] as const
158
+
159
+ static auth = { required: true }
160
+
161
+ static actionAuth = {
162
+ deleteReport: { permissions: ['reports.delete'] },
163
+ banUser: { roles: ['admin', 'moderator'] },
164
+ }
165
+
166
+ // Action authorize receives the payload
167
+ static actionAuth = {
168
+ editProfile: {
169
+ authorize: (auth, payload) => auth.session?.id === payload.userId
170
+ },
171
+ adminDelete: {
172
+ roles: ['admin'],
173
+ authorize: async (auth, payload) => {
174
+ if (payload.itemId === 'protected') {
175
+ return { allowed: false, reason: 'Item is protected' }
176
+ }
177
+ return true
178
+ }
179
+ },
180
+ }
181
+ }
182
+ ```
183
+
184
+ ### Reusable Auth Rules
185
+
186
+ Auth configs are plain objects — compose and reuse them:
187
+
188
+ ```typescript
189
+ // auth/rules.ts
190
+ export const adminOnly = { required: true, roles: ['admin'] }
191
+ export const proOnly = {
192
+ required: true,
193
+ authorize: async (auth) => {
194
+ const plan = await db.getUserPlan(auth.session?.id)
195
+ return plan === 'pro'
196
+ }
197
+ }
198
+ export const ownerOnly = (field = 'userId') => ({
199
+ authorize: (auth, payload) => auth.session?.id === payload?.[field]
200
+ })
201
+
202
+ // Components import and use
203
+ export class LiveAdminPanel extends LiveComponent<State> {
204
+ static auth = adminOnly
205
+ static actionAuth = { delete: ownerOnly('targetUserId') }
206
+ }
207
+ ```
208
+
209
+ ## Using `$auth` in Actions
210
+
211
+ ```typescript
212
+ export class MyComponent extends LiveComponent<State> {
213
+ async myAction() {
214
+ // Session data (shape defined by your provider)
215
+ const id = this.$auth.session?.id
216
+ const name = this.$auth.session?.name
217
+ const plan = this.$auth.session?.plan
218
+
219
+ // Built-in helpers
220
+ this.$auth.authenticated // boolean
221
+ this.$auth.hasRole('admin') // boolean
222
+ this.$auth.hasAnyRole(['admin', 'mod'])
223
+ this.$auth.hasAllRoles(['user', 'verified'])
224
+ this.$auth.hasPermission('chat.write')
225
+ this.$auth.hasAllPermissions(['users.read', 'users.write'])
226
+ this.$auth.hasAnyPermission(['chat.read', 'chat.write'])
227
+
228
+ return { id }
229
+ }
230
+ }
231
+ ```
232
+
233
+ ### `$auth` API
234
+
235
+ ```typescript
236
+ interface LiveAuthContext {
237
+ readonly authenticated: boolean
238
+ readonly session?: LiveAuthSession // dev defines the shape
239
+ readonly user?: LiveAuthSession // deprecated alias for session
240
+ readonly token?: string
241
+ readonly authenticatedAt?: number
242
+
243
+ hasRole(role: string): boolean
244
+ hasAnyRole(roles: string[]): boolean
245
+ hasAllRoles(roles: string[]): boolean
246
+ hasPermission(permission: string): boolean
247
+ hasAnyPermission(permissions: string[]): boolean
248
+ hasAllPermissions(permissions: string[]): boolean
249
+ }
250
+
251
+ interface LiveAuthSession {
252
+ id: string
253
+ roles?: string[]
254
+ permissions?: string[]
255
+ [key: string]: unknown // dev adds any fields
256
+ }
257
+ ```
258
+
259
+ ## State Security Levels
260
+
261
+ ```
262
+ this.state → client reads AND writes (bidirectional sync)
263
+ this.$private → client NEVER sees (server-only)
264
+ this.$auth → set by framework, immutable, read-only
265
+ ```
266
+
267
+ Use `$private` for sensitive flags (loggedIn, internal IDs). Use `state` for display data. Never trust `state` for security — the client can send `PROPERTY_UPDATE` to modify it.
268
+
269
+ ## Client-Side: Authentication
270
+
271
+ ### Authenticate on Connection
272
+
273
+ ```tsx
274
+ <LiveComponentsProvider
275
+ auth={{ token }} // Sent via AUTH message inside WebSocket (never in URL)
276
+ autoConnect={true}
277
+ >
278
+ <App />
279
+ </LiveComponentsProvider>
280
+ ```
281
+
282
+ ### Dynamic Authentication
283
+
284
+ ```tsx
285
+ function LoginForm() {
286
+ const { authenticated, authenticate, $auth } = useLiveComponents()
287
+
288
+ const handleLogin = async () => {
289
+ const success = await authenticate({ token })
290
+ // Components with AUTH_DENIED auto re-mount
291
+ }
292
+
293
+ // Session data from the server
294
+ console.log($auth.session?.name)
295
+
296
+ const handleLogout = () => reconnect() // reconnect without token = anonymous
297
+ }
298
+ ```
299
+
300
+ ### Auth in Component Proxy
301
+
302
+ ```tsx
303
+ const panel = Live.use(AdminPanel)
304
+
305
+ panel.$authenticated // boolean
306
+ panel.$auth.authenticated // boolean
307
+ panel.$auth.session // { id, name, roles, ... } or null
308
+
309
+ if (panel.$error?.includes('AUTH_DENIED')) {
310
+ return <p>Access denied</p>
311
+ }
312
+ ```
313
+
314
+ ### Auto Re-mount on Auth Change
315
+
316
+ ```
317
+ 1. Live.use(AdminPanel) → AUTH_DENIED (not logged in)
318
+ 2. authenticate({ token: 'admin-token' })
319
+ 3. AdminPanel re-mounts automatically
320
+ ```
321
+
322
+ ## Auth Providers
323
+
324
+ ### Creating a Custom Provider
325
+
326
+ ```typescript
327
+ import type { LiveAuthProvider, LiveAuthCredentials, LiveAuthContext } from '@fluxstack/live'
328
+ import { AuthenticatedContext } from '@fluxstack/live'
329
+
330
+ export class MyAuthProvider implements LiveAuthProvider {
331
+ readonly name = 'my-auth'
332
+
333
+ async authenticate(credentials: LiveAuthCredentials): Promise<LiveAuthContext | null> {
334
+ const token = credentials.token as string
335
+ if (!token) return null
336
+
337
+ const user = await validateToken(token)
338
+ if (!user) return null
339
+
340
+ // Everything here goes to $auth.session
341
+ return new AuthenticatedContext({
342
+ id: user.id,
343
+ name: user.name,
344
+ email: user.email,
345
+ avatar: user.avatar,
346
+ plan: user.plan,
347
+ roles: user.roles,
348
+ permissions: user.permissions,
349
+ }, token)
350
+ }
351
+
352
+ // Optional: custom action authorization
353
+ async authorizeAction(context: LiveAuthContext, componentName: string, action: string): Promise<boolean> {
354
+ return true
355
+ }
356
+
357
+ // Optional: custom room authorization
358
+ async authorizeRoom(context: LiveAuthContext, roomId: string): Promise<boolean> {
359
+ if (roomId.startsWith('vip-') && !context.hasRole('premium')) return false
360
+ return true
361
+ }
362
+ }
363
+ ```
364
+
365
+ ### Non-User Sessions (Bots, Devices, Services)
366
+
367
+ The session is generic not limited to users:
368
+
369
+ ```typescript
370
+ // Bot provider
371
+ return new AuthenticatedContext({
372
+ id: 'bot-1',
373
+ type: 'bot',
374
+ botName: 'NotifyBot',
375
+ allowedChannels: ['general', 'alerts'],
376
+ roles: ['bot'],
377
+ })
378
+
379
+ // Device/IoT provider
380
+ return new AuthenticatedContext({
381
+ id: 'sensor-42',
382
+ type: 'device',
383
+ model: 'TempSensor-v3',
384
+ location: { lat: -23.5, lng: -46.6 },
385
+ roles: ['device'],
386
+ })
387
+ ```
388
+
389
+ ### Registering Providers
390
+
391
+ ```typescript
392
+ // app/server/index.ts
393
+ import { liveAuthManager } from '@core/server/live'
394
+ liveAuthManager.register(new MyAuthProvider())
395
+ ```
396
+
397
+ ## Security Layers (Action Execution Order)
398
+
399
+ 1. **Blocklist** - Internal methods (destroy, setState, emit) always blocked
400
+ 2. **Private methods** - `_x` or `#x` blocked
401
+ 3. **publicActions** - Must be in whitelist (mandatory)
402
+ 4. **actionAuth** - Declarative roles/permissions + custom `authorize()`
403
+ 5. **Method exists** - Must exist on instance
404
+ 6. **Object.prototype** - toString, valueOf blocked
405
+
406
+ ## Auth Verification Cascade
407
+
408
+ ### Mount (component):
409
+ ```
410
+ 1. static auth.required? → authenticated?
411
+ 2. static auth.roles? → hasAnyRole() (OR)
412
+ 3. static auth.permissions? → hasAllPermissions() (AND)
413
+ 4. static auth.authorize? → custom function
414
+ All pass → mount allowed
415
+ ```
416
+
417
+ ### Action:
418
+ ```
419
+ 1. actionAuth[action].roles? → hasAnyRole() (OR)
420
+ 2. actionAuth[action].permissions? hasAllPermissions() (AND)
421
+ 3. actionAuth[action].authorize? → custom function(auth, payload)
422
+ 4. provider.authorizeAction? → if provider implements
423
+ All pass → action allowed
424
+ ```
425
+
426
+ ## Configuration Types
427
+
428
+ ```typescript
429
+ interface LiveComponentAuth {
430
+ required?: boolean
431
+ roles?: string[]
432
+ permissions?: string[]
433
+ authorize?: (auth: LiveAuthContext) => boolean | { allowed: boolean; reason?: string } | Promise<...>
434
+ }
435
+
436
+ interface LiveActionAuth {
437
+ roles?: string[]
438
+ permissions?: string[]
439
+ authorize?: (auth: LiveAuthContext, payload: unknown) => boolean | { allowed: boolean; reason?: string } | Promise<...>
440
+ }
441
+
442
+ type LiveActionAuthMap = Record<string, LiveActionAuth>
443
+ ```
444
+
445
+ ## Critical Rules
446
+
447
+ **ALWAYS:**
448
+ - Define `static publicActions` (MANDATORY without it, ALL actions are denied)
449
+ - Define `static auth` for protected components
450
+ - Use `$private` for sensitive data, never `state`
451
+ - Register auth providers before server starts
452
+ - Handle `AUTH_DENIED` errors in client UI
453
+
454
+ **NEVER:**
455
+ - Store auth flags in `state` (client can modify via PROPERTY_UPDATE)
456
+ - Trust client-side auth checks alone
457
+ - Expose tokens in error messages
458
+
459
+ ## Related
460
+
461
+ - [Live Components](./live-components.md) - Base component documentation
462
+ - [Live Rooms](./live-rooms.md) - Room-based communication