create-fluxstack 1.17.1 → 1.18.1

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 (93) hide show
  1. package/LLMD/resources/live-auth.md +462 -465
  2. package/app/client/.live-stubs/LiveAdminPanel.js +15 -0
  3. package/app/client/.live-stubs/LiveCounter.js +9 -0
  4. package/app/client/.live-stubs/LiveForm.js +11 -0
  5. package/app/client/.live-stubs/LiveLocalCounter.js +8 -0
  6. package/app/client/.live-stubs/LivePingPong.js +10 -0
  7. package/app/client/.live-stubs/LiveRoomChat.js +11 -0
  8. package/app/client/.live-stubs/LiveSharedCounter.js +10 -0
  9. package/app/client/.live-stubs/LiveUpload.js +15 -0
  10. package/app/client/src/App.tsx +45 -3
  11. package/app/client/src/components/AppLayout.tsx +10 -1
  12. package/app/client/src/components/ErrorBoundary.tsx +117 -0
  13. package/app/client/src/components/LiveErrorBoundary.tsx +87 -0
  14. package/app/client/src/components/LiveUploadWidget.tsx +10 -14
  15. package/app/client/src/lib/eden-api.ts +6 -0
  16. package/app/client/src/lib/plugin-hooks.ts +82 -0
  17. package/app/client/src/live/AuthDemo.tsx +0 -1
  18. package/app/client/src/live/FormDemo.tsx +1 -1
  19. package/app/client/src/live/PingPongDemo.tsx +4 -1
  20. package/app/client/src/live/RoomChatDemo.tsx +90 -50
  21. package/app/client/src/live/SharedCounterDemo.tsx +5 -0
  22. package/app/server/auth/AuthManager.ts +24 -0
  23. package/app/server/auth/contracts.ts +12 -1
  24. package/app/server/auth/errors.ts +84 -0
  25. package/app/server/auth/guards/TokenGuard.ts +5 -2
  26. package/app/server/auth/index.ts +1 -1
  27. package/app/server/auth/providers/InMemoryProvider.ts +1 -1
  28. package/app/server/index.ts +3 -4
  29. package/app/server/live/LiveAdminPanel.ts +8 -8
  30. package/app/server/live/LiveForm.ts +1 -1
  31. package/app/server/live/LiveProtectedChat.ts +5 -5
  32. package/app/server/live/LiveRoomChat.ts +50 -28
  33. package/app/server/live/LiveUpload.ts +17 -3
  34. package/app/server/live/auto-generated-components.ts +26 -0
  35. package/app/server/live/rooms/ChatRoom.ts +22 -2
  36. package/app/server/routes/auth.routes.ts +29 -20
  37. package/app/server/routes/index.ts +29 -10
  38. package/app/server/routes/room.routes.ts +6 -6
  39. package/config/index.ts +3 -3
  40. package/config/system/app.config.ts +1 -1
  41. package/config/system/auth.config.ts +1 -1
  42. package/config/system/build.config.ts +8 -6
  43. package/config/system/client.config.ts +6 -4
  44. package/config/system/database.config.ts +1 -1
  45. package/config/system/logger.config.ts +1 -1
  46. package/config/system/monitoring.config.ts +6 -4
  47. package/config/system/plugins.config.ts +1 -1
  48. package/config/system/runtime.config.ts +1 -1
  49. package/config/system/server.config.ts +1 -1
  50. package/config/system/services.config.ts +1 -1
  51. package/config/system/session.config.ts +3 -3
  52. package/config/system/system.config.ts +1 -1
  53. package/core/build/vite-plugins.ts +3 -2
  54. package/core/cli/generators/plugin.ts +1 -1
  55. package/core/config/index.ts +8 -1
  56. package/core/framework/server.ts +19 -3
  57. package/core/index.ts +1 -1
  58. package/core/plugins/index.ts +1 -1
  59. package/core/plugins/manager.ts +5 -1
  60. package/core/plugins/types.ts +17 -1
  61. package/core/server/index.ts +5 -2
  62. package/core/server/live/index.ts +8 -71
  63. package/core/server/plugin-client-hooks.ts +97 -0
  64. package/core/types/types.ts +1 -0
  65. package/core/utils/version.ts +1 -1
  66. package/create-fluxstack.ts +1 -1
  67. package/package.json +107 -104
  68. package/src/client/components/ui/StatusBadge.tsx +23 -0
  69. package/core/utils/config-schema.ts +0 -480
  70. package/core/utils/env.ts +0 -305
  71. package/plugins/crypto-auth/README.md +0 -788
  72. package/plugins/crypto-auth/ai-context.md +0 -1282
  73. package/plugins/crypto-auth/cli/make-protected-route.command.ts +0 -383
  74. package/plugins/crypto-auth/client/CryptoAuthClient.ts +0 -302
  75. package/plugins/crypto-auth/client/components/AuthProvider.tsx +0 -131
  76. package/plugins/crypto-auth/client/components/LoginButton.tsx +0 -138
  77. package/plugins/crypto-auth/client/components/ProtectedRoute.tsx +0 -89
  78. package/plugins/crypto-auth/client/components/index.ts +0 -12
  79. package/plugins/crypto-auth/client/index.ts +0 -12
  80. package/plugins/crypto-auth/config/index.ts +0 -34
  81. package/plugins/crypto-auth/index.ts +0 -173
  82. package/plugins/crypto-auth/package.json +0 -66
  83. package/plugins/crypto-auth/server/AuthMiddleware.ts +0 -181
  84. package/plugins/crypto-auth/server/CryptoAuthLiveProvider.ts +0 -58
  85. package/plugins/crypto-auth/server/CryptoAuthService.ts +0 -186
  86. package/plugins/crypto-auth/server/index.ts +0 -25
  87. package/plugins/crypto-auth/server/middlewares/cryptoAuthAdmin.ts +0 -66
  88. package/plugins/crypto-auth/server/middlewares/cryptoAuthOptional.ts +0 -26
  89. package/plugins/crypto-auth/server/middlewares/cryptoAuthPermissions.ts +0 -77
  90. package/plugins/crypto-auth/server/middlewares/cryptoAuthRequired.ts +0 -45
  91. package/plugins/crypto-auth/server/middlewares/helpers.ts +0 -155
  92. package/plugins/crypto-auth/server/middlewares/index.ts +0 -22
  93. package/plugins/crypto-auth/server/middlewares.ts +0 -19
