@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,172 @@
1
+ /**
2
+ * Update Checker Module
3
+ *
4
+ * Periodically checks npm registry for new versions of uplink-chat.
5
+ * Broadcasts update notifications to connected WebSocket clients.
6
+ *
7
+ * - Checks on startup and every 1 hour
8
+ * - 5 second timeout (non-blocking, fails silently if offline)
9
+ * - Caches results to avoid redundant checks
10
+ * - No external dependencies
11
+ */
12
+
13
+ import { readFileSync } from 'fs';
14
+ import { join, dirname } from 'path';
15
+ import { fileURLToPath } from 'url';
16
+ import https from 'https';
17
+ import http from 'http';
18
+ import { createLogger } from './logger.js';
19
+
20
+ const log = createLogger('UpdateChecker');
21
+
22
+ const __filename = fileURLToPath(import.meta.url);
23
+ const __dirname = dirname(__filename);
24
+ const ROOT = join(__dirname, '..');
25
+
26
+ const REGISTRY_URL = 'https://registry.npmjs.org/uplink-chat/latest';
27
+ const CHECK_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
28
+ const FETCH_TIMEOUT_MS = 5000; // 5 seconds
29
+
30
+ // Cache
31
+ let cachedResult = null; // { current, latest, updateAvailable, checkedAt }
32
+
33
+ /**
34
+ * Simple semver comparison (no external dependency)
35
+ * Supports standard x.y.z format
36
+ * Returns: -1 if a < b, 0 if equal, 1 if a > b
37
+ */
38
+ function compareSemver(a, b) {
39
+ const partsA = a.replace(/^v/, '').split('.').map(Number);
40
+ const partsB = b.replace(/^v/, '').split('.').map(Number);
41
+
42
+ for (let i = 0; i < 3; i++) {
43
+ const numA = partsA[i] || 0;
44
+ const numB = partsB[i] || 0;
45
+ if (numA < numB) return -1;
46
+ if (numA > numB) return 1;
47
+ }
48
+ return 0;
49
+ }
50
+
51
+ /**
52
+ * Get current version from package.json
53
+ */
54
+ function getCurrentVersion() {
55
+ try {
56
+ const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8'));
57
+ return pkg.version || '0.0.0';
58
+ } catch {
59
+ return '0.0.0';
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Fetch latest version from npm registry with timeout
65
+ * Returns version string or null on failure
66
+ */
67
+ function fetchLatestVersion() {
68
+ return new Promise((resolve) => {
69
+ const url = new URL(REGISTRY_URL);
70
+ const transport = url.protocol === 'https:' ? https : http;
71
+
72
+ const req = transport.get(url.href, { timeout: FETCH_TIMEOUT_MS }, (res) => {
73
+ if (res.statusCode !== 200) {
74
+ res.resume(); // Drain the response
75
+ resolve(null);
76
+ return;
77
+ }
78
+
79
+ let data = '';
80
+ res.on('data', (chunk) => { data += chunk; });
81
+ res.on('end', () => {
82
+ try {
83
+ const json = JSON.parse(data);
84
+ resolve(json.version || null);
85
+ } catch {
86
+ resolve(null);
87
+ }
88
+ });
89
+ });
90
+
91
+ req.on('error', () => resolve(null));
92
+ req.on('timeout', () => {
93
+ req.destroy();
94
+ resolve(null);
95
+ });
96
+
97
+ // Overall timeout safety net
98
+ setTimeout(() => {
99
+ req.destroy();
100
+ resolve(null);
101
+ }, FETCH_TIMEOUT_MS + 1000);
102
+ });
103
+ }
104
+
105
+ /**
106
+ * Check for updates and broadcast if available
107
+ * @param {Function} broadcastFn - Function to broadcast WebSocket messages
108
+ */
109
+ async function checkForUpdate(broadcastFn) {
110
+ const current = getCurrentVersion();
111
+ const latest = await fetchLatestVersion();
112
+
113
+ if (!latest) {
114
+ // Offline or registry error — skip silently
115
+ return;
116
+ }
117
+
118
+ const updateAvailable = compareSemver(current, latest) < 0;
119
+
120
+ cachedResult = {
121
+ current,
122
+ latest,
123
+ updateAvailable,
124
+ checkedAt: Date.now(),
125
+ };
126
+
127
+ if (updateAvailable && broadcastFn) {
128
+ broadcastFn({
129
+ type: 'update_available',
130
+ current,
131
+ latest,
132
+ });
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Start the update checker
138
+ * Call after server is listening.
139
+ *
140
+ * @param {Object} server - HTTP server instance (unused, kept for API consistency)
141
+ * @param {Function} broadcastFn - Function to broadcast a message to all WS clients
142
+ */
143
+ export function startUpdateChecker(server, broadcastFn) {
144
+ if (!broadcastFn) {
145
+ log.warn('No broadcast function provided, skipping');
146
+ return;
147
+ }
148
+
149
+ // Initial check (non-blocking, delayed slightly to not interfere with startup)
150
+ setTimeout(() => {
151
+ checkForUpdate(broadcastFn).catch(() => {
152
+ // Silently ignore errors
153
+ });
154
+ }, 3000);
155
+
156
+ // Periodic re-check every hour
157
+ const interval = setInterval(() => {
158
+ checkForUpdate(broadcastFn).catch(() => {
159
+ // Silently ignore errors
160
+ });
161
+ }, CHECK_INTERVAL_MS);
162
+
163
+ // Don't keep the process alive just for update checking
164
+ if (interval.unref) interval.unref();
165
+ }
166
+
167
+ /**
168
+ * Get cached update result (for new client connections)
169
+ */
170
+ export function getCachedUpdateResult() {
171
+ return cachedResult;
172
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Filename Sanitization Utility
3
+ *
4
+ * Prevents path traversal attacks by sanitizing user-provided filenames.
5
+ * Removes path components and allows only safe characters.
6
+ */
7
+
8
+ import path from 'path';
9
+
10
+ /**
11
+ * Sanitize a filename to prevent path traversal attacks
12
+ * - Removes path components (uses only basename)
13
+ * - Allows only alphanumeric characters, dashes, underscores, and dots
14
+ * - Removes leading dots (hidden files)
15
+ * - Limits length
16
+ *
17
+ * @param {string} filename - The raw filename from user input
18
+ * @param {Object} options - Sanitization options
19
+ * @param {number} options.maxLength - Maximum filename length (default: 255)
20
+ * @param {string} options.fallback - Fallback filename if sanitization results in empty string (default: 'unnamed')
21
+ * @returns {string} Sanitized filename safe for filesystem operations
22
+ */
23
+ export function sanitizeFilename(filename, options = {}) {
24
+ const { maxLength = 255, fallback = 'unnamed' } = options;
25
+
26
+ if (!filename || typeof filename !== 'string') {
27
+ return fallback;
28
+ }
29
+
30
+ // Step 1: Extract only the basename (removes path traversal like ../../../)
31
+ let sanitized = path.basename(filename);
32
+
33
+ // Step 2: Remove any remaining path-like characters (backslash for Windows)
34
+ sanitized = sanitized.replace(/[\\\/]/g, '');
35
+
36
+ // Step 3: Allow only alphanumeric, dash, underscore, and dot
37
+ sanitized = sanitized.replace(/[^a-zA-Z0-9._-]/g, '');
38
+
39
+ // Step 4: Remove leading dots (prevent hidden files like .htaccess)
40
+ sanitized = sanitized.replace(/^[.]+/, '');
41
+
42
+ // Step 5: Trim whitespace (should be gone from step 3, but safety check)
43
+ sanitized = sanitized.trim();
44
+
45
+ // Step 6: Limit length
46
+ if (sanitized.length > maxLength) {
47
+ // Preserve extension if possible
48
+ const lastDot = sanitized.lastIndexOf('.');
49
+ if (lastDot > 0 && lastDot > sanitized.length - maxLength) {
50
+ const ext = sanitized.slice(lastDot);
51
+ sanitized = sanitized.slice(0, maxLength - ext.length) + ext;
52
+ } else {
53
+ sanitized = sanitized.slice(0, maxLength);
54
+ }
55
+ }
56
+
57
+ // Step 7: If empty after sanitization, use fallback
58
+ if (!sanitized || sanitized.length === 0) {
59
+ return fallback;
60
+ }
61
+
62
+ return sanitized;
63
+ }
64
+
65
+ /**
66
+ * Extract file extension from a filename safely
67
+ * Returns the extension including the dot (e.g., '.jpg')
68
+ *
69
+ * @param {string} filename - The filename to extract extension from
70
+ * @returns {string} File extension or empty string if none
71
+ */
72
+ export function getSafeExtension(filename) {
73
+ if (!filename || typeof filename !== 'string') {
74
+ return '';
75
+ }
76
+
77
+ // Get basename first to avoid path traversal in extension
78
+ const basename = path.basename(filename);
79
+
80
+ // Find the last dot
81
+ const lastDot = basename.lastIndexOf('.');
82
+
83
+ // No extension found
84
+ if (lastDot <= 0) {
85
+ return '';
86
+ }
87
+
88
+ // Extract extension
89
+ const ext = basename.slice(lastDot).toLowerCase();
90
+
91
+ // Validate extension characters (only allow alphanumeric)
92
+ const validExt = ext.replace(/[^a-zA-Z0-9.]/g, '');
93
+
94
+ return validExt;
95
+ }
96
+
97
+ /**
98
+ * Check if a filename contains path traversal attempts
99
+ * Useful for logging/warning without modifying the filename
100
+ *
101
+ * @param {string} filename - The filename to check
102
+ * @returns {boolean} True if path traversal is detected
103
+ */
104
+ export function containsPathTraversal(filename) {
105
+ if (!filename || typeof filename !== 'string') {
106
+ return false;
107
+ }
108
+
109
+ // Check for common path traversal patterns
110
+ const traversalPatterns = [
111
+ /\.\.\//, // ../
112
+ /\.\.\\/, // ..\
113
+ /%2e%2e%2f/i, // URL encoded ../
114
+ /%2e%2e\//i, // URL encoded ../ variant
115
+ /\.\.\/%2f/i, // Mixed encoding
116
+ /^\//, // Absolute path Unix
117
+ /^[a-z]:/i, // Absolute path Windows
118
+ /\\/, // Backslash (path separator)
119
+ /\x00/, // Null byte
120
+ ];
121
+
122
+ return traversalPatterns.some(pattern => pattern.test(filename));
123
+ }
124
+
125
+ export default {
126
+ sanitizeFilename,
127
+ getSafeExtension,
128
+ containsPathTraversal
129
+ };
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Utils Module - Common utilities and helpers
3
+ */
4
+
5
+ import fs from 'fs/promises';
6
+ import path from 'path';
7
+ import http from 'http';
8
+ import https from 'https';
9
+
10
+ // Re-export withRetry from unified utility module
11
+ import { withRetry as _withRetry } from '../utils/with-retry.js';
12
+ export const withRetry = _withRetry;
13
+
14
+ // ===========================================
15
+ // LOGGING
16
+ // ===========================================
17
+ const LOG_LEVEL = process.env.LOG_LEVEL || 'debug';
18
+ const LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
19
+
20
+ export function log(level, ...args) {
21
+ if (LOG_LEVELS[level] >= LOG_LEVELS[LOG_LEVEL]) {
22
+ const sanitized = args.map(arg => {
23
+ if (typeof arg === 'string') {
24
+ return LOG_LEVEL !== 'debug' && arg.length > 100
25
+ ? arg.substring(0, 100) + '...[truncated]'
26
+ : arg;
27
+ }
28
+ return arg;
29
+ });
30
+ console[level === 'debug' ? 'log' : level](...sanitized);
31
+ }
32
+ }
33
+
34
+ // ===========================================
35
+ // FETCH WITH TIMEOUT
36
+ // ===========================================
37
+ export async function fetchWithTimeout(url, options = {}, timeoutMs = 45000) {
38
+ const controller = new AbortController();
39
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
40
+
41
+ try {
42
+ const response = await fetch(url, {
43
+ ...options,
44
+ signal: controller.signal
45
+ });
46
+ return response;
47
+ } catch (err) {
48
+ if (err.name === 'AbortError') {
49
+ throw new Error(`Request timed out after ${timeoutMs}ms - gateway may be busy`);
50
+ }
51
+ throw err;
52
+ } finally {
53
+ clearTimeout(timeoutId);
54
+ }
55
+ }
56
+
57
+ // ===========================================
58
+ // RETRY HELPER
59
+ // ===========================================
60
+ // withRetry is now re-exported from ../utils/with-retry.js (see top of file)
61
+
62
+ // ===========================================
63
+ // FILE CLEANUP
64
+ // ===========================================
65
+ export async function cleanupOldFiles(dir, maxAgeMs, extensions = []) {
66
+ try {
67
+ const files = await fs.readdir(dir);
68
+ const now = Date.now();
69
+ let cleaned = 0;
70
+
71
+ for (const file of files) {
72
+ if (extensions.length > 0 && !extensions.some(ext => file.endsWith(ext))) {
73
+ continue;
74
+ }
75
+
76
+ const filePath = path.join(dir, file);
77
+ try {
78
+ const stat = await fs.stat(filePath);
79
+ if (now - stat.mtimeMs > maxAgeMs) {
80
+ await fs.unlink(filePath);
81
+ cleaned++;
82
+ }
83
+ } catch (e) {
84
+ // File may have been deleted
85
+ }
86
+ }
87
+
88
+ if (cleaned > 0) {
89
+ log('debug', `Cleaned up ${cleaned} old files from ${dir}`);
90
+ }
91
+ return cleaned;
92
+ } catch (e) {
93
+ // Directory may not exist
94
+ return 0;
95
+ }
96
+ }
97
+
98
+ // ===========================================
99
+ // HTTP KEEP-ALIVE AGENTS
100
+ // ===========================================
101
+ export const httpAgent = new http.Agent({ keepAlive: true, maxSockets: 10 });
102
+ export const httpsAgent = new https.Agent({ keepAlive: true, maxSockets: 10 });
103
+
104
+ // ===========================================
105
+ // SSRF PREVENTION
106
+ // ===========================================
107
+ export function isPrivateIP(hostname) {
108
+ const privatePatterns = [
109
+ /^localhost$/i,
110
+ /^127\./,
111
+ /^10\./,
112
+ /^172\.(1[6-9]|2[0-9]|3[0-1])\./,
113
+ /^192\.168\./,
114
+ /^0\./,
115
+ /^169\.254\./,
116
+ /^::1$/,
117
+ /^fc00:/i,
118
+ /^fe80:/i,
119
+ ];
120
+ return privatePatterns.some(pattern => pattern.test(hostname));
121
+ }
122
+
123
+ export function isAllowedExternalHost(hostname) {
124
+ const allowedHosts = [
125
+ /\.openai\.com$/i,
126
+ /\.anthropic\.com$/i,
127
+ /\.elevenlabs\.io$/i,
128
+ /localhost$/i,
129
+ /^127\./,
130
+ ];
131
+ return allowedHosts.some(pattern => pattern.test(hostname));
132
+ }
133
+
134
+ // NOTE: Request tracking has been consolidated into server.js
135
+ // All request tracking functions are provided via requestHelpers from server.js
136
+ // Do NOT add duplicate tracking here — see H-38 audit finding
137
+
138
+ export default {
139
+ log,
140
+ fetchWithTimeout,
141
+ withRetry,
142
+ cleanupOldFiles,
143
+ httpAgent,
144
+ httpsAgent,
145
+ isPrivateIP,
146
+ isAllowedExternalHost,
147
+ };