create-fluxstack 1.14.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 (62) hide show
  1. package/LLMD/resources/live-components.md +207 -12
  2. package/app/client/.live-stubs/LiveAdminPanel.js +5 -0
  3. package/app/client/.live-stubs/LiveChat.js +7 -0
  4. package/app/client/.live-stubs/LiveCounter.js +9 -0
  5. package/app/client/.live-stubs/LiveForm.js +11 -0
  6. package/app/client/.live-stubs/LiveLocalCounter.js +8 -0
  7. package/app/client/.live-stubs/LiveRoomChat.js +10 -0
  8. package/app/client/.live-stubs/LiveTodoList.js +9 -0
  9. package/app/client/.live-stubs/LiveUpload.js +15 -0
  10. package/app/client/src/App.tsx +11 -0
  11. package/app/client/src/components/AppLayout.tsx +16 -8
  12. package/app/client/src/live/LiveDebuggerPanel.tsx +1 -1
  13. package/app/client/src/live/TodoListDemo.tsx +158 -0
  14. package/app/server/auth/DevAuthProvider.ts +2 -2
  15. package/app/server/auth/JWTAuthProvider.example.ts +2 -2
  16. package/app/server/index.ts +2 -2
  17. package/app/server/live/LiveAdminPanel.ts +1 -1
  18. package/app/server/live/LiveProtectedChat.ts +1 -1
  19. package/app/server/live/LiveTodoList.ts +110 -0
  20. package/app/server/routes/room.routes.ts +1 -2
  21. package/core/build/live-components-generator.ts +1 -1
  22. package/core/build/vite-plugins.ts +28 -0
  23. package/core/client/components/LiveDebugger.tsx +1 -1
  24. package/core/client/hooks/useLiveUpload.ts +3 -4
  25. package/core/client/index.ts +37 -31
  26. package/core/framework/server.ts +1 -1
  27. package/core/server/index.ts +1 -2
  28. package/core/server/live/auto-generated-components.ts +6 -3
  29. package/core/server/live/index.ts +95 -21
  30. package/core/server/live/websocket-plugin.ts +27 -1087
  31. package/core/types/types.ts +76 -1025
  32. package/core/utils/version.ts +1 -1
  33. package/create-fluxstack.ts +1 -1
  34. package/package.json +5 -1
  35. package/plugins/crypto-auth/index.ts +1 -1
  36. package/plugins/crypto-auth/server/CryptoAuthLiveProvider.ts +2 -2
  37. package/vite.config.ts +40 -12
  38. package/core/client/LiveComponentsProvider.tsx +0 -531
  39. package/core/client/components/Live.tsx +0 -111
  40. package/core/client/hooks/AdaptiveChunkSizer.ts +0 -215
  41. package/core/client/hooks/state-validator.ts +0 -130
  42. package/core/client/hooks/useChunkedUpload.ts +0 -359
  43. package/core/client/hooks/useLiveChunkedUpload.ts +0 -87
  44. package/core/client/hooks/useLiveComponent.ts +0 -853
  45. package/core/client/hooks/useLiveDebugger.ts +0 -392
  46. package/core/client/hooks/useRoom.ts +0 -409
  47. package/core/client/hooks/useRoomProxy.ts +0 -382
  48. package/core/server/live/ComponentRegistry.ts +0 -1128
  49. package/core/server/live/FileUploadManager.ts +0 -446
  50. package/core/server/live/LiveComponentPerformanceMonitor.ts +0 -931
  51. package/core/server/live/LiveDebugger.ts +0 -462
  52. package/core/server/live/LiveLogger.ts +0 -144
  53. package/core/server/live/LiveRoomManager.ts +0 -278
  54. package/core/server/live/RoomEventBus.ts +0 -234
  55. package/core/server/live/RoomStateManager.ts +0 -172
  56. package/core/server/live/SingleConnectionManager.ts +0 -0
  57. package/core/server/live/StateSignature.ts +0 -705
  58. package/core/server/live/WebSocketConnectionManager.ts +0 -710
  59. package/core/server/live/auth/LiveAuthContext.ts +0 -71
  60. package/core/server/live/auth/LiveAuthManager.ts +0 -304
  61. package/core/server/live/auth/index.ts +0 -19
  62. package/core/server/live/auth/types.ts +0 -179