@@ -0,0 +1,15 @@
1
+ export class LiveAdminPanel {
2
+ static componentName = 'LiveAdminPanel'
3
+ static defaultState = {
4
+ users: [
5
+ { id: '1', name: 'Alice', role: 'admin', createdAt: Date.now() },
6
+ { id: '2', name: 'Bob', role: 'user', createdAt: Date.now() },
7
+ { id: '3', name: 'Carol', role: 'moderator', createdAt: Date.now() },
8
+ ],
9
+ audit: [],
10
+ currentUser: null,
11
+ currentRoles: [],
12
+ isAdmin: false,
13
+ }
14
+ static publicActions = ['getAuthInfo', 'init', 'listUsers', 'addUser', 'deleteUser', 'clearAudit']
15
+ }
@@ -0,0 +1,9 @@
1
+ export class LiveCounter {
2
+ static componentName = 'LiveCounter'
3
+ static defaultState = {
4
+ count: 0,
5
+ lastUpdatedBy: null,
6
+ connectedUsers: 0
7
+ }
8
+ static publicActions = ['increment', 'decrement', 'reset']
9
+ }
@@ -0,0 +1,11 @@
1
+ export class LiveForm {
2
+ static componentName = 'LiveForm'
3
+ static defaultState = {
4
+ name: '',
5
+ email: '',
6
+ message: '',
7
+ submitted: false,
8
+ submittedAt: null
9
+ }
10
+ static publicActions = ['submit', 'reset', 'validate', 'setValue']
11
+ }
@@ -0,0 +1,8 @@
1
+ export class LiveLocalCounter {
2
+ static componentName = 'LiveLocalCounter'
3
+ static defaultState = {
4
+ count: 0,
5
+ clicks: 0
6
+ }
7
+ static publicActions = ['increment', 'decrement', 'reset']
8
+ }
@@ -0,0 +1,10 @@
1
+ export class LivePingPong {
2
+ static componentName = 'LivePingPong'
3
+ static defaultState = {
4
+ username: '',
5
+ onlineCount: 0,
6
+ totalPings: 0,
7
+ lastPingBy: null,
8
+ }
9
+ static publicActions = ['ping']
10
+ }
@@ -0,0 +1,11 @@
1
+ export class LiveRoomChat {
2
+ static componentName = 'LiveRoomChat'
3
+ static defaultState = {
4
+ username: '',
5
+ activeRoom: null,
6
+ rooms: [],
7
+ messages: {},
8
+ customRooms: []
9
+ }
10
+ static publicActions = ['createRoom', 'joinRoom', 'leaveRoom', 'switchRoom', 'sendMessage', 'setUsername']
11
+ }
@@ -0,0 +1,10 @@
1
+ export class LiveSharedCounter {
2
+ static componentName = 'LiveSharedCounter'
3
+ static defaultState = {
4
+ username: '',
5
+ count: 0,
6
+ lastUpdatedBy: null,
7
+ onlineCount: 0
8
+ }
9
+ static publicActions = ['increment', 'decrement', 'reset']
10
+ }
@@ -0,0 +1,15 @@
1
+ export class LiveUpload {
2
+ static componentName = 'LiveUpload'
3
+ static defaultState = {
4
+ status: 'idle',
5
+ progress: 0,
6
+ fileName: '',
7
+ fileSize: 0,
8
+ fileType: '',
9
+ fileUrl: '',
10
+ bytesUploaded: 0,
11
+ totalBytes: 0,
12
+ error: null
13
+ }
14
+ static publicActions = ['startUpload', 'updateProgress', 'completeUpload', 'failUpload', 'reset']
15
+ }
@@ -1,7 +1,8 @@
1
- import { useState, useEffect } from 'react'
1
+ import { useState, useEffect, useRef } from 'react'
2
2
  import { Routes, Route } from 'react-router'
