@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,432 @@
1
+ // ============================================
2
+ // ACTIVITY PANEL MODULE
3
+ // Token usage, raw API responses, error logs, tool visibility
4
+ // ============================================
5
+
6
+ import { UplinkCore } from './core.js';
7
+
8
+ // ========== CONSTANTS ==========
9
+ const MAX_LOGS = 50;
10
+ const INIT_RETRY_DELAY_MS = 100;
11
+ const ERROR_FLASH_DURATION_MS = 2000;
12
+ const TOOL_FLASH_DURATION_MS = 1000;
13
+ const RESPONSE_TRUNCATE_LENGTH = 500;
14
+
15
+ // State
16
+ let panelVisible = false;
17
+ let tokenUsage = { prompt: 0, completion: 0, total: 0 };
18
+ let apiLogs = [];
19
+ let errorLogs = [];
20
+ let toolCalls = [];
21
+
22
+ // Create activity panel
23
+ const panel = document.createElement('div');
24
+ panel.className = 'dev-panel';
25
+ panel.innerHTML = `
26
+ <div class="dev-panel-header">
27
+ <span class="dev-panel-title">Activity</span>
28
+ <button class="dev-panel-close" title="Close">&times;</button>
29
+ </div>
30
+ <div class="dev-panel-tabs">
31
+ <button class="dev-tab active" data-tab="tokens">Tokens</button>
32
+ <button class="dev-tab" data-tab="api">API</button>
33
+ <button class="dev-tab" data-tab="tools">Tools</button>
34
+ <button class="dev-tab" data-tab="errors">Errors</button>
35
+ </div>
36
+ <div class="dev-panel-content">
37
+ <div class="dev-tab-content active" id="devTokens">
38
+ <div class="token-stats">
39
+ <div class="token-stat">
40
+ <span class="token-label">Prompt</span>
41
+ <span class="token-value" id="tokenPrompt">0</span>
42
+ </div>
43
+ <div class="token-stat">
44
+ <span class="token-label">Completion</span>
45
+ <span class="token-value" id="tokenCompletion">0</span>
46
+ </div>
47
+ <div class="token-stat total">
48
+ <span class="token-label">Total</span>
49
+ <span class="token-value" id="tokenTotal">0</span>
50
+ </div>
51
+ </div>
52
+ <div class="token-session">
53
+ <span class="token-label">Session Total</span>
54
+ <span class="token-value" id="tokenSession">0</span>
55
+ </div>
56
+ <button class="dev-btn" id="resetTokens">Reset Session</button>
57
+ </div>
58
+ <div class="dev-tab-content" id="devApi">
59
+ <div class="api-log-list" id="apiLogList">
60
+ <div class="empty-log">No API calls yet</div>
61
+ </div>
62
+ </div>
63
+ <div class="dev-tab-content" id="devTools">
64
+ <div class="tool-list" id="toolList">
65
+ <div class="empty-log">No tool calls yet</div>
66
+ </div>
67
+ </div>
68
+ <div class="dev-tab-content" id="devErrors">
69
+ <div class="error-list" id="errorList">
70
+ <div class="empty-log">No errors 🎉</div>
71
+ </div>
72
+ <button class="dev-btn" id="clearErrors">Clear Errors</button>
73
+ </div>
74
+ </div>
75
+ `;
76
+
77
+ // Session token tracking
78
+ let sessionTokens = 0;
79
+
80
+ function init() {
81
+ const app = document.querySelector('.app');
82
+ if (!app) {
83
+ setTimeout(init, 100);
84
+ return;
85
+ }
86
+
87
+ // Add panel to DOM
88
+ document.body.appendChild(panel);
89
+
90
+ // Listen for activity button click (in header)
91
+ const activityBtn = document.getElementById('activityBtn');
92
+ if (activityBtn) {
93
+ activityBtn.addEventListener('click', togglePanel);
94
+ }
95
+
96
+ // Close button
97
+ panel.querySelector('.dev-panel-close').addEventListener('click', () => {
98
+ if (window.UplinkPanels) {
99
+ window.UplinkPanels.close('activity');
100
+ } else {
101
+ panelVisible = false;
102
+ panel.classList.remove('visible');
103
+ }
104
+ });
105
+
106
+ // Tab switching
107
+ panel.querySelectorAll('.dev-tab').forEach(tab => {
108
+ tab.addEventListener('click', () => switchTab(tab.dataset.tab));
109
+ });
110
+
111
+ // Reset tokens button
112
+ document.getElementById('resetTokens').addEventListener('click', () => {
113
+ sessionTokens = 0;
114
+ updateTokenDisplay();
115
+ });
116
+
117
+ // Clear errors button
118
+ document.getElementById('clearErrors').addEventListener('click', () => {
119
+ errorLogs = [];
120
+ updateErrorDisplay();
121
+ });
122
+
123
+ // Intercept fetch to capture API calls
124
+ interceptFetch();
125
+
126
+ // Register with panel manager
127
+ if (window.UplinkPanels) {
128
+ window.UplinkPanels.register('activity', {
129
+ element: panel,
130
+ isOpen: () => panelVisible,
131
+ open: () => {
132
+ panelVisible = true;
133
+ panel.classList.add('visible');
134
+ },
135
+ close: () => {
136
+ panelVisible = false;
137
+ panel.classList.remove('visible');
138
+ }
139
+ });
140
+ }
141
+
142
+ if (window.UplinkLogger?.debug) {
143
+ window.UplinkLogger.debug('Developer: Initialized');
144
+ }
145
+ }
146
+
147
+ function togglePanel() {
148
+ if (window.UplinkPanels) {
149
+ panelVisible = window.UplinkPanels.toggle('activity');
150
+ } else {
151
+ panelVisible = !panelVisible;
152
+ panel.classList.toggle('visible', panelVisible);
153
+ }
154
+ }
155
+
156
+ function switchTab(tabName) {
157
+ panel.querySelectorAll('.dev-tab').forEach(t => {
158
+ t.classList.toggle('active', t.dataset.tab === tabName);
159
+ });
160
+ panel.querySelectorAll('.dev-tab-content').forEach(c => {
161
+ c.classList.toggle('active', c.id === 'dev' + tabName.charAt(0).toUpperCase() + tabName.slice(1));
162
+ });
163
+ }
164
+
165
+ function interceptFetch() {
166
+ // Use FetchUtils hook system instead of monkey-patching window.fetch (H-25)
167
+ // This avoids double-wrapping when both fetch-utils.js and developer.js run
168
+ if (window.UplinkFetch) {
169
+ window.UplinkFetch.registerHook('afterResponse', async (url, options, response, duration) => {
170
+ if (url.startsWith('/api/')) {
171
+ try {
172
+ const data = await response.json();
173
+ logApiCall(url, options, data, duration, response.status);
174
+ if (data.usage) updateTokens(data.usage);
175
+ if (data.toolCalls) logToolCalls(data.toolCalls);
176
+ } catch (e) {
177
+ logApiCall(url, options, null, duration, response.status);
178
+ }
179
+ }
180
+ });
181
+ window.UplinkFetch.registerHook('onError', async (url, options, error) => {
182
+ logError(error, url);
183
+ });
184
+ return;
185
+ }
186
+
187
+ // Fallback: direct monkey-patch if FetchUtils not available
188
+ const originalFetch = window.fetch;
189
+ window.fetch = async function(...args) {
190
+ const url = typeof args[0] === 'string' ? args[0] : args[0].url;
191
+ const startTime = Date.now();
192
+
193
+ try {
194
+ const response = await originalFetch.apply(this, args);
195
+
196
+ // Clone response to read body without consuming it
197
+ const cloned = response.clone();
198
+
199
+ // Log API calls to our endpoints
200
+ if (url.startsWith('/api/')) {
201
+ const duration = Date.now() - startTime;
202
+
203
+ try {
204
+ const data = await cloned.json();
205
+ logApiCall(url, args[1], data, duration, response.status);
206
+
207
+ // Extract token usage if present
208
+ if (data.usage) {
209
+ updateTokens(data.usage);
210
+ }
211
+
212
+ // Extract tool calls if present
213
+ if (data.toolCalls) {
214
+ logToolCalls(data.toolCalls);
215
+ }
216
+ } catch (e) {
217
+ // Not JSON, just log basic info
218
+ logApiCall(url, args[1], null, duration, response.status);
219
+ }
220
+ }
221
+
222
+ return response;
223
+ } catch (error) {
224
+ logError(error, url);
225
+ throw error;
226
+ }
227
+ };
228
+ }
229
+
230
+ function logApiCall(url, options, response, duration, status) {
231
+ const entry = {
232
+ timestamp: new Date().toISOString(),
233
+ url,
234
+ method: options?.method || 'GET',
235
+ status,
236
+ duration,
237
+ response: response ? JSON.stringify(response, null, 2) : null
238
+ };
239
+
240
+ apiLogs.unshift(entry);
241
+ if (apiLogs.length > MAX_LOGS) apiLogs.pop();
242
+
243
+ updateApiDisplay();
244
+ }
245
+
246
+ function logToolCalls(tools) {
247
+ tools.forEach(tool => {
248
+ toolCalls.unshift({
249
+ timestamp: new Date().toISOString(),
250
+ name: tool.name || tool.function?.name || 'unknown',
251
+ args: tool.arguments || tool.function?.arguments || {},
252
+ result: tool.result
253
+ });
254
+ });
255
+
256
+ if (toolCalls.length > MAX_LOGS) {
257
+ toolCalls = toolCalls.slice(0, MAX_LOGS);
258
+ }
259
+
260
+ updateToolDisplay();
261
+ }
262
+
263
+ function logError(error, context = '') {
264
+ const msg = error.message || String(error);
265
+
266
+ // "Failed to fetch" = browser-level network error (offline, tab resume, tunnel flaky).
267
+ // Log to console for debugging but don't surface in error panel — not actionable for user.
268
+ // Real server errors (4xx/5xx) still show in the panel.
269
+ if (msg === 'Failed to fetch') {
270
+ console.warn('[Network]', context || 'fetch failed');
271
+ return;
272
+ }
273
+
274
+ errorLogs.unshift({
275
+ timestamp: new Date().toISOString(),
276
+ message: msg,
277
+ context,
278
+ stack: error.stack
279
+ });
280
+
281
+ if (errorLogs.length > MAX_LOGS) errorLogs.pop();
282
+
283
+ updateErrorDisplay();
284
+
285
+ // Flash the activity button to indicate error
286
+ const activityBtn = document.getElementById('activityBtn');
287
+ if (activityBtn) {
288
+ activityBtn.classList.add('has-error');
289
+ setTimeout(() => activityBtn.classList.remove('has-error'), 2000);
290
+ }
291
+ }
292
+
293
+ function updateTokens(usage) {
294
+ tokenUsage = {
295
+ prompt: usage.prompt_tokens || usage.promptTokens || 0,
296
+ completion: usage.completion_tokens || usage.completionTokens || 0,
297
+ total: usage.total_tokens || usage.totalTokens || 0
298
+ };
299
+
300
+ sessionTokens += tokenUsage.total;
301
+ updateTokenDisplay();
302
+ }
303
+
304
+ function updateTokenDisplay() {
305
+ document.getElementById('tokenPrompt').textContent = tokenUsage.prompt.toLocaleString();
306
+ document.getElementById('tokenCompletion').textContent = tokenUsage.completion.toLocaleString();
307
+ document.getElementById('tokenTotal').textContent = tokenUsage.total.toLocaleString();
308
+ document.getElementById('tokenSession').textContent = sessionTokens.toLocaleString();
309
+ }
310
+
311
+ function updateApiDisplay() {
312
+ const list = document.getElementById('apiLogList');
313
+ if (apiLogs.length === 0) {
314
+ list.innerHTML = '<div class="empty-log">No API calls yet</div>';
315
+ return;
316
+ }
317
+
318
+ list.innerHTML = apiLogs.map(log => `
319
+ <div class="api-log-entry">
320
+ <div class="api-log-header">
321
+ <span class="api-method ${escapeHtml(log.method)}">${escapeHtml(log.method)}</span>
322
+ <span class="api-url">${escapeHtml(log.url)}</span>
323
+ <span class="api-status status-${Math.floor(log.status/100)}">${log.status}</span>
324
+ <span class="api-duration">${log.duration}ms</span>
325
+ </div>
326
+ ${log.response ? `
327
+ <details class="api-response">
328
+ <summary>Response</summary>
329
+ <pre>${escapeHtml(truncate(log.response, 500))}</pre>
330
+ </details>
331
+ ` : ''}
332
+ </div>
333
+ `).join('');
334
+ }
335
+
336
+ function updateToolDisplay() {
337
+ const list = document.getElementById('toolList');
338
+ if (toolCalls.length === 0) {
339
+ list.innerHTML = '<div class="empty-log">No tool calls yet</div>';
340
+ return;
341
+ }
342
+
343
+ list.innerHTML = toolCalls.map(tool => `
344
+ <div class="tool-entry">
345
+ <div class="tool-header">
346
+ <span class="tool-name">🔧 ${escapeHtml(tool.name)}</span>
347
+ <span class="tool-time">${formatTime(tool.timestamp)}</span>
348
+ </div>
349
+ ${tool.args ? `
350
+ <details class="tool-args">
351
+ <summary>Arguments</summary>
352
+ <pre>${escapeHtml(JSON.stringify(tool.args, null, 2))}</pre>
353
+ </details>
354
+ ` : ''}
355
+ </div>
356
+ `).join('');
357
+ }
358
+
359
+ function updateErrorDisplay() {
360
+ const list = document.getElementById('errorList');
361
+ if (errorLogs.length === 0) {
362
+ list.innerHTML = '<div class="empty-log">No errors 🎉</div>';
363
+ return;
364
+ }
365
+
366
+ list.innerHTML = errorLogs.map(err => `
367
+ <div class="error-entry">
368
+ <div class="error-header">
369
+ <span class="error-message">${escapeHtml(err.message)}</span>
370
+ <span class="error-time">${formatTime(err.timestamp)}</span>
371
+ </div>
372
+ ${err.context ? `<div class="error-context">${escapeHtml(err.context)}</div>` : ''}
373
+ </div>
374
+ `).join('');
375
+ }
376
+
377
+ // Utilities
378
+ function escapeHtml(str) {
379
+ if (!str) return '';
380
+ return str.replace(/[&<>"']/g, m => ({
381
+ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
382
+ })[m]);
383
+ }
384
+
385
+ function truncate(str, len) {
386
+ if (str.length <= len) return str;
387
+ return str.slice(0, len) + '...\n[truncated]';
388
+ }
389
+
390
+ function formatTime(iso) {
391
+ return new Date(iso).toLocaleTimeString();
392
+ }
393
+
394
+ // Log a simple tool name (from streaming indicator)
395
+ function logTool(toolName) {
396
+ toolCalls.unshift({
397
+ timestamp: new Date().toISOString(),
398
+ name: toolName,
399
+ args: null,
400
+ result: null,
401
+ streaming: true
402
+ });
403
+
404
+ if (toolCalls.length > MAX_LOGS) {
405
+ toolCalls = toolCalls.slice(0, MAX_LOGS);
406
+ }
407
+
408
+ updateToolDisplay();
409
+
410
+ // Flash the activity button to indicate tool usage
411
+ const activityBtn = document.getElementById('activityBtn');
412
+ if (activityBtn) {
413
+ activityBtn.classList.add('has-tool');
414
+ setTimeout(() => activityBtn.classList.remove('has-tool'), 1000);
415
+ }
416
+ }
417
+
418
+ // Expose for external use
419
+ export const UplinkDeveloper = {
420
+ show: () => { panelVisible = true; panel.classList.add('visible'); },
421
+ hide: () => { panelVisible = false; panel.classList.remove('visible'); },
422
+ logError,
423
+ logTool,
424
+ logToolCall: (name, args, result) => logToolCalls([{ name, arguments: args, result }]),
425
+ updateTokens
426
+ };
427
+
428
+ // Backward compat: assign to window
429
+ window.UplinkDeveloper = UplinkDeveloper;
430
+
431
+ // Register with core for coordinated initialization
432
+ UplinkCore.registerModule('developer', init);
@@ -0,0 +1,124 @@
1
+ // ============================================
2
+ // ENCRYPTION MODULE
3
+ // Web Crypto API utilities for encrypted storage
4
+ // ============================================
5
+
6
+ import { UplinkCore } from './core.js';
7
+
8
+ const SALT_KEY = 'uplink-salt';
9
+ const CRYPTO_ALGO = 'AES-GCM';
10
+ const PBKDF2_ITERATIONS = 600000; // OWASP 2023 recommendation for SHA-256
11
+ const PBKDF2_ITERATIONS_LEGACY = 100000; // Legacy iteration count for migration
12
+
13
+ // Derive encryption key from password
14
+ async function deriveKey(password, salt, iterations = PBKDF2_ITERATIONS) {
15
+ const encoder = new TextEncoder();
16
+ const keyMaterial = await crypto.subtle.importKey(
17
+ 'raw',
18
+ encoder.encode(password),
19
+ 'PBKDF2',
20
+ false,
21
+ ['deriveKey']
22
+ );
23
+
24
+ return crypto.subtle.deriveKey(
25
+ {
26
+ name: 'PBKDF2',
27
+ salt: salt,
28
+ iterations: iterations,
29
+ hash: 'SHA-256'
30
+ },
31
+ keyMaterial,
32
+ { name: CRYPTO_ALGO, length: 256 },
33
+ false,
34
+ ['encrypt', 'decrypt']
35
+ );
36
+ }
37
+
38
+ // Encrypt data
39
+ async function encrypt(data, password) {
40
+ let salt = localStorage.getItem(SALT_KEY);
41
+ if (!salt) {
42
+ const newSalt = crypto.getRandomValues(new Uint8Array(16));
43
+ salt = btoa(String.fromCharCode(...newSalt));
44
+ localStorage.setItem(SALT_KEY, salt);
45
+ }
46
+ const saltBytes = Uint8Array.from(atob(salt), c => c.charCodeAt(0));
47
+
48
+ const key = await deriveKey(password, saltBytes);
49
+ const iv = crypto.getRandomValues(new Uint8Array(12));
50
+ const encoder = new TextEncoder();
51
+
52
+ const encrypted = await crypto.subtle.encrypt(
53
+ { name: CRYPTO_ALGO, iv: iv },
54
+ key,
55
+ encoder.encode(JSON.stringify(data))
56
+ );
57
+
58
+ return {
59
+ v: 1, // Version flag for PBKDF2 iteration count
60
+ iv: btoa(String.fromCharCode(...iv)),
61
+ data: btoa(String.fromCharCode(...new Uint8Array(encrypted)))
62
+ };
63
+ }
64
+
65
+ // Decrypt data
66
+ async function decrypt(encryptedObj, password) {
67
+ const salt = localStorage.getItem(SALT_KEY);
68
+ if (!salt) throw new Error('No salt found');
69
+
70
+ const saltBytes = Uint8Array.from(atob(salt), c => c.charCodeAt(0));
71
+
72
+ // Determine iteration count based on version (v1 = 600k, undefined = legacy 100k)
73
+ const version = encryptedObj.v;
74
+ const iterations = version === 1 ? PBKDF2_ITERATIONS : PBKDF2_ITERATIONS_LEGACY;
75
+
76
+ const key = await deriveKey(password, saltBytes, iterations);
77
+ const iv = Uint8Array.from(atob(encryptedObj.iv), c => c.charCodeAt(0));
78
+ const data = Uint8Array.from(atob(encryptedObj.data), c => c.charCodeAt(0));
79
+
80
+ const decrypted = await crypto.subtle.decrypt(
81
+ { name: CRYPTO_ALGO, iv: iv },
82
+ key,
83
+ data
84
+ );
85
+
86
+ const decoder = new TextDecoder();
87
+ return JSON.parse(decoder.decode(decrypted));
88
+ }
89
+
90
+ // Verify password by attempting decryption
91
+ async function verifyPassword(password) {
92
+ try {
93
+ const encrypted = localStorage.getItem('uplink-history-encrypted');
94
+ if (encrypted) {
95
+ await decrypt(JSON.parse(encrypted), password);
96
+ }
97
+ return true;
98
+ } catch (e) {
99
+ return false;
100
+ }
101
+ }
102
+
103
+ // Clear encryption data
104
+ function clearEncryption() {
105
+ localStorage.removeItem('uplink-history-encrypted');
106
+ localStorage.removeItem(SALT_KEY);
107
+ }
108
+
109
+ // Export API
110
+ export const UplinkEncryption = {
111
+ encrypt,
112
+ decrypt,
113
+ verifyPassword,
114
+ clearEncryption,
115
+ SALT_KEY
116
+ };
117
+
118
+ export { encrypt, decrypt, verifyPassword, clearEncryption, SALT_KEY };
119
+
120
+ // Backward compat: assign to window
121
+ window.UplinkEncryption = UplinkEncryption;
122
+
123
+ // Register with core if available
124
+ UplinkCore.registerModule('encryption');
@@ -0,0 +1,122 @@
1
+ // ============================================
2
+ // ERROR MESSAGES MODULE
3
+ // User-friendly error messages
4
+ // ============================================
5
+
6
+ /**
7
+ * Map technical errors to user-friendly messages
8
+ */
9
+ const errorMessages = {
10
+ // Network errors
11
+ 'Failed to fetch': 'Unable to connect to the server. Check your internet connection.',
12
+ 'NetworkError': 'Network error. Please check your connection and try again.',
13
+ 'net::ERR_CONNECTION_REFUSED': 'Server is not responding. Is Uplink running?',
14
+ 'ECONNREFUSED': 'Cannot connect to the AI gateway. Check your gateway settings.',
15
+
16
+ // Gateway errors
17
+ 'Gateway error: 401': 'Authentication failed. Check your gateway token in settings.',
18
+ 'Gateway error: 403': 'Access denied. Your gateway token may be invalid.',
19
+ 'Gateway error: 404': 'Gateway endpoint not found. Check your gateway URL.',
20
+ 'Gateway error: 429': 'Too many requests. Please wait a moment and try again.',
21
+ 'Gateway error: 500': 'The AI service encountered an error. Try again in a moment.',
22
+ 'Gateway error: 502': 'Gateway is temporarily unavailable. Try again shortly.',
23
+ 'Gateway error: 503': 'AI service is overloaded. Please try again later.',
24
+
25
+ // Timeout errors
26
+ 'Stream read timed out': 'Response took too long. The AI might be busy with a complex task.',
27
+ 'timeout': 'Request timed out. Try a shorter message or try again.',
28
+ 'AbortError': 'Request was cancelled.',
29
+
30
+ // TTS errors
31
+ 'ElevenLabs error: 401': 'Voice API key is invalid. Check your ElevenLabs settings.',
32
+ 'ElevenLabs error: 429': 'Voice quota exceeded. Try again later or check your plan.',
33
+ 'No text to speak': 'Nothing to read aloud.',
34
+
35
+ // Whisper/transcription errors
36
+ 'Whisper API error': 'Speech recognition failed. Try speaking again.',
37
+
38
+ // WebSocket errors
39
+ 'WebSocket': 'Real-time connection lost. Reconnecting...',
40
+
41
+ // Generic
42
+ 'Unknown error': 'Something went wrong. Please try again.'
43
+ };
44
+
45
+ /**
46
+ * Get user-friendly error message
47
+ * @param {Error|string} error - The error object or message
48
+ * @returns {string} User-friendly message
49
+ */
50
+ export function getFriendlyMessage(error) {
51
+ const message = error?.message || String(error);
52
+
53
+ // Check for exact matches first
54
+ if (errorMessages[message]) {
55
+ return errorMessages[message];
56
+ }
57
+
58
+ // Check for partial matches
59
+ for (const [key, friendly] of Object.entries(errorMessages)) {
60
+ if (message.includes(key)) {
61
+ return friendly;
62
+ }
63
+ }
64
+
65
+ // Check for HTTP status codes
66
+ const statusMatch = message.match(/(\d{3})/);
67
+ if (statusMatch) {
68
+ const status = statusMatch[1];
69
+ switch (status) {
70
+ case '400': return 'Invalid request. Please try again.';
71
+ case '401': return 'Authentication required. Check your settings.';
72
+ case '403': return 'Access denied.';
73
+ case '404': return 'Service not found. Check your configuration.';
74
+ case '429': return 'Too many requests. Please wait and try again.';
75
+ case '500': return 'Server error. Please try again later.';
76
+ case '502': return 'Service temporarily unavailable.';
77
+ case '503': return 'Service overloaded. Please try again later.';
78
+ }
79
+ }
80
+
81
+ // If message is already user-friendly (no technical jargon), return it
82
+ if (!message.includes('Error:') &&
83
+ !message.includes('::') &&
84
+ !message.includes('ECONNREFUSED') &&
85
+ message.length < 100) {
86
+ return message;
87
+ }
88
+
89
+ // Fallback
90
+ return 'Something went wrong. Please try again.';
91
+ }
92
+
93
+ /**
94
+ * Get error with action suggestion
95
+ * @param {Error|string} error - The error
96
+ * @returns {{ message: string, action?: string }}
97
+ */
98
+ export function getErrorWithAction(error) {
99
+ const message = getFriendlyMessage(error);
100
+ const errorStr = error?.message || String(error);
101
+
102
+ let action = null;
103
+
104
+ if (errorStr.includes('401') || errorStr.includes('403')) {
105
+ action = 'Check Settings';
106
+ } else if (errorStr.includes('ECONNREFUSED') || errorStr.includes('Failed to fetch')) {
107
+ action = 'Check Connection';
108
+ } else if (errorStr.includes('429')) {
109
+ action = 'Wait & Retry';
110
+ }
111
+
112
+ return { message, action };
113
+ }
114
+
115
+ // Export API
116
+ export const UplinkErrors = {
117
+ getFriendlyMessage,
118
+ getErrorWithAction
119
+ };
120
+
121
+ // Backward compat: assign to window
122
+ window.UplinkErrors = UplinkErrors;