create-fluxstack 1.17.1 → 1.18.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/resources/live-auth.md +462 -465
- package/app/client/.live-stubs/LiveAdminPanel.js +15 -0
- package/app/client/.live-stubs/LiveCounter.js +9 -0
- package/app/client/.live-stubs/LiveForm.js +11 -0
- package/app/client/.live-stubs/LiveLocalCounter.js +8 -0
- package/app/client/.live-stubs/LivePingPong.js +10 -0
- package/app/client/.live-stubs/LiveRoomChat.js +11 -0
- package/app/client/.live-stubs/LiveSharedCounter.js +10 -0
- package/app/client/.live-stubs/LiveUpload.js +15 -0
- package/app/client/src/App.tsx +45 -3
- package/app/client/src/components/AppLayout.tsx +10 -1
- package/app/client/src/components/ErrorBoundary.tsx +117 -0
- package/app/client/src/components/LiveErrorBoundary.tsx +87 -0
- package/app/client/src/components/LiveUploadWidget.tsx +10 -14
- package/app/client/src/lib/eden-api.ts +6 -0
- package/app/client/src/lib/plugin-hooks.ts +82 -0
- package/app/client/src/live/AuthDemo.tsx +0 -1
- package/app/client/src/live/FormDemo.tsx +1 -1
- package/app/client/src/live/PingPongDemo.tsx +4 -1
- package/app/client/src/live/RoomChatDemo.tsx +90 -50
- package/app/client/src/live/SharedCounterDemo.tsx +5 -0
- package/app/server/auth/AuthManager.ts +24 -0
- package/app/server/auth/contracts.ts +12 -1
- package/app/server/auth/errors.ts +84 -0
- package/app/server/auth/guards/TokenGuard.ts +5 -2
- package/app/server/auth/index.ts +1 -1
- package/app/server/auth/providers/InMemoryProvider.ts +1 -1
- package/app/server/index.ts +3 -4
- package/app/server/live/LiveAdminPanel.ts +8 -8
- package/app/server/live/LiveForm.ts +1 -1
- package/app/server/live/LiveProtectedChat.ts +5 -5
- package/app/server/live/LiveRoomChat.ts +50 -28
- package/app/server/live/LiveUpload.ts +17 -3
- package/app/server/live/auto-generated-components.ts +26 -0
- package/app/server/live/rooms/ChatRoom.ts +17 -2
- package/app/server/routes/auth.routes.ts +29 -20
- package/app/server/routes/index.ts +9 -0
- package/app/server/routes/room.routes.ts +6 -6
- package/config/index.ts +3 -3
- package/config/system/app.config.ts +1 -1
- package/config/system/auth.config.ts +1 -1
- package/config/system/build.config.ts +8 -6
- package/config/system/client.config.ts +6 -4
- package/config/system/database.config.ts +1 -1
- package/config/system/logger.config.ts +1 -1
- package/config/system/monitoring.config.ts +6 -4
- package/config/system/plugins.config.ts +1 -1
- package/config/system/runtime.config.ts +1 -1
- package/config/system/server.config.ts +1 -1
- package/config/system/services.config.ts +1 -1
- package/config/system/session.config.ts +3 -3
- package/config/system/system.config.ts +1 -1
- package/core/build/vite-plugins.ts +3 -2
- package/core/cli/generators/plugin.ts +1 -1
- package/core/config/index.ts +8 -1
- package/core/framework/server.ts +7 -3
- package/core/index.ts +1 -1
- package/core/plugins/index.ts +1 -1
- package/core/plugins/manager.ts +5 -1
- package/core/plugins/types.ts +17 -1
- package/core/server/index.ts +5 -2
- package/core/server/live/index.ts +8 -71
- package/core/server/plugin-client-hooks.ts +97 -0
- package/core/types/types.ts +1 -0
- package/core/utils/version.ts +1 -1
- package/create-fluxstack.ts +1 -1
- package/package.json +8 -5
- package/src/client/components/ui/StatusBadge.tsx +23 -0
- package/core/utils/config-schema.ts +0 -480
- package/core/utils/env.ts +0 -305
- package/plugins/crypto-auth/README.md +0 -788
- package/plugins/crypto-auth/ai-context.md +0 -1282
- package/plugins/crypto-auth/cli/make-protected-route.command.ts +0 -383
- package/plugins/crypto-auth/client/CryptoAuthClient.ts +0 -302
- package/plugins/crypto-auth/client/components/AuthProvider.tsx +0 -131
- package/plugins/crypto-auth/client/components/LoginButton.tsx +0 -138
- package/plugins/crypto-auth/client/components/ProtectedRoute.tsx +0 -89
- package/plugins/crypto-auth/client/components/index.ts +0 -12
- package/plugins/crypto-auth/client/index.ts +0 -12
- package/plugins/crypto-auth/config/index.ts +0 -34
- package/plugins/crypto-auth/index.ts +0 -173
- package/plugins/crypto-auth/package.json +0 -66
- package/plugins/crypto-auth/server/AuthMiddleware.ts +0 -181
- package/plugins/crypto-auth/server/CryptoAuthLiveProvider.ts +0 -58
- package/plugins/crypto-auth/server/CryptoAuthService.ts +0 -186
- package/plugins/crypto-auth/server/index.ts +0 -25
- package/plugins/crypto-auth/server/middlewares/cryptoAuthAdmin.ts +0 -66
- package/plugins/crypto-auth/server/middlewares/cryptoAuthOptional.ts +0 -26
- package/plugins/crypto-auth/server/middlewares/cryptoAuthPermissions.ts +0 -77
- package/plugins/crypto-auth/server/middlewares/cryptoAuthRequired.ts +0 -45
- package/plugins/crypto-auth/server/middlewares/helpers.ts +0 -155
- package/plugins/crypto-auth/server/middlewares/index.ts +0 -22
- package/plugins/crypto-auth/server/middlewares.ts +0 -19
|
@@ -1,465 +1,462 @@
|
|
|
1
|
-
# Live Components Authentication
|
|
2
|
-
|
|
3
|
-
**Version:** 1.
|
|
4
|
-
|
|
5
|
-
## Quick Facts
|
|
6
|
-
|
|
7
|
-
- **`publicActions` is the foundation** - Only whitelisted methods can be called remotely
|
|
8
|
-
- Declarative auth
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
static
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
static
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
static
|
|
103
|
-
static
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
)
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
): Promise<
|
|
334
|
-
|
|
335
|
-
if (
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
return
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
-
|
|
449
|
-
-
|
|
450
|
-
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|