3
3
  import { api } from './lib/eden-api'
4
- import { LiveComponentsProvider } from '@/core/client'
4
+ import { LiveComponentsProvider, useLiveComponents } from '@/core/client'
5
+ import { executeHook } from './lib/plugin-hooks'
5
6
  import { FormDemo } from './live/FormDemo'
6
7
  import { CounterDemo } from './live/CounterDemo'
7
8
  import { UploadDemo } from './live/UploadDemo'
@@ -14,6 +15,46 @@ import { DemoPage } from './components/DemoPage'
14
15
  import { HomePage } from './pages/HomePage'
15
16
  import { ApiTestPage } from './pages/ApiTestPage'
16
17
 
18
+ function NotFoundPage() {
19
+ return (
20
+ <div className="flex flex-col items-center justify-center min-h-[60vh] text-center px-4">
21
+ <h1 className="text-6xl font-black text-white mb-4">404</h1>
22
+ <p className="text-xl text-gray-400 mb-6">Pagina nao encontrada</p>
23
+ <p className="text-sm text-gray-500 mb-8">
24
+ O caminho <code className="text-purple-400">{window.location.pathname}</code> nao existe.
25
+ </p>
26
+ <a
27
+ href="/"
28
+ className="px-6 py-3 rounded-xl bg-purple-500/20 border border-purple-500/30 text-purple-200 hover:bg-purple-500/30 transition-all"
29
+ >
30
+ Voltar ao inicio
31
+ </a>
32
+ </div>
33
+ )
34
+ }
35
+
36
+ /**
37
+ * Executes the 'onLiveConnect' plugin hook once when
38
+ * the LiveComponents WebSocket connection is established.
39
+ */
40
+ function PluginHookExecutor() {
41
+ const { connected, getWebSocket } = useLiveComponents()
42
+ const firedRef = useRef(false)
43
+
44
+ useEffect(() => {
45
+ if (connected && !firedRef.current) {
46
+ firedRef.current = true
47
+
48
+ executeHook('onLiveConnect', { connected, getWebSocket }).catch(() => {})
49
+ }
50
+ if (!connected) {
51
+ firedRef.current = false
52
+ }
53
+ }, [connected, getWebSocket])
54
+
55
+ return null
56
+ }
57
+
17
58
  function AppContent() {
18
59
  const [apiStatus, setApiStatus] = useState<'checking' | 'online' | 'offline'>('checking')
19
60
  const [apiResponse, setApiResponse] = useState<string>('')
@@ -150,7 +191,7 @@ function AppContent() {
150
191
  </DemoPage>
151
192
  }
152
193
  />
153
- <Route path="*" element={<HomePage apiStatus={apiStatus} />} />
194
+ <Route path="*" element={<NotFoundPage />} />
154
195
  </Route>
155
196
  </Routes>
156
197
  )