@@ -1,446 +0,0 @@
1
- import { writeFile, mkdir, unlink } from 'fs/promises'
2
- import { existsSync } from 'fs'
3
- import { join, extname, basename } from 'path'
4
- import { liveLog, liveWarn } from './LiveLogger'
5
- import type {
6
- ActiveUpload,
7
- FileUploadStartMessage,
8
- FileUploadChunkMessage,
9
- FileUploadCompleteMessage,
10
- FileUploadProgressResponse,
11
- FileUploadCompleteResponse
12
- } from '@core/types/types'
13
-
14
- // ๐Ÿ”’ Magic bytes mapping for content validation
15
- // Validates actual file content, not just the MIME type header
16
- const MAGIC_BYTES: Record<string, { bytes: number[]; offset?: number }[]> = {
17
- 'image/jpeg': [{ bytes: [0xFF, 0xD8, 0xFF] }],
18
- 'image/png': [{ bytes: [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A] }],
19
- 'image/gif': [
20
- { bytes: [0x47, 0x49, 0x46, 0x38, 0x37, 0x61] }, // GIF87a
21
- { bytes: [0x47, 0x49, 0x46, 0x38, 0x39, 0x61] }, // GIF89a
22
- ],
23
- 'image/webp': [
24
- { bytes: [0x52, 0x49, 0x46, 0x46], offset: 0 }, // RIFF header
25
- // Byte 8-11 should be WEBP, checked separately
26
- ],
27
- 'application/pdf': [{ bytes: [0x25, 0x50, 0x44, 0x46] }], // %PDF
28
- 'application/zip': [
29
- { bytes: [0x50, 0x4B, 0x03, 0x04] }, // PK\x03\x04
30
- { bytes: [0x50, 0x4B, 0x05, 0x06] }, // Empty archive
31
- ],
32
- 'application/gzip': [{ bytes: [0x1F, 0x8B] }],
33
- }
34
-
35
- export class FileUploadManager {
36
- private activeUploads = new Map<string, ActiveUpload>()
37
- private readonly maxUploadSize = 50 * 1024 * 1024 // ๐Ÿ”’ 50MB max (reduced from 500MB)
38
- private readonly chunkTimeout = 30000 // 30 seconds timeout per chunk
39
- // ๐Ÿ”’ Per-user upload quota tracking
40
- private userUploadBytes = new Map<string, number>() // userId -> total bytes uploaded
41
- private readonly maxBytesPerUser = 500 * 1024 * 1024 // ๐Ÿ”’ 500MB per user total
42
- private readonly quotaResetInterval = 24 * 60 * 60 * 1000 // Reset quotas daily
43
- // ๐Ÿ”’ Default allowed MIME types - safe file types only
44
- private readonly allowedTypes: string[] = [
45
- 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml',
46
- 'application/pdf',
47
- 'text/plain', 'text/csv', 'text/markdown',
48
- 'application/json',
49
- 'application/zip', 'application/gzip',
50
- ]
51
- // ๐Ÿ”’ Blocked file extensions that could be dangerous
52
- private readonly blockedExtensions: Set<string> = new Set([
53
- '.exe', '.bat', '.cmd', '.com', '.msi', '.scr', '.pif',
54
- '.sh', '.bash', '.zsh', '.csh',
55
- '.ps1', '.psm1', '.psd1',
56
- '.vbs', '.vbe', '.js', '.jse', '.wsf', '.wsh',
57
- '.dll', '.sys', '.drv', '.so', '.dylib',
58
- ])
59
-
60
- constructor() {
61
- // Cleanup stale uploads every 5 minutes
62
- setInterval(() => this.cleanupStaleUploads(), 5 * 60 * 1000)
63
- // ๐Ÿ”’ Reset per-user upload quotas daily
64
- setInterval(() => this.resetUploadQuotas(), this.quotaResetInterval)
65
- }
66
-
67
- async startUpload(message: FileUploadStartMessage, userId?: string): Promise<{ success: boolean; error?: string }> {
68
- try {
69
- const { uploadId, componentId, filename, fileType, fileSize, chunkSize = 64 * 1024 } = message
70
-
71
- // ๐Ÿ”’ Validate file size
72
- if (fileSize > this.maxUploadSize) {
73
- throw new Error(`File too large: ${fileSize} bytes. Max: ${this.maxUploadSize} bytes`)
74
- }
75
-
76
- // ๐Ÿ”’ Per-user upload quota check
77
- if (userId) {
78
- const currentUsage = this.userUploadBytes.get(userId) || 0
79
- if (currentUsage + fileSize > this.maxBytesPerUser) {
80
- throw new Error(`Upload quota exceeded for user. Used: ${currentUsage} bytes, limit: ${this.maxBytesPerUser} bytes`)
81
- }
82
- }
83
-
84
- // ๐Ÿ”’ Validate MIME type against allowlist
85
- if (this.allowedTypes.length > 0 && !this.allowedTypes.includes(fileType)) {
86
- throw new Error(`File type not allowed: ${fileType}`)
87
- }
88
-
89
- // ๐Ÿ”’ Validate filename - sanitize and check extension
90
- const safeBase = basename(filename) // Strip any path traversal
91
- const ext = extname(safeBase).toLowerCase()
92
- if (this.blockedExtensions.has(ext)) {
93
- throw new Error(`File extension not allowed: ${ext}`)
94
- }
95
-
96
- // ๐Ÿ”’ Double extension bypass prevention (e.g., malware.exe.jpg)
97
- const parts = safeBase.split('.')
98
- if (parts.length > 2) {
99
- // Check all intermediate extensions
100
- for (let i = 1; i < parts.length - 1; i++) {
101
- const intermediateExt = '.' + parts[i].toLowerCase()
102
- if (this.blockedExtensions.has(intermediateExt)) {
103
- throw new Error(`Suspicious double extension detected: ${intermediateExt} in ${safeBase}`)
104
- }
105
- }
106
- }
107
-
108
- // ๐Ÿ”’ Validate filename length
109
- if (safeBase.length > 255) {
110
- throw new Error('Filename too long')
111
- }
112
-
113
- // Check if upload already exists
114
- if (this.activeUploads.has(uploadId)) {
115
- throw new Error(`Upload ${uploadId} already in progress`)
116
- }
117
-
118
- // Calculate total chunks
119
- const totalChunks = Math.ceil(fileSize / chunkSize)
120
-
121
- // Create upload record
122
- const upload: ActiveUpload = {
123
- uploadId,
124
- componentId,
125
- filename,
126
- fileType,
127
- fileSize,
128
- totalChunks,
129
- receivedChunks: new Map(),
130
- bytesReceived: 0, // Track actual bytes for adaptive chunking
131
- startTime: Date.now(),
132
- lastChunkTime: Date.now()
133
- }
134
-
135
- this.activeUploads.set(uploadId, upload)
136
-
137
- // ๐Ÿ”’ Reserve quota for this upload
138
- if (userId) {
139
- const currentUsage = this.userUploadBytes.get(userId) || 0
140
- this.userUploadBytes.set(userId, currentUsage + fileSize)
141
- }
142
-
143
- liveLog('messages', componentId, '๐Ÿ“ค Upload started:', {
144
- uploadId,
145
- componentId,
146
- filename,
147
- fileType,
148
- fileSize,
149
- totalChunks,
150
- userId: userId || 'anonymous'
151
- })
152
-
153
- return { success: true }
154
-
155
- } catch (error: any) {
156
- console.error('โŒ Upload start failed:', error.message)
157
- return { success: false, error: error.message }
158
- }
159
- }
160
-
161
- async receiveChunk(message: FileUploadChunkMessage, ws: any, binaryData: Buffer | null = null): Promise<FileUploadProgressResponse | null> {
162
- try {
163
- const { uploadId, chunkIndex, totalChunks, data } = message
164
-
165
- const upload = this.activeUploads.get(uploadId)
166
- if (!upload) {
167
- throw new Error(`Upload ${uploadId} not found`)
168
- }
169
-
170
- // Validate chunk index
171
- if (chunkIndex < 0 || chunkIndex >= totalChunks) {
172
- throw new Error(`Invalid chunk index: ${chunkIndex}`)
173
- }
174
-
175
- // Check if chunk already received
176
- if (upload.receivedChunks.has(chunkIndex)) {
177
- liveLog('messages', upload.componentId, `๐Ÿ“ฆ Chunk ${chunkIndex} already received for upload ${uploadId}`)
178
- } else {
179
- // Store chunk data - use binary data if available, otherwise use base64 string
180
- let chunkBytes: number
181
-
182
- if (binaryData) {
183
- // Binary protocol: store Buffer directly (more efficient)
184
- upload.receivedChunks.set(chunkIndex, binaryData)
185
- chunkBytes = binaryData.length
186
- } else {
187
- // JSON protocol: store base64 string (legacy support)
188
- upload.receivedChunks.set(chunkIndex, data as string)
189
- chunkBytes = Buffer.from(data as string, 'base64').length
190
- }
191
-
192
- upload.lastChunkTime = Date.now()
193
- upload.bytesReceived += chunkBytes
194
-
195
- liveLog('messages', upload.componentId, `๐Ÿ“ฆ Received chunk ${chunkIndex + 1}/${totalChunks} for upload ${uploadId} (${chunkBytes} bytes, total: ${upload.bytesReceived}/${upload.fileSize})${binaryData ? ' [binary]' : ' [base64]'}`)
196
- }
197
-
198
- // Calculate progress based on actual bytes received (supports adaptive chunking)
199
- const progress = (upload.bytesReceived / upload.fileSize) * 100
200
- const bytesUploaded = upload.bytesReceived
201
-
202
- // Log completion status (but don't finalize until COMPLETE message)
203
- if (upload.bytesReceived >= upload.fileSize) {
204
- liveLog('messages', upload.componentId, `โœ… All bytes received for upload ${uploadId} (${upload.bytesReceived}/${upload.fileSize}), waiting for COMPLETE message`)
205
- }
206
-
207
- return {
208
- type: 'FILE_UPLOAD_PROGRESS',
209
- componentId: upload.componentId,
210
- uploadId: upload.uploadId,
211
- chunkIndex,
212
- totalChunks,
213
- bytesUploaded: Math.min(bytesUploaded, upload.fileSize),
214
- totalBytes: upload.fileSize,
215
- progress: Math.min(progress, 100),
216
- timestamp: Date.now()
217
- }
218
-
219
- } catch (error: any) {
220
- console.error(`โŒ Chunk receive failed for upload ${message.uploadId}:`, error.message)
221
- throw error
222
- }
223
- }
224
-
225
- private async finalizeUpload(upload: ActiveUpload): Promise<void> {
226
- try {
227
- liveLog('messages', upload.componentId, `โœ… Upload completed: ${upload.uploadId}`)
228
-
229
- // Assemble file from chunks
230
- const fileUrl = await this.assembleFile(upload)
231
-
232
- // Cleanup
233
- this.activeUploads.delete(upload.uploadId)
234
-
235
- } catch (error: any) {
236
- console.error(`โŒ Upload finalization failed for ${upload.uploadId}:`, error.message)
237
- throw error
238
- }
239
- }
240
-
241
- async completeUpload(message: FileUploadCompleteMessage): Promise<FileUploadCompleteResponse> {
242
- try {
243
- const { uploadId } = message
244
-
245
- const upload = this.activeUploads.get(uploadId)
246
- if (!upload) {
247
- throw new Error(`Upload ${uploadId} not found`)
248
- }
249
-
250
- liveLog('messages', upload.componentId, `โœ… Upload completion requested: ${uploadId}`)
251
-
252
- // Validate bytes received (supports adaptive chunking)
253
- if (upload.bytesReceived !== upload.fileSize) {
254
- const bytesShort = upload.fileSize - upload.bytesReceived
255
- throw new Error(`Incomplete upload: received ${upload.bytesReceived}/${upload.fileSize} bytes (${bytesShort} bytes short)`)
256
- }
257
-
258
- // ๐Ÿ”’ Content validation: verify file magic bytes match claimed MIME type
259
- this.validateContentMagicBytes(upload)
260
-
261
- liveLog('messages', upload.componentId, `โœ… Upload validation passed: ${uploadId} (${upload.bytesReceived} bytes)`)
262
-
263
- // Assemble file from chunks
264
- const fileUrl = await this.assembleFile(upload)
265
-
266
- // Cleanup
267
- this.activeUploads.delete(uploadId)
268
-
269
- return {
270
- type: 'FILE_UPLOAD_COMPLETE',
271
- componentId: upload.componentId,
272
- uploadId: upload.uploadId,
273
- success: true,
274
- filename: upload.filename,
275
- fileUrl,
276
- timestamp: Date.now()
277
- }
278
-
279
- } catch (error: any) {
280
- console.error(`โŒ Upload completion failed for ${message.uploadId}:`, error.message)
281
-
282
- return {
283
- type: 'FILE_UPLOAD_COMPLETE',
284
- componentId: '',
285
- uploadId: message.uploadId,
286
- success: false,
287
- error: error.message,
288
- timestamp: Date.now()
289
- }
290
- }
291
- }
292
-
293
- private async assembleFile(upload: ActiveUpload): Promise<string> {
294
- try {
295
- // Create uploads directory if it doesn't exist
296
- const uploadsDir = './uploads'
297
- if (!existsSync(uploadsDir)) {
298
- await mkdir(uploadsDir, { recursive: true })
299
- }
300
-
301
- // ๐Ÿ”’ Generate secure unique filename using UUID (prevents path traversal and name collisions)
302
- const extension = extname(basename(upload.filename)).toLowerCase()
303
- const safeFilename = `${crypto.randomUUID()}${extension}`
304
- const filePath = join(uploadsDir, safeFilename)
305
-
306
- // Assemble chunks in order
307
- const chunks: Buffer[] = []
308
- for (let i = 0; i < upload.totalChunks; i++) {
309
- const chunkData = upload.receivedChunks.get(i)
310
- if (chunkData) {
311
- // Handle both Buffer (binary protocol) and string (base64 JSON protocol)
312
- if (Buffer.isBuffer(chunkData)) {
313
- chunks.push(chunkData)
314
- } else {
315
- chunks.push(Buffer.from(chunkData, 'base64'))
316
- }
317
- }
318
- }
319
-
320
- // Write assembled file
321
- const fileBuffer = Buffer.concat(chunks)
322
- await writeFile(filePath, fileBuffer)
323
-
324
- liveLog('messages', upload.componentId, `๐Ÿ“ File assembled: ${filePath}`)
325
- return `/uploads/${safeFilename}`
326
-
327
- } catch (error) {
328
- console.error('โŒ File assembly failed:', error)
329
- throw error
330
- }
331
- }
332
-
333
- private cleanupStaleUploads(): void {
334
- const now = Date.now()
335
- const staleUploads: string[] = []
336
-
337
- for (const [uploadId, upload] of this.activeUploads) {
338
- const timeSinceLastChunk = now - upload.lastChunkTime
339
-
340
- if (timeSinceLastChunk > this.chunkTimeout * 2) {
341
- staleUploads.push(uploadId)
342
- }
343
- }
344
-
345
- for (const uploadId of staleUploads) {
346
- this.activeUploads.delete(uploadId)
347
- liveLog('messages', null, `๐Ÿงน Cleaned up stale upload: ${uploadId}`)
348
- }
349
-
350
- if (staleUploads.length > 0) {
351
- liveLog('messages', null, `๐Ÿงน Cleaned up ${staleUploads.length} stale uploads`)
352
- }
353
- }
354
-
355
- /**
356
- * ๐Ÿ”’ Validate that the first bytes of the uploaded file match the claimed MIME type.
357
- * Prevents attacks where a malicious file is uploaded with a fake MIME type header.
358
- */
359
- private validateContentMagicBytes(upload: ActiveUpload): void {
360
- const expectedSignatures = MAGIC_BYTES[upload.fileType]
361
- if (!expectedSignatures) {
362
- // No magic bytes defined for this type (text types, SVG, JSON, etc.) - skip binary check
363
- // For text types, we could add content sniffing but it's less critical
364
- return
365
- }
366
-
367
- // Get the first chunk to read magic bytes
368
- const firstChunk = upload.receivedChunks.get(0)
369
- if (!firstChunk) {
370
- throw new Error('Cannot validate file content: first chunk missing')
371
- }
372
-
373
- const headerBuffer = Buffer.isBuffer(firstChunk)
374
- ? firstChunk
375
- : Buffer.from(firstChunk, 'base64')
376
-
377
- // Check if any of the expected signatures match
378
- let matched = false
379
- for (const sig of expectedSignatures) {
380
- const offset = sig.offset ?? 0
381
- if (headerBuffer.length < offset + sig.bytes.length) {
382
- continue // File too small for this signature
383
- }
384
-
385
- let sigMatches = true
386
- for (let i = 0; i < sig.bytes.length; i++) {
387
- if (headerBuffer[offset + i] !== sig.bytes[i]) {
388
- sigMatches = false
389
- break
390
- }
391
- }
392
-
393
- if (sigMatches) {
394
- matched = true
395
- break
396
- }
397
- }
398
-
399
- if (!matched) {
400
- liveWarn('messages', upload.componentId, `๐Ÿ”’ Content validation failed for upload ${upload.uploadId}: ` +
401
- `claimed type ${upload.fileType} does not match file magic bytes`)
402
- throw new Error(
403
- `File content does not match claimed type '${upload.fileType}'. ` +
404
- `The file may be disguised as a different format.`
405
- )
406
- }
407
- }
408
-
409
- /**
410
- * ๐Ÿ”’ Reset per-user upload quotas (called periodically)
411
- */
412
- private resetUploadQuotas(): void {
413
- const userCount = this.userUploadBytes.size
414
- this.userUploadBytes.clear()
415
- if (userCount > 0) {
416
- liveLog('messages', null, `๐Ÿ”’ Reset upload quotas for ${userCount} users`)
417
- }
418
- }
419
-
420
- /**
421
- * Get per-user upload usage
422
- */
423
- getUserUploadUsage(userId: string): { used: number; limit: number; remaining: number } {
424
- const used = this.userUploadBytes.get(userId) || 0
425
- return {
426
- used,
427
- limit: this.maxBytesPerUser,
428
- remaining: Math.max(0, this.maxBytesPerUser - used)
429
- }
430
- }
431
-
432
- getUploadStatus(uploadId: string): ActiveUpload | null {
433
- return this.activeUploads.get(uploadId) || null
434
- }
435
-
436
- getStats() {
437
- return {
438
- activeUploads: this.activeUploads.size,
439
- maxUploadSize: this.maxUploadSize,
440
- allowedTypes: this.allowedTypes
441
- }
442
- }
443
- }
444
-
445
- // Global instance
446
- export const fileUploadManager = new FileUploadManager()