@mooncompany/uplink-chat 0.5.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.

Potentially problematic release.


This version of @mooncompany/uplink-chat might be problematic. Click here for more details.

Files changed (158) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +185 -0
  3. package/bin/uplink.js +279 -0
  4. package/middleware/error-handler.js +69 -0
  5. package/package.json +93 -0
  6. package/public/css/agents.36b98c0f.css +1469 -0
  7. package/public/css/agents.css +1469 -0
  8. package/public/css/app.a6a7f8f5.css +2731 -0
  9. package/public/css/app.css +2731 -0
  10. package/public/css/artifacts.css +444 -0
  11. package/public/css/commands.css +55 -0
  12. package/public/css/connection.css +131 -0
  13. package/public/css/dashboard.css +233 -0
  14. package/public/css/developer.css +328 -0
  15. package/public/css/files.css +123 -0
  16. package/public/css/markdown.css +156 -0
  17. package/public/css/message-actions.css +278 -0
  18. package/public/css/mobile.css +614 -0
  19. package/public/css/panels-unified.css +483 -0
  20. package/public/css/premium.css +415 -0
  21. package/public/css/realtime.css +189 -0
  22. package/public/css/satellites.css +401 -0
  23. package/public/css/shortcuts.css +185 -0
  24. package/public/css/split-view.4def0262.css +673 -0
  25. package/public/css/split-view.css +673 -0
  26. package/public/css/theme-generator.css +391 -0
  27. package/public/css/themes.css +387 -0
  28. package/public/css/timestamps.css +54 -0
  29. package/public/css/variables.css +78 -0
  30. package/public/dist/bundle.b55050c4.js +15757 -0
  31. package/public/favicon.svg +24 -0
  32. package/public/img/agents/ada.png +0 -0
  33. package/public/img/agents/clarice.png +0 -0
  34. package/public/img/agents/dennis-nedry.png +0 -0
  35. package/public/img/agents/elliot-alderson.png +0 -0
  36. package/public/img/agents/main.png +0 -0
  37. package/public/img/agents/scotty.png +0 -0
  38. package/public/img/agents/top-flight-security.png +0 -0
  39. package/public/index.html +1083 -0
  40. package/public/js/agents-data.js +234 -0
  41. package/public/js/agents-ui.js +72 -0
  42. package/public/js/agents.js +1525 -0
  43. package/public/js/app.js +79 -0
  44. package/public/js/appearance-settings.js +111 -0
  45. package/public/js/artifacts.js +432 -0
  46. package/public/js/audio-queue.js +168 -0
  47. package/public/js/bootstrap.js +54 -0
  48. package/public/js/chat.js +1211 -0
  49. package/public/js/commands.js +581 -0
  50. package/public/js/connection-api.js +121 -0
  51. package/public/js/connection.js +1231 -0
  52. package/public/js/context-tracker.js +271 -0
  53. package/public/js/core.js +172 -0
  54. package/public/js/dashboard.js +452 -0
  55. package/public/js/developer.js +432 -0
  56. package/public/js/encryption.js +124 -0
  57. package/public/js/errors.js +122 -0
  58. package/public/js/event-bus.js +77 -0
  59. package/public/js/fetch-utils.js +171 -0
  60. package/public/js/file-handler.js +229 -0
  61. package/public/js/files.js +352 -0
  62. package/public/js/gateway-chat.js +538 -0
  63. package/public/js/logger.js +112 -0
  64. package/public/js/markdown.js +190 -0
  65. package/public/js/message-actions.js +431 -0
  66. package/public/js/message-renderer.js +288 -0
  67. package/public/js/missed-messages.js +235 -0
  68. package/public/js/mobile-debug.js +95 -0
  69. package/public/js/notifications.js +367 -0
  70. package/public/js/offline-queue.js +178 -0
  71. package/public/js/onboarding.js +543 -0
  72. package/public/js/panels.js +156 -0
  73. package/public/js/premium.js +412 -0
  74. package/public/js/realtime-voice.js +844 -0
  75. package/public/js/satellite-sync.js +256 -0
  76. package/public/js/satellite-ui.js +175 -0
  77. package/public/js/satellites.js +1516 -0
  78. package/public/js/settings.js +1087 -0
  79. package/public/js/shortcuts.js +381 -0
  80. package/public/js/split-chat.js +1234 -0
  81. package/public/js/split-resize.js +211 -0
  82. package/public/js/splitview.js +340 -0
  83. package/public/js/storage.js +408 -0
  84. package/public/js/streaming-handler.js +324 -0
  85. package/public/js/stt-settings.js +316 -0
  86. package/public/js/theme-generator.js +661 -0
  87. package/public/js/themes.js +164 -0
  88. package/public/js/timestamps.js +198 -0
  89. package/public/js/tts-settings.js +575 -0
  90. package/public/js/ui.js +267 -0
  91. package/public/js/update-notifier.js +143 -0
  92. package/public/js/utils/constants.js +165 -0
  93. package/public/js/utils/sanitize.js +93 -0
  94. package/public/js/utils/sse-parser.js +195 -0
  95. package/public/js/voice.js +883 -0
  96. package/public/manifest.json +58 -0
  97. package/public/moon_texture.jpg +0 -0
  98. package/public/sw.js +221 -0
  99. package/public/three.min.js +6 -0
  100. package/server/channel.js +529 -0
  101. package/server/chat.js +270 -0
  102. package/server/config-store.js +362 -0
  103. package/server/config.js +159 -0
  104. package/server/context.js +131 -0
  105. package/server/gateway-commands.js +211 -0
  106. package/server/gateway-proxy.js +318 -0
  107. package/server/index.js +22 -0
  108. package/server/logger.js +89 -0
  109. package/server/middleware/auth.js +188 -0
  110. package/server/middleware.js +218 -0
  111. package/server/openclaw-discover.js +308 -0
  112. package/server/premium/index.js +156 -0
  113. package/server/premium/license.js +140 -0
  114. package/server/realtime/bridge.js +837 -0
  115. package/server/realtime/index.js +349 -0
  116. package/server/realtime/tts-stream.js +446 -0
  117. package/server/routes/agents.js +564 -0
  118. package/server/routes/artifacts.js +174 -0
  119. package/server/routes/chat.js +311 -0
  120. package/server/routes/config-settings.js +345 -0
  121. package/server/routes/config.js +603 -0
  122. package/server/routes/files.js +307 -0
  123. package/server/routes/index.js +18 -0
  124. package/server/routes/media.js +451 -0
  125. package/server/routes/missed-messages.js +107 -0
  126. package/server/routes/premium.js +75 -0
  127. package/server/routes/push.js +156 -0
  128. package/server/routes/satellite.js +406 -0
  129. package/server/routes/status.js +251 -0
  130. package/server/routes/stt.js +35 -0
  131. package/server/routes/voice.js +260 -0
  132. package/server/routes/webhooks.js +203 -0
  133. package/server/routes.js +206 -0
  134. package/server/runtime-config.js +336 -0
  135. package/server/share.js +305 -0
  136. package/server/stt/faster-whisper.js +72 -0
  137. package/server/stt/groq.js +51 -0
  138. package/server/stt/index.js +196 -0
  139. package/server/stt/openai.js +49 -0
  140. package/server/sync.js +244 -0
  141. package/server/tailscale-https.js +175 -0
  142. package/server/tts.js +646 -0
  143. package/server/update-checker.js +172 -0
  144. package/server/utils/filename.js +129 -0
  145. package/server/utils.js +147 -0
  146. package/server/watchdog.js +318 -0
  147. package/server/websocket/broadcast.js +359 -0
  148. package/server/websocket/connections.js +339 -0
  149. package/server/websocket/index.js +215 -0
  150. package/server/websocket/routing.js +277 -0
  151. package/server/websocket/sync.js +102 -0
  152. package/server.js +404 -0
  153. package/utils/detect-tool-usage.js +93 -0
  154. package/utils/errors.js +158 -0
  155. package/utils/html-escape.js +84 -0
  156. package/utils/id-sanitize.js +94 -0
  157. package/utils/response.js +130 -0
  158. package/utils/with-retry.js +105 -0
