claw-dashboard 1.8.4 → 2.0.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.
- package/.c8rc.json +38 -0
- package/.dockerignore +68 -0
- package/.github/dependabot.yml +45 -0
- package/.github/pull_request_template.md +39 -0
- package/.github/workflows/ci.yml +147 -0
- package/.github/workflows/release.yml +40 -0
- package/.github/workflows/security.yml +84 -0
- package/.husky/pre-commit +1 -0
- package/.planning/codebase/ARCHITECTURE.md +206 -0
- package/.planning/codebase/INTEGRATIONS.md +150 -0
- package/.planning/codebase/STACK.md +122 -0
- package/.planning/codebase/STRUCTURE.md +201 -0
- package/CHANGELOG.md +293 -0
- package/CONTRIBUTING.md +378 -0
- package/Dockerfile +54 -0
- package/FEATURES.md +195 -21
- package/README.md +83 -6
- package/TODO.md +28 -0
- package/build-cjs.js +127 -0
- package/cjs-shim.js +13 -0
- package/dist/clawdash +1729 -0
- package/dist/clawdash.meta.json +2236 -0
- package/dist/widgets.cjs +6511 -0
- package/docker-compose.yml +67 -0
- package/docs/API.md +1050 -0
- package/docs/PLUGINS.md +1504 -0
- package/esbuild.config.js +158 -0
- package/eslint.config.js +56 -0
- package/examples/plugins/README.md +122 -0
- package/examples/plugins/api-status/index.js +294 -0
- package/examples/plugins/api-status/plugin.json +19 -0
- package/examples/plugins/hello-world/index.js +104 -0
- package/examples/plugins/hello-world/plugin.json +15 -0
- package/examples/plugins/system-metrics-chart/index.js +339 -0
- package/examples/plugins/system-metrics-chart/plugin.json +18 -0
- package/examples/plugins/weather-widget/index.js +136 -0
- package/examples/plugins/weather-widget/plugin.json +19 -0
- package/index.cjs +23005 -0
- package/index.js +5331 -512
- package/jest.config.js +11 -0
- package/man/clawdash.1 +276 -0
- package/package.json +54 -5
- package/schemas/plugin-manifest.json +128 -0
- package/scripts/release.js +595 -0
- package/src/alerts.js +693 -0
- package/src/auto-save.js +584 -0
- package/src/cache.js +390 -0
- package/src/checksum.js +146 -0
- package/src/cli/args.js +110 -0
- package/src/cli/export-schedule.js +423 -0
- package/src/cli/export-snapshot.js +138 -0
- package/src/cli/help.js +69 -0
- package/src/cli/import-snapshot.js +230 -0
- package/src/cli/index.js +14 -0
- package/src/cli/list-templates.js +69 -0
- package/src/cli/validate-config.js +76 -0
- package/src/cli/validate-plugin.js +187 -0
- package/src/cli/version.js +15 -0
- package/src/config-validator.js +586 -0
- package/src/config-watcher.js +401 -0
- package/src/config.js +684 -0
- package/src/container-detector.js +499 -0
- package/src/database.js +734 -0
- package/src/differential-render.js +327 -0
- package/src/errors.js +169 -0
- package/src/export-scheduler.js +581 -0
- package/src/gateway-manager.js +685 -0
- package/src/hints.js +371 -0
- package/src/loading-states.js +509 -0
- package/src/logger.js +185 -0
- package/src/memory-pressure.js +467 -0
- package/src/performance-monitor.js +374 -0
- package/src/plugin-errors.js +586 -0
- package/src/plugin-manifest-validator.js +219 -0
- package/src/plugin-reload.js +481 -0
- package/src/plugin-scaffold.js +1941 -0
- package/src/plugin-validator.js +284 -0
- package/src/retry.js +218 -0
- package/src/security.js +860 -0
- package/src/snapshot.js +372 -0
- package/src/splash.js +115 -0
- package/src/theme-selector.js +411 -0
- package/src/themes.js +874 -0
- package/src/transitions.js +534 -0
- package/src/types.d.ts +174 -0
- package/src/validation.js +926 -0
- package/src/web-server.js +885 -0
- package/src/widgets/builtin-widgets.js +1057 -0
- package/src/widgets/config-processor.js +401 -0
- package/src/widgets/dependency-resolver.js +596 -0
- package/src/widgets/index.js +79 -0
- package/src/widgets/plugin-api.js +845 -0
- package/src/widgets/widget-error-boundary.js +773 -0
- package/src/widgets/widget-error-isolation.js +551 -0
- package/src/widgets/widget-loader.js +1437 -0
- package/src/workers/system-worker.js +130 -0
- package/src/workers/worker-pool.js +633 -0
- package/tests/alerts.test.js +437 -0
- package/tests/auto-save.test.js +529 -0
- package/tests/cache.test.js +317 -0
- package/tests/cli.test.js +351 -0
- package/tests/command-palette.test.js +320 -0
- package/tests/config-processor.test.js +452 -0
- package/tests/config-validator.test.js +710 -0
- package/tests/config-watcher.test.js +594 -0
- package/tests/config.test.js +755 -0
- package/tests/database.test.js +438 -0
- package/tests/errors.test.js +189 -0
- package/tests/example-plugins.test.js +624 -0
- package/tests/integration.test.js +1321 -0
- package/tests/loading-states.test.js +300 -0
- package/tests/manifest-validation-on-load.test.js +671 -0
- package/tests/performance-monitor.test.js +190 -0
- package/tests/plugin-api-rate-limit.test.js +302 -0
- package/tests/plugin-errors.test.js +311 -0
- package/tests/plugin-lifecycle-e2e.test.js +1036 -0
- package/tests/plugin-reload.test.js +764 -0
- package/tests/rate-limiter.test.js +539 -0
- package/tests/retry.test.js +308 -0
- package/tests/security.test.js +411 -0
- package/tests/settings-modal.test.js +226 -0
- package/tests/theme-selector.test.js +57 -0
- package/tests/utils.js +242 -0
- package/tests/utils.test.js +317 -0
- package/tests/validate-plugin-cli.test.js +359 -0
- package/tests/validation.test.js +837 -0
- package/tests/web-server.test.js +646 -0
- package/tests/widget-arrange-mode.test.js +183 -0
- package/tests/widget-config-hot-reload.test.js +465 -0
- package/tests/widget-dependency.test.js +591 -0
- package/tests/widget-error-boundary.test.js +749 -0
- package/tests/widget-error-isolation.test.js +469 -0
- package/tests/widget-integration.test.js +1147 -0
- package/tests/widget-refresh-intervals.test.js +284 -0
- package/tests/worker-pool.test.js +522 -0
package/src/cache.js
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache module with TTL support for system metrics
|
|
3
|
+
* Provides caching for expensive systeminformation calls
|
|
4
|
+
* Supports worker threads for heavy operations
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import config from './config.js';
|
|
8
|
+
import logger from './logger.js';
|
|
9
|
+
|
|
10
|
+
// Worker pool for heavy operations (lazy-loaded)
|
|
11
|
+
let workerPool = null;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get the worker pool instance (lazy initialization)
|
|
15
|
+
* @returns {Object|null} Worker pool instance or null if not available
|
|
16
|
+
*/
|
|
17
|
+
async function getWorkerPool() {
|
|
18
|
+
if (!config.WORKERS?.ENABLED) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!workerPool) {
|
|
23
|
+
try {
|
|
24
|
+
const { default: pool } = await import('./workers/worker-pool.js');
|
|
25
|
+
workerPool = pool;
|
|
26
|
+
} catch (error) {
|
|
27
|
+
logger.debug('Worker pool not available:', error.message);
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return workerPool;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// In-memory cache store
|
|
36
|
+
const cache = new Map();
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Cache configuration for different data types
|
|
40
|
+
*/
|
|
41
|
+
// Use CACHE_CONFIG from config.js
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get a cached value if still valid
|
|
45
|
+
* @param {string} key - Cache key
|
|
46
|
+
* @returns {any|null} Cached value or null if expired/missing
|
|
47
|
+
*/
|
|
48
|
+
export function get(key) {
|
|
49
|
+
const entry = cache.get(key);
|
|
50
|
+
if (!entry) return null;
|
|
51
|
+
|
|
52
|
+
if (Date.now() > entry.expiresAt) {
|
|
53
|
+
cache.delete(key);
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return entry.value;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Set a cached value with TTL
|
|
62
|
+
* @param {string} key - Cache key
|
|
63
|
+
* @param {any} value - Value to cache
|
|
64
|
+
* @param {number} ttl - Time to live in milliseconds (optional, uses config default)
|
|
65
|
+
*/
|
|
66
|
+
export function set(key, value, ttl) {
|
|
67
|
+
const cacheTtlConfig = config.CACHE_CONFIG[key] || { ttl: config.CACHE_TTL.DEFAULT };
|
|
68
|
+
const actualTtl = ttl || cacheTtlConfig.ttl;
|
|
69
|
+
|
|
70
|
+
cache.set(key, {
|
|
71
|
+
value,
|
|
72
|
+
expiresAt: Date.now() + actualTtl,
|
|
73
|
+
createdAt: Date.now(),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get or fetch data with caching
|
|
79
|
+
* @param {string} key - Cache key
|
|
80
|
+
* @param {Function} fetcher - Async function to fetch data if cache miss
|
|
81
|
+
* @param {number} ttl - Optional TTL override
|
|
82
|
+
* @returns {Promise<any>} Cached or fresh data
|
|
83
|
+
*/
|
|
84
|
+
export async function getOrFetch(key, fetcher, ttl) {
|
|
85
|
+
const cached = get(key);
|
|
86
|
+
if (cached !== null) {
|
|
87
|
+
return cached;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const data = await fetcher();
|
|
91
|
+
set(key, data, ttl);
|
|
92
|
+
return data;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Execute a systeminformation command via worker thread or fallback
|
|
97
|
+
* @param {string} command - Worker command name
|
|
98
|
+
* @param {Function} fallbackFn - Fallback function for direct execution
|
|
99
|
+
* @returns {Promise<any>} Command result
|
|
100
|
+
*/
|
|
101
|
+
async function executeWithWorker(command, fallbackFn) {
|
|
102
|
+
const pool = await getWorkerPool();
|
|
103
|
+
if (pool) {
|
|
104
|
+
try {
|
|
105
|
+
return await pool.execute(command);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
logger.debug(`Worker execution failed for ${command}, using fallback: ${error.message}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return fallbackFn();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Get cached CPU data or fetch fresh
|
|
115
|
+
* Uses worker threads when available to avoid blocking the UI
|
|
116
|
+
* @returns {Promise<Object>} CPU load data
|
|
117
|
+
*/
|
|
118
|
+
export async function getCpuData() {
|
|
119
|
+
return getOrFetch('cpu', async () => {
|
|
120
|
+
try {
|
|
121
|
+
// Add 2 second timeout for CPU data
|
|
122
|
+
const timeoutMs = 2000;
|
|
123
|
+
const fetchPromise = executeWithWorker('currentLoad', async () => {
|
|
124
|
+
const si = await import('systeminformation');
|
|
125
|
+
return await si.currentLoad();
|
|
126
|
+
});
|
|
127
|
+
const timeoutPromise = new Promise((_, reject) =>
|
|
128
|
+
setTimeout(() => reject(new Error('CPU data timeout')), timeoutMs)
|
|
129
|
+
);
|
|
130
|
+
return await Promise.race([fetchPromise, timeoutPromise]);
|
|
131
|
+
} catch (e) {
|
|
132
|
+
logger.warn(`systeminformation.currentLoad() failed: ${e.message}`);
|
|
133
|
+
throw e;
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Get cached memory data or fetch fresh
|
|
140
|
+
* Uses worker threads when available to avoid blocking the UI
|
|
141
|
+
* @returns {Promise<Object>} Memory data
|
|
142
|
+
*/
|
|
143
|
+
export async function getMemoryData() {
|
|
144
|
+
return getOrFetch('memory', async () => {
|
|
145
|
+
try {
|
|
146
|
+
// Add 2 second timeout for memory data
|
|
147
|
+
const timeoutMs = 2000;
|
|
148
|
+
const fetchPromise = executeWithWorker('mem', async () => {
|
|
149
|
+
const si = await import('systeminformation');
|
|
150
|
+
return await si.mem();
|
|
151
|
+
});
|
|
152
|
+
const timeoutPromise = new Promise((_, reject) =>
|
|
153
|
+
setTimeout(() => reject(new Error('Memory data timeout')), timeoutMs)
|
|
154
|
+
);
|
|
155
|
+
return await Promise.race([fetchPromise, timeoutPromise]);
|
|
156
|
+
} catch (e) {
|
|
157
|
+
logger.warn(`systeminformation.mem() failed: ${e.message}`);
|
|
158
|
+
throw e;
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Get cached GPU data or fetch fresh
|
|
165
|
+
* Uses worker threads when available to avoid blocking the UI
|
|
166
|
+
* @returns {Promise<Object>} GPU/graphics data
|
|
167
|
+
*/
|
|
168
|
+
export async function getGpuData() {
|
|
169
|
+
return getOrFetch('gpu', async () => {
|
|
170
|
+
try {
|
|
171
|
+
return await executeWithWorker('graphics', async () => {
|
|
172
|
+
const si = await import('systeminformation');
|
|
173
|
+
return await si.graphics();
|
|
174
|
+
});
|
|
175
|
+
} catch (e) {
|
|
176
|
+
logger.warn(`systeminformation.graphics() failed: ${e.message}`);
|
|
177
|
+
throw e;
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Get cached network data or fetch fresh
|
|
184
|
+
* Uses worker threads when available to avoid blocking the UI
|
|
185
|
+
* Falls back to empty array on timeout (non-critical data)
|
|
186
|
+
* @returns {Promise<Object>} Network stats data
|
|
187
|
+
*/
|
|
188
|
+
export async function getNetworkData() {
|
|
189
|
+
return getOrFetch('network', async () => {
|
|
190
|
+
try {
|
|
191
|
+
// Add timeout wrapper for network stats
|
|
192
|
+
const timeoutMs = 2000; // 2 second timeout
|
|
193
|
+
const fetchPromise = executeWithWorker('networkStats', async () => {
|
|
194
|
+
const si = await import('systeminformation');
|
|
195
|
+
return await si.networkStats();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const timeoutPromise = new Promise((_, reject) =>
|
|
199
|
+
setTimeout(() => reject(new Error('Network stats timeout')), timeoutMs)
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
return await Promise.race([fetchPromise, timeoutPromise]);
|
|
203
|
+
} catch (e) {
|
|
204
|
+
logger.warn(`Network data fetch failed or timed out: ${e.message}`);
|
|
205
|
+
// Return empty array - network stats are nice-to-have, not critical
|
|
206
|
+
return [];
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Get cached disk data or fetch fresh
|
|
213
|
+
* Uses worker threads when available to avoid blocking the UI
|
|
214
|
+
* @returns {Promise<Object>} Disk size data
|
|
215
|
+
*/
|
|
216
|
+
export async function getDiskData() {
|
|
217
|
+
return getOrFetch('disk', async () => {
|
|
218
|
+
try {
|
|
219
|
+
return await executeWithWorker('fsSize', async () => {
|
|
220
|
+
const si = await import('systeminformation');
|
|
221
|
+
return await si.fsSize();
|
|
222
|
+
});
|
|
223
|
+
} catch (e) {
|
|
224
|
+
logger.warn(`systeminformation.fsSize() failed: ${e.message}`);
|
|
225
|
+
throw e;
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Get cached system info data or fetch fresh
|
|
232
|
+
* Uses native os module and short-circuit for speed
|
|
233
|
+
* System info rarely changes, so we cache aggressively
|
|
234
|
+
* @returns {Promise<Object>} System info (osInfo, versions, time)
|
|
235
|
+
*/
|
|
236
|
+
export async function getSystemData() {
|
|
237
|
+
return getOrFetch('system', async () => {
|
|
238
|
+
try {
|
|
239
|
+
// Use native os module for faster results
|
|
240
|
+
const os = await import('os');
|
|
241
|
+
|
|
242
|
+
// Build system data from native os module (much faster than systeminformation)
|
|
243
|
+
const systemData = {
|
|
244
|
+
os: {
|
|
245
|
+
platform: os.platform(),
|
|
246
|
+
distro: os.type(),
|
|
247
|
+
release: os.release(),
|
|
248
|
+
arch: os.arch(),
|
|
249
|
+
},
|
|
250
|
+
ver: {
|
|
251
|
+
node: process.version,
|
|
252
|
+
},
|
|
253
|
+
time: {
|
|
254
|
+
uptime: os.uptime(),
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
// Try to get more detailed info via worker (non-blocking)
|
|
259
|
+
// This runs in background and updates cache if successful
|
|
260
|
+
executeWithWorker('systemData', async () => {
|
|
261
|
+
const si = await import('systeminformation');
|
|
262
|
+
const [osInfo, versions, time] = await Promise.all([
|
|
263
|
+
si.osInfo(),
|
|
264
|
+
si.versions(),
|
|
265
|
+
si.time(),
|
|
266
|
+
]);
|
|
267
|
+
return { os: osInfo, ver: versions, time };
|
|
268
|
+
}).then(detailedData => {
|
|
269
|
+
// Update cache with detailed data in background
|
|
270
|
+
set('system', detailedData);
|
|
271
|
+
}).catch(() => {
|
|
272
|
+
// Ignore errors - we already have basic data
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
return systemData;
|
|
276
|
+
} catch (e) {
|
|
277
|
+
logger.warn(`System data fetch failed: ${e.message}`);
|
|
278
|
+
throw e;
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Force refresh a specific cache entry
|
|
285
|
+
* @param {string} key - Cache key to refresh
|
|
286
|
+
*/
|
|
287
|
+
export function invalidate(key) {
|
|
288
|
+
cache.delete(key);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Clear all cache entries
|
|
293
|
+
*/
|
|
294
|
+
export function clear() {
|
|
295
|
+
cache.clear();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Get cache status for debugging
|
|
300
|
+
* @returns {Object} Cache status info
|
|
301
|
+
*/
|
|
302
|
+
export function getStatus() {
|
|
303
|
+
const now = Date.now();
|
|
304
|
+
const status = {};
|
|
305
|
+
|
|
306
|
+
for (const [key, entry] of cache) {
|
|
307
|
+
const remaining = Math.max(0, entry.expiresAt - now);
|
|
308
|
+
status[key] = {
|
|
309
|
+
cached: true,
|
|
310
|
+
age: now - entry.createdAt,
|
|
311
|
+
ttlRemaining: remaining,
|
|
312
|
+
configTtl: config.CACHE_CONFIG[key]?.ttl || config.CACHE_TTL.DEFAULT,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return status;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Debounce utility for rapid key presses
|
|
321
|
+
* @param {Function} fn - Function to debounce
|
|
322
|
+
* @param {number} delay - Delay in milliseconds
|
|
323
|
+
* @returns {Function} Debounced function
|
|
324
|
+
*/
|
|
325
|
+
export function debounce(fn, delay) {
|
|
326
|
+
let timeoutId = null;
|
|
327
|
+
let lastArgs = null;
|
|
328
|
+
|
|
329
|
+
return function(...args) {
|
|
330
|
+
lastArgs = args;
|
|
331
|
+
|
|
332
|
+
if (timeoutId) {
|
|
333
|
+
clearTimeout(timeoutId);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
timeoutId = setTimeout(() => {
|
|
337
|
+
fn.apply(this, lastArgs);
|
|
338
|
+
timeoutId = null;
|
|
339
|
+
}, delay);
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Throttle utility for rate limiting
|
|
345
|
+
* @param {Function} fn - Function to throttle
|
|
346
|
+
* @param {number} limit - Minimum interval between calls in ms
|
|
347
|
+
* @returns {Function} Throttled function
|
|
348
|
+
*/
|
|
349
|
+
export function throttle(fn, limit) {
|
|
350
|
+
let lastCall = 0;
|
|
351
|
+
let timeoutId = null;
|
|
352
|
+
|
|
353
|
+
return function(...args) {
|
|
354
|
+
const now = Date.now();
|
|
355
|
+
const remaining = limit - (now - lastCall);
|
|
356
|
+
|
|
357
|
+
if (remaining <= 0) {
|
|
358
|
+
if (timeoutId) {
|
|
359
|
+
clearTimeout(timeoutId);
|
|
360
|
+
timeoutId = null;
|
|
361
|
+
}
|
|
362
|
+
lastCall = now;
|
|
363
|
+
fn.apply(this, args);
|
|
364
|
+
} else if (!timeoutId) {
|
|
365
|
+
timeoutId = setTimeout(() => {
|
|
366
|
+
lastCall = Date.now();
|
|
367
|
+
timeoutId = null;
|
|
368
|
+
fn.apply(this, args);
|
|
369
|
+
}, remaining);
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export default {
|
|
375
|
+
get,
|
|
376
|
+
set,
|
|
377
|
+
getOrFetch,
|
|
378
|
+
getCpuData,
|
|
379
|
+
getMemoryData,
|
|
380
|
+
getGpuData,
|
|
381
|
+
getNetworkData,
|
|
382
|
+
getDiskData,
|
|
383
|
+
getSystemData,
|
|
384
|
+
invalidate,
|
|
385
|
+
clear,
|
|
386
|
+
getStatus,
|
|
387
|
+
debounce,
|
|
388
|
+
throttle,
|
|
389
|
+
CACHE_CONFIG: config.CACHE_CONFIG,
|
|
390
|
+
};
|
package/src/checksum.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checksum verification utilities for OpenClaw gateway responses
|
|
3
|
+
* Provides integrity verification for data received from gateway endpoints
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import crypto from 'crypto';
|
|
7
|
+
import config from './config.js';
|
|
8
|
+
import logger from './logger.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Supported hash algorithms
|
|
12
|
+
* @typedef {'sha256' | 'sha512' | 'md5'} HashAlgorithm
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const SUPPORTED_ALGORITHMS = ['sha256', 'sha512', 'md5'];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Compute checksum of data using specified algorithm
|
|
19
|
+
* @param {string|Buffer} data - Data to compute checksum for
|
|
20
|
+
* @param {HashAlgorithm} [algorithm] - Hash algorithm to use (defaults to config)
|
|
21
|
+
* @returns {string} - Computed checksum in hex format
|
|
22
|
+
*/
|
|
23
|
+
export function computeChecksum(data, algorithm = null) {
|
|
24
|
+
const algo = algorithm || config.CHECKSUM.ALGORITHM;
|
|
25
|
+
|
|
26
|
+
if (!SUPPORTED_ALGORITHMS.includes(algo)) {
|
|
27
|
+
throw new Error(`Unsupported hash algorithm: ${algo}. Supported: ${SUPPORTED_ALGORITHMS.join(', ')}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const hash = crypto.createHash(algo);
|
|
31
|
+
hash.update(data);
|
|
32
|
+
return hash.digest('hex');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Verify response checksum against computed checksum
|
|
37
|
+
* @param {string|Buffer} data - Response body data
|
|
38
|
+
* @param {string} expectedChecksum - Expected checksum from header
|
|
39
|
+
* @param {HashAlgorithm} [algorithm] - Hash algorithm to use
|
|
40
|
+
* @returns {boolean} - True if checksums match
|
|
41
|
+
*/
|
|
42
|
+
export function verifyChecksum(data, expectedChecksum, algorithm = null) {
|
|
43
|
+
if (!expectedChecksum) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const computed = computeChecksum(data, algorithm);
|
|
49
|
+
// Use timing-safe comparison to prevent timing attacks
|
|
50
|
+
return crypto.timingSafeEqual(
|
|
51
|
+
Buffer.from(computed, 'hex'),
|
|
52
|
+
Buffer.from(expectedChecksum, 'hex')
|
|
53
|
+
);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
logger.debug(`Checksum verification failed: ${err.message}`);
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Verify HTTP response checksum from header
|
|
62
|
+
* @param {http.IncomingMessage} response - HTTP response object
|
|
63
|
+
* @param {string} responseBody - Response body as string
|
|
64
|
+
* @returns {{verified: boolean, checksum: string|null, error: string|null}} - Verification result
|
|
65
|
+
*/
|
|
66
|
+
export function verifyResponseChecksum(response, responseBody) {
|
|
67
|
+
if (!config.CHECKSUM.ENABLED) {
|
|
68
|
+
return { verified: true, checksum: null, error: null };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const headerName = config.CHECKSUM.HEADER_NAME.toLowerCase();
|
|
72
|
+
const expectedChecksum = response.headers[headerName];
|
|
73
|
+
|
|
74
|
+
// Check if response has checksum header
|
|
75
|
+
if (!expectedChecksum) {
|
|
76
|
+
if (config.CHECKSUM.STRICT_MODE) {
|
|
77
|
+
return {
|
|
78
|
+
verified: false,
|
|
79
|
+
checksum: null,
|
|
80
|
+
error: `Missing ${config.CHECKSUM.HEADER_NAME} header (strict mode enabled)`
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
// Non-strict mode: accept responses without checksum
|
|
84
|
+
return { verified: true, checksum: null, error: null };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Validate checksum format (hex string)
|
|
88
|
+
const checksumStr = Array.isArray(expectedChecksum) ? expectedChecksum[0] : expectedChecksum;
|
|
89
|
+
if (!/^[a-f0-9]+$/i.test(checksumStr)) {
|
|
90
|
+
return {
|
|
91
|
+
verified: false,
|
|
92
|
+
checksum: checksumStr,
|
|
93
|
+
error: 'Invalid checksum format: expected hex string'
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Verify checksum
|
|
98
|
+
const isValid = verifyChecksum(responseBody, checksumStr);
|
|
99
|
+
|
|
100
|
+
if (!isValid) {
|
|
101
|
+
return {
|
|
102
|
+
verified: false,
|
|
103
|
+
checksum: checksumStr,
|
|
104
|
+
error: 'Checksum mismatch: computed checksum does not match header'
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { verified: true, checksum: checksumStr, error: null };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Get checksum metadata for logging/debugging
|
|
113
|
+
* @param {http.IncomingMessage} response - HTTP response object
|
|
114
|
+
* @returns {Object} - Checksum metadata
|
|
115
|
+
*/
|
|
116
|
+
export function getChecksumMetadata(response) {
|
|
117
|
+
const headerName = config.CHECKSUM.HEADER_NAME.toLowerCase();
|
|
118
|
+
const checksum = response.headers[headerName];
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
algorithm: config.CHECKSUM.ALGORITHM,
|
|
122
|
+
headerName: config.CHECKSUM.HEADER_NAME,
|
|
123
|
+
headerPresent: !!checksum,
|
|
124
|
+
checksum: checksum || null,
|
|
125
|
+
strictMode: config.CHECKSUM.STRICT_MODE,
|
|
126
|
+
enabled: config.CHECKSUM.ENABLED,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Check if an error is a checksum verification failure
|
|
132
|
+
* @param {Error} error - Error to check
|
|
133
|
+
* @returns {boolean} - True if checksum verification error
|
|
134
|
+
*/
|
|
135
|
+
export function isChecksumError(error) {
|
|
136
|
+
return error && error.code === 'CHECKSUM_ERROR';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export default {
|
|
140
|
+
computeChecksum,
|
|
141
|
+
verifyChecksum,
|
|
142
|
+
verifyResponseChecksum,
|
|
143
|
+
getChecksumMetadata,
|
|
144
|
+
isChecksumError,
|
|
145
|
+
SUPPORTED_ALGORITHMS,
|
|
146
|
+
};
|
package/src/cli/args.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Argument Parsing Module
|
|
3
|
+
* Handles parsing of command-line arguments for Claw Dashboard
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import config from '../config.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Parse command-line arguments
|
|
10
|
+
* @returns {Object} Parsed CLI options
|
|
11
|
+
*/
|
|
12
|
+
export function parseCliArgs() {
|
|
13
|
+
const args = process.argv.slice(2);
|
|
14
|
+
const options = {
|
|
15
|
+
help: false,
|
|
16
|
+
version: false,
|
|
17
|
+
debug: false,
|
|
18
|
+
web: false,
|
|
19
|
+
webPort: config.WEB.DEFAULT_PORT,
|
|
20
|
+
webHost: config.WEB.HOST,
|
|
21
|
+
watch: false,
|
|
22
|
+
watchPlugins: false,
|
|
23
|
+
command: null,
|
|
24
|
+
commandArgs: [],
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Check for commands first
|
|
28
|
+
if (args.length > 0 && !args[0].startsWith('-')) {
|
|
29
|
+
const firstArg = args[0];
|
|
30
|
+
if (firstArg === 'create-plugin') {
|
|
31
|
+
options.command = 'create-plugin';
|
|
32
|
+
options.commandArgs = args.slice(1);
|
|
33
|
+
return options;
|
|
34
|
+
}
|
|
35
|
+
if (firstArg === 'validate-plugin') {
|
|
36
|
+
options.command = 'validate-plugin';
|
|
37
|
+
options.commandArgs = args.slice(1);
|
|
38
|
+
return options;
|
|
39
|
+
}
|
|
40
|
+
if (firstArg === 'validate-config') {
|
|
41
|
+
options.command = 'validate-config';
|
|
42
|
+
options.commandArgs = args.slice(1);
|
|
43
|
+
return options;
|
|
44
|
+
}
|
|
45
|
+
if (firstArg === 'export-snapshot') {
|
|
46
|
+
options.command = 'export-snapshot';
|
|
47
|
+
options.commandArgs = args.slice(1);
|
|
48
|
+
return options;
|
|
49
|
+
}
|
|
50
|
+
if (firstArg === 'import-snapshot') {
|
|
51
|
+
options.command = 'import-snapshot';
|
|
52
|
+
options.commandArgs = args.slice(1);
|
|
53
|
+
return options;
|
|
54
|
+
}
|
|
55
|
+
if (firstArg === 'list-templates') {
|
|
56
|
+
options.command = 'list-templates';
|
|
57
|
+
options.commandArgs = args.slice(1);
|
|
58
|
+
return options;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (let i = 0; i < args.length; i++) {
|
|
63
|
+
const arg = args[i];
|
|
64
|
+
switch (arg) {
|
|
65
|
+
case '-h':
|
|
66
|
+
case '--help':
|
|
67
|
+
options.help = true;
|
|
68
|
+
break;
|
|
69
|
+
case '-v':
|
|
70
|
+
case '--version':
|
|
71
|
+
options.version = true;
|
|
72
|
+
break;
|
|
73
|
+
case '-d':
|
|
74
|
+
case '--debug':
|
|
75
|
+
options.debug = true;
|
|
76
|
+
break;
|
|
77
|
+
case '-w':
|
|
78
|
+
case '--web':
|
|
79
|
+
options.web = true;
|
|
80
|
+
break;
|
|
81
|
+
case '-p':
|
|
82
|
+
case '--web-port':
|
|
83
|
+
options.web = true;
|
|
84
|
+
if (i + 1 < args.length) {
|
|
85
|
+
const port = parseInt(args[++i], 10);
|
|
86
|
+
if (!isNaN(port) && port > 0 && port < 65536) {
|
|
87
|
+
options.webPort = port;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
break;
|
|
91
|
+
case '--web-host':
|
|
92
|
+
options.web = true;
|
|
93
|
+
if (i + 1 < args.length) {
|
|
94
|
+
options.webHost = args[++i];
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
97
|
+
case '-W':
|
|
98
|
+
case '--watch':
|
|
99
|
+
options.watch = true;
|
|
100
|
+
break;
|
|
101
|
+
case '--watch-plugins':
|
|
102
|
+
options.watchPlugins = true;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return options;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export default { parseCliArgs };
|