create-fluxstack 1.13.0 → 1.15.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 (96) hide show
  1. package/LLMD/patterns/anti-patterns.md +100 -0
  2. package/LLMD/reference/routing.md +39 -39
  3. package/LLMD/resources/live-auth.md +20 -2
  4. package/LLMD/resources/live-components.md +300 -21
  5. package/LLMD/resources/live-logging.md +95 -33
  6. package/LLMD/resources/live-upload.md +59 -8
  7. package/app/client/.live-stubs/LiveAdminPanel.js +5 -0
  8. package/app/client/.live-stubs/LiveChat.js +7 -0
  9. package/app/client/.live-stubs/LiveCounter.js +9 -0
  10. package/app/client/.live-stubs/LiveForm.js +11 -0
  11. package/app/client/.live-stubs/LiveLocalCounter.js +8 -0
  12. package/app/client/.live-stubs/LiveRoomChat.js +10 -0
  13. package/app/client/.live-stubs/LiveTodoList.js +9 -0
  14. package/app/client/.live-stubs/LiveUpload.js +15 -0
  15. package/app/client/index.html +2 -2
  16. package/app/client/public/favicon.svg +46 -0
  17. package/app/client/src/App.tsx +13 -1
  18. package/app/client/src/assets/fluxstack-static.svg +46 -0
  19. package/app/client/src/assets/fluxstack.svg +183 -0
  20. package/app/client/src/components/AppLayout.tsx +146 -9
  21. package/app/client/src/components/BackButton.tsx +13 -13
  22. package/app/client/src/components/DemoPage.tsx +4 -4
  23. package/app/client/src/live/AuthDemo.tsx +23 -21
  24. package/app/client/src/live/ChatDemo.tsx +2 -2
  25. package/app/client/src/live/CounterDemo.tsx +12 -12
  26. package/app/client/src/live/FormDemo.tsx +2 -2
  27. package/app/client/src/live/LiveDebuggerPanel.tsx +779 -0
  28. package/app/client/src/live/RoomChatDemo.tsx +24 -16
  29. package/app/client/src/live/TodoListDemo.tsx +158 -0
  30. package/app/client/src/main.tsx +13 -13
  31. package/app/client/src/pages/ApiTestPage.tsx +6 -6
  32. package/app/client/src/pages/HomePage.tsx +80 -52
  33. package/app/server/auth/DevAuthProvider.ts +2 -2
  34. package/app/server/auth/JWTAuthProvider.example.ts +2 -2
  35. package/app/server/index.ts +2 -2
  36. package/app/server/live/LiveAdminPanel.ts +2 -1
  37. package/app/server/live/LiveChat.ts +78 -77
  38. package/app/server/live/LiveCounter.ts +1 -1
  39. package/app/server/live/LiveForm.ts +1 -0
  40. package/app/server/live/LiveLocalCounter.ts +38 -37
  41. package/app/server/live/LiveProtectedChat.ts +2 -1
  42. package/app/server/live/LiveRoomChat.ts +1 -0
  43. package/app/server/live/LiveTodoList.ts +110 -0
  44. package/app/server/live/LiveUpload.ts +1 -0
  45. package/app/server/live/register-components.ts +19 -19
  46. package/app/server/routes/room.routes.ts +1 -2
  47. package/config/system/runtime.config.ts +4 -0
  48. package/core/build/live-components-generator.ts +1 -1
  49. package/core/build/optimizer.ts +235 -235
  50. package/core/build/vite-plugins.ts +28 -0
  51. package/core/client/components/LiveDebugger.tsx +1324 -0
  52. package/core/client/hooks/useLiveUpload.ts +3 -4
  53. package/core/client/index.ts +41 -21
  54. package/core/framework/server.ts +1 -1
  55. package/core/plugins/built-in/index.ts +134 -134
  56. package/core/plugins/built-in/live-components/commands/create-live-component.ts +4 -0
  57. package/core/plugins/built-in/vite/index.ts +75 -21
  58. package/core/server/index.ts +14 -15
  59. package/core/server/live/auto-generated-components.ts +6 -3
  60. package/core/server/live/index.ts +95 -21
  61. package/core/server/live/websocket-plugin.ts +27 -862
  62. package/core/server/plugins/static-files-plugin.ts +179 -69
  63. package/core/types/build.ts +219 -219
  64. package/core/types/plugin.ts +107 -107
  65. package/core/types/types.ts +77 -890
  66. package/core/utils/logger/startup-banner.ts +82 -82
  67. package/core/utils/version.ts +6 -6
  68. package/create-fluxstack.ts +1 -1
  69. package/package.json +5 -1
  70. package/plugins/crypto-auth/index.ts +1 -1
  71. package/plugins/crypto-auth/server/CryptoAuthLiveProvider.ts +2 -2
  72. package/vite.config.ts +40 -12
  73. package/app/client/src/assets/react.svg +0 -1
  74. package/core/client/LiveComponentsProvider.tsx +0 -531
  75. package/core/client/components/Live.tsx +0 -105
  76. package/core/client/hooks/AdaptiveChunkSizer.ts +0 -215
  77. package/core/client/hooks/state-validator.ts +0 -130
  78. package/core/client/hooks/useChunkedUpload.ts +0 -359
  79. package/core/client/hooks/useLiveChunkedUpload.ts +0 -87
  80. package/core/client/hooks/useLiveComponent.ts +0 -843
  81. package/core/client/hooks/useRoom.ts +0 -409
  82. package/core/client/hooks/useRoomProxy.ts +0 -382
  83. package/core/server/live/ComponentRegistry.ts +0 -1099
  84. package/core/server/live/FileUploadManager.ts +0 -282
  85. package/core/server/live/LiveComponentPerformanceMonitor.ts +0 -931
  86. package/core/server/live/LiveLogger.ts +0 -111
  87. package/core/server/live/LiveRoomManager.ts +0 -262
  88. package/core/server/live/RoomEventBus.ts +0 -234
  89. package/core/server/live/RoomStateManager.ts +0 -172
  90. package/core/server/live/SingleConnectionManager.ts +0 -0
  91. package/core/server/live/StateSignature.ts +0 -645
  92. package/core/server/live/WebSocketConnectionManager.ts +0 -709
  93. package/core/server/live/auth/LiveAuthContext.ts +0 -71
  94. package/core/server/live/auth/LiveAuthManager.ts +0 -304
  95. package/core/server/live/auth/index.ts +0 -19
  96. package/core/server/live/auth/types.ts +0 -179
