@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.
- package/LICENSE +21 -0
- package/README.md +185 -0
- package/bin/uplink.js +279 -0
- package/middleware/error-handler.js +69 -0
- package/package.json +93 -0
- package/public/css/agents.36b98c0f.css +1469 -0
- package/public/css/agents.css +1469 -0
- package/public/css/app.a6a7f8f5.css +2731 -0
- package/public/css/app.css +2731 -0
- package/public/css/artifacts.css +444 -0
- package/public/css/commands.css +55 -0
- package/public/css/connection.css +131 -0
- package/public/css/dashboard.css +233 -0
- package/public/css/developer.css +328 -0
- package/public/css/files.css +123 -0
- package/public/css/markdown.css +156 -0
- package/public/css/message-actions.css +278 -0
- package/public/css/mobile.css +614 -0
- package/public/css/panels-unified.css +483 -0
- package/public/css/premium.css +415 -0
- package/public/css/realtime.css +189 -0
- package/public/css/satellites.css +401 -0
- package/public/css/shortcuts.css +185 -0
- package/public/css/split-view.4def0262.css +673 -0
- package/public/css/split-view.css +673 -0
- package/public/css/theme-generator.css +391 -0
- package/public/css/themes.css +387 -0
- package/public/css/timestamps.css +54 -0
- package/public/css/variables.css +78 -0
- package/public/dist/bundle.b55050c4.js +15757 -0
- package/public/favicon.svg +24 -0
- package/public/img/agents/ada.png +0 -0
- package/public/img/agents/clarice.png +0 -0
- package/public/img/agents/dennis-nedry.png +0 -0
- package/public/img/agents/elliot-alderson.png +0 -0
- package/public/img/agents/main.png +0 -0
- package/public/img/agents/scotty.png +0 -0
- package/public/img/agents/top-flight-security.png +0 -0
- package/public/index.html +1083 -0
- package/public/js/agents-data.js +234 -0
- package/public/js/agents-ui.js +72 -0
- package/public/js/agents.js +1525 -0
- package/public/js/app.js +79 -0
- package/public/js/appearance-settings.js +111 -0
- package/public/js/artifacts.js +432 -0
- package/public/js/audio-queue.js +168 -0
- package/public/js/bootstrap.js +54 -0
- package/public/js/chat.js +1211 -0
- package/public/js/commands.js +581 -0
- package/public/js/connection-api.js +121 -0
- package/public/js/connection.js +1231 -0
- package/public/js/context-tracker.js +271 -0
- package/public/js/core.js +172 -0
- package/public/js/dashboard.js +452 -0
- package/public/js/developer.js +432 -0
- package/public/js/encryption.js +124 -0
- package/public/js/errors.js +122 -0
- package/public/js/event-bus.js +77 -0
- package/public/js/fetch-utils.js +171 -0
- package/public/js/file-handler.js +229 -0
- package/public/js/files.js +352 -0
- package/public/js/gateway-chat.js +538 -0
- package/public/js/logger.js +112 -0
- package/public/js/markdown.js +190 -0
- package/public/js/message-actions.js +431 -0
- package/public/js/message-renderer.js +288 -0
- package/public/js/missed-messages.js +235 -0
- package/public/js/mobile-debug.js +95 -0
- package/public/js/notifications.js +367 -0
- package/public/js/offline-queue.js +178 -0
- package/public/js/onboarding.js +543 -0
- package/public/js/panels.js +156 -0
- package/public/js/premium.js +412 -0
- package/public/js/realtime-voice.js +844 -0
- package/public/js/satellite-sync.js +256 -0
- package/public/js/satellite-ui.js +175 -0
- package/public/js/satellites.js +1516 -0
- package/public/js/settings.js +1087 -0
- package/public/js/shortcuts.js +381 -0
- package/public/js/split-chat.js +1234 -0
- package/public/js/split-resize.js +211 -0
- package/public/js/splitview.js +340 -0
- package/public/js/storage.js +408 -0
- package/public/js/streaming-handler.js +324 -0
- package/public/js/stt-settings.js +316 -0
- package/public/js/theme-generator.js +661 -0
- package/public/js/themes.js +164 -0
- package/public/js/timestamps.js +198 -0
- package/public/js/tts-settings.js +575 -0
- package/public/js/ui.js +267 -0
- package/public/js/update-notifier.js +143 -0
- package/public/js/utils/constants.js +165 -0
- package/public/js/utils/sanitize.js +93 -0
- package/public/js/utils/sse-parser.js +195 -0
- package/public/js/voice.js +883 -0
- package/public/manifest.json +58 -0
- package/public/moon_texture.jpg +0 -0
- package/public/sw.js +221 -0
- package/public/three.min.js +6 -0
- package/server/channel.js +529 -0
- package/server/chat.js +270 -0
- package/server/config-store.js +362 -0
- package/server/config.js +159 -0
- package/server/context.js +131 -0
- package/server/gateway-commands.js +211 -0
- package/server/gateway-proxy.js +318 -0
- package/server/index.js +22 -0
- package/server/logger.js +89 -0
- package/server/middleware/auth.js +188 -0
- package/server/middleware.js +218 -0
- package/server/openclaw-discover.js +308 -0
- package/server/premium/index.js +156 -0
- package/server/premium/license.js +140 -0
- package/server/realtime/bridge.js +837 -0
- package/server/realtime/index.js +349 -0
- package/server/realtime/tts-stream.js +446 -0
- package/server/routes/agents.js +564 -0
- package/server/routes/artifacts.js +174 -0
- package/server/routes/chat.js +311 -0
- package/server/routes/config-settings.js +345 -0
- package/server/routes/config.js +603 -0
- package/server/routes/files.js +307 -0
- package/server/routes/index.js +18 -0
- package/server/routes/media.js +451 -0
- package/server/routes/missed-messages.js +107 -0
- package/server/routes/premium.js +75 -0
- package/server/routes/push.js +156 -0
- package/server/routes/satellite.js +406 -0
- package/server/routes/status.js +251 -0
- package/server/routes/stt.js +35 -0
- package/server/routes/voice.js +260 -0
- package/server/routes/webhooks.js +203 -0
- package/server/routes.js +206 -0
- package/server/runtime-config.js +336 -0
- package/server/share.js +305 -0
- package/server/stt/faster-whisper.js +72 -0
- package/server/stt/groq.js +51 -0
- package/server/stt/index.js +196 -0
- package/server/stt/openai.js +49 -0
- package/server/sync.js +244 -0
- package/server/tailscale-https.js +175 -0
- package/server/tts.js +646 -0
- package/server/update-checker.js +172 -0
- package/server/utils/filename.js +129 -0
- package/server/utils.js +147 -0
- package/server/watchdog.js +318 -0
- package/server/websocket/broadcast.js +359 -0
- package/server/websocket/connections.js +339 -0
- package/server/websocket/index.js +215 -0
- package/server/websocket/routing.js +277 -0
- package/server/websocket/sync.js +102 -0
- package/server.js +404 -0
- package/utils/detect-tool-usage.js +93 -0
- package/utils/errors.js +158 -0
- package/utils/html-escape.js +84 -0
- package/utils/id-sanitize.js +94 -0
- package/utils/response.js +130 -0
- 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
|
+
};
|
package/server/utils.js
ADDED
|
@@ -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
|
+
};
|