create-fluxstack 1.13.0 โ†’ 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 (61) 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 +94 -10
  5. package/LLMD/resources/live-logging.md +95 -33
  6. package/LLMD/resources/live-upload.md +59 -8
  7. package/app/client/index.html +2 -2
  8. package/app/client/public/favicon.svg +46 -0
  9. package/app/client/src/App.tsx +2 -1
  10. package/app/client/src/assets/fluxstack-static.svg +46 -0
  11. package/app/client/src/assets/fluxstack.svg +183 -0
  12. package/app/client/src/components/AppLayout.tsx +138 -9
  13. package/app/client/src/components/BackButton.tsx +13 -13
  14. package/app/client/src/components/DemoPage.tsx +4 -4
  15. package/app/client/src/live/AuthDemo.tsx +23 -21
  16. package/app/client/src/live/ChatDemo.tsx +2 -2
  17. package/app/client/src/live/CounterDemo.tsx +12 -12
  18. package/app/client/src/live/FormDemo.tsx +2 -2
  19. package/app/client/src/live/LiveDebuggerPanel.tsx +779 -0
  20. package/app/client/src/live/RoomChatDemo.tsx +24 -16
  21. package/app/client/src/main.tsx +13 -13
  22. package/app/client/src/pages/ApiTestPage.tsx +6 -6
  23. package/app/client/src/pages/HomePage.tsx +80 -52
  24. package/app/server/live/LiveAdminPanel.ts +1 -0
  25. package/app/server/live/LiveChat.ts +78 -77
  26. package/app/server/live/LiveCounter.ts +1 -1
  27. package/app/server/live/LiveForm.ts +1 -0
  28. package/app/server/live/LiveLocalCounter.ts +38 -37
  29. package/app/server/live/LiveProtectedChat.ts +1 -0
  30. package/app/server/live/LiveRoomChat.ts +1 -0
  31. package/app/server/live/LiveUpload.ts +1 -0
  32. package/app/server/live/register-components.ts +19 -19
  33. package/config/system/runtime.config.ts +4 -0
  34. package/core/build/optimizer.ts +235 -235
  35. package/core/client/components/Live.tsx +17 -11
  36. package/core/client/components/LiveDebugger.tsx +1324 -0
  37. package/core/client/hooks/AdaptiveChunkSizer.ts +215 -215
  38. package/core/client/hooks/useLiveComponent.ts +11 -1
  39. package/core/client/hooks/useLiveDebugger.ts +392 -0
  40. package/core/client/index.ts +14 -0
  41. package/core/plugins/built-in/index.ts +134 -134
  42. package/core/plugins/built-in/live-components/commands/create-live-component.ts +4 -0
  43. package/core/plugins/built-in/vite/index.ts +75 -21
  44. package/core/server/index.ts +15 -15
  45. package/core/server/live/ComponentRegistry.ts +55 -26
  46. package/core/server/live/FileUploadManager.ts +188 -24
  47. package/core/server/live/LiveDebugger.ts +462 -0
  48. package/core/server/live/LiveLogger.ts +38 -5
  49. package/core/server/live/LiveRoomManager.ts +17 -1
  50. package/core/server/live/StateSignature.ts +87 -27
  51. package/core/server/live/WebSocketConnectionManager.ts +11 -10
  52. package/core/server/live/auto-generated-components.ts +1 -1
  53. package/core/server/live/websocket-plugin.ts +233 -8
  54. package/core/server/plugins/static-files-plugin.ts +179 -69
  55. package/core/types/build.ts +219 -219
  56. package/core/types/plugin.ts +107 -107
  57. package/core/types/types.ts +145 -9
  58. package/core/utils/logger/startup-banner.ts +82 -82
  59. package/core/utils/version.ts +6 -6
  60. package/package.json +1 -1
  61. package/app/client/src/assets/react.svg +0 -1
@@ -1,35 +1,115 @@
1
1
  import { writeFile, mkdir, unlink } from 'fs/promises'
2
2
  import { existsSync } from 'fs'