@@ -0,0 +1,267 @@
1
+ // ============================================
2
+ // UI MODULE
3
+ // Starfield, mode switching, common UI utilities
4
+ // ============================================
5
+
6
+ import { UplinkCore } from './core.js';
7
+ import { UplinkStorage } from './storage.js';
8
+
9
+ // DOM elements
10
+ let starsContainer;
11
+ let textModeTab, voiceModeTab;
12
+ let textInputRow, voiceInputRow;
13
+ let encryptionBadge;
14
+
15
+ // Session timeout
16
+ const SESSION_TIMEOUT = 4 * 60 * 60 * 1000; // 4 hours
17
+ let sessionTimeoutId = null;
18
+
19
+ // AbortController for event listeners cleanup
20
+ let eventsAbortController = null;
21
+
22
+ function init() {
23
+ // Abort previous event listeners to prevent stacking if init called multiple times
24
+ if (eventsAbortController) {
25
+ eventsAbortController.abort();
26
+ }
27
+ eventsAbortController = new AbortController();
28
+
29
+ starsContainer = document.getElementById('stars');
30
+ textModeTab = document.getElementById('textModeTab');
31
+ voiceModeTab = document.getElementById('voiceModeTab');
32
+ textInputRow = document.getElementById('textInputRow');
33
+ voiceInputRow = document.getElementById('voiceInputRow');
34
+ encryptionBadge = document.getElementById('encryptionBadge');
35
+
36
+ // Generate starfield
37
+ if (starsContainer) generateStars();
38
+
39
+ // Mode switching
40
+ if (textModeTab && voiceModeTab) setupModeTabs();
41
+
42
+ // Visibility change handling
43
+ setupVisibilityHandling();
44
+
45
+ // Session timeout
46
+ setupSessionTimeout();
47
+
48
+ // Audio pre-warming
49
+ setupAudioPrewarm();
50
+
51
+ // Listen for unlocked event
52
+ window.addEventListener('uplink:unlocked', updateUI, { signal: eventsAbortController.signal });
53
+
54
+ // Dynamic bottom padding for messages
55
+ setupPaddingObserver();
56
+
57
+ console.log('UI: Initialized');
58
+ }
59
+
60
+ function generateStars() {
61
+ for (let i = 0; i < 150; i++) {
62
+ const star = document.createElement('div');
63
+ star.className = 'star';
64
+ star.style.left = Math.random() * 100 + '%';
65
+ star.style.top = Math.random() * 100 + '%';
66
+ star.style.width = star.style.height = (Math.random() * 2 + 0.5) + 'px';
67
+ star.style.setProperty('--opacity', Math.random() * 0.7 + 0.3);
68
+ star.style.setProperty('--duration', (Math.random() * 4 + 2) + 's');
69
+ star.style.animationDelay = -(Math.random() * 10) + 's';
70
+ starsContainer.appendChild(star);
71
+ }
72
+ }
73
+
74
+ function setupModeTabs() {
75
+ textModeTab.addEventListener('click', () => setMode('text'));
76
+ voiceModeTab.addEventListener('click', () => setMode('voice'));
77
+
78
+ // Desktop header mode toggle buttons
79
+ const headerTextMode = document.getElementById('headerTextMode');
80
+ const headerVoiceMode = document.getElementById('headerVoiceMode');
81
+ headerTextMode?.addEventListener('click', () => setMode('text'));
82
+ headerVoiceMode?.addEventListener('click', () => setMode('voice'));
83
+ }
84
+
85
+ function setMode(mode) {
86
+ UplinkCore.mode = mode;
87
+
88
+ textModeTab?.classList.toggle('active', mode === 'text');
89
+ voiceModeTab?.classList.toggle('active', mode === 'voice');
90
+
91
+ // Sync desktop header mode buttons
92
+ const headerTextMode = document.getElementById('headerTextMode');
93
+ const headerVoiceMode = document.getElementById('headerVoiceMode');
94
+ headerTextMode?.classList.toggle('active', mode === 'text');
95
+ headerVoiceMode?.classList.toggle('active', mode === 'voice');
96
+ textInputRow?.classList.toggle('active', mode === 'text');
97
+ voiceInputRow?.classList.toggle('active', mode === 'voice');
98
+
99
+ // Start moon animation when switching to voice mode
100
+ if (mode === 'voice' && window.UplinkVoice?.startMoonAnimation) {
101
+ window.UplinkVoice.startMoonAnimation();
102
+ }
103
+
104
+ // Recalculate messages padding for new input area height
105
+ setTimeout(updateMessagesPadding, 50);
106
+
107
+ // Save preference
108
+ UplinkStorage.saveSettings({ mode });
109
+ }
110
+
111
+ function updateUI() {
112
+ // Update encryption badge
113
+ updateEncryptionBadge();
114
+
115
+ // Apply saved mode
116
+ const settings = UplinkStorage.loadSettings();
117
+ if (settings.mode) setMode(settings.mode);
118
+ }
119
+
120
+ function updateEncryptionBadge() {
121
+ if (!encryptionBadge) return;
122
+
123
+ if (UplinkCore.encryptionEnabled) {
124
+ encryptionBadge.style.display = 'flex';
125
+ if (UplinkCore.state.currentPassword) {
126
+ encryptionBadge.classList.remove('warning');
127
+ encryptionBadge.innerHTML = '<span>🔒</span><span>Encrypted</span>';
128
+ } else {
129
+ encryptionBadge.classList.add('warning');
130
+ encryptionBadge.innerHTML = '<span>🔓</span><span>Locked</span>';
131
+ }
132
+ } else {
133
+ encryptionBadge.style.display = 'none';
134
+ }
135
+ }
136
+
137
+ function setupVisibilityHandling() {
138
+ document.addEventListener('visibilitychange', () => {
139
+ const paused = document.hidden;
140
+
141
+ // Pause star animations
142
+ document.querySelectorAll('.star').forEach(star => {
143
+ star.style.animationPlayState = paused ? 'paused' : 'running';
144
+ });
145
+ }, { signal: eventsAbortController.signal });
146
+ }
147
+
148
+ function setupSessionTimeout() {
149
+ function resetTimeout() {
150
+ if (!UplinkCore.encryptionEnabled || !UplinkCore.state.currentPassword) return;
151
+
152
+ if (sessionTimeoutId) clearTimeout(sessionTimeoutId);
153
+
154
+ sessionTimeoutId = setTimeout(() => {
155
+ console.log('Session timeout - clearing password');
156
+ UplinkCore.state.currentPassword = null;
157
+ updateEncryptionBadge();
158
+
159
+ // Show unlock screen
160
+ const onboarding = window.UplinkOnboarding;
161
+ if (onboarding) onboarding.showUnlock();
162
+ }, SESSION_TIMEOUT);
163
+ }
164
+
165
+ // Reset on activity (using signal to prevent stacking)
166
+ const signal = eventsAbortController.signal;
167
+ document.addEventListener('click', resetTimeout, { signal });
168
+ document.addEventListener('keypress', resetTimeout, { signal });
169
+ document.addEventListener('touchstart', resetTimeout, { signal });
170
+ }
171
+
172
+ function setupAudioPrewarm() {
173
+ let prewarmed = false;
174
+
175
+ function prewarm() {
176
+ if (prewarmed) return;
177
+ prewarmed = true;
178
+
179
+ const ctx = new (window.AudioContext || window.webkitAudioContext)();
180
+ ctx.resume().catch(err => console.error('UI: AudioContext resume failed:', err));
181
+
182
+ const audio = document.getElementById('audio');
183
+ if (audio) audio.load();
184
+ }
185
+
186
+ document.addEventListener('click', prewarm, { once: true });
187
+ document.addEventListener('touchstart', prewarm, { once: true });
188
+ document.addEventListener('keydown', prewarm, { once: true });
189
+ }
190
+
191
+ // Dynamic padding for messages area to clear fixed input + audio player
192
+ function updateMessagesPadding() {
193
+ const messages = document.getElementById('messages');
194
+ if (!messages) return;
195
+
196
+ const inputArea = document.querySelector('.input-area');
197
+ const modeTabs = document.querySelector('.mode-tabs');
198
+ const audioBar = document.getElementById('audioPlayerBar');
199
+
200
+ let bottomHeight = 0;
201
+ if (inputArea) bottomHeight += inputArea.offsetHeight;
202
+ if (modeTabs) bottomHeight += modeTabs.offsetHeight;
203
+ if (audioBar && audioBar.style.display !== 'none') bottomHeight += audioBar.offsetHeight;
204
+
205
+ // Add a small buffer
206
+ bottomHeight += 16;
207
+
208
+ messages.style.paddingBottom = bottomHeight + 'px';
209
+ }
210
+
211
+ // Observe input area size changes (textarea growing, mode switching)
212
+ function setupPaddingObserver() {
213
+ const inputArea = document.querySelector('.input-area');
214
+ if (!inputArea) return;
215
+
216
+ // ResizeObserver for when textarea grows/shrinks
217
+ if (window.ResizeObserver) {
218
+ const ro = new ResizeObserver(() => updateMessagesPadding());
219
+ ro.observe(inputArea);
220
+ // Also observe voice input row
221
+ const voiceRow = document.getElementById('voiceInputRow');
222
+ if (voiceRow) ro.observe(voiceRow);
223
+ }
224
+
225
+ // Listen for audio player show/hide
226
+ const audioBar = document.getElementById('audioPlayerBar');
227
+ if (audioBar && window.MutationObserver) {
228
+ const mo = new MutationObserver(() => updateMessagesPadding());
229
+ mo.observe(audioBar, { attributes: true, attributeFilter: ['style'] });
230
+ }
231
+
232
+ // Recalc on resize and mode switch
233
+ // Debounced to avoid excessive padding calculations during resize
234
+ window.addEventListener('resize', UplinkCore.debounce(updateMessagesPadding, 150));
235
+
236
+ // Initial calculation
237
+ updateMessagesPadding();
238
+ }
239
+
240
+ // Cleanup function
241
+ function destroy() {
242
+ if (eventsAbortController) {
243
+ eventsAbortController.abort();
244
+ eventsAbortController = null;
245
+ }
246
+ if (sessionTimeoutId) {
247
+ clearTimeout(sessionTimeoutId);
248
+ sessionTimeoutId = null;
249
+ }
250
+ }
251
+
252
+ // Export API
253
+ export const UplinkUI = {
254
+ setMode,
255
+ updateEncryptionBadge,
256
+ updateMessagesPadding,
257
+ generateStars,
258
+ destroy
259
+ };
260
+
261
+ export { setMode, updateEncryptionBadge, updateMessagesPadding, generateStars, destroy };
262
+
263
+ // Backward compat: assign to window
264
+ window.UplinkUI = UplinkUI;
265
+
266
+ // Register and init
267
+ UplinkCore.registerModule('ui', init);
@@ -0,0 +1,143 @@
1
+ // ============================================
2
+ // UPDATE NOTIFIER MODULE
3
+ // Shows a banner when a new version is available
4
+ // ============================================
5
+
6
+ /**
7
+ * Check if this version's update was already dismissed
8
+ */
9
+ function isDismissed(version) {
10
+ try {
11
+ return localStorage.getItem('dismissed-update-' + version) === 'true';
12
+ } catch {
13
+ return false;
14
+ }
15
+ }
16
+
17
+ /**
18
+ * Mark this version's update as dismissed
19
+ */
20
+ function dismissVersion(version) {
21
+ try {
22
+ localStorage.setItem('dismissed-update-' + version, 'true');
23
+ } catch {
24
+ // Ignore storage errors
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Remove existing update banner if present
30
+ */
31
+ function removeBanner() {
32
+ const existing = document.querySelector('.update-banner');
33
+ if (existing) {
34
+ existing.classList.add('update-banner-hiding');
35
+ setTimeout(() => {
36
+ if (existing.parentNode) {
37
+ existing.parentNode.removeChild(existing);
38
+ }
39
+ }, 300);
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Pre-fill the chat input with an update prompt for the bot
45
+ */
46
+ function prefillBotPrompt(latest) {
47
+ const input = document.getElementById('userInput');
48
+ if (input) {
49
+ input.value = 'Update Uplink to the latest version (v' + latest + ') and restart the server.';
50
+ input.focus();
51
+ // Trigger input event so auto-resize works
52
+ input.dispatchEvent(new Event('input', { bubbles: true }));
53
+ }
54
+ removeBanner();
55
+ }
56
+
57
+ /**
58
+ * Show the update banner
59
+ */
60
+ function showBanner(current, latest) {
61
+ if (isDismissed(latest)) return;
62
+
63
+ removeBanner();
64
+
65
+ const banner = document.createElement('div');
66
+ banner.className = 'update-banner';
67
+
68
+ // Banner text
69
+ const text = document.createElement('span');
70
+ text.className = 'update-banner-text';
71
+ text.textContent = '\u2B21 Uplink v' + latest + ' is available \u2014 you\'re on v' + current;
72
+
73
+ // "Let my bot handle it" button
74
+ const botBtn = document.createElement('button');
75
+ botBtn.className = 'update-banner-bot-btn';
76
+ botBtn.textContent = 'Let my bot handle it';
77
+ botBtn.addEventListener('click', function(e) {
78
+ e.preventDefault();
79
+ prefillBotPrompt(latest);
80
+ });
81
+
82
+ // Dismiss button
83
+ const dismissBtn = document.createElement('button');
84
+ dismissBtn.className = 'update-banner-dismiss';
85
+ dismissBtn.textContent = '\u2715';
86
+ dismissBtn.setAttribute('aria-label', 'Dismiss update notification');
87
+ dismissBtn.addEventListener('click', function() {
88
+ dismissVersion(latest);
89
+ removeBanner();
90
+ });
91
+
92
+ banner.appendChild(text);
93
+ banner.appendChild(botBtn);
94
+ banner.appendChild(dismissBtn);
95
+
96
+ // Insert at the top of the chat area
97
+ const chatMessages = document.getElementById('chatMessages');
98
+ if (chatMessages) {
99
+ chatMessages.insertBefore(banner, chatMessages.firstChild);
100
+ } else {
101
+ document.body.insertBefore(banner, document.body.firstChild);
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Handle incoming WebSocket messages
107
+ */
108
+ function handleWsMessage(data) {
109
+ if (!data || data.type !== 'update_available') return;
110
+ if (!data.current || !data.latest) return;
111
+
112
+ showBanner(data.current, data.latest);
113
+ }
114
+
115
+ /**
116
+ * Initialize the update notifier
117
+ */
118
+ function init() {
119
+ // Listen for WebSocket messages via custom event
120
+ window.addEventListener('uplink:ws-message', function(e) {
121
+ if (e.detail) {
122
+ handleWsMessage(e.detail);
123
+ }
124
+ });
125
+
126
+ if (window.UplinkLogger && window.UplinkLogger.debug) {
127
+ window.UplinkLogger.debug('[UpdateNotifier] Initialized');
128
+ }
129
+ }
130
+
131
+ // Register with UplinkCore if available
132
+ UplinkCore.registerModule('update-notifier', init);
133
+
134
+ // Expose for testing
135
+ export const UplinkUpdateNotifier = {
136
+ showBanner: showBanner,
137
+ handleWsMessage: handleWsMessage,
138
+ };
139
+
140
+ import { UplinkCore } from './core.js';
141
+
142
+ // Backward compat: assign to window
143
+ window.UplinkUpdateNotifier = UplinkUpdateNotifier;
@@ -0,0 +1,165 @@
1
+ // ============================================
2
+ // CONSTANTS UTILITY
3
+ // Centralized magic numbers and configuration
4
+ // ============================================
5
+
6
+ // ===========================================
7
+ // File Size Limits
8
+ // ===========================================
9
+ export const FILE_LIMITS = {
10
+ // Audio upload limit (25MB)
11
+ AUDIO_MAX_SIZE: 25 * 1024 * 1024,
12
+
13
+ // Image upload limit (10MB)
14
+ IMAGE_MAX_SIZE: 10 * 1024 * 1024,
15
+
16
+ // Video upload limit (50MB)
17
+ VIDEO_MAX_SIZE: 50 * 1024 * 1024,
18
+
19
+ // Generic file upload limit
20
+ DEFAULT_MAX_SIZE: 10 * 1024 * 1024,
21
+ };
22
+
23
+ // ===========================================
24
+ // Timeout Values (milliseconds)
25
+ // ===========================================
26
+ export const TIMEOUTS = {
27
+ // Default fetch timeout
28
+ FETCH_DEFAULT: 45000,
29
+
30
+ // Extended timeout for AI chat operations
31
+ CHAT_REQUEST: 300000,
32
+
33
+ // Timeout for TTS generation
34
+ TTS_GENERATION: 60000,
35
+
36
+ // Timeout for transcription
37
+ TRANSCRIPTION: 45000,
38
+
39
+ // WebSocket ping interval
40
+ WS_PING_INTERVAL: 30000,
41
+
42
+ // WebSocket connection timeout
43
+ WS_CONNECTION: 10000,
44
+
45
+ // Reconnection delay (base)
46
+ RECONNECT_BASE: 1000,
47
+
48
+ // Maximum reconnection delay
49
+ RECONNECT_MAX: 30000,
50
+
51
+ // Typing indicator timeout
52
+ TYPING_INDICATOR: 60000,
53
+
54
+ // Request tracking cleanup
55
+ REQUEST_CLEANUP: 300000,
56
+ };
57
+
58
+ // ===========================================
59
+ // Rate Limits
60
+ // ===========================================
61
+ export const RATE_LIMITS = {
62
+ // WebSocket messages per minute
63
+ WS_MESSAGES_PER_MINUTE: 30,
64
+
65
+ // WebSocket rate limit window (ms)
66
+ WS_WINDOW_MS: 60000,
67
+
68
+ // General API rate limit (requests per window)
69
+ API_REQUESTS_PER_WINDOW: 100,
70
+
71
+ // Strict rate limit (for sensitive endpoints)
72
+ STRICT_REQUESTS_PER_WINDOW: 10,
73
+
74
+ // Rate limit window (15 minutes)
75
+ API_WINDOW_MS: 15 * 60 * 1000,
76
+ };
77
+
78
+ // ===========================================
79
+ // Retry Configuration
80
+ // ===========================================
81
+ export const RETRY = {
82
+ // Maximum retry attempts
83
+ MAX_ATTEMPTS: 3,
84
+
85
+ // Base delay between retries (ms)
86
+ BASE_DELAY: 1000,
87
+
88
+ // Maximum delay between retries (ms)
89
+ MAX_DELAY: 10000,
90
+
91
+ // Exponential backoff multiplier
92
+ BACKOFF_MULTIPLIER: 2,
93
+ };
94
+
95
+ // ===========================================
96
+ // Message Limits
97
+ // ===========================================
98
+ export const MESSAGE_LIMITS = {
99
+ // Maximum message length
100
+ MAX_LENGTH: 50000,
101
+
102
+ // Maximum messages in sync storage
103
+ MAX_SYNC_MESSAGES: 100,
104
+
105
+ // Maximum activity items to keep
106
+ MAX_ACTIVITY_ITEMS: 50,
107
+
108
+ // Truncation length for previews
109
+ PREVIEW_LENGTH: 100,
110
+ };
111
+
112
+ // ===========================================
113
+ // UI Constants
114
+ // ===========================================
115
+ export const UI = {
116
+ // Maximum textarea height
117
+ TEXTAREA_MAX_HEIGHT: 150,
118
+
119
+ // Scroll threshold for auto-scroll
120
+ SCROLL_THRESHOLD: 100,
121
+
122
+ // Animation durations
123
+ ANIMATION_FAST: 150,
124
+ ANIMATION_NORMAL: 300,
125
+ ANIMATION_SLOW: 500,
126
+
127
+ // Mobile breakpoint
128
+ MOBILE_BREAKPOINT: 768,
129
+
130
+ // Button feedback animation (copy, etc.)
131
+ BUTTON_FEEDBACK_DURATION: 1500,
132
+
133
+ // Text truncation lengths (L-07: magic numbers)
134
+ TEXT_TRUNCATE_REPLY: 150,
135
+ TEXT_TRUNCATE_FORK: 500,
136
+ TEXT_TRUNCATE_EDIT: 200,
137
+
138
+ // Form constraints
139
+ SATELLITE_NAME_MAX_LENGTH: 32,
140
+ };
141
+
142
+ // ===========================================
143
+ // Validation Patterns
144
+ // ===========================================
145
+ export const PATTERNS = {
146
+ // Satellite ID pattern
147
+ SATELLITE_ID: /^[a-zA-Z0-9_-]{1,32}$/,
148
+
149
+ // Share ID pattern
150
+ SHARE_ID: /^[a-zA-Z0-9]{1,12}$/,
151
+
152
+ // URL pattern (basic)
153
+ URL: /^https?:\/\/.+/i,
154
+ };
155
+
156
+ // Default export for convenience
157
+ export default {
158
+ FILE_LIMITS,
159
+ TIMEOUTS,
160
+ RATE_LIMITS,
161
+ RETRY,
162
+ MESSAGE_LIMITS,
163
+ UI,
164
+ PATTERNS,
165
+ };
@@ -0,0 +1,93 @@
1
+ // ============================================
2
+ // SANITIZE UTILITY
3
+ // DOMPurify wrapper for HTML sanitization
4
+ // ============================================
5
+
6
+ /**
7
+ * Sanitize HTML content using DOMPurify (if available) or basic escaping
8
+ *
9
+ * @param {string} html - The HTML string to sanitize
10
+ * @param {Object} options - DOMPurify configuration options
11
+ * @returns {string} - Sanitized HTML string
12
+ */
13
+ export function sanitizeHTML(html, options = {}) {
14
+ if (typeof html !== 'string') {
15
+ return '';
16
+ }
17
+
18
+ // Use DOMPurify if available (should be loaded via CDN or npm)
19
+ if (typeof DOMPurify !== 'undefined') {
20
+ const defaultOptions = {
21
+ ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'ul', 'ol', 'li', 'code', 'pre', 'blockquote', 'span', 'img', 'h2', 'h3', 'h4', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'del', 'hr'],
22
+ ALLOWED_ATTR: ['href', 'target', 'rel', 'class', 'src', 'alt'],
23
+ ALLOW_DATA_ATTR: false,
24
+ ADD_ATTR: ['target'],
25
+ ...options
26
+ };
27
+
28
+ // Force safe link behavior
29
+ DOMPurify.addHook('afterSanitizeAttributes', (node) => {
30
+ if (node.tagName === 'A') {
31
+ node.setAttribute('target', '_blank');
32
+ node.setAttribute('rel', 'noopener noreferrer');
33
+ }
34
+ });
35
+
36
+ return DOMPurify.sanitize(html, defaultOptions);
37
+ }
38
+
39
+ // Fallback: basic HTML escaping if DOMPurify not available
40
+ if (typeof window !== 'undefined' && window.UplinkLogger?.warn) {
41
+ window.UplinkLogger.warn('DOMPurify not loaded, falling back to basic escaping');
42
+ }
43
+ return escapeHTML(html);
44
+ }
45
+
46
+ /**
47
+ * Basic HTML escaping - converts special characters to HTML entities
48
+ * Use sanitizeHTML for untrusted content that may contain markup
49
+ *
50
+ * @param {string} str - The string to escape
51
+ * @returns {string} - Escaped string safe for HTML insertion
52
+ */
53
+ export function escapeHTML(str) {
54
+ if (typeof str !== 'string') {
55
+ return '';
56
+ }
57
+
58
+ const div = document.createElement('div');
59
+ div.textContent = str;
60
+ return div.innerHTML;
61
+ }
62
+
63
+ /**
64
+ * Escape HTML and convert newlines to <br> tags
65
+ * Useful for displaying multi-line text content
66
+ *
67
+ * @param {string} str - The string to escape
68
+ * @returns {string} - Escaped string with line breaks
69
+ */
70
+ export function escapeHTMLWithBreaks(str) {
71
+ return escapeHTML(str).replace(/\n/g, '<br>');
72
+ }
73
+
74
+ /**
75
+ * Sanitize a string for use in CSS class names
76
+ *
77
+ * @param {string} str - The string to sanitize
78
+ * @returns {string} - Safe CSS class name
79
+ */
80
+ export function sanitizeClassName(str) {
81
+ if (typeof str !== 'string') {
82
+ return '';
83
+ }
84
+ return str.replace(/[^a-zA-Z0-9_-]/g, '').substring(0, 50);
85
+ }
86
+
87
+ // Default export for convenience
88
+ export default {
89
+ sanitizeHTML,
90
+ escapeHTML,
91
+ escapeHTMLWithBreaks,
92
+ sanitizeClassName
93
+ };