create-fluxstack 1.12.0 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LLMD/INDEX.md +8 -1
- package/LLMD/agent.md +867 -0
- package/LLMD/config/environment-vars.md +30 -0
- package/LLMD/resources/live-auth.md +447 -0
- package/LLMD/resources/live-components.md +79 -21
- package/LLMD/resources/live-logging.md +158 -0
- package/LLMD/resources/live-upload.md +1 -1
- package/LLMD/resources/rest-auth.md +290 -0
- package/README.md +520 -340
- package/app/client/src/App.tsx +11 -0
- package/app/client/src/components/AppLayout.tsx +1 -0
- package/app/client/src/live/AuthDemo.tsx +332 -0
- package/app/client/src/live/RoomChatDemo.tsx +24 -105
- package/app/server/auth/AuthManager.ts +213 -0
- package/app/server/auth/DevAuthProvider.ts +66 -0
- package/app/server/auth/HashManager.ts +123 -0
- package/app/server/auth/JWTAuthProvider.example.ts +101 -0
- package/app/server/auth/RateLimiter.ts +106 -0
- package/app/server/auth/contracts.ts +192 -0
- package/app/server/auth/guards/SessionGuard.ts +167 -0
- package/app/server/auth/guards/TokenGuard.ts +202 -0
- package/app/server/auth/index.ts +174 -0
- package/app/server/auth/middleware.ts +163 -0
- package/app/server/auth/providers/InMemoryProvider.ts +162 -0
- package/app/server/auth/sessions/SessionManager.ts +164 -0
- package/app/server/cache/CacheManager.ts +81 -0
- package/app/server/cache/MemoryDriver.ts +112 -0
- package/app/server/cache/contracts.ts +49 -0
- package/app/server/cache/index.ts +42 -0
- package/app/server/index.ts +14 -0
- package/app/server/live/LiveAdminPanel.ts +173 -0
- package/app/server/live/LiveCounter.ts +1 -0
- package/app/server/live/LiveLocalCounter.ts +13 -8
- package/app/server/live/LiveProtectedChat.ts +150 -0
- package/app/server/live/LiveRoomChat.ts +45 -203
- package/app/server/routes/auth.routes.ts +278 -0
- package/app/server/routes/index.ts +2 -0
- package/config/index.ts +8 -0
- package/config/system/auth.config.ts +49 -0
- package/config/system/session.config.ts +33 -0
- package/core/client/LiveComponentsProvider.tsx +76 -5
- package/core/client/components/Live.tsx +2 -1
- package/core/client/hooks/useLiveComponent.ts +47 -4
- package/core/client/index.ts +2 -1
- package/core/framework/server.ts +36 -4
- package/core/plugins/built-in/live-components/commands/create-live-component.ts +15 -8
- package/core/plugins/built-in/monitoring/index.ts +10 -3
- package/core/plugins/built-in/vite/index.ts +95 -18
- package/core/plugins/config.ts +5 -4
- package/core/plugins/discovery.ts +11 -2
- package/core/plugins/manager.ts +11 -5
- package/core/plugins/module-resolver.ts +1 -1
- package/core/plugins/registry.ts +53 -25
- package/core/server/live/ComponentRegistry.ts +79 -24
- package/core/server/live/LiveComponentPerformanceMonitor.ts +9 -8
- package/core/server/live/LiveLogger.ts +111 -0
- package/core/server/live/LiveRoomManager.ts +5 -4
- package/core/server/live/StateSignature.ts +644 -643
- package/core/server/live/auth/LiveAuthContext.ts +71 -0
- package/core/server/live/auth/LiveAuthManager.ts +304 -0
- package/core/server/live/auth/index.ts +19 -0
- package/core/server/live/auth/types.ts +179 -0
- package/core/server/live/auto-generated-components.ts +8 -2
- package/core/server/live/index.ts +16 -0
- package/core/server/live/websocket-plugin.ts +92 -16
- package/core/templates/create-project.ts +0 -3
- package/core/types/types.ts +133 -13
- package/core/utils/index.ts +17 -17
- package/core/utils/logger/index.ts +5 -2
- package/core/utils/version.ts +1 -1
- package/package.json +1 -8
- package/plugins/crypto-auth/index.ts +6 -0
- package/plugins/crypto-auth/server/CryptoAuthLiveProvider.ts +58 -0
- package/plugins/crypto-auth/server/index.ts +24 -21
- package/rest-tests/README.md +57 -0
- package/rest-tests/auth-token.http +113 -0
- package/rest-tests/auth.http +112 -0
- package/rest-tests/rooms-token.http +69 -0
- package/rest-tests/users-token.http +62 -0
- package/.dockerignore +0 -81
- package/Dockerfile +0 -70
- package/LIVE_COMPONENTS_REVIEW.md +0 -781
|
@@ -90,6 +90,36 @@
|
|
|
90
90
|
| `BUILD_REMOVE_UNUSED_CSS` | boolean | `false` | Remove unused CSS | build.config.ts |
|
|
91
91
|
| `BUILD_OPTIMIZE_IMAGES` | boolean | `false` | Optimize images | build.config.ts |
|
|
92
92
|
|
|
93
|
+
## Live Component Logging
|
|
94
|
+
|
|
95
|
+
| Variable | Type | Default | Description | Config File |
|
|
96
|
+
|----------|------|---------|-------------|-------------|
|
|
97
|
+
| `LIVE_LOGGING` | string | `undefined` | Global live component logs: `true`, `false`, or comma-separated categories (`lifecycle,rooms,messages`) | env only |
|
|
98
|
+
|
|
99
|
+
> Per-component logging is controlled via `static logging` on the class. See [Live Logging](../resources/live-logging.md).
|
|
100
|
+
|
|
101
|
+
## Authentication
|
|
102
|
+
|
|
103
|
+
| Variable | Type | Default | Description | Config File |
|
|
104
|
+
|----------|------|---------|-------------|-------------|
|
|
105
|
+
| `AUTH_DEFAULT_GUARD` | enum | `'session'` | Auth guard: `session`, `token` | auth.config.ts |
|
|
106
|
+
| `AUTH_DEFAULT_PROVIDER` | enum | `'memory'` | User provider: `memory`, `database` | auth.config.ts |
|
|
107
|
+
| `AUTH_HASH_ALGORITHM` | enum | `'bcrypt'` | Hash algorithm: `bcrypt`, `argon2id` | auth.config.ts |
|
|
108
|
+
| `AUTH_BCRYPT_ROUNDS` | number | `10` | Bcrypt cost rounds | auth.config.ts |
|
|
109
|
+
| `AUTH_RATE_LIMIT_MAX_ATTEMPTS` | number | `5` | Max login attempts before lockout | auth.config.ts |
|
|
110
|
+
| `AUTH_RATE_LIMIT_DECAY_SECONDS` | number | `60` | Rate limit window (seconds) | auth.config.ts |
|
|
111
|
+
| `AUTH_TOKEN_TTL` | number | `86400` | Bearer token TTL (seconds, token guard only) | auth.config.ts |
|
|
112
|
+
|
|
113
|
+
## Session
|
|
114
|
+
|
|
115
|
+
| Variable | Type | Default | Description | Config File |
|
|
116
|
+
|----------|------|---------|-------------|-------------|
|
|
117
|
+
| `SESSION_COOKIE` | string | `'fluxstack_session'` | Session cookie name | session.config.ts |
|
|
118
|
+
| `SESSION_LIFETIME` | number | `7200` | Session duration (seconds) | session.config.ts |
|
|
119
|
+
| `SESSION_HTTP_ONLY` | boolean | `true` | HttpOnly cookie flag | session.config.ts |
|
|
120
|
+
| `SESSION_SECURE` | boolean | `false` | Secure cookie flag (HTTPS only) | session.config.ts |
|
|
121
|
+
| `SESSION_SAME_SITE` | enum | `'lax'` | SameSite policy: `lax`, `strict`, `none` | session.config.ts |
|
|
122
|
+
|
|
93
123
|
## Logging
|
|
94
124
|
|
|
95
125
|
| Variable | Type | Default | Description | Config File |
|
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
# Live Components Authentication
|
|
2
|
+
|
|
3
|
+
**Version:** 1.14.0 | **Updated:** 2025-02-12
|
|
4
|
+
|
|
5
|
+
## Quick Facts
|
|
6
|
+
|
|
7
|
+
- Declarative auth configuration via `static auth` and `static actionAuth`
|
|
8
|
+
- Role-based access control (RBAC) with OR logic
|
|
9
|
+
- Permission-based access control with AND logic
|
|
10
|
+
- Auto re-mount when authentication changes
|
|
11
|
+
- Pluggable auth providers (JWT, Crypto, Custom)
|
|
12
|
+
- `$auth` helper available in component actions
|
|
13
|
+
|
|
14
|
+
## Server-Side: Protected Components
|
|
15
|
+
|
|
16
|
+
### Basic Protection (Auth Required)
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
// app/server/live/ProtectedChat.ts
|
|
20
|
+
import { LiveComponent } from '@core/types/types'
|
|
21
|
+
import type { LiveComponentAuth } from '@core/server/live/auth/types'
|
|
22
|
+
|
|
23
|
+
export class ProtectedChat extends LiveComponent<typeof ProtectedChat.defaultState> {
|
|
24
|
+
static componentName = 'ProtectedChat'
|
|
25
|
+
static defaultState = {
|
|
26
|
+
messages: [] as string[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Auth required to mount this component
|
|
30
|
+
static auth: LiveComponentAuth = {
|
|
31
|
+
required: true
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async sendMessage(payload: { text: string }) {
|
|
35
|
+
// Only authenticated users can call this
|
|
36
|
+
return { success: true }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Role-Based Protection
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
// app/server/live/AdminPanel.ts
|
|
45
|
+
import { LiveComponent } from '@core/types/types'
|
|
46
|
+
import type { LiveComponentAuth } from '@core/server/live/auth/types'
|
|
47
|
+
|
|
48
|
+
export class AdminPanel extends LiveComponent<typeof AdminPanel.defaultState> {
|
|
49
|
+
static componentName = 'AdminPanel'
|
|
50
|
+
static defaultState = {
|
|
51
|
+
users: [] as { id: string; name: string; role: string }[]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Requires auth + admin OR moderator role (OR logic)
|
|
55
|
+
static auth: LiveComponentAuth = {
|
|
56
|
+
required: true,
|
|
57
|
+
roles: ['admin', 'moderator']
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async deleteUser(payload: { userId: string }) {
|
|
61
|
+
// User info available via $auth
|
|
62
|
+
console.log(`User ${this.$auth.user?.id} deleting ${payload.userId}`)
|
|
63
|
+
return { success: true }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Permission-Based Protection
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
// app/server/live/ContentEditor.ts
|
|
72
|
+
import { LiveComponent } from '@core/types/types'
|
|
73
|
+
import type { LiveComponentAuth } from '@core/server/live/auth/types'
|
|
74
|
+
|
|
75
|
+
export class ContentEditor extends LiveComponent<typeof ContentEditor.defaultState> {
|
|
76
|
+
static componentName = 'ContentEditor'
|
|
77
|
+
static defaultState = {
|
|
78
|
+
content: ''
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Requires ALL permissions (AND logic)
|
|
82
|
+
static auth: LiveComponentAuth = {
|
|
83
|
+
required: true,
|
|
84
|
+
permissions: ['content.read', 'content.write']
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Per-Action Protection
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
// app/server/live/ModerationPanel.ts
|
|
93
|
+
import { LiveComponent } from '@core/types/types'
|
|
94
|
+
import type { LiveComponentAuth, LiveActionAuthMap } from '@core/server/live/auth/types'
|
|
95
|
+
|
|
96
|
+
export class ModerationPanel extends LiveComponent<typeof ModerationPanel.defaultState> {
|
|
97
|
+
static componentName = 'ModerationPanel'
|
|
98
|
+
static defaultState = {
|
|
99
|
+
reports: [] as any[]
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Component-level: any authenticated user
|
|
103
|
+
static auth: LiveComponentAuth = {
|
|
104
|
+
required: true
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Per-action auth
|
|
108
|
+
static actionAuth: LiveActionAuthMap = {
|
|
109
|
+
deleteReport: { permissions: ['reports.delete'] },
|
|
110
|
+
banUser: { roles: ['admin', 'moderator'] }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Anyone authenticated can view
|
|
114
|
+
async getReports() {
|
|
115
|
+
return { reports: this.state.reports }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Requires reports.delete permission
|
|
119
|
+
async deleteReport(payload: { reportId: string }) {
|
|
120
|
+
return { success: true }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Requires admin OR moderator role
|
|
124
|
+
async banUser(payload: { userId: string }) {
|
|
125
|
+
return { success: true }
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Using $auth in Actions
|
|
131
|
+
|
|
132
|
+
The `$auth` helper provides access to the authenticated user context:
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
export class MyComponent extends LiveComponent<State> {
|
|
136
|
+
async myAction() {
|
|
137
|
+
// Check if authenticated
|
|
138
|
+
if (!this.$auth.authenticated) {
|
|
139
|
+
throw new Error('Not authenticated')
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Get user info
|
|
143
|
+
const userId = this.$auth.user?.id
|
|
144
|
+
const userName = this.$auth.user?.name
|
|
145
|
+
|
|
146
|
+
// Check roles
|
|
147
|
+
if (this.$auth.hasRole('admin')) {
|
|
148
|
+
// Admin-only logic
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (this.$auth.hasAnyRole(['admin', 'moderator'])) {
|
|
152
|
+
// Admin OR moderator logic
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Check permissions
|
|
156
|
+
if (this.$auth.hasPermission('users.delete')) {
|
|
157
|
+
// Has specific permission
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (this.$auth.hasAllPermissions(['users.read', 'users.write'])) {
|
|
161
|
+
// Has ALL permissions
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return { userId }
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### $auth API
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
interface LiveAuthContext {
|
|
173
|
+
readonly authenticated: boolean
|
|
174
|
+
readonly user?: {
|
|
175
|
+
id: string
|
|
176
|
+
roles?: string[]
|
|
177
|
+
permissions?: string[]
|
|
178
|
+
[key: string]: unknown // Custom fields
|
|
179
|
+
}
|
|
180
|
+
readonly token?: string
|
|
181
|
+
readonly authenticatedAt?: number
|
|
182
|
+
|
|
183
|
+
hasRole(role: string): boolean
|
|
184
|
+
hasAnyRole(roles: string[]): boolean
|
|
185
|
+
hasAllRoles(roles: string[]): boolean
|
|
186
|
+
hasPermission(permission: string): boolean
|
|
187
|
+
hasAnyPermission(permissions: string[]): boolean
|
|
188
|
+
hasAllPermissions(permissions: string[]): boolean
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Client-Side: Authentication
|
|
193
|
+
|
|
194
|
+
### Authenticate on Connection
|
|
195
|
+
|
|
196
|
+
Pass auth credentials when the WebSocket connects:
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
// app/client/src/App.tsx
|
|
200
|
+
import { LiveComponentsProvider } from '@/core/client'
|
|
201
|
+
|
|
202
|
+
function App() {
|
|
203
|
+
const token = localStorage.getItem('auth_token')
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<LiveComponentsProvider
|
|
207
|
+
auth={{ token }} // Sent as query param on connect
|
|
208
|
+
autoConnect={true}
|
|
209
|
+
>
|
|
210
|
+
<AppContent />
|
|
211
|
+
</LiveComponentsProvider>
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Dynamic Authentication
|
|
217
|
+
|
|
218
|
+
Authenticate after connection via `useLiveComponents`:
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
import { useLiveComponents } from '@/core/client'
|
|
222
|
+
|
|
223
|
+
function LoginForm() {
|
|
224
|
+
const { authenticated, authenticate } = useLiveComponents()
|
|
225
|
+
const [token, setToken] = useState('')
|
|
226
|
+
|
|
227
|
+
const handleLogin = async () => {
|
|
228
|
+
const success = await authenticate({ token })
|
|
229
|
+
if (success) {
|
|
230
|
+
// Components with auth errors will auto re-mount
|
|
231
|
+
console.log('Authenticated!')
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return (
|
|
236
|
+
<div>
|
|
237
|
+
<p>Status: {authenticated ? 'Logged in' : 'Not logged in'}</p>
|
|
238
|
+
<input value={token} onChange={e => setToken(e.target.value)} />
|
|
239
|
+
<button onClick={handleLogin}>Login</button>
|
|
240
|
+
</div>
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Checking Auth Status in Components
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
import { Live } from '@/core/client'
|
|
249
|
+
import { AdminPanel } from '@server/live/AdminPanel'
|
|
250
|
+
|
|
251
|
+
function AdminSection() {
|
|
252
|
+
const panel = Live.use(AdminPanel)
|
|
253
|
+
|
|
254
|
+
// Check if authenticated on WebSocket level
|
|
255
|
+
if (!panel.$authenticated) {
|
|
256
|
+
return <p>Please log in</p>
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Check for auth errors
|
|
260
|
+
if (panel.$error?.includes('AUTH_DENIED')) {
|
|
261
|
+
return <p>Access denied: {panel.$error}</p>
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return <div>{/* Admin content */}</div>
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Auto Re-mount on Auth Change
|
|
269
|
+
|
|
270
|
+
When authentication changes from `false` to `true`, components that failed with `AUTH_DENIED` automatically retry mounting:
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
// No manual code needed!
|
|
274
|
+
// 1. User tries to mount AdminPanel → AUTH_DENIED
|
|
275
|
+
// 2. User calls authenticate({ token: 'admin-token' })
|
|
276
|
+
// 3. AdminPanel automatically re-mounts with auth context
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## Auth Providers
|
|
280
|
+
|
|
281
|
+
### Creating a Custom Provider
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
// app/server/auth/MyAuthProvider.ts
|
|
285
|
+
import type {
|
|
286
|
+
LiveAuthProvider,
|
|
287
|
+
LiveAuthCredentials,
|
|
288
|
+
LiveAuthContext
|
|
289
|
+
} from '@core/server/live/auth/types'
|
|
290
|
+
import { AuthenticatedContext } from '@core/server/live/auth/LiveAuthContext'
|
|
291
|
+
|
|
292
|
+
export class MyAuthProvider implements LiveAuthProvider {
|
|
293
|
+
readonly name = 'my-auth'
|
|
294
|
+
|
|
295
|
+
async authenticate(credentials: LiveAuthCredentials): Promise<LiveAuthContext | null> {
|
|
296
|
+
const token = credentials.token as string
|
|
297
|
+
if (!token) return null
|
|
298
|
+
|
|
299
|
+
// Validate token (JWT decode, database lookup, etc.)
|
|
300
|
+
const user = await validateToken(token)
|
|
301
|
+
if (!user) return null
|
|
302
|
+
|
|
303
|
+
return new AuthenticatedContext(
|
|
304
|
+
{
|
|
305
|
+
id: user.id,
|
|
306
|
+
name: user.name,
|
|
307
|
+
roles: user.roles,
|
|
308
|
+
permissions: user.permissions
|
|
309
|
+
},
|
|
310
|
+
token
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Optional: Custom action authorization
|
|
315
|
+
async authorizeAction(
|
|
316
|
+
context: LiveAuthContext,
|
|
317
|
+
componentName: string,
|
|
318
|
+
action: string
|
|
319
|
+
): Promise<boolean> {
|
|
320
|
+
// Custom logic (rate limiting, business rules, etc.)
|
|
321
|
+
return true
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Optional: Custom room authorization
|
|
325
|
+
async authorizeRoom(
|
|
326
|
+
context: LiveAuthContext,
|
|
327
|
+
roomId: string
|
|
328
|
+
): Promise<boolean> {
|
|
329
|
+
// Example: VIP rooms require premium role
|
|
330
|
+
if (roomId.startsWith('vip-') && !context.hasRole('premium')) {
|
|
331
|
+
return false
|
|
332
|
+
}
|
|
333
|
+
return true
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Registering Providers
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
// app/server/index.ts
|
|
342
|
+
import { liveAuthManager } from '@core/server/live/auth'
|
|
343
|
+
import { MyAuthProvider } from './auth/MyAuthProvider'
|
|
344
|
+
|
|
345
|
+
// Register provider
|
|
346
|
+
liveAuthManager.register(new MyAuthProvider())
|
|
347
|
+
|
|
348
|
+
// Optional: Set as default (first registered is default)
|
|
349
|
+
liveAuthManager.setDefault('my-auth')
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### Built-in DevAuthProvider (Development)
|
|
353
|
+
|
|
354
|
+
For testing, a `DevAuthProvider` with simple tokens is available:
|
|
355
|
+
|
|
356
|
+
```typescript
|
|
357
|
+
// Tokens available in development:
|
|
358
|
+
// - 'admin-token' → role: admin, all permissions
|
|
359
|
+
// - 'user-token' → role: user, basic permissions
|
|
360
|
+
// - 'mod-token' → role: moderator
|
|
361
|
+
|
|
362
|
+
// Already registered in dev mode automatically
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
## Auth Flow Diagram
|
|
366
|
+
|
|
367
|
+
```
|
|
368
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
369
|
+
│ CLIENT │
|
|
370
|
+
├─────────────────────────────────────────────────────────────┤
|
|
371
|
+
│ 1. Connect WebSocket (optional: ?token=xxx) │
|
|
372
|
+
│ 2. authenticate({ token }) → AUTH message │
|
|
373
|
+
│ 3. Live.use(ProtectedComponent) → COMPONENT_MOUNT │
|
|
374
|
+
│ 4. If AUTH_DENIED, wait for auth change │
|
|
375
|
+
│ 5. Auth changes → auto re-mount │
|
|
376
|
+
└─────────────────────────────────────────────────────────────┘
|
|
377
|
+
│
|
|
378
|
+
▼
|
|
379
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
380
|
+
│ SERVER │
|
|
381
|
+
├─────────────────────────────────────────────────────────────┤
|
|
382
|
+
│ 1. WebSocket connect → store authContext on ws.data │
|
|
383
|
+
│ 2. AUTH message → liveAuthManager.authenticate() │
|
|
384
|
+
│ 3. COMPONENT_MOUNT → check static auth config │
|
|
385
|
+
│ 4. CALL_ACTION → check static actionAuth config │
|
|
386
|
+
│ 5. Component has access to this.$auth │
|
|
387
|
+
└─────────────────────────────────────────────────────────────┘
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
## Configuration Types
|
|
391
|
+
|
|
392
|
+
```typescript
|
|
393
|
+
// Component-level auth
|
|
394
|
+
interface LiveComponentAuth {
|
|
395
|
+
required?: boolean // Must be authenticated to mount
|
|
396
|
+
roles?: string[] // Required roles (OR logic - any role)
|
|
397
|
+
permissions?: string[] // Required permissions (AND logic - all)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Action-level auth
|
|
401
|
+
interface LiveActionAuth {
|
|
402
|
+
roles?: string[] // Required roles (OR logic)
|
|
403
|
+
permissions?: string[] // Required permissions (AND logic)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
type LiveActionAuthMap = Record<string, LiveActionAuth>
|
|
407
|
+
|
|
408
|
+
// Credentials from client
|
|
409
|
+
interface LiveAuthCredentials {
|
|
410
|
+
token?: string
|
|
411
|
+
publicKey?: string // For crypto auth
|
|
412
|
+
signature?: string // For crypto auth
|
|
413
|
+
timestamp?: number
|
|
414
|
+
nonce?: string
|
|
415
|
+
[key: string]: unknown // Custom fields
|
|
416
|
+
}
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
## Critical Rules
|
|
420
|
+
|
|
421
|
+
**ALWAYS:**
|
|
422
|
+
- Define `static auth` for protected components
|
|
423
|
+
- Define `static actionAuth` for protected actions
|
|
424
|
+
- Use `$auth.hasRole()` / `$auth.hasPermission()` in action logic
|
|
425
|
+
- Register auth providers before server starts
|
|
426
|
+
- Handle `AUTH_DENIED` errors in client UI
|
|
427
|
+
|
|
428
|
+
**NEVER:**
|
|
429
|
+
- Store sensitive data in component state
|
|
430
|
+
- Trust client-side auth checks alone (always verify server-side)
|
|
431
|
+
- Expose tokens in error messages
|
|
432
|
+
- Skip auth on actions that modify data
|
|
433
|
+
|
|
434
|
+
**AUTH LOGIC:**
|
|
435
|
+
```typescript
|
|
436
|
+
// Roles: OR logic (any role grants access)
|
|
437
|
+
roles: ['admin', 'moderator'] // admin OR moderator
|
|
438
|
+
|
|
439
|
+
// Permissions: AND logic (all permissions required)
|
|
440
|
+
permissions: ['users.read', 'users.write'] // BOTH required
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
## Related
|
|
444
|
+
|
|
445
|
+
- [Live Components](./live-components.md) - Base component documentation
|
|
446
|
+
- [Live Rooms](./live-rooms.md) - Room-based communication
|
|
447
|
+
- [Plugin System](../core/plugin-system.md) - Auth as plugin
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
# Live Components
|
|
2
2
|
|
|
3
|
-
**Version:** 1.
|
|
3
|
+
**Version:** 1.13.0 | **Updated:** 2025-02-09
|
|
4
4
|
|
|
5
5
|
## Quick Facts
|
|
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
|
- Automatic state persistence and re-hydration
|
|
10
10
|
- Room-based event system for multi-user sync
|
|
11
11
|
- Type-safe client-server communication (FluxStackWebSocket)
|
|
12
12
|
- Built-in connection management and recovery
|
|
13
13
|
- **Client component links** - Ctrl+Click navigation
|
|
14
14
|
|
|
15
|
-
## LiveComponent Class Structure (v1.
|
|
15
|
+
## LiveComponent Class Structure (v1.13.0)
|
|
16
16
|
|
|
17
17
|
Server-side component extends `LiveComponent` with **static defaultState**:
|
|
18
18
|
|
|
@@ -25,28 +25,38 @@ import type { CounterDemo as _Client } from '@client/src/live/CounterDemo'
|
|
|
25
25
|
|
|
26
26
|
export class LiveCounter extends LiveComponent<typeof LiveCounter.defaultState> {
|
|
27
27
|
static componentName = 'LiveCounter'
|
|
28
|
+
static logging = ['lifecycle', 'messages'] as const // Per-component logging (optional)
|
|
28
29
|
static defaultState = {
|
|
29
30
|
count: 0
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
//
|
|
33
|
+
// Declarar propriedades do estado (TypeScript)
|
|
34
|
+
declare count: number
|
|
35
|
+
|
|
36
|
+
// ✅ Direct state access - auto-syncs with frontend
|
|
33
37
|
async increment() {
|
|
34
|
-
this.
|
|
35
|
-
return { success: true, count: this.
|
|
38
|
+
this.count++
|
|
39
|
+
return { success: true, count: this.count }
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
async decrement() {
|
|
39
|
-
this.
|
|
40
|
-
return { success: true, count: this.
|
|
43
|
+
this.count--
|
|
44
|
+
return { success: true, count: this.count }
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
async reset() {
|
|
44
|
-
this.
|
|
48
|
+
this.count = 0
|
|
45
49
|
return { success: true }
|
|
46
50
|
}
|
|
47
51
|
}
|
|
48
52
|
```
|
|
49
53
|
|
|
54
|
+
### Key Changes in v1.13.0
|
|
55
|
+
|
|
56
|
+
1. **Direct state access** - `this.count++` instead of `this.state.count++`
|
|
57
|
+
2. **declare keyword** - TypeScript hint for dynamic properties
|
|
58
|
+
3. **Cleaner code** - No need to prefix with `this.state.`
|
|
59
|
+
|
|
50
60
|
### Key Changes in v1.12.0
|
|
51
61
|
|
|
52
62
|
1. **Static defaultState inside class** - No external export needed
|
|
@@ -120,29 +130,67 @@ export class MyComponent extends LiveComponent<typeof MyComponent.defaultState>
|
|
|
120
130
|
|
|
121
131
|
## State Management
|
|
122
132
|
|
|
123
|
-
### Reactive State (
|
|
133
|
+
### Reactive State Proxy (How It Works)
|
|
134
|
+
|
|
135
|
+
State mutations auto-sync with the frontend via two layers:
|
|
136
|
+
|
|
137
|
+
**Layer 1 — Proxy** (`this.state`): A `Proxy` wraps the internal state object. Any `set` on `this.state` compares old vs new value and, if changed, emits `STATE_DELTA` to the client automatically.
|
|
138
|
+
|
|
139
|
+
**Layer 2 — Direct Accessors** (`this.count`): On construction, `createDirectStateAccessors()` defines a getter/setter via `Object.defineProperty` for each key in `defaultState`. The setter delegates to the proxy, so it also triggers `STATE_DELTA`.
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
this.count++ → accessor setter → proxy set → STATE_DELTA
|
|
143
|
+
this.state.count++ → proxy set → STATE_DELTA
|
|
144
|
+
this.setState({count: 1}) → Object.assign + single STATE_DELTA (batch)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Direct State Access (v1.13.0) ✨
|
|
124
148
|
|
|
125
|
-
State
|
|
149
|
+
State properties are accessible directly on `this`:
|
|
126
150
|
|
|
127
151
|
```typescript
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
152
|
+
// Declare properties for TypeScript
|
|
153
|
+
declare count: number
|
|
154
|
+
declare message: string
|
|
155
|
+
|
|
156
|
+
// ✅ Direct access - auto-syncs via proxy!
|
|
157
|
+
this.count++
|
|
158
|
+
this.message = 'Hello'
|
|
131
159
|
|
|
132
|
-
//
|
|
160
|
+
// ✅ Also works (v1.12.0 style) - same proxy underneath
|
|
161
|
+
this.state.count++
|
|
133
162
|
```
|
|
134
163
|
|
|
164
|
+
> **Performance note:** Each direct assignment emits one `STATE_DELTA`. For multiple properties at once, use `setState` (single emit).
|
|
165
|
+
|
|
135
166
|
### setState (Batch Updates)
|
|
136
167
|
|
|
137
168
|
Use `setState` for multiple properties at once (single emit):
|
|
138
169
|
|
|
139
170
|
```typescript
|
|
140
|
-
// ✅ Batch update - one
|
|
171
|
+
// ✅ Batch update - one STATE_DELTA event
|
|
141
172
|
this.setState({
|
|
142
173
|
count: newCount,
|
|
143
174
|
lastUpdatedBy: userId,
|
|
144
175
|
updatedAt: new Date().toISOString()
|
|
145
176
|
})
|
|
177
|
+
|
|
178
|
+
// ✅ Function updater (access previous state)
|
|
179
|
+
this.setState(prev => ({
|
|
180
|
+
count: prev.count + 1,
|
|
181
|
+
lastUpdatedBy: userId
|
|
182
|
+
}))
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
> `setState` writes directly to `_state` (bypasses proxy) and emits a single `STATE_DELTA` with all changed keys. More efficient than N individual assignments.
|
|
186
|
+
|
|
187
|
+
### setValue (Generic Action)
|
|
188
|
+
|
|
189
|
+
Built-in action to set any state key from the client:
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
// Client can call this without defining a custom action
|
|
193
|
+
await component.setValue({ key: 'count', value: 42 })
|
|
146
194
|
```
|
|
147
195
|
|
|
148
196
|
### getSerializableState
|
|
@@ -483,6 +531,7 @@ app/client/src/live/
|
|
|
483
531
|
Each server file contains:
|
|
484
532
|
- `static componentName` - Component identifier
|
|
485
533
|
- `static defaultState` - Initial state object
|
|
534
|
+
- `static logging` - Per-component log control (optional, see [Live Logging](./live-logging.md))
|
|
486
535
|
- Component class extending `LiveComponent`
|
|
487
536
|
- Client link via `import type { Demo as _Client }`
|
|
488
537
|
|
|
@@ -536,6 +585,7 @@ export class MyComponent extends LiveComponent<State> {
|
|
|
536
585
|
- Define `static componentName` matching class name
|
|
537
586
|
- Define `static defaultState` inside the class
|
|
538
587
|
- Use `typeof ClassName.defaultState` for type parameter
|
|
588
|
+
- Use `declare` for each state property (TypeScript type hint)
|
|
539
589
|
- Call `super.destroy()` in destroy method if overriding
|
|
540
590
|
- Use `emitRoomEventWithState` for state changes in rooms
|
|
541
591
|
- Handle errors in actions (throw Error)
|
|
@@ -547,17 +597,22 @@ export class MyComponent extends LiveComponent<State> {
|
|
|
547
597
|
- Forget `static componentName` (breaks minification)
|
|
548
598
|
- Emit room events without subscribing first
|
|
549
599
|
- Store non-serializable data in state
|
|
600
|
+
- Use reserved names for state properties (id, state, ws, room, userId, $room, $rooms, broadcastToRoom, roomType)
|
|
550
601
|
|
|
551
|
-
**STATE UPDATES (v1.
|
|
602
|
+
**STATE UPDATES (v1.13.0) — all auto-sync via Proxy:**
|
|
552
603
|
```typescript
|
|
553
|
-
// ✅
|
|
604
|
+
// ✅ Direct access (1 prop → 1 STATE_DELTA)
|
|
605
|
+
declare count: number
|
|
606
|
+
this.count++
|
|
607
|
+
|
|
608
|
+
// ✅ Also works (same proxy underneath)
|
|
554
609
|
this.state.count++
|
|
555
610
|
|
|
556
|
-
// ✅ Multiple properties
|
|
611
|
+
// ✅ Multiple properties → use setState (1 STATE_DELTA for all)
|
|
557
612
|
this.setState({ a: 1, b: 2, c: 3 })
|
|
558
613
|
|
|
559
614
|
// ❌ Don't use setState for single property (unnecessary)
|
|
560
|
-
this.setState({ count: this.
|
|
615
|
+
this.setState({ count: this.count + 1 })
|
|
561
616
|
```
|
|
562
617
|
|
|
563
618
|
---
|
|
@@ -697,7 +752,10 @@ export function UploadDemo() {
|
|
|
697
752
|
|
|
698
753
|
## Related
|
|
699
754
|
|
|
755
|
+
- [Live Auth](./live-auth.md) - Authentication for Live Components
|
|
756
|
+
- [Live Logging](./live-logging.md) - Per-component logging control
|
|
757
|
+
- [Live Rooms](./live-rooms.md) - Multi-room real-time communication
|
|
758
|
+
- [Live Upload](./live-upload.md) - Chunked file upload
|
|
700
759
|
- [Project Structure](../patterns/project-structure.md)
|
|
701
760
|
- [Type Safety Patterns](../patterns/type-safety.md)
|
|
702
761
|
- [WebSocket Plugin](../core/plugin-system.md)
|
|
703
|
-
- [Live Upload](./live-upload.md)
|