create-fluxstack 1.12.1 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/LLMD/INDEX.md +8 -1
  2. package/LLMD/agent.md +867 -0
  3. package/LLMD/config/environment-vars.md +30 -0
  4. package/LLMD/patterns/anti-patterns.md +100 -0
  5. package/LLMD/reference/routing.md +39 -39
  6. package/LLMD/resources/live-auth.md +465 -0
  7. package/LLMD/resources/live-components.md +168 -26
  8. package/LLMD/resources/live-logging.md +220 -0
  9. package/LLMD/resources/live-upload.md +59 -8
  10. package/LLMD/resources/rest-auth.md +290 -0
  11. package/README.md +520 -340
  12. package/app/client/index.html +2 -2
  13. package/app/client/public/favicon.svg +46 -0
  14. package/app/client/src/App.tsx +13 -1
  15. package/app/client/src/assets/fluxstack-static.svg +46 -0
  16. package/app/client/src/assets/fluxstack.svg +183 -0
  17. package/app/client/src/components/AppLayout.tsx +139 -9
  18. package/app/client/src/components/BackButton.tsx +13 -13
  19. package/app/client/src/components/DemoPage.tsx +4 -4
  20. package/app/client/src/live/AuthDemo.tsx +334 -0
  21. package/app/client/src/live/ChatDemo.tsx +2 -2
  22. package/app/client/src/live/CounterDemo.tsx +12 -12
  23. package/app/client/src/live/FormDemo.tsx +2 -2
  24. package/app/client/src/live/LiveDebuggerPanel.tsx +779 -0
  25. package/app/client/src/live/RoomChatDemo.tsx +24 -16
  26. package/app/client/src/main.tsx +13 -13
  27. package/app/client/src/pages/ApiTestPage.tsx +6 -6
  28. package/app/client/src/pages/HomePage.tsx +80 -52
  29. package/app/server/auth/AuthManager.ts +213 -0
  30. package/app/server/auth/DevAuthProvider.ts +66 -0
  31. package/app/server/auth/HashManager.ts +123 -0
  32. package/app/server/auth/JWTAuthProvider.example.ts +101 -0
  33. package/app/server/auth/RateLimiter.ts +106 -0
  34. package/app/server/auth/contracts.ts +192 -0
  35. package/app/server/auth/guards/SessionGuard.ts +167 -0
  36. package/app/server/auth/guards/TokenGuard.ts +202 -0
  37. package/app/server/auth/index.ts +174 -0
  38. package/app/server/auth/middleware.ts +163 -0
  39. package/app/server/auth/providers/InMemoryProvider.ts +162 -0
  40. package/app/server/auth/sessions/SessionManager.ts +164 -0
  41. package/app/server/cache/CacheManager.ts +81 -0
  42. package/app/server/cache/MemoryDriver.ts +112 -0
  43. package/app/server/cache/contracts.ts +49 -0
  44. package/app/server/cache/index.ts +42 -0
  45. package/app/server/index.ts +14 -0
  46. package/app/server/live/LiveAdminPanel.ts +174 -0
  47. package/app/server/live/LiveChat.ts +78 -77
  48. package/app/server/live/LiveCounter.ts +1 -0
  49. package/app/server/live/LiveForm.ts +1 -0
  50. package/app/server/live/LiveLocalCounter.ts +38 -32
  51. package/app/server/live/LiveProtectedChat.ts +151 -0
  52. package/app/server/live/LiveRoomChat.ts +1 -0
  53. package/app/server/live/LiveUpload.ts +1 -0
  54. package/app/server/live/register-components.ts +19 -19
  55. package/app/server/routes/auth.routes.ts +278 -0
  56. package/app/server/routes/index.ts +2 -0
  57. package/config/index.ts +8 -0
  58. package/config/system/auth.config.ts +49 -0
  59. package/config/system/runtime.config.ts +4 -0
  60. package/config/system/session.config.ts +33 -0
  61. package/core/build/optimizer.ts +235 -235
  62. package/core/client/LiveComponentsProvider.tsx +76 -5
  63. package/core/client/components/Live.tsx +17 -10
  64. package/core/client/components/LiveDebugger.tsx +1324 -0
  65. package/core/client/hooks/AdaptiveChunkSizer.ts +215 -215
  66. package/core/client/hooks/useLiveComponent.ts +58 -5
  67. package/core/client/hooks/useLiveDebugger.ts +392 -0
  68. package/core/client/index.ts +16 -1
  69. package/core/framework/server.ts +36 -4
  70. package/core/plugins/built-in/index.ts +134 -134
  71. package/core/plugins/built-in/live-components/commands/create-live-component.ts +19 -8
  72. package/core/plugins/built-in/monitoring/index.ts +10 -3
  73. package/core/plugins/built-in/vite/index.ts +151 -20
  74. package/core/plugins/config.ts +5 -4
  75. package/core/plugins/discovery.ts +11 -2
  76. package/core/plugins/manager.ts +11 -5
  77. package/core/plugins/module-resolver.ts +1 -1
  78. package/core/plugins/registry.ts +53 -25
  79. package/core/server/index.ts +15 -15
  80. package/core/server/live/ComponentRegistry.ts +134 -50
  81. package/core/server/live/FileUploadManager.ts +188 -24
  82. package/core/server/live/LiveComponentPerformanceMonitor.ts +9 -8
  83. package/core/server/live/LiveDebugger.ts +462 -0
  84. package/core/server/live/LiveLogger.ts +144 -0
  85. package/core/server/live/LiveRoomManager.ts +22 -5
  86. package/core/server/live/StateSignature.ts +704 -643
  87. package/core/server/live/WebSocketConnectionManager.ts +11 -10
  88. package/core/server/live/auth/LiveAuthContext.ts +71 -0
  89. package/core/server/live/auth/LiveAuthManager.ts +304 -0
  90. package/core/server/live/auth/index.ts +19 -0
  91. package/core/server/live/auth/types.ts +179 -0
  92. package/core/server/live/auto-generated-components.ts +8 -2
  93. package/core/server/live/index.ts +16 -0
  94. package/core/server/live/websocket-plugin.ts +323 -22
  95. package/core/server/plugins/static-files-plugin.ts +179 -69
  96. package/core/templates/create-project.ts +0 -3
  97. package/core/types/build.ts +219 -219
  98. package/core/types/plugin.ts +107 -107
  99. package/core/types/types.ts +278 -22
  100. package/core/utils/index.ts +17 -17
  101. package/core/utils/logger/index.ts +5 -2
  102. package/core/utils/logger/startup-banner.ts +82 -82
  103. package/core/utils/version.ts +6 -6
  104. package/package.json +1 -8
  105. package/plugins/crypto-auth/index.ts +6 -0
  106. package/plugins/crypto-auth/server/CryptoAuthLiveProvider.ts +58 -0
  107. package/plugins/crypto-auth/server/index.ts +24 -21
  108. package/rest-tests/README.md +57 -0
  109. package/rest-tests/auth-token.http +113 -0
  110. package/rest-tests/auth.http +112 -0
  111. package/rest-tests/rooms-token.http +69 -0
  112. package/rest-tests/users-token.http +62 -0
  113. package/.dockerignore +0 -81
  114. package/Dockerfile +0 -70
  115. package/LIVE_COMPONENTS_REVIEW.md +0 -781
  116. package/app/client/src/assets/react.svg +0 -1