3
- import { join, extname } from 'path'
4
- import type {
5
- ActiveUpload,
6
- FileUploadStartMessage,
3
+ import { join, extname, basename } from 'path'
4
+ import { liveLog, liveWarn } from './LiveLogger'
5
+ import type {
6
+ ActiveUpload,
7
+ FileUploadStartMessage,
7
8
  FileUploadChunkMessage,
8
9
  FileUploadCompleteMessage,
9
10
  FileUploadProgressResponse,
10
11
  FileUploadCompleteResponse
11
12
  } from '@core/types/types'
12
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
+
13
35
  export class FileUploadManager {
14
36
  private activeUploads = new Map<string, ActiveUpload>()
15
- private readonly maxUploadSize = 500 * 1024 * 1024 // 500MB max (aceita qualquer arquivo)
37
+ private readonly maxUploadSize = 50 * 1024 * 1024 // ๐Ÿ”’ 50MB max (reduced from 500MB)
16
38
  private readonly chunkTimeout = 30000 // 30 seconds timeout per chunk
17
- private readonly allowedTypes: string[] = [] // Array vazio = aceita todos os tipos de arquivo
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
+ ])
18
59
 
19
60
  constructor() {
20
61
  // Cleanup stale uploads every 5 minutes
21
62
  setInterval(() => this.cleanupStaleUploads(), 5 * 60 * 1000)
63
+ // ๐Ÿ”’ Reset per-user upload quotas daily
64
+ setInterval(() => this.resetUploadQuotas(), this.quotaResetInterval)
22
65
  }
23
66
 