@@ -1,69 +1,179 @@
1
- // 🔥 FluxStack Static Files Plugin - Serve Public Files & Uploads
2
-
3
- import { existsSync, statSync } from 'fs'
4
- import { mkdir } from 'fs/promises'
5
- import { resolve } from 'path'
6
- import type { Plugin, PluginContext } from '../../plugins/types'
7
-
8
- export const staticFilesPlugin: Plugin = {
9
- name: 'static-files',
10
- description: 'Serve static files and uploads',
11
- author: 'FluxStack Team',
12
- priority: 'normal',
13
- category: 'core',
14
- tags: ['static', 'files', 'uploads'],
15
-
16
- setup: async (context: PluginContext) => {
17
- const projectRoot = process.cwd()
18
- const publicDir = resolve(projectRoot, 'public')
19
- const uploadsDir = resolve(projectRoot, 'uploads')
20
-
21
- // Create directories if they don't exist
22
- await mkdir(publicDir, { recursive: true })
23
- await mkdir(uploadsDir, { recursive: true })
24
- await mkdir(resolve(uploadsDir, 'avatars'), { recursive: true })
25
-
26
- // Helper to serve files from a directory
27
- const serveFile = (baseDir: string) => ({ params, set }: any) => {
28
- const requestedPath = params['*'] || ''
29
- const filePath = resolve(baseDir, requestedPath)
30
-
31
- // Path traversal protection
32
- if (!filePath.startsWith(baseDir)) {
33
- set.status = 400
34
- return { error: 'Invalid path' }
35
- }
36
-
37
- // Check if file exists
38
- if (!existsSync(filePath)) {
39
- set.status = 404
40
- return { error: 'File not found' }
41
- }
42
-
43
- // Check if it's a file (not directory)
44
- try {
45
- if (!statSync(filePath).isFile()) {
46
- set.status = 404
47
- return { error: 'Not a file' }
48
- }
49
- } catch {
50
- set.status = 404
51
- return { error: 'File not found' }
52
- }
53
-
54
- // Set cache header (1 year)
55
- set.headers['cache-control'] = 'public, max-age=31536000'
56
-
57
- // Bun.file() handles: content-type, content-length, streaming
58
- return Bun.file(filePath)
59
- }
60
-
61
- // Register routes
62
- context.app.get('/api/static/*', serveFile(publicDir))
63
- context.app.get('/api/uploads/*', serveFile(uploadsDir))
64
-
65
- context.logger.debug('📁 Static files plugin ready', {
66
- routes: ['/api/static/*', '/api/uploads/*']
67
- })
68
- }
69
- }
1
+ // FluxStack Static Files Plugin - Serve Public Files & Uploads
2
+
3
+ import { mkdir } from 'fs/promises'
4
+ import { resolve, extname, basename } from 'path'
5
+ import type { Plugin, PluginContext } from '../../plugins/types'
6
+ import { pluginsConfig } from '@config'
7
+
8
+ /** MIME types that should force a download instead of rendering inline */
9
+ const DANGEROUS_MIME_TYPES = new Set([
10
+ 'application/x-msdownload',
11
+ 'application/x-executable',
12
+ 'application/x-sharedlib',
13
+ 'application/x-mach-binary',
14
+ 'application/x-dosexec',
15
+ 'application/x-httpd-php',
16
+ 'application/java-archive',
17
+ 'application/x-sh',
18
+ 'application/x-csh',
19
+ 'application/x-bat',
20
+ ])
21
+
22
+ /** File extensions that should always force a download */
23
+ const DANGEROUS_EXTENSIONS = new Set([
24
+ '.exe', '.dll', '.bat', '.cmd', '.com', '.msi',
25
+ '.sh', '.csh', '.bash', '.ps1', '.vbs', '.wsf',
26
+ '.php', '.jsp', '.asp', '.aspx', '.py', '.rb', '.pl',
27
+ '.jar', '.war', '.class',
28
+ '.scr', '.pif', '.hta',
29
+ '.svg', // SVG can contain embedded scripts
30
+ ])
31
+
32
+ /** Extensions that carry a content hash in their filename (immutable) */
33
+ const HASHED_EXT = /\.[0-9a-f]{8,}\.\w+$/
34
+
35
+ /**
36
+ * Generate a weak ETag from Bun.file() metadata.
37
+ * Uses file.lastModified (ms timestamp) which is available without reading the file.
38
+ * Size comes from stat() since BunFile.size is unreliable until contents are read.
39
+ */
40
+ function generateETag(size: number, lastModified: number): string {
41
+ return `W/"${size.toString(16)}-${lastModified.toString(16)}"`
42
+ }
43
+
44
+ /** Sanitize a filename for use in Content-Disposition header */
45
+ function sanitizeFilename(name: string): string {
46
+ return name.replace(/[/\\:\0\x01-\x1f\x7f]/g, '_').replace(/"/g, '\\"')
47
+ }
48
+
49
+ /** Check if a MIME type or extension should force download */
50
+ function shouldForceDownload(filePath: string, mimeType: string | undefined): boolean {
51
+ const ext = extname(filePath).toLowerCase()
52
+ if (DANGEROUS_EXTENSIONS.has(ext)) return true
53
+ if (mimeType && DANGEROUS_MIME_TYPES.has(mimeType)) return true
54
+ return false
55
+ }
56
+
57
+ export const staticFilesPlugin: Plugin = {
58
+ name: 'static-files',
59
+ description: 'Serve static files and uploads',
60
+ author: 'FluxStack Team',
61
+ priority: 'normal',
62
+ category: 'core',
63
+ tags: ['static', 'files', 'uploads'],
64
+
65
+ setup: async (context: PluginContext) => {
66
+ if (!pluginsConfig.staticFilesEnabled) {
67
+ context.logger.debug('Static files plugin disabled')
68
+ return
69
+ }
70
+
71
+ const projectRoot = process.cwd()
72
+ const publicDir = resolve(projectRoot, pluginsConfig.staticPublicDir ?? 'public')
73
+ const uploadsDir = resolve(projectRoot, pluginsConfig.staticUploadsDir ?? 'uploads')
74
+ const cacheMaxAge = pluginsConfig.staticCacheMaxAge
75
+ const enablePublic = pluginsConfig.staticEnablePublic
76
+ const enableUploads = pluginsConfig.staticEnableUploads
77
+
78
+ // Async handler — uses Bun.file() APIs instead of Node fs
79
+ const serveFile = (baseDir: string, isUpload: boolean) => async ({ params, set, request }: any) => {
80
+ const requestedPath: string = params['*'] || ''
81
+
82
+ // Reject null bytes early — prevents filesystem confusion
83
+ if (requestedPath.includes('\0')) {
84
+ set.status = 400
85
+ return { error: 'Invalid path' }
86
+ }
87
+
88
+ const filePath = resolve(baseDir, requestedPath)
89
+
90
+ // Path traversal protection
91
+ if (!filePath.startsWith(baseDir)) {
92
+ set.status = 400
93
+ return { error: 'Invalid path' }
94
+ }
95
+
96
+ const file = Bun.file(filePath)
97
+
98
+ // Bun.file().stat() — single async call, no Node fs import needed.
99
+ // Returns size, isFile(), ctime etc. without reading file contents.
100
+ let stat: Awaited<ReturnType<typeof file.stat>>
101
+ try {
102
+ stat = await file.stat()
103
+ if (!stat.isFile()) {
104
+ set.status = 404
105
+ return { error: 'Not a file' }
106
+ }
107
+ } catch {
108
+ set.status = 404
109
+ return { error: 'File not found' }
110
+ }
111
+
112
+ // ETag from stat.size (reliable) + file.lastModified (Bun-native, no extra syscall)
113
+ const etag = generateETag(stat.size, file.lastModified)
114
+ const lastModified = new Date(file.lastModified).toUTCString()
115
+
116
+ // Conditional request: If-None-Match takes priority over If-Modified-Since
117
+ const ifNoneMatch = request?.headers?.get?.('if-none-match')
118
+ if (ifNoneMatch && ifNoneMatch === etag) {
119
+ set.status = 304
120
+ return null
121
+ }
122
+
123
+ const ifModifiedSince = request?.headers?.get?.('if-modified-since')
124
+ if (!ifNoneMatch && ifModifiedSince) {
125
+ const clientDate = new Date(ifModifiedSince).getTime()
126
+ if (!isNaN(clientDate) && file.lastModified <= clientDate) {
127
+ set.status = 304
128
+ return null
129
+ }
130
+ }
131
+
132
+ // Security headers
133
+ set.headers['x-content-type-options'] = 'nosniff'
134
+ set.headers['etag'] = etag
135
+ set.headers['last-modified'] = lastModified
136
+
137
+ // Cache strategy: hashed assets are immutable, uploads get short cache
138
+ if (!isUpload && HASHED_EXT.test(requestedPath)) {
139
+ set.headers['cache-control'] = `public, max-age=${cacheMaxAge}, immutable`
140
+ } else if (isUpload) {
141
+ set.headers['cache-control'] = 'public, max-age=3600, must-revalidate'
142
+ } else {
143
+ set.headers['cache-control'] = `public, max-age=${cacheMaxAge}`
144
+ }
145
+
146
+ // Force download for dangerous MIME types
147
+ // file.type is resolved from extension by Bun — no disk I/O
148
+ if (shouldForceDownload(filePath, file.type)) {
149
+ const fileName = sanitizeFilename(basename(requestedPath) || 'download')
150
+ set.headers['content-disposition'] = `attachment; filename="${fileName}"`
151
+ }
152
+
153
+ // Returning Bun.file() directly lets Bun use sendfile(2) for zero-copy transfer
154
+ return file
155
+ }
156
+
157
+ // Register routes based on config flags
158
+ if (enablePublic) {
159
+ await mkdir(publicDir, { recursive: true })
160
+ context.app.get('/api/static/*', serveFile(publicDir, false))
161
+ context.logger.debug('Static public files route registered: /api/static/*')
162
+ }
163
+
164
+ if (enableUploads) {
165
+ await mkdir(uploadsDir, { recursive: true })
166
+ context.app.get('/api/uploads/*', serveFile(uploadsDir, true))
167
+ context.logger.debug('Static uploads route registered: /api/uploads/*')
168
+ }
169
+
170
+ const routes = [
171
+ ...(enablePublic ? ['/api/static/*'] : []),
172
+ ...(enableUploads ? ['/api/uploads/*'] : [])
173
+ ]
174
+
175
+ if (routes.length > 0) {
176
+ context.logger.debug('Static files plugin ready', { routes })
177
+ }
178
+ }
179
+ }