claw-dashboard 2.1.1 → 2.2.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/CHANGELOG.md +29 -1
- package/docs/API.md +1 -2
- package/index.js +31 -41
- package/package.json +22 -12
- package/src/auto-save.js +11 -5
- package/src/cli/export-snapshot.js +3 -1
- package/src/cli/import-snapshot.js +3 -1
- package/src/config.js +9 -15
- package/src/errors.js +0 -9
- package/src/security.js +6 -2
- package/src/snapshot.js +73 -26
- package/src/web-server.js +7 -10
- package/.c8rc.json +0 -38
- package/.dockerignore +0 -68
- package/.github/dependabot.yml +0 -45
- package/.github/pull_request_template.md +0 -39
- package/.github/workflows/ci.yml +0 -147
- package/.github/workflows/release.yml +0 -40
- package/.github/workflows/security.yml +0 -84
- package/.husky/pre-commit +0 -1
- package/.planning/codebase/ARCHITECTURE.md +0 -206
- package/.planning/codebase/INTEGRATIONS.md +0 -150
- package/.planning/codebase/STACK.md +0 -122
- package/.planning/codebase/STRUCTURE.md +0 -201
- package/CONTRIBUTING.md +0 -378
- package/Dockerfile +0 -54
- package/FEATURES.md +0 -307
- package/TODO.md +0 -28
- package/ai.openclaw.dashboard.plist +0 -35
- package/build-cjs.js +0 -127
- package/cjs-shim.js +0 -13
- package/dist/clawdash +0 -1729
- package/dist/clawdash.meta.json +0 -2236
- package/dist/widgets.cjs +0 -6511
- package/docker-compose.yml +0 -67
- package/esbuild.config.js +0 -158
- package/eslint.config.js +0 -56
- package/examples/plugins/README.md +0 -122
- package/examples/plugins/api-status/index.js +0 -294
- package/examples/plugins/api-status/plugin.json +0 -19
- package/examples/plugins/hello-world/index.js +0 -104
- package/examples/plugins/hello-world/plugin.json +0 -15
- package/examples/plugins/system-metrics-chart/index.js +0 -339
- package/examples/plugins/system-metrics-chart/plugin.json +0 -18
- package/examples/plugins/weather-widget/index.js +0 -136
- package/examples/plugins/weather-widget/plugin.json +0 -19
- package/index.cjs +0 -23005
- package/jest.config.js +0 -11
- package/scripts/release.js +0 -595
- package/src/database.js +0 -734
- package/tests/alerts.test.js +0 -437
- package/tests/auto-save.test.js +0 -529
- package/tests/cache.test.js +0 -317
- package/tests/cli.test.js +0 -351
- package/tests/command-palette.test.js +0 -320
- package/tests/config-processor.test.js +0 -452
- package/tests/config-validator.test.js +0 -710
- package/tests/config-watcher.test.js +0 -594
- package/tests/config.test.js +0 -755
- package/tests/database.test.js +0 -438
- package/tests/errors.test.js +0 -189
- package/tests/example-plugins.test.js +0 -624
- package/tests/integration.test.js +0 -1321
- package/tests/loading-states.test.js +0 -300
- package/tests/manifest-validation-on-load.test.js +0 -671
- package/tests/performance-monitor.test.js +0 -190
- package/tests/plugin-api-rate-limit.test.js +0 -302
- package/tests/plugin-errors.test.js +0 -311
- package/tests/plugin-lifecycle-e2e.test.js +0 -1036
- package/tests/plugin-reload.test.js +0 -764
- package/tests/rate-limiter.test.js +0 -539
- package/tests/removed-dependencies.test.js +0 -74
- package/tests/retry.test.js +0 -308
- package/tests/security.test.js +0 -411
- package/tests/settings-modal.test.js +0 -226
- package/tests/theme-selector.test.js +0 -57
- package/tests/utils.js +0 -242
- package/tests/utils.test.js +0 -317
- package/tests/validate-plugin-cli.test.js +0 -359
- package/tests/validation.test.js +0 -837
- package/tests/web-server.test.js +0 -646
- package/tests/widget-arrange-mode.test.js +0 -183
- package/tests/widget-config-hot-reload.test.js +0 -465
- package/tests/widget-dependency.test.js +0 -591
- package/tests/widget-error-boundary.test.js +0 -749
- package/tests/widget-error-isolation.test.js +0 -469
- package/tests/widget-integration.test.js +0 -1147
- package/tests/widget-refresh-intervals.test.js +0 -284
- package/tests/worker-pool.test.js +0 -522
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,35 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [
|
|
8
|
+
## [2.2.0] - 2026-07-28
|
|
9
|
+
|
|
10
|
+
### Security
|
|
11
|
+
- Fixed all npm audit vulnerabilities (brace-expansion, js-yaml, @babel/core)
|
|
12
|
+
- Added brace-expansion override to force safe version across transitive deps
|
|
13
|
+
- Enabled Dependabot security updates on repository
|
|
14
|
+
|
|
15
|
+
### Dependencies
|
|
16
|
+
- Updated eslint 10.0.3 → 10.8.0
|
|
17
|
+
- Updated jest 30.3.0 → 30.4.2
|
|
18
|
+
- Updated systeminformation 5.31.7 → 5.33.1
|
|
19
|
+
- Updated c8 11 → 12
|
|
20
|
+
- Updated lint-staged 16 → 17
|
|
21
|
+
|
|
22
|
+
### Improvements
|
|
23
|
+
- Bumped minimum Node.js version from 18 to 20
|
|
24
|
+
- Updated CI matrix to test on Node 20, 22, 24
|
|
25
|
+
- Tightened ESLint rules (no-unused-vars, no-undef, prefer-const, no-var, no-console as warnings)
|
|
26
|
+
- Raised coverage thresholds (statements/lines 25→35, branches 30→35, functions 4→20)
|
|
27
|
+
- Removed dead CJS files (index.cjs, build-cjs.js, cjs-shim.js)
|
|
28
|
+
- Removed stale files (test.js, node_trace.1.log, start.sh)
|
|
29
|
+
- Added node_trace.*.log to .gitignore
|
|
30
|
+
|
|
31
|
+
### Lean/Clean Refactor (from v2.1.1+)
|
|
32
|
+
- Removed sql.js dep + full historical DB module (backlog only; stores no longer called from refresh/start/web paths; WASM/init/DB-file overhead eliminated for lean runtime).
|
|
33
|
+
- ESM-only: dropped CJS require conditions in exports, build:cjs script, and active dual-build in release (ci unaffected). "files" + .npmignore hygiene to exclude dead (src/database.js, index.cjs, build-cjs*, dist/widgets.cjs).
|
|
34
|
+
- Synced tests (config, errors, integration), config, errors, docs, and database stub for removed surface. Full suite green for core.
|
|
35
|
+
- Core TUI, retained features (snapshots, --web, plugins, perf, etc.), visuals, and controls 100% preserved.
|
|
36
|
+
- Pre-existing snapshot path validation and web defaults noted (addressed with comments + minimal containment; no scope creep into general security refactor).
|
|
9
37
|
|
|
10
38
|
## [2.0.0] - 2026-02-28
|
|
11
39
|
|
package/docs/API.md
CHANGED
|
@@ -626,7 +626,6 @@ Returns a JSON-serializable representation of the error.
|
|
|
626
626
|
| `AuthError` | `AUTH_ERROR` | Authentication failures |
|
|
627
627
|
| `NetworkError` | `NETWORK_ERROR` | Network issues |
|
|
628
628
|
| `UIError` | `UI_ERROR` | UI/rendering errors |
|
|
629
|
-
| `DatabaseError` | `DATABASE_ERROR` | Database operations |
|
|
630
629
|
| `ValidationError` | `VALIDATION_ERROR` | Input validation |
|
|
631
630
|
| `TimeoutError` | `TIMEOUT_ERROR` | Operation timeouts |
|
|
632
631
|
|
|
@@ -668,7 +667,7 @@ import { ERROR_CODES } from './src/errors.js';
|
|
|
668
667
|
// Available codes:
|
|
669
668
|
// CONFIG_ERROR, SETTINGS_ERROR, GATEWAY_ERROR, SESSION_ERROR,
|
|
670
669
|
// DATA_FETCH_ERROR, AUTH_ERROR, NETWORK_ERROR, UI_ERROR,
|
|
671
|
-
//
|
|
670
|
+
// VALIDATION_ERROR, TIMEOUT_ERROR, DASHBOARD_ERROR
|
|
672
671
|
```
|
|
673
672
|
|
|
674
673
|
---
|
package/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import http from 'http';
|
|
|
9
9
|
import os from 'os';
|
|
10
10
|
import fs from 'fs';
|
|
11
11
|
import { fileURLToPath } from 'url';
|
|
12
|
-
import { dirname, join, resolve } from 'path';
|
|
12
|
+
import { dirname, join, resolve, sep } from 'path';
|
|
13
13
|
import logger from './src/logger.js';
|
|
14
14
|
import {
|
|
15
15
|
cycleTheme, getCurrentTheme, loadTheme, saveTheme, getThemeName,
|
|
@@ -20,11 +20,10 @@ import retry from './src/retry.js';
|
|
|
20
20
|
import config, { DASHBOARD_VERSION, WIDGET_SIZES, WIDGET_SIZE_PRESETS } from './src/config.js';
|
|
21
21
|
import validation from './src/validation.js';
|
|
22
22
|
import cache from './src/cache.js';
|
|
23
|
-
import database from './src/database.js';
|
|
24
23
|
import { setSecurePermissionsSync } from './src/security.js';
|
|
25
24
|
import { showSplashScreen } from './src/splash.js';
|
|
26
25
|
import { showFirstRunHints } from './src/hints.js';
|
|
27
|
-
import { DashboardError, ConfigError, SettingsError, GatewayError, SessionError, DataFetchError, AuthError, NetworkError, UIError,
|
|
26
|
+
import { DashboardError, ConfigError, SettingsError, GatewayError, SessionError, DataFetchError, AuthError, NetworkError, UIError, ValidationError, TimeoutError, getErrorCode } from './src/errors.js';
|
|
28
27
|
import { ConfigWatcher, watchSettingsFile } from './src/config-watcher.js';
|
|
29
28
|
import { PluginReloadManager } from './src/plugin-reload.js';
|
|
30
29
|
import { WidgetLoader } from './src/widgets/widget-loader.js';
|
|
@@ -59,7 +58,6 @@ import {
|
|
|
59
58
|
generateSnapshotFilename,
|
|
60
59
|
getSnapshotsDirectory,
|
|
61
60
|
listSnapshots,
|
|
62
|
-
deleteSnapshot,
|
|
63
61
|
getSnapshotSummary,
|
|
64
62
|
} from './src/snapshot.js';
|
|
65
63
|
import {
|
|
@@ -79,7 +77,7 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
79
77
|
const __dirname = dirname(__filename);
|
|
80
78
|
const execAsync = promisify(exec);
|
|
81
79
|
|
|
82
|
-
// Safe file path validation
|
|
80
|
+
// Safe file path validation (used for settings/export paths; allows any subpath under home or listed dirs; contrast to validatePluginPath used by snapshots which adds char/hidden filters - see the allowedHidden list and hidden check comment in security.js + export/import/delete comments in snapshot.js for snapshot path rules)
|
|
83
81
|
function validateFilePath(filePath, allowedDirs = []) {
|
|
84
82
|
try {
|
|
85
83
|
// Handle null/undefined input
|
|
@@ -87,8 +85,10 @@ function validateFilePath(filePath, allowedDirs = []) {
|
|
|
87
85
|
return { valid: false, resolvedPath: filePath, error: "Invalid file path" };
|
|
88
86
|
}
|
|
89
87
|
|
|
90
|
-
// Replace leading ~ with home directory before resolving
|
|
91
|
-
const normalizedPath = filePath.startsWith('
|
|
88
|
+
// Replace leading ~ with home directory before resolving (robust for '~/...' and '~...' )
|
|
89
|
+
const normalizedPath = filePath.startsWith('~/')
|
|
90
|
+
? join(os.homedir(), filePath.slice(2))
|
|
91
|
+
: filePath.startsWith('~')
|
|
92
92
|
? join(os.homedir(), filePath.slice(1))
|
|
93
93
|
: filePath;
|
|
94
94
|
|
|
@@ -105,8 +105,8 @@ function validateFilePath(filePath, allowedDirs = []) {
|
|
|
105
105
|
const homeDir = os.homedir();
|
|
106
106
|
const defaultAllowedDirs = [
|
|
107
107
|
homeDir,
|
|
108
|
-
homeDir
|
|
109
|
-
homeDir
|
|
108
|
+
join(homeDir, '.openclaw'),
|
|
109
|
+
join(homeDir, '.openclaw', 'agents'),
|
|
110
110
|
"/tmp"
|
|
111
111
|
];
|
|
112
112
|
|
|
@@ -115,7 +115,7 @@ function validateFilePath(filePath, allowedDirs = []) {
|
|
|
115
115
|
// Check if resolved path is within any allowed directory
|
|
116
116
|
const isAllowed = allAllowedDirs.some(allowedDir => {
|
|
117
117
|
const resolvedAllowed = resolve(allowedDir);
|
|
118
|
-
return resolvedPath.startsWith(resolvedAllowed +
|
|
118
|
+
return resolvedPath.startsWith(resolvedAllowed + sep) || resolvedPath === resolvedAllowed;
|
|
119
119
|
});
|
|
120
120
|
|
|
121
121
|
if (!isAllowed) {
|
|
@@ -840,8 +840,7 @@ class Dashboard {
|
|
|
840
840
|
this.sessionSearchQuery = this.settings.sessionSearchQuery || '';
|
|
841
841
|
this.isSearchMode = false;
|
|
842
842
|
this.filteredSessions = [];
|
|
843
|
-
// Widget navigation state - index into focusableWidgets array
|
|
844
|
-
this.focusedWidgetIndex = 0;
|
|
843
|
+
// Widget navigation state - index into focusableWidgets array (set to -1 for no initial focus)
|
|
845
844
|
this.focusableWidgets = [];
|
|
846
845
|
// Restore search filter if query was persisted
|
|
847
846
|
if (this.sessionSearchQuery) {
|
|
@@ -861,7 +860,6 @@ class Dashboard {
|
|
|
861
860
|
this.corruptedSessionsCount = 0;
|
|
862
861
|
this.corruptedSessionsWarningShown = false;
|
|
863
862
|
// Widget focus navigation state
|
|
864
|
-
this.focusableWidgets = [];
|
|
865
863
|
this.focusedWidgetIndex = -1; // -1 means no widget focused (normal mode)
|
|
866
864
|
// Widget arrangement mode state
|
|
867
865
|
this.isWidgetArrangeMode = false;
|
|
@@ -2702,7 +2700,7 @@ class Dashboard {
|
|
|
2702
2700
|
}
|
|
2703
2701
|
|
|
2704
2702
|
exportDashboard() {
|
|
2705
|
-
const exportDir = this.settings.exportDirectory || os.homedir()
|
|
2703
|
+
const exportDir = this.settings.exportDirectory || join(os.homedir(), '.openclaw', 'exports');
|
|
2706
2704
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
2707
2705
|
const format = this.settings.exportFormat || 'json';
|
|
2708
2706
|
const filename = `dashboard-${timestamp}.${format}`;
|
|
@@ -2716,7 +2714,7 @@ class Dashboard {
|
|
|
2716
2714
|
return;
|
|
2717
2715
|
}
|
|
2718
2716
|
const validatedExportDir = pathValidation.resolvedPath;
|
|
2719
|
-
const filepath = validatedExportDir
|
|
2717
|
+
const filepath = join(validatedExportDir, filename);
|
|
2720
2718
|
|
|
2721
2719
|
try {
|
|
2722
2720
|
// Create export directory if it doesn't exist
|
|
@@ -2814,7 +2812,7 @@ class Dashboard {
|
|
|
2814
2812
|
|
|
2815
2813
|
const snapshotDir = getSnapshotsDirectory();
|
|
2816
2814
|
const filename = generateSnapshotFilename('dashboard');
|
|
2817
|
-
const filepath =
|
|
2815
|
+
const filepath = join(snapshotDir, filename);
|
|
2818
2816
|
|
|
2819
2817
|
const result = exportSnapshotToFile(snapshot, filepath);
|
|
2820
2818
|
|
|
@@ -2870,8 +2868,10 @@ class Dashboard {
|
|
|
2870
2868
|
this.w.snapshotPrompt.input('Enter snapshot file path:', '', (err, value) => {
|
|
2871
2869
|
if (!err && value && value.trim()) {
|
|
2872
2870
|
let filePath = value.trim();
|
|
2873
|
-
if (filePath.startsWith('
|
|
2874
|
-
filePath = os.homedir()
|
|
2871
|
+
if (filePath.startsWith('~/')) {
|
|
2872
|
+
filePath = join(os.homedir(), filePath.slice(2));
|
|
2873
|
+
} else if (filePath.startsWith('~')) {
|
|
2874
|
+
filePath = join(os.homedir(), filePath.slice(1));
|
|
2875
2875
|
}
|
|
2876
2876
|
this.loadAndApplySnapshot(filePath);
|
|
2877
2877
|
}
|
|
@@ -3816,12 +3816,12 @@ class Dashboard {
|
|
|
3816
3816
|
break;
|
|
3817
3817
|
case 11: // Cycle export directory: ~/.openclaw/exports -> ~/Downloads -> ~/Desktop -> Custom
|
|
3818
3818
|
const exportDirs = [
|
|
3819
|
-
os.homedir()
|
|
3820
|
-
os.homedir()
|
|
3821
|
-
os.homedir()
|
|
3819
|
+
join(os.homedir(), '.openclaw', 'exports'),
|
|
3820
|
+
join(os.homedir(), 'Downloads'),
|
|
3821
|
+
join(os.homedir(), 'Desktop'),
|
|
3822
3822
|
'custom'
|
|
3823
3823
|
];
|
|
3824
|
-
const currentExportDir = this.settings.exportDirectory || os.homedir()
|
|
3824
|
+
const currentExportDir = this.settings.exportDirectory || join(os.homedir(), '.openclaw', 'exports');
|
|
3825
3825
|
let currentDirIdx = exportDirs.indexOf(currentExportDir);
|
|
3826
3826
|
if (currentDirIdx === -1) {
|
|
3827
3827
|
// Current dir is custom, start from beginning
|
|
@@ -3845,8 +3845,10 @@ class Dashboard {
|
|
|
3845
3845
|
this.w.customPathPrompt.input('Enter custom export path (~ for home):', currentExportDir, (err, value) => {
|
|
3846
3846
|
if (!err && value && value.trim()) {
|
|
3847
3847
|
let customPath = value.trim();
|
|
3848
|
-
if (customPath.startsWith('
|
|
3849
|
-
customPath = os.homedir()
|
|
3848
|
+
if (customPath.startsWith('~/')) {
|
|
3849
|
+
customPath = join(os.homedir(), customPath.slice(2));
|
|
3850
|
+
} else if (customPath.startsWith('~')) {
|
|
3851
|
+
customPath = join(os.homedir(), customPath.slice(1));
|
|
3850
3852
|
}
|
|
3851
3853
|
const pathValidation = validateFilePath(customPath);
|
|
3852
3854
|
if (pathValidation.valid) {
|
|
@@ -4619,28 +4621,20 @@ class Dashboard {
|
|
|
4619
4621
|
async start(updateProgress) {
|
|
4620
4622
|
const progress = updateProgress || (() => {});
|
|
4621
4623
|
|
|
4622
|
-
// Initialize the database
|
|
4623
|
-
progress(15, 'Initializing database...');
|
|
4624
|
-
await database.initDatabase();
|
|
4625
|
-
|
|
4626
|
-
// Clean up old data (older than 30 days) on startup
|
|
4627
|
-
progress(25, 'Cleaning up old data...');
|
|
4628
|
-
database.cleanupOldData(30);
|
|
4629
|
-
|
|
4630
4624
|
// Initialize gateway manager with settings
|
|
4631
|
-
progress(
|
|
4625
|
+
progress(15, 'Initializing gateway...');
|
|
4632
4626
|
gatewayManager.init(this.settings);
|
|
4633
4627
|
|
|
4634
4628
|
// Start performance monitoring
|
|
4635
|
-
progress(
|
|
4629
|
+
progress(30, 'Starting performance monitor...');
|
|
4636
4630
|
performanceMonitor.start();
|
|
4637
4631
|
|
|
4638
4632
|
// Wire up worker pool for metrics tracking
|
|
4639
|
-
progress(
|
|
4633
|
+
progress(45, 'Starting workers...');
|
|
4640
4634
|
setWorkerPool(workerPool);
|
|
4641
4635
|
|
|
4642
4636
|
// Start watching for config hot-reload
|
|
4643
|
-
progress(
|
|
4637
|
+
progress(60, 'Setting up config watcher...');
|
|
4644
4638
|
this.startConfigWatcher();
|
|
4645
4639
|
|
|
4646
4640
|
// Start watching for plugin hot-reload if --watch flag is set
|
|
@@ -5135,7 +5129,7 @@ class Dashboard {
|
|
|
5135
5129
|
}
|
|
5136
5130
|
}
|
|
5137
5131
|
} catch (err) {
|
|
5138
|
-
|
|
5132
|
+
|
|
5139
5133
|
logger.error('Session fetch error:', err.message);
|
|
5140
5134
|
this.data.sessions = this.data.sessions || [];
|
|
5141
5135
|
this.data.openclaw = { gateway: { reachable: false } };
|
|
@@ -5216,8 +5210,6 @@ class Dashboard {
|
|
|
5216
5210
|
});
|
|
5217
5211
|
|
|
5218
5212
|
this.render();
|
|
5219
|
-
// Store metrics in database for historical tracking
|
|
5220
|
-
database.storeMetricsSnapshot(this.data);
|
|
5221
5213
|
} catch (e) {}
|
|
5222
5214
|
}
|
|
5223
5215
|
|
|
@@ -5737,8 +5729,6 @@ class WebDashboard extends Dashboard {
|
|
|
5737
5729
|
}
|
|
5738
5730
|
|
|
5739
5731
|
// Initialize data fetching (reuse parent's start() logic but without UI)
|
|
5740
|
-
await database.initDatabase();
|
|
5741
|
-
database.cleanupOldData(30);
|
|
5742
5732
|
gatewayManager.init(this.settings);
|
|
5743
5733
|
performanceMonitor.start();
|
|
5744
5734
|
|
package/package.json
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claw-dashboard",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "A beautiful console dashboard for monitoring OpenClaw instances — inspired by htop/btop/mactop",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": {
|
|
8
|
-
"import": "./index.js"
|
|
9
|
-
"require": "./index.cjs"
|
|
8
|
+
"import": "./index.js"
|
|
10
9
|
},
|
|
11
10
|
"./widgets": {
|
|
12
|
-
"import": "./src/widgets/index.js"
|
|
13
|
-
"require": "./dist/widgets.cjs"
|
|
11
|
+
"import": "./src/widgets/index.js"
|
|
14
12
|
},
|
|
15
13
|
"./package.json": "./package.json"
|
|
16
14
|
},
|
|
@@ -20,6 +18,19 @@
|
|
|
20
18
|
"man": [
|
|
21
19
|
"./man/clawdash.1"
|
|
22
20
|
],
|
|
21
|
+
"files": [
|
|
22
|
+
"index.js",
|
|
23
|
+
"src/**/*.js",
|
|
24
|
+
"src/**/*.ts",
|
|
25
|
+
"!src/database.js",
|
|
26
|
+
"man/clawdash.1",
|
|
27
|
+
"docs/*.md",
|
|
28
|
+
"schemas/*.json",
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE",
|
|
31
|
+
"CHANGELOG.md",
|
|
32
|
+
"package.json"
|
|
33
|
+
],
|
|
23
34
|
"type": "module",
|
|
24
35
|
"scripts": {
|
|
25
36
|
"start": "node index.js",
|
|
@@ -31,7 +42,6 @@
|
|
|
31
42
|
"coverage:clean": "rm -rf coverage",
|
|
32
43
|
"build": "node esbuild.config.js",
|
|
33
44
|
"build:dev": "node esbuild.config.js --dev",
|
|
34
|
-
"build:cjs": "node build-cjs.js",
|
|
35
45
|
"lint": "eslint src/ --ignore-pattern 'src/workers/**'",
|
|
36
46
|
"release": "node scripts/release.js",
|
|
37
47
|
"release:sign": "node scripts/release.js --sign",
|
|
@@ -57,23 +67,22 @@
|
|
|
57
67
|
"author": "Kevin Smith",
|
|
58
68
|
"license": "MIT",
|
|
59
69
|
"engines": {
|
|
60
|
-
"node": ">=
|
|
70
|
+
"node": ">=20"
|
|
61
71
|
},
|
|
62
72
|
"dependencies": {
|
|
63
73
|
"@pm2/blessed": "^0.1.81",
|
|
64
74
|
"blessed": "npm:@pm2/blessed@^0.1.81",
|
|
65
75
|
"chalk": "^5.3.0",
|
|
66
|
-
"sql.js": "^1.14.0",
|
|
67
76
|
"systeminformation": "^5.21.22"
|
|
68
77
|
},
|
|
69
78
|
"devDependencies": {
|
|
70
79
|
"@eslint/js": "^10.0.1",
|
|
71
|
-
"c8": "^
|
|
72
|
-
"esbuild": "^0.
|
|
80
|
+
"c8": "^12.0.0",
|
|
81
|
+
"esbuild": "^0.28.1",
|
|
73
82
|
"eslint": "^10.0.2",
|
|
74
83
|
"husky": "^9.1.7",
|
|
75
84
|
"jest": "^30.2.0",
|
|
76
|
-
"lint-staged": "^
|
|
85
|
+
"lint-staged": "^17.2.0"
|
|
77
86
|
},
|
|
78
87
|
"lint-staged": {
|
|
79
88
|
"src/**/*.{js,mjs,cjs}": [
|
|
@@ -85,6 +94,7 @@
|
|
|
85
94
|
"xml2js": "^0.6.2",
|
|
86
95
|
"form-data": "^4.0.0",
|
|
87
96
|
"qs": "^6.14.1",
|
|
88
|
-
"tough-cookie": "^5.0.0"
|
|
97
|
+
"tough-cookie": "^5.0.0",
|
|
98
|
+
"brace-expansion": "^5.0.8"
|
|
89
99
|
}
|
|
90
100
|
}
|
package/src/auto-save.js
CHANGED
|
@@ -22,15 +22,21 @@ function validateFilePath(filePath) {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
// Expand tilde to home directory
|
|
25
|
-
const resolvedPath = filePath.startsWith('
|
|
25
|
+
const resolvedPath = filePath.startsWith('~/')
|
|
26
|
+
? path.join(os.homedir(), filePath.slice(2))
|
|
27
|
+
: filePath.startsWith('~')
|
|
26
28
|
? path.join(os.homedir(), filePath.slice(1))
|
|
27
29
|
: path.resolve(filePath);
|
|
28
30
|
|
|
29
|
-
// Check for path traversal - allow home directory and temp directories
|
|
31
|
+
// Check for path traversal - allow home directory and temp directories (sep + === to prevent prefix collisions e.g. /home/userfoo; matches index/snapshot/security style)
|
|
30
32
|
const homeDir = os.homedir();
|
|
31
33
|
const tempDirs = ['/tmp', os.tmpdir()];
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
+
const resolvedHome = path.resolve(homeDir);
|
|
35
|
+
const isInAllowedDir = resolvedPath.startsWith(resolvedHome + path.sep) || resolvedPath === resolvedHome ||
|
|
36
|
+
tempDirs.some(tmpDir => {
|
|
37
|
+
const rtmp = path.resolve(tmpDir);
|
|
38
|
+
return resolvedPath.startsWith(rtmp + path.sep) || resolvedPath === rtmp;
|
|
39
|
+
});
|
|
34
40
|
if (!isInAllowedDir) {
|
|
35
41
|
return { valid: false, error: 'Path must be within home or temp directory' };
|
|
36
42
|
}
|
|
@@ -354,7 +360,7 @@ export class AutoSaveManager {
|
|
|
354
360
|
}
|
|
355
361
|
|
|
356
362
|
// Ensure directory exists
|
|
357
|
-
const dir =
|
|
363
|
+
const dir = path.dirname(pathValidation.resolvedPath);
|
|
358
364
|
if (!fs.existsSync(dir)) {
|
|
359
365
|
fs.mkdirSync(dir, { recursive: true });
|
|
360
366
|
}
|
|
@@ -82,7 +82,9 @@ Examples:
|
|
|
82
82
|
let filePath;
|
|
83
83
|
if (outputPath) {
|
|
84
84
|
let resolvedPath = outputPath;
|
|
85
|
-
if (outputPath.startsWith('
|
|
85
|
+
if (outputPath.startsWith('~/')) {
|
|
86
|
+
resolvedPath = join(os.homedir(), outputPath.slice(2));
|
|
87
|
+
} else if (outputPath.startsWith('~')) {
|
|
86
88
|
resolvedPath = join(os.homedir(), outputPath.slice(1));
|
|
87
89
|
}
|
|
88
90
|
filePath = resolve(resolvedPath);
|
|
@@ -117,7 +117,9 @@ Examples:
|
|
|
117
117
|
try {
|
|
118
118
|
// Resolve the path
|
|
119
119
|
let resolvedPath = filePath;
|
|
120
|
-
if (filePath.startsWith('
|
|
120
|
+
if (filePath.startsWith('~/')) {
|
|
121
|
+
resolvedPath = join(os.homedir(), filePath.slice(2));
|
|
122
|
+
} else if (filePath.startsWith('~')) {
|
|
121
123
|
resolvedPath = join(os.homedir(), filePath.slice(1));
|
|
122
124
|
}
|
|
123
125
|
resolvedPath = resolve(resolvedPath);
|
package/src/config.js
CHANGED
|
@@ -101,17 +101,6 @@ export const CACHE_CONFIG = {
|
|
|
101
101
|
container: { ttl: CACHE_TTL.CONTAINER },
|
|
102
102
|
};
|
|
103
103
|
|
|
104
|
-
// ============================================================================
|
|
105
|
-
// DATABASE SETTINGS
|
|
106
|
-
// ============================================================================
|
|
107
|
-
|
|
108
|
-
export const DATABASE = {
|
|
109
|
-
PATH: os.homedir() + '/.openclaw/dashboard-history.db',
|
|
110
|
-
SAVE_INTERVAL_MS: 30000, // Save every 30 seconds
|
|
111
|
-
CLEANUP_INTERVAL_MS: 60 * 60 * 1000, // Cleanup every hour
|
|
112
|
-
DEFAULT_RETENTION_DAYS: 30,
|
|
113
|
-
};
|
|
114
|
-
|
|
115
104
|
// ============================================================================
|
|
116
105
|
// CHECKSUM VERIFICATION SETTINGS
|
|
117
106
|
// ============================================================================
|
|
@@ -375,6 +364,12 @@ export const WORKER_OVERLOAD = {
|
|
|
375
364
|
|
|
376
365
|
export const WEB = {
|
|
377
366
|
DEFAULT_PORT: 18790, // Default port for web interface
|
|
367
|
+
// SECURITY NOTE (pre-existing defaults; --web is for remote/trusted use only):
|
|
368
|
+
// - HOST '0.0.0.0' binds all interfaces (consider '127.0.0.1' for localhost-only).
|
|
369
|
+
// - CORS '*' allows all (restrict to specific origins in production).
|
|
370
|
+
// - AUTH.ENABLED=false (opt-in only; enable + keys for production).
|
|
371
|
+
// Health endpoint intentionally unauthed. See web-server.js and README for guidance.
|
|
372
|
+
// These were not changed in lean trim to avoid UX break for existing --web users.
|
|
378
373
|
HOST: '0.0.0.0', // Bind to all interfaces by default
|
|
379
374
|
CORS_ORIGIN: '*', // CORS origin (restrict in production)
|
|
380
375
|
REQUEST_TIMEOUT: 30000, // Request timeout in milliseconds
|
|
@@ -396,8 +391,8 @@ export const WEB = {
|
|
|
396
391
|
},
|
|
397
392
|
// CORS configuration
|
|
398
393
|
CORS: {
|
|
399
|
-
// Production: specify allowed origins as array (e.g., ['https://example.com'])
|
|
400
|
-
// Development: use '*' to allow all origins
|
|
394
|
+
// SECURITY: Production: specify allowed origins as array (e.g., ['https://example.com'])
|
|
395
|
+
// Development: use '*' to allow all origins (default here; see note on WEB.HOST/AUTH)
|
|
401
396
|
ALLOWED_ORIGINS: '*', // Default to allow all (restrict in production)
|
|
402
397
|
ALLOWED_METHODS: ['GET', 'POST', 'OPTIONS'],
|
|
403
398
|
ALLOWED_HEADERS: ['Content-Type', 'Authorization'],
|
|
@@ -406,7 +401,7 @@ export const WEB = {
|
|
|
406
401
|
},
|
|
407
402
|
// Authentication configuration
|
|
408
403
|
AUTH: {
|
|
409
|
-
ENABLED: false, // Disabled by default
|
|
404
|
+
ENABLED: false, // Disabled by default - must explicitly enable (see WEB security note above)
|
|
410
405
|
HEADER_NAME: 'Authorization', // HTTP header for API key
|
|
411
406
|
SCHEME: 'Bearer', // Auth scheme (Bearer, ApiKey, etc.)
|
|
412
407
|
KEY_PREFIX: 'cd_', // Prefix for auto-generated API keys
|
|
@@ -656,7 +651,6 @@ export default {
|
|
|
656
651
|
UI,
|
|
657
652
|
CACHE_TTL,
|
|
658
653
|
CACHE_CONFIG,
|
|
659
|
-
DATABASE,
|
|
660
654
|
RETRY,
|
|
661
655
|
DEFAULT_RETRY_OPTIONS,
|
|
662
656
|
AUTO_RETRY,
|
package/src/errors.js
CHANGED
|
@@ -94,14 +94,6 @@ export class UIError extends DashboardError {
|
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
// Database errors
|
|
98
|
-
export class DatabaseError extends DashboardError {
|
|
99
|
-
constructor(message, details = {}) {
|
|
100
|
-
super(message, 'DATABASE_ERROR', details);
|
|
101
|
-
this.name = 'DatabaseError';
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
97
|
// Validation errors
|
|
106
98
|
export class ValidationError extends DashboardError {
|
|
107
99
|
constructor(message, details = {}) {
|
|
@@ -147,7 +139,6 @@ export const ERROR_CODES = {
|
|
|
147
139
|
AUTH_ERROR: 'AUTH_ERROR',
|
|
148
140
|
NETWORK_ERROR: 'NETWORK_ERROR',
|
|
149
141
|
UI_ERROR: 'UI_ERROR',
|
|
150
|
-
DATABASE_ERROR: 'DATABASE_ERROR',
|
|
151
142
|
VALIDATION_ERROR: 'VALIDATION_ERROR',
|
|
152
143
|
TIMEOUT_ERROR: 'TIMEOUT_ERROR',
|
|
153
144
|
CHECKSUM_ERROR: 'CHECKSUM_ERROR',
|
package/src/security.js
CHANGED
|
@@ -394,8 +394,12 @@ function validatePluginPath(inputPath, options = {}) {
|
|
|
394
394
|
|
|
395
395
|
// Check for hidden files/directories (starting with .)
|
|
396
396
|
if (part.startsWith('.') && part !== '.' && part !== '..') {
|
|
397
|
-
// Allow specific hidden files like .gitkeep but not arbitrary ones
|
|
398
|
-
|
|
397
|
+
// Allow specific hidden files like .gitkeep but not arbitrary ones.
|
|
398
|
+
// '.openclaw' whitelisted for standard ~/.openclaw/snapshots + /plugins flows (PATHS.OPENCLAW_DIR etc.)
|
|
399
|
+
// and user arbitrary paths under dot-dirs; other dot components still rejected for security.
|
|
400
|
+
// This rule (via shared validatePluginPath) applies to snapshot export/import (even with allowAbsolute:true, e.g. ~/.config/.secret/snap.json or /tmp/.foo/bar rejected); delete uses allowedDirs.
|
|
401
|
+
// See the validateFilePath comment in index.js (settings uses different/broader rules under ~), and export/import/delete comments in snapshot.js.
|
|
402
|
+
const allowedHidden = ['.gitkeep', '.gitignore', '.npmignore', '.openclaw'];
|
|
399
403
|
if (!allowedHidden.includes(part)) {
|
|
400
404
|
return { valid: false, path: null, error: `Hidden files/directories are not allowed: ${part}` };
|
|
401
405
|
}
|