@@ -1,236 +1,236 @@
1
- import { readFileSync, writeFileSync, statSync, readdirSync } from "fs"
2
- import { join, extname } from "path"
3
- import { gzipSync } from "zlib"
4
- import type { OptimizationConfig, OptimizationResult } from "../types/build"
5
- import { buildLogger } from "../utils/build-logger"
6
-
7
- export interface OptimizerConfig {
8
- treeshake: boolean
9
- compress: boolean
10
- removeUnusedCSS: boolean
11
- optimizeImages: boolean
12
- bundleAnalysis: boolean
13
- }
14
-
15
- export class Optimizer {
16
- private config: OptimizerConfig
17
-
18
- constructor(config: OptimizerConfig) {
19
- this.config = config
20
- }
21
-
22
- async optimize(buildPath: string): Promise<OptimizationResult> {
23
- buildLogger.section('Build Optimization', '🔧')
24
-
25
- const startTime = Date.now()
26
- const results: OptimizationResult = {
27
- success: true,
28
- duration: 0,
29
- originalSize: 0,
30
- optimizedSize: 0,
31
- compressionRatio: 0,
32
- optimizations: []
33
- }
34
-
35
- try {
36
- // Get original size
37
- results.originalSize = await this.calculateDirectorySize(buildPath)
38
- buildLogger.step(`Original size: ${buildLogger.formatSize(results.originalSize)}`)
39
-
40
- // Apply optimizations (minification removed for compatibility)
41
-
42
- if (this.config.compress) {
43
- await this.compressAssets(buildPath, results)
44
- }
45
-
46
- if (this.config.removeUnusedCSS) {
47
- await this.removeUnusedCSS(buildPath, results)
48
- }
49
-
50
- if (this.config.optimizeImages) {
51
- await this.optimizeImages(buildPath, results)
52
- }
53
-
54
- if (this.config.bundleAnalysis) {
55
- await this.analyzeBundles(buildPath, results)
56
- }
57
-
58
- // Calculate final size and compression ratio
59
- results.optimizedSize = await this.calculateDirectorySize(buildPath)
60
- results.compressionRatio = results.originalSize > 0
61
- ? ((results.originalSize - results.optimizedSize) / results.originalSize) * 100
62
- : 0
63
-
64
- results.duration = Date.now() - startTime
65
-
66
- buildLogger.success(`Optimization completed in ${buildLogger.formatDuration(results.duration)}`)
67
-
68
- // Create optimization summary table
69
- const optimizationData = results.optimizations.map(opt => ({
70
- type: opt.type,
71
- description: opt.description,
72
- saved: buildLogger.formatSize(opt.sizeSaved)
73
- }))
74
-
75
- if (optimizationData.length > 0) {
76
- buildLogger.table(
77
- [
78
- { header: 'Optimization', key: 'type', width: 20, align: 'left', color: 'cyan' },
79
- { header: 'Description', key: 'description', width: 35, align: 'left' },
80
- { header: 'Size Saved', key: 'saved', width: 12, align: 'right', color: 'green' }
81
- ],
82
- optimizationData
83
- )
84
- }
85
-
86
- return results
87
-
88
- } catch (error) {
89
- results.success = false
90
- results.duration = Date.now() - startTime
91
- results.error = error instanceof Error ? error.message : "Unknown optimization error"
92
-
93
- buildLogger.error(`Optimization failed: ${results.error}`)
94
- return results
95
- }
96
- }
97
-
98
- // Minification methods removed for compatibility with Bun bundler
99
-
100
- private async compressAssets(buildPath: string, results: OptimizationResult): Promise<void> {
101
- buildLogger.step("Compressing assets...")
102
-
103
- const files = this.getFilesRecursively(buildPath)
104
- let compressedCount = 0
105
-
106
- for (const file of files) {
107
- const ext = extname(file).toLowerCase()
108
-
109
- if (['.js', '.css', '.html', '.json', '.svg'].includes(ext)) {
110
- try {
111
- const content = readFileSync(file)
112
- const compressed = gzipSync(content)
113
-
114
- // Only create .gz file if it's significantly smaller
115
- if (compressed.length < content.length * 0.9) {
116
- writeFileSync(file + '.gz', compressed)
117
- compressedCount++
118
- }
119
- } catch (error) {
120
- // Silently skip files that can't be compressed
121
- }
122
- }
123
- }
124
-
125
- buildLogger.success(`Compressed ${compressedCount} files`)
126
- results.optimizations.push({
127
- type: 'compression',
128
- description: `Created gzip versions for ${compressedCount} files`,
129
- sizeSaved: 0
130
- })
131
- }
132
-
133
- private async removeUnusedCSS(buildPath: string, results: OptimizationResult): Promise<void> {
134
- buildLogger.step("Analyzing CSS...")
135
-
136
- // This is a placeholder - real implementation would use PurgeCSS or similar
137
- results.optimizations.push({
138
- type: 'css-purging',
139
- description: 'CSS purging not implemented yet',
140
- sizeSaved: 0
141
- })
142
- }
143
-
144
- private async optimizeImages(buildPath: string, results: OptimizationResult): Promise<void> {
145
- buildLogger.step("Optimizing images...")
146
-
147
- // This is a placeholder - real implementation would use imagemin or similar
148
- results.optimizations.push({
149
- type: 'image-optimization',
150
- description: 'Image optimization not implemented yet',
151
- sizeSaved: 0
152
- })
153
- }
154
-
155
- private async analyzeBundles(buildPath: string, results: OptimizationResult): Promise<void> {
156
- buildLogger.step("Analyzing bundles...")
157
-
158
- const files = this.getFilesRecursively(buildPath)
159
- const jsFiles = files.filter(f => extname(f) === '.js')
160
-
161
- let totalJSSize = 0
162
- for (const file of jsFiles) {
163
- totalJSSize += statSync(file).size
164
- }
165
-
166
- results.optimizations.push({
167
- type: 'bundle-analysis',
168
- description: `Analyzed ${jsFiles.length} JS bundles (${(totalJSSize / 1024).toFixed(2)} KB total)`,
169
- sizeSaved: 0
170
- })
171
- }
172
-
173
- private async calculateDirectorySize(dirPath: string): Promise<number> {
174
- const files = this.getFilesRecursively(dirPath)
175
- let totalSize = 0
176
-
177
- for (const file of files) {
178
- try {
179
- totalSize += statSync(file).size
180
- } catch (error) {
181
- // Ignore files that can't be read
182
- }
183
- }
184
-
185
- return totalSize
186
- }
187
-
188
- private getFilesRecursively(dir: string): string[] {
189
- const files: string[] = []
190
-
191
- try {
192
- const items = readdirSync(dir, { withFileTypes: true })
193
-
194
- for (const item of items) {
195
- const fullPath = join(dir, item.name)
196
-
197
- if (item.isDirectory()) {
198
- files.push(...this.getFilesRecursively(fullPath))
199
- } else {
200
- files.push(fullPath)
201
- }
202
- }
203
- } catch (error) {
204
- // Ignore directories that can't be read
205
- }
206
-
207
- return files
208
- }
209
-
210
- async createOptimizationReport(result: OptimizationResult): Promise<string> {
211
- const report = `
212
- # Build Optimization Report
213
-
214
- ## Summary
215
- - **Status**: ${result.success ? '✅ Success' : '❌ Failed'}
216
- - **Duration**: ${result.duration}ms
217
- - **Original Size**: ${(result.originalSize / 1024).toFixed(2)} KB
218
- - **Optimized Size**: ${(result.optimizedSize / 1024).toFixed(2)} KB
219
- - **Size Reduction**: ${result.compressionRatio.toFixed(2)}%
220
-
221
- ## Optimizations Applied
222
-
223
- ${result.optimizations.map(opt =>
224
- `### ${opt.type.charAt(0).toUpperCase() + opt.type.slice(1)}
225
- - ${opt.description}
226
- - Size Saved: ${(opt.sizeSaved / 1024).toFixed(2)} KB`
227
- ).join('\n\n')}
228
-
229
- ${result.error ? `## Error\n${result.error}` : ''}
230
-
231
- Generated at: ${new Date().toISOString()}
232
- `
233
-
234
- return report.trim()
235
- }
1
+ import { readFileSync, writeFileSync, statSync, readdirSync } from "fs"
2
+ import { join, extname } from "path"
3
+ import { gzipSync } from "zlib"
4
+ import type { OptimizationConfig, OptimizationResult } from "../types/build"
5
+ import { buildLogger } from "../utils/build-logger"
6
+
7
+ export interface OptimizerConfig {
8
+ treeshake: boolean
9
+ compress: boolean
10
+ removeUnusedCSS: boolean
11
+ optimizeImages: boolean
12
+ bundleAnalysis: boolean
13
+ }
14
+
15
+ export class Optimizer {
16
+ private config: OptimizerConfig
17
+
18
+ constructor(config: OptimizerConfig) {
19
+ this.config = config
20
+ }
21
+
22
+ async optimize(buildPath: string): Promise<OptimizationResult> {
23
+ buildLogger.section('Build Optimization', '🔧')
24
+
25
+ const startTime = Date.now()
26
+ const results: OptimizationResult = {
27
+ success: true,
28
+ duration: 0,
29
+ originalSize: 0,
30
+ optimizedSize: 0,
31
+ compressionRatio: 0,
32
+ optimizations: []
33
+ }
34
+
35
+ try {
36
+ // Get original size
37
+ results.originalSize = await this.calculateDirectorySize(buildPath)
38
+ buildLogger.step(`Original size: ${buildLogger.formatSize(results.originalSize)}`)
39
+
40
+ // Apply optimizations (minification removed for compatibility)
41
+
42
+ if (this.config.compress) {
43
+ await this.compressAssets(buildPath, results)
44
+ }
45
+
46
+ if (this.config.removeUnusedCSS) {
47
+ await this.removeUnusedCSS(buildPath, results)
48
+ }
49
+
50
+ if (this.config.optimizeImages) {
51
+ await this.optimizeImages(buildPath, results)
52
+ }
53
+
54
+ if (this.config.bundleAnalysis) {
55
+ await this.analyzeBundles(buildPath, results)
56
+ }
57
+
58
+ // Calculate final size and compression ratio
59
+ results.optimizedSize = await this.calculateDirectorySize(buildPath)
60
+ results.compressionRatio = results.originalSize > 0
61
+ ? ((results.originalSize - results.optimizedSize) / results.originalSize) * 100
62
+ : 0
63
+
64
+ results.duration = Date.now() - startTime
65
+
66
+ buildLogger.success(`Optimization completed in ${buildLogger.formatDuration(results.duration)}`)
67
+
68
+ // Create optimization summary table
69
+ const optimizationData = results.optimizations.map(opt => ({
70
+ type: opt.type,
71
+ description: opt.description,
72
+ saved: buildLogger.formatSize(opt.sizeSaved)
73
+ }))
74
+
75
+ if (optimizationData.length > 0) {
76
+ buildLogger.table(
77
+ [
78
+ { header: 'Optimization', key: 'type', width: 20, align: 'left', color: 'cyan' },
79
+ { header: 'Description', key: 'description', width: 35, align: 'left' },
80
+ { header: 'Size Saved', key: 'saved', width: 12, align: 'right', color: 'green' }
81
+ ],
82
+ optimizationData
83
+ )
84
+ }
85
+
86
+ return results
87
+
88
+ } catch (error) {
89
+ results.success = false
90
+ results.duration = Date.now() - startTime
91
+ results.error = error instanceof Error ? error.message : "Unknown optimization error"
92
+
93
+ buildLogger.error(`Optimization failed: ${results.error}`)
94
+ return results
95
+ }
96
+ }
97
+
98
+ // Minification methods removed for compatibility with Bun bundler
99
+
100
+ private async compressAssets(buildPath: string, results: OptimizationResult): Promise<void> {
101
+ buildLogger.step("Compressing assets...")
102
+
103
+ const files = this.getFilesRecursively(buildPath)
104
+ let compressedCount = 0
105
+
106
+ for (const file of files) {
107
+ const ext = extname(file).toLowerCase()
108
+
109
+ if (['.js', '.css', '.html', '.json', '.svg'].includes(ext)) {
110
+ try {
111
+ const content = readFileSync(file)
112
+ const compressed = gzipSync(content)
113
+
114
+ // Only create .gz file if it's significantly smaller
115
+ if (compressed.length < content.length * 0.9) {
116
+ writeFileSync(file + '.gz', compressed)
117
+ compressedCount++
118
+ }
119
+ } catch (error) {
120
+ // Silently skip files that can't be compressed
121
+ }
122
+ }
123
+ }
124
+
125
+ buildLogger.success(`Compressed ${compressedCount} files`)
126
+ results.optimizations.push({
127
+ type: 'compression',
128
+ description: `Created gzip versions for ${compressedCount} files`,
129
+ sizeSaved: 0
130
+ })
131
+ }
132
+
133
+ private async removeUnusedCSS(buildPath: string, results: OptimizationResult): Promise<void> {
134
+ buildLogger.step("Analyzing CSS...")
135
+
136
+ // This is a placeholder - real implementation would use PurgeCSS or similar
137
+ results.optimizations.push({
138
+ type: 'css-purging',
139
+ description: 'CSS purging not implemented yet',
140
+ sizeSaved: 0
141
+ })
142
+ }
143
+
144
+ private async optimizeImages(buildPath: string, results: OptimizationResult): Promise<void> {
145
+ buildLogger.step("Optimizing images...")
146
+
147
+ // This is a placeholder - real implementation would use imagemin or similar
148
+ results.optimizations.push({
149
+ type: 'image-optimization',
150
+ description: 'Image optimization not implemented yet',
151
+ sizeSaved: 0
152
+ })
153
+ }
154
+
155
+ private async analyzeBundles(buildPath: string, results: OptimizationResult): Promise<void> {
156
+ buildLogger.step("Analyzing bundles...")
157
+
158
+ const files = this.getFilesRecursively(buildPath)
159
+ const jsFiles = files.filter(f => extname(f) === '.js')
160
+
161
+ let totalJSSize = 0
162
+ for (const file of jsFiles) {
163
+ totalJSSize += statSync(file).size
164
+ }
165
+
166
+ results.optimizations.push({
167
+ type: 'bundle-analysis',
168
+ description: `Analyzed ${jsFiles.length} JS bundles (${(totalJSSize / 1024).toFixed(2)} KB total)`,
169
+ sizeSaved: 0
170
+ })
171
+ }
172
+
173
+ private async calculateDirectorySize(dirPath: string): Promise<number> {
174
+ const files = this.getFilesRecursively(dirPath)
175
+ let totalSize = 0
176
+
177
+ for (const file of files) {
178
+ try {
179
+ totalSize += statSync(file).size
180
+ } catch (error) {
181
+ // Ignore files that can't be read
182
+ }
183
+ }
184
+
185
+ return totalSize
186
+ }
187
+
188
+ private getFilesRecursively(dir: string): string[] {
189
+ const files: string[] = []
190
+
191
+ try {
192
+ const items = readdirSync(dir, { withFileTypes: true })
193
+
194
+ for (const item of items) {
195
+ const fullPath = join(dir, item.name)
196
+
197
+ if (item.isDirectory()) {
198
+ files.push(...this.getFilesRecursively(fullPath))
199
+ } else {
200
+ files.push(fullPath)
201
+ }
202
+ }
203
+ } catch (error) {
204
+ // Ignore directories that can't be read
205
+ }
206
+
207
+ return files
208
+ }
209
+
210
+ async createOptimizationReport(result: OptimizationResult): Promise<string> {
211
+ const report = `
212
+ # Build Optimization Report
213
+
214
+ ## Summary
215
+ - **Status**: ${result.success ? '✅ Success' : '❌ Failed'}
216
+ - **Duration**: ${result.duration}ms
217
+ - **Original Size**: ${(result.originalSize / 1024).toFixed(2)} KB
218
+ - **Optimized Size**: ${(result.optimizedSize / 1024).toFixed(2)} KB
219
+ - **Size Reduction**: ${result.compressionRatio.toFixed(2)}%
220
+
221
+ ## Optimizations Applied
222
+
223
+ ${result.optimizations.map(opt =>
224
+ `### ${opt.type.charAt(0).toUpperCase() + opt.type.slice(1)}
225
+ - ${opt.description}
226
+ - Size Saved: ${(opt.sizeSaved / 1024).toFixed(2)} KB`
227
+ ).join('\n\n')}
228
+
229
+ ${result.error ? `## Error\n${result.error}` : ''}
230
+
231
+ Generated at: ${new Date().toISOString()}
232
+ `
233
+
234
+ return report.trim()
235
+ }
236
236
  }
@@ -4,11 +4,23 @@
4
4
  import React, { createContext, useContext, useEffect, useRef, useState, useCallback } from 'react'
5
5
  import type { WebSocketMessage, WebSocketResponse } from '../types/types'
6
6
 
7
+ /** Auth credentials to send during WebSocket connection */
8
+ export interface LiveAuthOptions {
9
+ /** JWT or opaque token */
10
+ token?: string
11
+ /** Provider name (if multiple auth providers configured) */
12
+ provider?: string
13
+ /** Additional credentials (publicKey, signature, etc.) */
14
+ [key: string]: unknown
15
+ }
16
+
7
17
  export interface LiveComponentsContextValue {
8
18
  connected: boolean
9
19
  connecting: boolean
10
20
  error: string | null
11
21
  connectionId: string | null
22
+ /** Whether the WebSocket connection is authenticated */
23
+ authenticated: boolean
12
24
 
13
25
  // Send message without waiting for response
14
26
  sendMessage: (message: WebSocketMessage) => Promise<void>
@@ -28,6 +40,9 @@ export interface LiveComponentsContextValue {
28
40
  // Manual reconnect
29
41
  reconnect: () => void
30
42
 
43
+ // Authenticate (or re-authenticate) the WebSocket connection
44
+ authenticate: (credentials: LiveAuthOptions) => Promise<boolean>
45
+
31
46
  // Get current WebSocket instance (for advanced use)
32
47
  getWebSocket: () => WebSocket | null
33
48
  }
@@ -37,6 +52,8 @@ const LiveComponentsContext = createContext<LiveComponentsContextValue | null>(n
37
52
  export interface LiveComponentsProviderProps {
38
53
  children: React.ReactNode
39
54
  url?: string
55
+ /** Auth credentials to send on connection */
56
+ auth?: LiveAuthOptions
40
57
  autoConnect?: boolean
41
58
  reconnectInterval?: number
42
59
  maxReconnectAttempts?: number
@@ -47,6 +64,7 @@ export interface LiveComponentsProviderProps {
47
64
  export function LiveComponentsProvider({
48
65
  children,
49
66
  url,
67
+ auth,
50
68
  autoConnect = true,
51
69
  reconnectInterval = 1000,
52
70
  maxReconnectAttempts = 5,
@@ -56,12 +74,23 @@ export function LiveComponentsProvider({
56
74
 
57
75
  // Get WebSocket URL dynamically
58
76
  const getWebSocketUrl = () => {
59
- if (url) return url
60
- if (typeof window === 'undefined') return 'ws://localhost:3000/api/live/ws'
77
+ let baseUrl: string
78
+ if (url) {
79
+ baseUrl = url
80
+ } else if (typeof window === 'undefined') {
81
+ baseUrl = 'ws://localhost:3000/api/live/ws'
82
+ } else {
83
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
84
+ baseUrl = `${protocol}//${window.location.host}/api/live/ws`
85
+ }
61
86
 
62
- // Always use current host - works for both dev and production
63
- const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
64
- return `${protocol}//${window.location.host}/api/live/ws`
87
+ // Append auth token as query param if provided
88
+ if (auth?.token) {
89
+ const separator = baseUrl.includes('?') ? '&' : '?'
90
+ return `${baseUrl}${separator}token=${encodeURIComponent(auth.token)}`
91
+ }
92
+
93
+ return baseUrl
65
94
  }