24
- async startUpload(message: FileUploadStartMessage): Promise<{ success: boolean; error?: string }> {
67
+ async startUpload(message: FileUploadStartMessage, userId?: string): Promise<{ success: boolean; error?: string }> {
25
68
  try {
26
69
  const { uploadId, componentId, filename, fileType, fileSize, chunkSize = 64 * 1024 } = message
27
70
 
28
- // Validate file size (sem restriรงรฃo de tipo)
71
+ // ๐Ÿ”’ Validate file size
29
72
  if (fileSize > this.maxUploadSize) {
30
73
  throw new Error(`File too large: ${fileSize} bytes. Max: ${this.maxUploadSize} bytes`)
31
74
  }
32
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
+
33
113
  // Check if upload already exists
34
114
  if (this.activeUploads.has(uploadId)) {
35
115
  throw new Error(`Upload ${uploadId} already in progress`)
@@ -54,13 +134,20 @@ export class FileUploadManager {
54
134
 
55
135
  this.activeUploads.set(uploadId, upload)
56
136
 
57
- console.log('๐Ÿ“ค Upload started:', {
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:', {
58
144
  uploadId,
59
145
  componentId,
60
146
  filename,
61
147
  fileType,
62
148
  fileSize,
63
- totalChunks
149
+ totalChunks,
150
+ userId: userId || 'anonymous'
64
151
  })
65
152
 
66
153
  return { success: true }
@@ -87,7 +174,7 @@ export class FileUploadManager {
87
174
 
88
175
  // Check if chunk already received
89
176
  if (upload.receivedChunks.has(chunkIndex)) {
90
- console.log(`๐Ÿ“ฆ Chunk ${chunkIndex} already received for upload ${uploadId}`)
177
+ liveLog('messages', upload.componentId, `๐Ÿ“ฆ Chunk ${chunkIndex} already received for upload ${uploadId}`)
91
178
  } else {
92
179
  // Store chunk data - use binary data if available, otherwise use base64 string
93
180
  let chunkBytes: number
@@ -105,7 +192,7 @@ export class FileUploadManager {
105
192
  upload.lastChunkTime = Date.now()
106
193
  upload.bytesReceived += chunkBytes
107
194
 
108
- console.log(`๐Ÿ“ฆ Received chunk ${chunkIndex + 1}/${totalChunks} for upload ${uploadId} (${chunkBytes} bytes, total: ${upload.bytesReceived}/${upload.fileSize})${binaryData ? ' [binary]' : ' [base64]'}`)
195
+ liveLog('messages', upload.componentId, `๐Ÿ“ฆ Received chunk ${chunkIndex + 1}/${totalChunks} for upload ${uploadId} (${chunkBytes} bytes, total: ${upload.bytesReceived}/${upload.fileSize})${binaryData ? ' [binary]' : ' [base64]'}`)
109
196
  }
110
197
 
111
198
  // Calculate progress based on actual bytes received (supports adaptive chunking)
@@ -114,7 +201,7 @@ export class FileUploadManager {
114
201
 
115
202
  // Log completion status (but don't finalize until COMPLETE message)
116
203
  if (upload.bytesReceived >= upload.fileSize) {
117
- console.log(`โœ… All bytes received for upload ${uploadId} (${upload.bytesReceived}/${upload.fileSize}), waiting for COMPLETE message`)
204
+ liveLog('messages', upload.componentId, `โœ… All bytes received for upload ${uploadId} (${upload.bytesReceived}/${upload.fileSize}), waiting for COMPLETE message`)
118
205
  }
119
206
 
120
207
  return {
@@ -137,7 +224,7 @@ export class FileUploadManager {
137
224
 
138
225
  private async finalizeUpload(upload: ActiveUpload): Promise<void> {
139
226
  try {
140
- console.log(`โœ… Upload completed: ${upload.uploadId}`)
227
+ liveLog('messages', upload.componentId, `โœ… Upload completed: ${upload.uploadId}`)
141
228
 
142
229
  // Assemble file from chunks
143
230
  const fileUrl = await this.assembleFile(upload)
@@ -160,7 +247,7 @@ export class FileUploadManager {
160
247
  throw new Error(`Upload ${uploadId} not found`)
161
248
  }
162
249
 
163
- console.log(`โœ… Upload completion requested: ${uploadId}`)
250
+ liveLog('messages', upload.componentId, `โœ… Upload completion requested: ${uploadId}`)
164
251
 
165
252
  // Validate bytes received (supports adaptive chunking)
166
253
  if (upload.bytesReceived !== upload.fileSize) {
@@ -168,8 +255,10 @@ export class FileUploadManager {
168
255
  throw new Error(`Incomplete upload: received ${upload.bytesReceived}/${upload.fileSize} bytes (${bytesShort} bytes short)`)
169
256
  }
170
257
 
171
- console.log(`โœ… Upload validation passed: ${uploadId} (${upload.bytesReceived} bytes)`)
258
+ // ๐Ÿ”’ Content validation: verify file magic bytes match claimed MIME type
259
+ this.validateContentMagicBytes(upload)
172
260
 
261
+ liveLog('messages', upload.componentId, `โœ… Upload validation passed: ${uploadId} (${upload.bytesReceived} bytes)`)
173
262
 
174
263
  // Assemble file from chunks
175
264
  const fileUrl = await this.assembleFile(upload)
@@ -209,11 +298,9 @@ export class FileUploadManager {
209
298
  await mkdir(uploadsDir, { recursive: true })
210
299
  }
211
300
 
212
- // Generate unique filename
213
- const timestamp = Date.now()
214
- const extension = extname(upload.filename)
215
- const baseName = upload.filename.replace(extension, '')
216
- const safeFilename = `${baseName}_${timestamp}${extension}`
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}`
217
304
  const filePath = join(uploadsDir, safeFilename)
218
305
 
219
306
  // Assemble chunks in order
@@ -234,7 +321,7 @@ export class FileUploadManager {
234
321
  const fileBuffer = Buffer.concat(chunks)
235
322
  await writeFile(filePath, fileBuffer)
236
323
 
237
- console.log(`๐Ÿ“ File assembled: ${filePath}`)
324
+ liveLog('messages', upload.componentId, `๐Ÿ“ File assembled: ${filePath}`)
238
325
  return `/uploads/${safeFilename}`
239
326
 
240
327
  } catch (error) {
@@ -257,11 +344,88 @@ export class FileUploadManager {
257
344
 
258
345
  for (const uploadId of staleUploads) {
259
346
  this.activeUploads.delete(uploadId)
260
- console.log(`๐Ÿงน Cleaned up stale upload: ${uploadId}`)
347
+ liveLog('messages', null, `๐Ÿงน Cleaned up stale upload: ${uploadId}`)
261
348
  }
262
349
 
263
350
  if (staleUploads.length > 0) {
264
- console.log(`๐Ÿงน Cleaned up ${staleUploads.length} stale uploads`)
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)
265
429
  }
266
430
  }
267
431