@@ -173,6 +214,7 @@ function App() {
173
214
  heartbeatInterval={30000}
174
215
  debug={false}
175
216
  >
217
+ <PluginHookExecutor />
176
218
  <AppContent />
177
219
  </LiveComponentsProvider>
178
220
  )
@@ -28,7 +28,9 @@ const routeFlameHue: Record<string, string> = {
28
28
  '/api-test': '90deg', // lima
29
29
  }
30
30
 
31
- // Cache favicon blob URLs by hue to avoid recreating blobs on every navigation
31
+ // Cache favicon blob URLs by hue to avoid recreating blobs on every navigation.
32
+ // Limited to MAX_FAVICON_CACHE entries; old blob URLs are revoked to prevent leaks.
33
+ const MAX_FAVICON_CACHE = 20
32
34
  const faviconUrlCache = new Map<string, string>()
33
35
 
34
36
  export function AppLayout() {
@@ -43,6 +45,13 @@ export function AppLayout() {
43
45
  const hue = routeFlameHue[location.pathname] || '0deg'
44
46
  let url = faviconUrlCache.get(hue)
45
47
  if (!url) {
48
+ // Evict oldest entry if cache is full, revoking blob URL to free memory
49
+ if (faviconUrlCache.size >= MAX_FAVICON_CACHE) {
50
+ const oldestKey = faviconUrlCache.keys().next().value!
51
+ const oldestUrl = faviconUrlCache.get(oldestKey)!
52
+ URL.revokeObjectURL(oldestUrl)
53
+ faviconUrlCache.delete(oldestKey)
54
+ }
46
55
  const colored = faviconSvg.replace(
47
56
  '<svg ',
48
57
  `<svg style="filter: hue-rotate(${hue})" `
@@ -0,0 +1,117 @@
1
+ import { Component, type ReactNode, type ErrorInfo } from 'react'
2
+
3
+ interface ErrorBoundaryProps {
4
+ children: ReactNode
5
+ fallback?: ReactNode | ((error: Error, reset: () => void) => ReactNode)
6
+ onError?: (error: Error, errorInfo: ErrorInfo) => void
7
+ }
8
+
9
+ interface ErrorBoundaryState {
10
+ hasError: boolean
11
+ error: Error | null
12
+ }
13
+
14
+ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
15
+ state: ErrorBoundaryState = { hasError: false, error: null }
16
+
17
+ static getDerivedStateFromError(error: Error): ErrorBoundaryState {
18
+ return { hasError: true, error }
19
+ }
20
+
21
+ componentDidCatch(error: Error, errorInfo: ErrorInfo) {
22
+ console.error('[FluxStack] Component error:', error, errorInfo.componentStack)
23
+ this.props.onError?.(error, errorInfo)
24
+ }
25
+
26
+ reset = () => {
27
+ this.setState({ hasError: false, error: null })
28
+ }
29
+
30
+ render() {
31
+ if (this.state.hasError && this.state.error) {
32
+ if (typeof this.props.fallback === 'function') {
33
+ return this.props.fallback(this.state.error, this.reset)
34
+ }
35
+ if (this.props.fallback) {
36
+ return this.props.fallback
37
+ }
38
+ return <DefaultErrorFallback error={this.state.error} onReset={this.reset} />
39
+ }
40
+ return this.props.children
41
+ }
42
+ }
43
+
44
+ function DefaultErrorFallback({ error, onReset }: { error: Error; onReset: () => void }) {
45
+ const isDev = import.meta.env.DEV
46
+
47
+ return (
48
+ <div className="relative flex min-h-screen items-center justify-center overflow-hidden px-6 py-10 bg-gradient-to-br from-slate-900 via-purple-900/80 to-slate-900">
49
+ <style>{`
50
+ @keyframes fluxstack-slow-pulse {
51
+ 0%, 100% { transform: scale(1); opacity: 0.9; }
52
+ 50% { transform: scale(1.06); opacity: 1; }
53
+ }
54
+ `}</style>
55
+
56
+ <div className="pointer-events-none absolute inset-0">
57
+ <div className="absolute left-1/2 top-1/2 h-72 w-72 -translate-x-1/2 -translate-y-1/2 rounded-full bg-linear-to-r from-purple-500/18 to-indigo-500/14 blur-3xl" />
58
+ </div>
59
+
60
+ <div className="relative w-full max-w-xl">
61
+ <div className="absolute -inset-2 rounded-3xl bg-linear-to-r from-purple-500/16 via-fuchsia-500/10 to-indigo-500/16 blur-2xl" />
62
+
63
+ <div className="relative rounded-2xl border border-white/[0.06] bg-white/[0.03] p-8 text-center shadow-2xl backdrop-blur-xl sm:p-10">
64
+ <div className="mx-auto mb-6 flex h-18 w-18 items-center justify-center rounded-2xl border border-white/[0.08] bg-white/[0.04] shadow-[0_0_0_1px_rgba(255,255,255,0.02)]">
65
+ <svg
66
+ className="h-9 w-9 text-purple-300"
67
+ style={{ animation: 'fluxstack-slow-pulse 3.6s ease-in-out infinite' }}
68
+ viewBox="0 0 24 24"
69
+ fill="none"
70
+ stroke="currentColor"
71
+ strokeWidth="1.7"
72
+ aria-hidden="true"
73
+ >
74
+ <path strokeLinecap="round" strokeLinejoin="round" d="M12 8.25v4.5" />
75
+ <path strokeLinecap="round" strokeLinejoin="round" d="M12 16.5h.008v.008H12V16.5Z" />
76
+ <path strokeLinecap="round" strokeLinejoin="round" d="M10.252 3.942 1.93 18.002A1.5 1.5 0 0 0 3.22 20.25h17.56a1.5 1.5 0 0 0 1.29-2.248L13.748 3.942a2 2 0 0 0-3.496 0Z" />
77
+ </svg>
78
+ </div>
79
+
80
+ <h2 className="mb-3 text-2xl font-semibold tracking-tight text-white sm:text-3xl">Something went wrong</h2>
81
+ <p className="mx-auto mb-7 max-w-md text-sm leading-6 text-white/60 sm:text-base">
82
+ FluxStack hit an unexpected rendering error in this view. You can safely retry and continue working.
83
+ </p>
84
+
85
+ {isDev && (
86
+ <details className="mb-7 overflow-hidden rounded-2xl border border-white/[0.06] bg-black/20 text-left group">
87
+ <summary className="flex cursor-pointer list-none items-center justify-between gap-3 px-4 py-3 text-sm font-medium text-white/75 transition-colors hover:text-white">
88
+ <span>Error details</span>
89
+ <svg className="h-4 w-4 text-white/45 transition-transform group-open:rotate-180" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.8" aria-hidden="true">
90
+ <path strokeLinecap="round" strokeLinejoin="round" d="m5 7.5 5 5 5-5" />
91
+ </svg>
92
+ </summary>
93
+ <div className="border-t border-white/[0.06] px-4 py-4">
94
+ <pre className="max-h-56 overflow-auto rounded-xl border border-white/[0.06] bg-black/35 p-4 font-mono text-xs leading-6 text-white/70 whitespace-pre-wrap break-words">
95
+ {error.message}
96
+ {error.stack && `\n\n${error.stack}`}
97
+ </pre>
98
+ </div>
99
+ </details>
100
+ )}
101
+
102
+ <button
103
+ onClick={onReset}
104
+ className="inline-flex items-center justify-center rounded-xl bg-linear-to-r from-purple-500 to-indigo-500 px-6 py-3 text-sm font-semibold text-white shadow-lg shadow-purple-950/30 transition-transform duration-200 hover:scale-[1.02] hover:from-purple-400 hover:to-indigo-400"
105
+ >
106
+ Try again
107
+ </button>
108
+
109
+ <p className="mt-4 text-xs leading-5 text-white/45 sm:text-sm">
110
+ If the issue persists, refresh the page or check the console for more context.
111
+ </p>
112
+ </div>
113
+ </div>
114
+ </div>
115
+ )
116
+ }
117
+
@@ -0,0 +1,87 @@
1
+ import type { ReactNode } from 'react'
2
+ import { ErrorBoundary } from './ErrorBoundary'
3
+
4
+ function LiveErrorFallback({ error, onReset }: { error: Error; onReset: () => void }) {
5
+ const isDev = import.meta.env.DEV
6
+
7
+ return (
8
+ <div className="relative flex min-h-[60vh] items-center justify-center overflow-hidden px-6 py-10">
9
+ <style>{`
10
+ @keyframes fluxstack-slow-pulse {
11
+ 0%, 100% { transform: scale(1); opacity: 0.9; }
12
+ 50% { transform: scale(1.06); opacity: 1; }
13
+ }
14
+ `}</style>
15
+
16
+ <div className="pointer-events-none absolute inset-0">
17
+ <div className="absolute left-1/2 top-1/2 h-72 w-72 -translate-x-1/2 -translate-y-1/2 rounded-full bg-linear-to-r from-amber-500/18 to-orange-500/14 blur-3xl" />
18
+ </div>
19
+
20
+ <div className="relative mx-auto w-full max-w-xl">
21
+ <div className="absolute -inset-2 rounded-3xl bg-linear-to-r from-amber-500/16 via-orange-500/10 to-amber-500/16 blur-2xl" />
22
+
23
+ <div className="relative rounded-2xl border border-white/[0.06] bg-white/[0.03] p-8 text-center shadow-2xl backdrop-blur-xl sm:p-10">
24
+ <div className="mx-auto mb-6 flex h-18 w-18 items-center justify-center rounded-2xl border border-white/[0.08] bg-white/[0.04] shadow-[0_0_0_1px_rgba(255,255,255,0.02)]">
25
+ <svg
26
+ className="h-9 w-9 text-amber-300"
27
+ style={{ animation: 'fluxstack-slow-pulse 3.6s ease-in-out infinite' }}
28
+ viewBox="0 0 24 24"
29
+ fill="none"
30
+ stroke="currentColor"
31
+ strokeWidth="1.7"
32
+ aria-hidden="true"
33
+ >
34
+ <path strokeLinecap="round" strokeLinejoin="round" d="M2.5 8.5c5.25-5.25 13.75-5.25 19 0" />
35
+ <path strokeLinecap="round" strokeLinejoin="round" d="M5.75 11.75c3.45-3.45 9.05-3.45 12.5 0" />
36
+ <path strokeLinecap="round" strokeLinejoin="round" d="M9.25 15.25c1.52-1.52 3.98-1.52 5.5 0" />
37
+ <path strokeLinecap="round" strokeLinejoin="round" d="M3 3l18 18" />
38
+ </svg>
39
+ </div>
40
+
41
+ <h3 className="mb-3 text-2xl font-semibold tracking-tight text-white sm:text-3xl">Live connection interrupted</h3>
42
+ <p className="mx-auto mb-7 max-w-md text-sm leading-6 text-white/60 sm:text-base">
43
+ This real-time FluxStack component lost its live connection or hit a transient runtime error.
44
+ </p>
45
+
46
+ {isDev && (
47
+ <details className="mb-7 overflow-hidden rounded-2xl border border-white/[0.06] bg-black/20 text-left group">
48
+ <summary className="flex cursor-pointer list-none items-center justify-between gap-3 px-4 py-3 text-sm font-medium text-white/75 transition-colors hover:text-white">
49
+ <span>Error details</span>
50
+ <svg className="h-4 w-4 text-white/45 transition-transform group-open:rotate-180" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.8" aria-hidden="true">
51
+ <path strokeLinecap="round" strokeLinejoin="round" d="m5 7.5 5 5 5-5" />
52
+ </svg>
53
+ </summary>
54
+ <div className="border-t border-white/[0.06] px-4 py-4">
55
+ <pre className="max-h-56 overflow-auto rounded-xl border border-white/[0.06] bg-black/35 p-4 font-mono text-xs leading-6 text-white/70 whitespace-pre-wrap break-words">
56
+ {error.message}
57
+ {error.stack && `\n\n${error.stack}`}
58
+ </pre>
59
+ </div>
60
+ </details>
61
+ )}
62
+
63
+ <button
64
+ onClick={onReset}
65
+ className="inline-flex items-center justify-center rounded-xl bg-linear-to-r from-amber-500 to-orange-500 px-6 py-3 text-sm font-semibold text-white shadow-lg shadow-orange-950/30 transition-transform duration-200 hover:scale-[1.02] hover:from-amber-400 hover:to-orange-400"
66
+ >
67
+ Reconnect
68
+ </button>
69
+
70
+ <p className="mt-4 text-xs leading-5 text-white/45 sm:text-sm">
71
+ Reconnecting usually restores live updates within a few seconds.
72
+ </p>
73
+ </div>
74
+ </div>
75
+ </div>
76
+ )
77
+ }
78
+
79
+ export function LiveErrorBoundary({ children }: { children: ReactNode }) {
80
+ return (
81
+ <ErrorBoundary
82
+ fallback={(error, reset) => <LiveErrorFallback error={error} onReset={reset} />}
83
+ >
84
+ {children}
85
+ </ErrorBoundary>
86
+ )
87
+ }
@@ -2,20 +2,16 @@ import { useEffect, useMemo, useState } from 'react'
2
2
  import { useLiveChunkedUpload } from '@/core/client'
3
3
  import type { LiveChunkedUploadOptions } from '@/core/client'
4
4
  import type { FileUploadCompleteResponse } from '@core/types/types'
5
- type LiveUploadActions = {
5
+ import { LiveUpload } from '@server/live/LiveUpload'
6
+
7
+ // Derive the state type from the actual LiveUpload component to avoid duplication
8
+ type LiveUploadState = typeof LiveUpload.defaultState
9
+
10
+ // Minimal interface for any Live.use() proxy compatible with LiveUpload
11
+ interface LiveUploadProxy {
6
12
  $componentId: string | null
7
13
  $connected: boolean
8
- $state: {
9
- status: 'idle' | 'uploading' | 'complete' | 'error'
10
- progress: number
11
- fileName: string
12
- fileSize: number
13
- fileType: string
14
- fileUrl: string
15
- bytesUploaded: number
16
- totalBytes: number
17
- error: string | null
18
- }
14
+ $state: LiveUploadState
19
15
  $error?: string | null
20
16
  startUpload: (payload: { fileName: string; fileSize: number; fileType: string }) => Promise<any>
21
17
  updateProgress: (payload: { progress: number; bytesUploaded: number; totalBytes: number }) => Promise<any>
@@ -25,7 +21,7 @@ type LiveUploadActions = {
25
21
  }
26
22
 
27
23
  export interface LiveUploadWidgetProps {
28
- live: LiveUploadActions
24
+ live: LiveUploadProxy
29
25
  title?: string
30
26
  description?: string
31
27
  allowPreview?: boolean
@@ -133,7 +129,7 @@ export function LiveUploadWidget({
133
129
  type="file"
134
130
  onChange={handleSelectFile}
135
131
  className="w-full text-sm text-gray-300 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:bg-white/10 file:text-white hover:file:bg-white/20"
136
- disabled={!live.$connected}
132
+ disabled={!live.$connected || uploading}
137
133
  />
138
134
 
139
135
  <div className="flex gap-3">
@@ -8,6 +8,7 @@
8
8
 
9
9
  import { createEdenClient, getErrorMessage } from '@/core/client/api'
10
10
  import type { App } from '@server/app'
11
+ import { executeHook } from './plugin-hooks'
11
12
 
12
13
  /**
13
14
  * API client with full type inference from server routes
@@ -70,6 +71,11 @@ export const api = createEdenClient<App>({
70
71
  // enableLogging: true,
71
72
  })
72
73
 
74
+ // Execute plugin hooks after Eden client is created
75
+ executeHook('onEdenInit', { eden: api }).catch(() => {
76
+ // Silently ignore — hooks are best-effort
77
+ })
78
+
73
79
  // Re-export utility for convenience
74
80
  export { getErrorMessage }
75
81
 
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Plugin Client Hooks
3
+ *
4
+ * Fetches JavaScript hooks registered by server-side plugins
5
+ * and executes them at specific hook points in the client lifecycle.
6
+ *
7
+ * Hooks are cached after the first fetch (one HTTP request per page load).
8
+ * Errors in individual hooks are caught and logged without crashing the app.
9
+ */
10
+
11
+ let cachedHooks: Record<string, string[]> | null = null
12
+ let loadPromise: Promise<Record<string, string[]>> | null = null
13
+
14
+ /**
15
+ * Fetch plugin hooks from the server. Results are cached so only
16
+ * one HTTP request is made per page load.
17
+ *
18
+ * The server signs the response with HMAC-SHA256 to ensure integrity.
19
+ * The signature is included for auditability — the client trusts the
20
+ * server via same-origin HTTPS.
21
+ */
22
+ export async function loadPluginHooks(): Promise<Record<string, string[]>> {
23
+ if (cachedHooks) return cachedHooks
24
+
25
+ // Deduplicate concurrent calls during initial load
26
+ if (loadPromise) return loadPromise
27
+
28
+ loadPromise = (async (): Promise<Record<string, string[]>> => {
29
+ try {
30
+ const res = await fetch('/api/__plugins/client-hooks')
31
+ if (!res.ok) {
32
+ console.warn(`[plugin-hooks] Failed to load hooks: HTTP ${res.status}`)
33
+ cachedHooks = {}
34
+ return cachedHooks
35
+ }
36
+ const data = await res.json()
37
+ cachedHooks = data.hooks ?? {}
38
+ return cachedHooks!
39
+ } catch (e) {
40
+ console.warn('[plugin-hooks] Failed to fetch hooks:', e)
41
+ cachedHooks = {}
42
+ return cachedHooks!
43
+ }
44
+ })()
45
+
46
+ return loadPromise
47
+ }
48
+
49
+ /**
50
+ * Execute all code strings registered for a given hook name.
51
+ *
52
+ * Each code string is wrapped in a `new Function(...)` call with the
53
+ * provided context keys as parameter names, so the code can reference
54
+ * them directly (e.g., `eden`, `connection`).
55
+ *
56
+ * Errors in individual hooks are caught and logged — they never crash
57
+ * the calling code.
58
+ *
59
+ * @param hookName - The hook point to execute (e.g., 'onEdenInit', 'onLiveConnect')
60
+ * @param context - Key/value pairs available as local variables inside the hook code
61
+ */
62
+ export async function executeHook(
63
+ hookName: string,
64
+ context?: Record<string, unknown>
65
+ ): Promise<void> {
66
+ const hooks = await loadPluginHooks()
67
+ const codes = hooks[hookName] || []
68
+
69
+ if (codes.length === 0) return
70
+
71
+ const keys = Object.keys(context || {})
72
+ const values = Object.values(context || {})
73
+
74
+ for (const code of codes) {
75
+ try {
76
+ const fn = new Function(...keys, code)
77
+ fn(...values)
78
+ } catch (e) {
79
+ console.warn(`[plugin-hooks] Error executing "${hookName}" hook:`, e)
80
+ }
81
+ }
82
+ }
@@ -10,7 +10,6 @@
10
10
 
11
11
  import { useState } from 'react'
12
12
  import { Live, useLiveComponents } from '@/core/client'
13
- import type { LiveAuthOptions } from '@/core/client'
14
13
  import { LiveCounter } from '@server/live/LiveCounter'
15
14
  import { LiveAdminPanel } from '@server/live/LiveAdminPanel'
16
15
 
@@ -87,7 +87,7 @@ export function FormDemo() {
87
87
  alert(err.message || 'Erro ao enviar')
88
88
  }
89
89
  }}
90
- disabled={!form.$connected}
90
+ disabled={!form.$connected || form.$loading}
91
91
  className="flex-1 px-4 py-3 bg-gradient-to-r from-purple-500 to-pink-500 text-white rounded-lg font-medium hover:shadow-lg hover:shadow-purple-500/30 transition-all disabled:opacity-50"
92
92
  >
93
93
  {form.$loading ? 'Enviando...' : 'Enviar'}
@@ -72,7 +72,10 @@ export function PingPongDemo() {
72
72
  intervalRef.current = setInterval(sendPing, 500)
73
73
  }
74
74
  return () => {
75
- if (intervalRef.current) clearInterval(intervalRef.current)
75
+ if (intervalRef.current != null) {
76
+ clearInterval(intervalRef.current)
77
+ intervalRef.current = null
78
+ }
76
79
  }
77
80
  }, [autoPing, live.$connected, sendPing])
78
81