66
95
 
67
96
  const wsUrl = getWebSocketUrl()
@@ -71,6 +100,7 @@ export function LiveComponentsProvider({
71
100
  const [connecting, setConnecting] = useState(false)
72
101
  const [error, setError] = useState<string | null>(null)
73
102
  const [connectionId, setConnectionId] = useState<string | null>(null)
103
+ const [authenticated, setAuthenticated] = useState(false)
74
104
 
75
105
  // Refs
76
106
  const wsRef = useRef<WebSocket | null>(null)
@@ -136,7 +166,30 @@ export function LiveComponentsProvider({
136
166
  // Handle connection established
137
167
  if (response.type === 'CONNECTION_ESTABLISHED') {
138
168
  setConnectionId(response.connectionId || null)
169
+ setAuthenticated((response as any).authenticated || false)
139
170
  log('🔗 Connection ID:', response.connectionId)
171
+ if ((response as any).authenticated) {
172
+ log('🔒 Authenticated as:', (response as any).userId)
173
+ }
174
+
175
+ // If auth credentials provided but not yet authenticated via query,
176
+ // send AUTH message with full credentials
177
+ if (auth && !auth.token && Object.keys(auth).some(k => auth[k])) {
178
+ sendMessageAndWait({
179
+ type: 'AUTH',
180
+ payload: auth
181
+ } as any).then(authResp => {
182
+ if ((authResp as any).authenticated) {
183
+ setAuthenticated(true)
184
+ log('🔒 Authenticated via message')
185
+ }
186
+ }).catch(() => {})
187
+ }
188
+ }
189
+
190
+ // Handle auth response
191
+ if (response.type === 'AUTH_RESPONSE') {
192
+ setAuthenticated((response as any).authenticated || false)
140
193
  }
141
194
 
142
195
  // Handle pending requests (request-response pattern)
@@ -403,6 +456,22 @@ export function LiveComponentsProvider({
403
456
  log('🗑️ Component unregistered', componentId)
404
457
  }, [log])
405
458
 
459
+ // Authenticate (or re-authenticate) the WebSocket connection
460
+ const authenticate = useCallback(async (credentials: LiveAuthOptions): Promise<boolean> => {
461
+ try {
462
+ const response = await sendMessageAndWait({
463
+ type: 'AUTH',
464
+ payload: credentials
465
+ } as any, 5000)
466
+
467
+ const success = (response as any).authenticated || false
468
+ setAuthenticated(success)
469
+ return success
470
+ } catch {
471
+ return false
472
+ }
473
+ }, [sendMessageAndWait])
474
+
406
475
  // Get WebSocket instance
407
476
  const getWebSocket = useCallback(() => {
408
477
  return wsRef.current
@@ -424,12 +493,14 @@ export function LiveComponentsProvider({
424
493
  connecting,
425
494
  error,
426
495
  connectionId,
496
+ authenticated,
427
497
  sendMessage,
428
498
  sendMessageAndWait,
429
499
  sendBinaryAndWait,
430
500
  registerComponent,
431
501
  unregisterComponent,
432
502
  reconnect,
503
+ authenticate,
433
504
  getWebSocket
434
505
  }
435
506
 
@@ -52,16 +52,23 @@ type ExtractState<T> = T extends { new(...args: any[]): { state: infer S } }
52
52
  ? S extends Record<string, any> ? S : Record<string, any>
53
53
  : ExtractDefaultState<T>
54
54
 
55
- // Extrai as Actions (métodos públicos async) da classe do servidor
55
+ // Extrai os nomes de publicActions como union type
56
+ type ExtractPublicActionNames<T> = T extends { publicActions: readonly (infer A)[] }
57
+ ? A extends string ? A : never
58
+ : never
59
+
60
+ // Extrai as Actions respeitando publicActions (MANDATORY)
61
+ // - Se publicActions está definido: somente métodos listados são expostos
62
+ // - Se publicActions NÃO está definido: nenhuma action disponível (secure by default)
56
63
  type ExtractActions<T> = T extends { new(...args: any[]): infer Instance }
57
- ? {
58
- [K in keyof Instance as Instance[K] extends (...args: any[]) => Promise<any>
59
- ? K extends 'setState' | 'getState' | 'getValue' | 'setValue' | 'setValues' | 'getSnapshot'
60
- ? never
61
- : K
62
- : never
63
- ]: Instance[K]
64
- }
64
+ ? T extends { publicActions: readonly string[] }
65
+ ? {
66
+ [K in keyof Instance as K extends ExtractPublicActionNames<T>
67
+ ? Instance[K] extends (...args: any[]) => Promise<any> ? K : never
68
+ : never
69
+ ]: Instance[K]
70
+ }
71
+ : Record<string, never>
65
72
  : Record<string, never>
66
73
 
67
74
  // ===== Opções do Live.use() =====
@@ -74,7 +81,7 @@ interface LiveUseOptions<TState> extends UseLiveComponentOptions {
74
81
  // ===== Hook Principal =====
75
82
 
76
83
  function useLive<
77
- T extends { new(...args: any[]): any; defaultState?: Record<string, any>; componentName: string },
84
+ T extends { new(...args: any[]): any; defaultState?: Record<string, any>; componentName: string; publicActions?: readonly string[] },
78
85
  TBroadcasts extends Record<string, any> = Record<string, any>
79
86
  >(
80
87
  ComponentClass: T,