@upx-us/shield 0.7.9 → 0.7.11
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 +26 -0
- package/dist/index.js +166 -7
- package/dist/src/index.d.ts +0 -1
- package/dist/src/index.js +12 -347
- package/openclaw.plugin.json +10 -1
- package/package.json +1 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,32 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## [0.7.11] — 2026-03-16
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Debug logging mode** (`debugLog: true`) — writes timestamped logs to `~/.openclaw/shield/logs/shield-debug.log`. Captures the full startup sequence with numbered checkpoints (1–11 + SIGTERM) so disconnection issues can be pinpointed exactly. Off by default — safe on all existing installs.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## [0.7.10] — 2026-03-16
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **`openclaw shield restart` command** — restarts Shield monitoring without a full gateway restart. Checks credentials, clears any stale update state, resets the session, and cycles the runtime. Use `--verbose` for step-by-step diagnostic output.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- **Plugin no longer silently stalls when telemetry consistently fails** — after 10 consecutive failed `reportInstance` calls, the plugin deactivates with a clear log message instead of running indefinitely with no output.
|
|
24
|
+
- **Update cycle cleanup after successful auto-update** — the periodic update check now properly stops all activity after calling a successful gateway restart, preventing a window where both the old and new instance could be active simultaneously.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- **Standalone `shield-bridge` binary removed** — the plugin now runs exclusively as an OpenClaw gateway plugin. Running `npx @upx-us/shield` directly shows a clear message with installation instructions. This eliminates a source of divergent behavior that caused the v0.7.7 incident.
|
|
29
|
+
- `bridge` terminology removed from all customer-visible log output and internal comments.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
7
33
|
## [0.7.9] — 2026-03-16
|
|
8
34
|
|
|
9
35
|
### Fixed
|
package/dist/index.js
CHANGED
|
@@ -319,8 +319,10 @@ const state = {
|
|
|
319
319
|
};
|
|
320
320
|
let firstEventDelivered = false;
|
|
321
321
|
let teardownPreviousRuntime = null;
|
|
322
|
+
let serviceStartFn = null;
|
|
322
323
|
const MAX_BACKOFF_MS = 5 * 60 * 1000;
|
|
323
324
|
const TELEMETRY_INTERVAL_MS = 5 * 60 * 1000;
|
|
325
|
+
const MAX_REGISTRATION_FAILURES = 10;
|
|
324
326
|
function getBackoffInterval(baseMs) {
|
|
325
327
|
if (state.consecutiveFailures === 0)
|
|
326
328
|
return baseMs;
|
|
@@ -486,6 +488,38 @@ exports.default = {
|
|
|
486
488
|
(0, log_1.setAdapter)(gatewayAdapter);
|
|
487
489
|
const pluginConfig = (api.pluginConfig ?? {});
|
|
488
490
|
log.debug('shield', 'Plugin config received', maskPluginConfigForLogs(pluginConfig));
|
|
491
|
+
const debugLogEnabled = pluginConfig.debugLog === true;
|
|
492
|
+
if (debugLogEnabled) {
|
|
493
|
+
try {
|
|
494
|
+
const { appendFileSync, mkdirSync } = require('fs');
|
|
495
|
+
const { join: pathJoin } = require('path');
|
|
496
|
+
const debugLogDir = pathJoin((0, os_1.homedir)(), '.openclaw', 'shield', 'logs');
|
|
497
|
+
const debugLogPath = pathJoin(debugLogDir, 'shield-debug.log');
|
|
498
|
+
mkdirSync(debugLogDir, { recursive: true });
|
|
499
|
+
const ts = () => new Date().toISOString();
|
|
500
|
+
const writeLine = (level, tag, msg, extra) => {
|
|
501
|
+
try {
|
|
502
|
+
const line = `${ts()} [${level.padEnd(5)}] [${tag}] ${msg}${extra !== undefined ? ' ' + JSON.stringify(extra) : ''}\n`;
|
|
503
|
+
appendFileSync(debugLogPath, line);
|
|
504
|
+
}
|
|
505
|
+
catch { }
|
|
506
|
+
};
|
|
507
|
+
const teeAdapter = {
|
|
508
|
+
debug(tag, msg, data) { gatewayAdapter.debug(tag, msg, data); writeLine('DEBUG', tag, msg, data); },
|
|
509
|
+
info(tag, msg) { gatewayAdapter.info(tag, msg); writeLine('INFO', tag, msg); },
|
|
510
|
+
warn(tag, msg) { gatewayAdapter.warn(tag, msg); writeLine('WARN', tag, msg); },
|
|
511
|
+
error(tag, msg, err) { gatewayAdapter.error(tag, msg, err); writeLine('ERROR', tag, msg, err ? String(err) : undefined); },
|
|
512
|
+
};
|
|
513
|
+
(0, log_1.setAdapter)(teeAdapter);
|
|
514
|
+
appendFileSync(debugLogPath, `\n${'─'.repeat(80)}\n` +
|
|
515
|
+
`${ts()} [SHIELD DEBUG SESSION START] v${version_1.VERSION}\n` +
|
|
516
|
+
`${'─'.repeat(80)}\n`);
|
|
517
|
+
log.info('shield', `Debug logging active → ${debugLogPath}`);
|
|
518
|
+
}
|
|
519
|
+
catch (err) {
|
|
520
|
+
log.warn('shield', `Failed to init debug log file: ${err instanceof Error ? err.message : String(err)}`);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
489
523
|
if (pluginConfig.enabled === false) {
|
|
490
524
|
log.info('shield', 'Monitoring disabled via config (enabled: false)');
|
|
491
525
|
return;
|
|
@@ -545,7 +579,7 @@ exports.default = {
|
|
|
545
579
|
.catch((err) => log.warn('shield', `Runtime cleanup before re-register failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
546
580
|
}
|
|
547
581
|
teardownPreviousRuntime = () => cleanupRuntime({ markStopped: true, resetGuard: true, flushRedactor: false });
|
|
548
|
-
|
|
582
|
+
const serviceDefinition = {
|
|
549
583
|
id: 'shield-monitor',
|
|
550
584
|
async start() {
|
|
551
585
|
if (!startGuard.begin()) {
|
|
@@ -555,8 +589,10 @@ exports.default = {
|
|
|
555
589
|
try {
|
|
556
590
|
await cleanupRuntime({ markStopped: false, resetGuard: false, flushRedactor: false });
|
|
557
591
|
const activeGeneration = ++runtimeGeneration;
|
|
592
|
+
log.info('shield', `[checkpoint:1] Service start() entered — generation=${activeGeneration}`);
|
|
558
593
|
let credentials = (0, config_1.loadCredentials)();
|
|
559
594
|
let validCreds = hasValidCredentials(credentials);
|
|
595
|
+
log.info('shield', `[checkpoint:2] Credentials loaded — valid=${validCreds} instanceId=${credentials?.instanceId ? credentials.instanceId.slice(0, 8) + '…' : 'missing'}`);
|
|
560
596
|
if (!validCreds && installationKey) {
|
|
561
597
|
log.info('shield', 'Installation key found — activating Shield (first-time setup)...');
|
|
562
598
|
const autoCreds = await performAutoRegistration(installationKey);
|
|
@@ -565,7 +601,7 @@ exports.default = {
|
|
|
565
601
|
startGuard.endFailure();
|
|
566
602
|
return;
|
|
567
603
|
}
|
|
568
|
-
log.info('shield', '✅ Shield activated! Starting monitoring
|
|
604
|
+
log.info('shield', '✅ Shield activated! Starting Shield monitoring...');
|
|
569
605
|
log.info('shield', ` Credentials saved to ${config_1.SHIELD_CONFIG_PATH}`);
|
|
570
606
|
log.info('shield', ' Tip: you can remove installationKey from config after first activation.');
|
|
571
607
|
credentials = autoCreds;
|
|
@@ -592,9 +628,12 @@ exports.default = {
|
|
|
592
628
|
const persistedStats = readAllTimeStats();
|
|
593
629
|
if (persistedStats.lastSync)
|
|
594
630
|
state.lastSync = persistedStats.lastSync;
|
|
595
|
-
log.info('shield', `
|
|
631
|
+
log.info('shield', `[checkpoint:3] Config loaded — sessionDirs=${config.sessionDirs.length} poll=${config.pollIntervalMs}ms dryRun=${config.dryRun}`);
|
|
632
|
+
log.info('shield', `Starting Shield v${version_1.VERSION} (poll: ${config.pollIntervalMs}ms, dryRun: ${config.dryRun})`);
|
|
596
633
|
(0, exclusions_1.initExclusions)((0, path_1.join)((0, os_1.homedir)(), '.openclaw', 'shield', 'data'));
|
|
634
|
+
log.info('shield', '[checkpoint:4] Exclusions initialized');
|
|
597
635
|
(0, case_monitor_1.initCaseMonitor)((0, path_1.join)((0, os_1.homedir)(), '.openclaw', 'shield', 'data'));
|
|
636
|
+
log.info('shield', '[checkpoint:5] Case monitor initialized');
|
|
598
637
|
if (config.localEventBuffer) {
|
|
599
638
|
(0, event_store_1.initEventStore)((0, path_1.join)((0, os_1.homedir)(), '.openclaw', 'shield', 'data'), { maxEvents: config.localEventLimit });
|
|
600
639
|
}
|
|
@@ -668,8 +707,10 @@ exports.default = {
|
|
|
668
707
|
catch { }
|
|
669
708
|
const autoUpdateMode = pluginConfig.autoUpdate ?? true;
|
|
670
709
|
const _bootState = (0, updater_1.loadUpdateState)();
|
|
710
|
+
log.info('shield', `[checkpoint:6] Pre-update-check — autoUpdate=${autoUpdateMode} pendingRestart=${_bootState.pendingRestart} updateAvailable=${_bootState.updateAvailable} latestVersion=${_bootState.latestVersion}`);
|
|
671
711
|
log.info('updater', `Startup update check (autoUpdate=${autoUpdateMode}, current=${version_1.VERSION}, pendingRestart=${_bootState.pendingRestart})`);
|
|
672
712
|
const startupUpdate = (0, updater_1.performAutoUpdate)(autoUpdateMode, _bootState.pendingRestart ? undefined : 0);
|
|
713
|
+
log.info('shield', `[checkpoint:7] Update check done — action=${startupUpdate.action}`);
|
|
673
714
|
if (startupUpdate.action === 'updated') {
|
|
674
715
|
log.info('updater', startupUpdate.message);
|
|
675
716
|
const restarted = (0, updater_1.requestGatewayRestart)();
|
|
@@ -701,10 +742,12 @@ exports.default = {
|
|
|
701
742
|
const { sendEvents, reportInstance } = await Promise.resolve().then(() => __importStar(require('./src/sender')));
|
|
702
743
|
const { init: initRedactor, flush: flushRedactor, redactEvent } = await Promise.resolve().then(() => __importStar(require('./src/redactor')));
|
|
703
744
|
const { validate } = await Promise.resolve().then(() => __importStar(require('./src/validator')));
|
|
745
|
+
log.info('shield', '[checkpoint:8] Dynamic imports loaded');
|
|
704
746
|
if (config.redactionEnabled)
|
|
705
747
|
initRedactor();
|
|
706
748
|
state.running = true;
|
|
707
749
|
persistState();
|
|
750
|
+
log.info('shield', '[checkpoint:9] state.running=true — entering poll loop');
|
|
708
751
|
const runTelemetry = async () => {
|
|
709
752
|
if (!state.running || activeGeneration !== runtimeGeneration)
|
|
710
753
|
return;
|
|
@@ -732,8 +775,21 @@ exports.default = {
|
|
|
732
775
|
if (activeGeneration !== runtimeGeneration)
|
|
733
776
|
return;
|
|
734
777
|
log.info('shield', `Instance report → Platform: success=${result.ok}`);
|
|
778
|
+
if (result.ok) {
|
|
779
|
+
state.consecutiveFailures = 0;
|
|
780
|
+
}
|
|
781
|
+
else {
|
|
782
|
+
state.consecutiveFailures++;
|
|
783
|
+
if (state.consecutiveFailures >= MAX_REGISTRATION_FAILURES) {
|
|
784
|
+
log.error('shield', `Instance report failed ${state.consecutiveFailures} consecutive times — Shield deactivated. Re-run: openclaw shield activate <KEY>`);
|
|
785
|
+
state.running = false;
|
|
786
|
+
markStateDirty();
|
|
787
|
+
persistState();
|
|
788
|
+
}
|
|
789
|
+
}
|
|
735
790
|
};
|
|
736
791
|
const runTelemetrySingleflight = createSingleflightRunner(runTelemetry);
|
|
792
|
+
log.info('shield', '[checkpoint:9a] Firing initial telemetry');
|
|
737
793
|
runTelemetrySingleflight().catch((err) => log.error('shield', `Telemetry error: ${err instanceof Error ? err.message : String(err)}`));
|
|
738
794
|
telemetryHandle = setInterval(() => {
|
|
739
795
|
if (activeGeneration !== runtimeGeneration || !state.running)
|
|
@@ -744,9 +800,12 @@ exports.default = {
|
|
|
744
800
|
if (updateResult.action !== 'none') {
|
|
745
801
|
log.info('updater', updateResult.message);
|
|
746
802
|
if (updateResult.action === 'updated') {
|
|
747
|
-
|
|
748
|
-
|
|
803
|
+
const restarted = (0, updater_1.requestGatewayRestart)();
|
|
804
|
+
if (restarted) {
|
|
805
|
+
cleanupRuntime({ markStopped: true, flushRedactor: true });
|
|
806
|
+
return;
|
|
749
807
|
}
|
|
808
|
+
log.warn('updater', 'Restart not available — new version loads on next manual restart');
|
|
750
809
|
}
|
|
751
810
|
}
|
|
752
811
|
}
|
|
@@ -887,15 +946,18 @@ exports.default = {
|
|
|
887
946
|
});
|
|
888
947
|
}, interval);
|
|
889
948
|
};
|
|
949
|
+
log.info('shield', '[checkpoint:10] First poll scheduled');
|
|
890
950
|
schedulePoll();
|
|
891
951
|
onSignalHandler = async () => {
|
|
892
952
|
if (!state.running)
|
|
893
953
|
return;
|
|
954
|
+
log.info('shield', '[checkpoint:SIGTERM] SIGTERM received — shutting down');
|
|
894
955
|
await cleanupRuntime({ markStopped: true, resetGuard: true, flushRedactor: true });
|
|
895
956
|
log.info('shield', 'Service stopped (signal)');
|
|
896
957
|
};
|
|
897
958
|
process.once('SIGTERM', onSignalHandler);
|
|
898
959
|
process.once('SIGINT', onSignalHandler);
|
|
960
|
+
log.info('shield', '[checkpoint:11] Startup complete — service running');
|
|
899
961
|
startGuard.endSuccess();
|
|
900
962
|
}
|
|
901
963
|
catch (err) {
|
|
@@ -909,7 +971,9 @@ exports.default = {
|
|
|
909
971
|
if (wasRunning)
|
|
910
972
|
log.info('shield', 'Service stopped');
|
|
911
973
|
},
|
|
912
|
-
}
|
|
974
|
+
};
|
|
975
|
+
serviceStartFn = serviceDefinition.start.bind(serviceDefinition);
|
|
976
|
+
api.registerService(serviceDefinition);
|
|
913
977
|
api.registerGatewayMethod('shield.status', ({ respond }) => {
|
|
914
978
|
const creds = (0, config_1.loadCredentials)();
|
|
915
979
|
const activated = state.activated || hasValidCredentials(creds);
|
|
@@ -1114,13 +1178,108 @@ exports.default = {
|
|
|
1114
1178
|
.description('Trigger an immediate poll cycle')
|
|
1115
1179
|
.action(async () => {
|
|
1116
1180
|
if (!pollFn) {
|
|
1117
|
-
console.error('
|
|
1181
|
+
console.error('Shield is not running');
|
|
1118
1182
|
return;
|
|
1119
1183
|
}
|
|
1120
1184
|
console.log('Flushing...');
|
|
1121
1185
|
await pollFn();
|
|
1122
1186
|
console.log('Done');
|
|
1123
1187
|
});
|
|
1188
|
+
shield.command('restart')
|
|
1189
|
+
.description('Restart Shield monitoring (use when status shows Disconnected)')
|
|
1190
|
+
.option('--verbose', 'Show detailed diagnostic output during restart')
|
|
1191
|
+
.action(async (opts) => {
|
|
1192
|
+
const verbose = opts.verbose ?? false;
|
|
1193
|
+
const step = (msg) => { if (verbose)
|
|
1194
|
+
console.log(` [restart] ${msg}`); };
|
|
1195
|
+
console.log('🛡️ Restarting Shield monitoring...');
|
|
1196
|
+
console.log('');
|
|
1197
|
+
step('Reading current state...');
|
|
1198
|
+
const wasRunning = state.running;
|
|
1199
|
+
const wasActivated = state.activated;
|
|
1200
|
+
const failures = state.consecutiveFailures ?? 0;
|
|
1201
|
+
const lastPollAgo = state.lastPollAt
|
|
1202
|
+
? `${Math.round((Date.now() - state.lastPollAt) / 1000)}s ago`
|
|
1203
|
+
: 'never';
|
|
1204
|
+
console.log(` Was running: ${wasRunning ? 'yes' : 'no'}`);
|
|
1205
|
+
console.log(` Activated: ${wasActivated ? 'yes' : 'no'}`);
|
|
1206
|
+
console.log(` Last poll: ${lastPollAgo}`);
|
|
1207
|
+
console.log(` Failures: ${failures}`);
|
|
1208
|
+
console.log('');
|
|
1209
|
+
step('Checking credentials...');
|
|
1210
|
+
const creds = (0, config_1.loadCredentials)();
|
|
1211
|
+
if (!hasValidCredentials(creds)) {
|
|
1212
|
+
console.error('❌ Cannot restart — Shield is not activated.');
|
|
1213
|
+
console.error(' Run: openclaw shield activate <YOUR_KEY>');
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
step(`Credentials OK (instance: ${(creds.instanceId || '').slice(0, 8)}…)`);
|
|
1217
|
+
step('Checking update state...');
|
|
1218
|
+
const updateState = (0, updater_1.loadUpdateState)();
|
|
1219
|
+
if (updateState.pendingRestart) {
|
|
1220
|
+
console.warn(' ⚠️ update-state.json has pendingRestart:true — clearing before restart');
|
|
1221
|
+
updateState.pendingRestart = false;
|
|
1222
|
+
updateState.restartAttempts = 0;
|
|
1223
|
+
const { saveUpdateState } = require('./src/updater');
|
|
1224
|
+
saveUpdateState(updateState);
|
|
1225
|
+
step('pendingRestart cleared');
|
|
1226
|
+
}
|
|
1227
|
+
if (updateState.updateAvailable && updateState.latestVersion === updateState.currentVersion) {
|
|
1228
|
+
console.warn(' ⚠️ Stale updateAvailable flag detected — clearing');
|
|
1229
|
+
updateState.updateAvailable = false;
|
|
1230
|
+
const { saveUpdateState } = require('./src/updater');
|
|
1231
|
+
saveUpdateState(updateState);
|
|
1232
|
+
step('Stale updateAvailable cleared');
|
|
1233
|
+
}
|
|
1234
|
+
step('Resetting session state...');
|
|
1235
|
+
state.running = false;
|
|
1236
|
+
state.consecutiveFailures = 0;
|
|
1237
|
+
state.lastPollAt = 0;
|
|
1238
|
+
state.captureSeenSinceLastSync = false;
|
|
1239
|
+
markStateDirty();
|
|
1240
|
+
persistState();
|
|
1241
|
+
step('State reset');
|
|
1242
|
+
step('Stopping current runtime...');
|
|
1243
|
+
if (typeof teardownPreviousRuntime === 'function') {
|
|
1244
|
+
await teardownPreviousRuntime();
|
|
1245
|
+
step('Runtime torn down');
|
|
1246
|
+
}
|
|
1247
|
+
else {
|
|
1248
|
+
step('No active runtime to tear down');
|
|
1249
|
+
}
|
|
1250
|
+
console.log(' Restarting...');
|
|
1251
|
+
step('Calling service start...');
|
|
1252
|
+
try {
|
|
1253
|
+
if (!serviceStartFn) {
|
|
1254
|
+
console.error('❌ Service not initialized — try: openclaw gateway restart');
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
await serviceStartFn();
|
|
1258
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
1259
|
+
const nowRunning = state.running;
|
|
1260
|
+
const nowLastPoll = state.lastPollAt
|
|
1261
|
+
? `${Math.round((Date.now() - state.lastPollAt) / 1000)}s ago`
|
|
1262
|
+
: 'not yet';
|
|
1263
|
+
console.log('');
|
|
1264
|
+
if (nowRunning) {
|
|
1265
|
+
console.log(`✅ Shield restarted successfully.`);
|
|
1266
|
+
console.log(` Running: yes | Last poll: ${nowLastPoll} | Failures: ${state.consecutiveFailures}`);
|
|
1267
|
+
}
|
|
1268
|
+
else {
|
|
1269
|
+
console.log('⚠️ Shield started but running flag is not set yet.');
|
|
1270
|
+
console.log(` Last poll: ${nowLastPoll} | Failures: ${state.consecutiveFailures}`);
|
|
1271
|
+
console.log(' Run `openclaw shield status` in a few seconds to confirm.');
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
catch (err) {
|
|
1275
|
+
console.error('');
|
|
1276
|
+
console.error(`❌ Restart failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1277
|
+
if (verbose) {
|
|
1278
|
+
console.error(err instanceof Error ? err.stack : '');
|
|
1279
|
+
}
|
|
1280
|
+
console.error(' If this persists, run: openclaw gateway restart');
|
|
1281
|
+
}
|
|
1282
|
+
});
|
|
1124
1283
|
shield.command('activate')
|
|
1125
1284
|
.description('Activate Shield with an Installation Key')
|
|
1126
1285
|
.argument('<key>', 'Installation Key from the Shield portal')
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/src/index.js
CHANGED
|
@@ -1,348 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
const os = __importStar(require("os"));
|
|
37
|
-
const path = __importStar(require("path"));
|
|
38
|
-
const updater_1 = require("./updater");
|
|
39
|
-
const config_1 = require("./config");
|
|
40
|
-
const log = __importStar(require("./log"));
|
|
41
|
-
const fetcher_1 = require("./fetcher");
|
|
42
|
-
const transformer_1 = require("./transformer");
|
|
43
|
-
const sender_1 = require("./sender");
|
|
44
|
-
const redactor_1 = require("./redactor");
|
|
45
|
-
const validator_1 = require("./validator");
|
|
46
|
-
const fs_1 = require("fs");
|
|
47
|
-
const version_1 = require("./version");
|
|
48
|
-
const event_store_1 = require("./event-store");
|
|
49
|
-
const case_monitor_1 = require("./case-monitor");
|
|
50
|
-
const exclusions_1 = require("./exclusions");
|
|
51
|
-
const SHIELD_DATA_DIR = path.join(os.homedir(), '.openclaw', 'shield', 'data');
|
|
52
|
-
function extractEventDetails(event) {
|
|
53
|
-
const path = event.target?.file_path || event.target?.file?.path || event.target?.process?.command_line;
|
|
54
|
-
const url = event.network?.http?.url || event.target?.url || event.url;
|
|
55
|
-
if (path)
|
|
56
|
-
return path.length > 100 ? path.slice(0, 97) + '…' : path;
|
|
57
|
-
if (url)
|
|
58
|
-
return url.length > 100 ? url.slice(0, 97) + '…' : url;
|
|
59
|
-
return undefined;
|
|
60
|
-
}
|
|
61
|
-
let running = true;
|
|
62
|
-
let lastTelemetryAt = 0;
|
|
63
|
-
let consecutiveFailures = 0;
|
|
64
|
-
let registrationOk = false;
|
|
65
|
-
const TELEMETRY_INTERVAL_MS = 5 * 60 * 1000;
|
|
66
|
-
const MAX_BACKOFF_MS = 5 * 60 * 1000;
|
|
67
|
-
const MAX_REGISTRATION_FAILURES = 10;
|
|
68
|
-
function getBackoffInterval(baseMs) {
|
|
69
|
-
if (consecutiveFailures === 0)
|
|
70
|
-
return baseMs;
|
|
71
|
-
const backoff = baseMs * Math.pow(2, Math.min(consecutiveFailures, 10));
|
|
72
|
-
return Math.min(backoff, MAX_BACKOFF_MS);
|
|
73
|
-
}
|
|
74
|
-
async function poll() {
|
|
75
|
-
const config = (0, config_1.loadConfig)();
|
|
76
|
-
if (config.redactionEnabled) {
|
|
77
|
-
(0, redactor_1.init)();
|
|
78
|
-
}
|
|
79
|
-
(0, exclusions_1.initExclusions)(SHIELD_DATA_DIR);
|
|
80
|
-
(0, case_monitor_1.initCaseMonitor)(SHIELD_DATA_DIR);
|
|
81
|
-
if (config.localEventBuffer) {
|
|
82
|
-
(0, event_store_1.initEventStore)(SHIELD_DATA_DIR, { maxEvents: config.localEventLimit });
|
|
83
|
-
}
|
|
84
|
-
log.info('bridge', `Starting — dryRun=${config.dryRun} poll=${config.pollIntervalMs}ms maxEvents=${config.maxEvents || 'unlimited'} redaction=${config.redactionEnabled} logLevel=${process.env.LOG_LEVEL || 'info'}`);
|
|
85
|
-
const _rawEnvStartup = process.env.SHIELD_AUTO_UPDATE;
|
|
86
|
-
const autoUpdateMode = _rawEnvStartup === 'false' ? false : _rawEnvStartup ?? true;
|
|
87
|
-
log.info('updater', `Startup update check (autoUpdate=${autoUpdateMode}, current=${version_1.VERSION})`);
|
|
88
|
-
const _bootState = (0, updater_1.loadUpdateState)();
|
|
89
|
-
const startupUpdate = (0, updater_1.performAutoUpdate)(autoUpdateMode, _bootState.pendingRestart ? undefined : 0);
|
|
90
|
-
if (startupUpdate.action === "none") {
|
|
91
|
-
log.info("updater", `Up to date (${version_1.VERSION})`);
|
|
92
|
-
}
|
|
93
|
-
else if (startupUpdate.action === "updated") {
|
|
94
|
-
log.info("updater", startupUpdate.message);
|
|
95
|
-
const restarted = (0, updater_1.requestGatewayRestart)();
|
|
96
|
-
if (restarted)
|
|
97
|
-
return;
|
|
98
|
-
log.warn("updater", "Gateway restart failed — continuing with current version. Restart manually to load the update.");
|
|
99
|
-
}
|
|
100
|
-
else if (startupUpdate.action === "notify") {
|
|
101
|
-
log.info("updater", startupUpdate.message);
|
|
102
|
-
}
|
|
103
|
-
else if (startupUpdate.action === "rollback" || startupUpdate.action === "error") {
|
|
104
|
-
log.warn("updater", startupUpdate.message);
|
|
105
|
-
}
|
|
106
|
-
while (running) {
|
|
107
|
-
try {
|
|
108
|
-
let entries = await (0, fetcher_1.fetchNewEntries)(config);
|
|
109
|
-
const now = Date.now();
|
|
110
|
-
if (now - lastTelemetryAt >= TELEMETRY_INTERVAL_MS) {
|
|
111
|
-
const hostSnapshot = config.collectHostMetrics ? (0, transformer_1.generateHostTelemetry)() : null;
|
|
112
|
-
const hostMeta = hostSnapshot?.event?.tool_metadata;
|
|
113
|
-
const agentId = process.env.OPENCLAW_AGENT_ID || 'main';
|
|
114
|
-
const instancePayload = {
|
|
115
|
-
machine: {
|
|
116
|
-
hostname: config.hostname,
|
|
117
|
-
os: process.platform,
|
|
118
|
-
arch: process.arch,
|
|
119
|
-
node_version: process.version,
|
|
120
|
-
public_ip: (0, transformer_1.getCachedPublicIp)() ?? '',
|
|
121
|
-
},
|
|
122
|
-
software: {
|
|
123
|
-
plugin_version: (0, version_1.getVersion)(),
|
|
124
|
-
openclaw_version: (0, transformer_1.resolveOpenClawVersion)(),
|
|
125
|
-
agent_label: (0, transformer_1.resolveAgentLabel)(agentId),
|
|
126
|
-
instance_name: (0, transformer_1.resolveAgentLabel)(agentId) || config.hostname,
|
|
127
|
-
...(hostMeta && {
|
|
128
|
-
gateway_bind: hostMeta['openclaw.gateway_bind'],
|
|
129
|
-
webhook_configured: hostMeta['openclaw.webhook_configured'],
|
|
130
|
-
browser_auth_required: hostMeta['openclaw.browser_auth_required'],
|
|
131
|
-
}),
|
|
132
|
-
},
|
|
133
|
-
};
|
|
134
|
-
if (!(0, transformer_1.getCachedPublicIp)()) {
|
|
135
|
-
log.warn('bridge', 'Public IP not yet resolved — sending empty public_ip so platform can enrich server-side');
|
|
136
|
-
}
|
|
137
|
-
const result = await (0, sender_1.reportInstance)(instancePayload, config.credentials);
|
|
138
|
-
log.info('bridge', `Instance report → Platform: success=${result.ok}`);
|
|
139
|
-
if (result.ok) {
|
|
140
|
-
registrationOk = true;
|
|
141
|
-
lastTelemetryAt = now;
|
|
142
|
-
if (result.score) {
|
|
143
|
-
log.info('bridge', `Protection score: ${result.score.badge} ${result.score.total}/100 (${result.score.grade})`);
|
|
144
|
-
if (result.score.recommendations?.length) {
|
|
145
|
-
for (const rec of result.score.recommendations) {
|
|
146
|
-
log.warn('bridge', `⚠ ${rec}`);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
else if (!registrationOk) {
|
|
152
|
-
consecutiveFailures++;
|
|
153
|
-
if (consecutiveFailures >= MAX_REGISTRATION_FAILURES) {
|
|
154
|
-
log.error('bridge', `reportInstance failed ${consecutiveFailures} consecutive times — instance not recognized. Re-run setup wizard. Exiting.`);
|
|
155
|
-
process.exit(1);
|
|
156
|
-
}
|
|
157
|
-
log.warn('bridge', `reportInstance failed (attempt ${consecutiveFailures}/${MAX_REGISTRATION_FAILURES}) — skipping events this cycle (platform may still be syncing)`);
|
|
158
|
-
continue;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
const _rawEnvLoop = process.env.SHIELD_AUTO_UPDATE;
|
|
162
|
-
const autoUpdateMode = _rawEnvLoop === 'false' ? false : _rawEnvLoop ?? true;
|
|
163
|
-
const updateResult = (0, updater_1.performAutoUpdate)(autoUpdateMode);
|
|
164
|
-
if (updateResult.action !== "none") {
|
|
165
|
-
if (updateResult.action === "notify") {
|
|
166
|
-
log.info("updater", updateResult.message);
|
|
167
|
-
}
|
|
168
|
-
else if (updateResult.action === "updated") {
|
|
169
|
-
log.info("updater", updateResult.message);
|
|
170
|
-
const restarted = (0, updater_1.requestGatewayRestart)();
|
|
171
|
-
if (restarted) {
|
|
172
|
-
running = false;
|
|
173
|
-
break;
|
|
174
|
-
}
|
|
175
|
-
log.warn("updater", "Gateway restart failed — continuing with current version. Restart manually to load the update.");
|
|
176
|
-
}
|
|
177
|
-
else if (updateResult.action === "rollback") {
|
|
178
|
-
log.warn("updater", updateResult.message);
|
|
179
|
-
}
|
|
180
|
-
else if (updateResult.action === "error") {
|
|
181
|
-
log.error("updater", updateResult.message);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
if (entries.length > 0) {
|
|
185
|
-
let envelopes = (0, transformer_1.transformEntries)(entries, config.sessionDirs);
|
|
186
|
-
const { valid: validEnvelopes, quarantined } = (0, validator_1.validate)(envelopes.map(e => e.event));
|
|
187
|
-
if (quarantined > 0) {
|
|
188
|
-
log.warn('bridge', `${quarantined} events quarantined (see ~/.openclaw/shield/data/quarantine.jsonl)`);
|
|
189
|
-
}
|
|
190
|
-
envelopes = envelopes.filter(e => validEnvelopes.includes(e.event));
|
|
191
|
-
if (config.maxEvents > 0 && envelopes.length > config.maxEvents) {
|
|
192
|
-
log.info('bridge', `Limiting to ${config.maxEvents} of ${envelopes.length} events (maxEvents cap)`);
|
|
193
|
-
envelopes = envelopes.slice(0, config.maxEvents);
|
|
194
|
-
}
|
|
195
|
-
if (config.redactionEnabled) {
|
|
196
|
-
envelopes = envelopes.map(env => {
|
|
197
|
-
const redacted = (0, redactor_1.redactEvent)(env);
|
|
198
|
-
log.debug('redactor', `tool=${env.event.tool_name} session=${env.event.session_id}`, { event: redacted.event });
|
|
199
|
-
return redacted;
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
log.info('bridge', `${entries.length} entries → ${envelopes.length} envelopes`);
|
|
203
|
-
const results = await (0, sender_1.sendEvents)(envelopes, config);
|
|
204
|
-
for (const r of results) {
|
|
205
|
-
if (r.success) {
|
|
206
|
-
log.info('bridge', `Result: success=${r.success} status=${r.statusCode} events=${r.eventCount}`);
|
|
207
|
-
}
|
|
208
|
-
else {
|
|
209
|
-
log.error('bridge', `Result: FAILED status=${r.statusCode} events=${r.eventCount} body=${r.body?.slice(0, 200)}`);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
if (config.localEventBuffer && results.some(r => r.success)) {
|
|
213
|
-
const summaries = envelopes.map(env => ({
|
|
214
|
-
ts: env.event.timestamp,
|
|
215
|
-
type: env.event.event_type || 'UNKNOWN',
|
|
216
|
-
tool: env.event.tool_name || 'unknown',
|
|
217
|
-
summary: env.event.tool_metadata?.["openclaw.display_summary"] || env.event.target?.command_line || env.event.tool_name || 'event',
|
|
218
|
-
session: env.event.session_id || '?',
|
|
219
|
-
model: env.event.model || '?',
|
|
220
|
-
redacted: !!env.source?.plugin?.redaction_applied,
|
|
221
|
-
details: extractEventDetails(env.event),
|
|
222
|
-
trigger_type: env.event.tool_metadata?.['trigger.type'] ?? undefined,
|
|
223
|
-
}));
|
|
224
|
-
(0, event_store_1.appendEvents)(summaries);
|
|
225
|
-
}
|
|
226
|
-
if (results.some(r => r.needsRegistration)) {
|
|
227
|
-
consecutiveFailures++;
|
|
228
|
-
registrationOk = false;
|
|
229
|
-
lastTelemetryAt = 0;
|
|
230
|
-
if (consecutiveFailures >= MAX_REGISTRATION_FAILURES) {
|
|
231
|
-
log.error('bridge', `Instance not recognized by platform after ${consecutiveFailures} attempts. Re-run the setup wizard. Exiting.`);
|
|
232
|
-
process.exit(1);
|
|
233
|
-
}
|
|
234
|
-
log.warn('bridge', `Instance not registered on platform (attempt ${consecutiveFailures}/${MAX_REGISTRATION_FAILURES}) — will re-register on next cycle`);
|
|
235
|
-
continue;
|
|
236
|
-
}
|
|
237
|
-
const pendingResult = results.find(r => r.pendingNamespace);
|
|
238
|
-
if (pendingResult) {
|
|
239
|
-
const retryAfterMs = Math.min(pendingResult.retryAfterMs ?? 300_000, MAX_BACKOFF_MS);
|
|
240
|
-
log.warn('bridge', `Namespace pending — holding cursors, retrying in ${Math.round(retryAfterMs / 1000)}s`);
|
|
241
|
-
await new Promise(r => setTimeout(r, retryAfterMs));
|
|
242
|
-
continue;
|
|
243
|
-
}
|
|
244
|
-
const allSuccess = results.every(r => r.success);
|
|
245
|
-
if (allSuccess) {
|
|
246
|
-
(0, fetcher_1.commitCursors)(config, entries);
|
|
247
|
-
if (config.redactionEnabled)
|
|
248
|
-
(0, redactor_1.flush)();
|
|
249
|
-
log.info('bridge', 'Cursors committed');
|
|
250
|
-
consecutiveFailures = 0;
|
|
251
|
-
}
|
|
252
|
-
else {
|
|
253
|
-
consecutiveFailures++;
|
|
254
|
-
log.warn('bridge', 'Some batches failed — cursors NOT updated (will retry next poll)');
|
|
255
|
-
}
|
|
256
|
-
if (config.maxEvents > 0) {
|
|
257
|
-
log.info('bridge', 'Test mode complete, exiting');
|
|
258
|
-
running = false;
|
|
259
|
-
break;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
else {
|
|
263
|
-
(0, fetcher_1.commitCursors)(config, []);
|
|
264
|
-
consecutiveFailures = 0;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
catch (err) {
|
|
268
|
-
consecutiveFailures++;
|
|
269
|
-
log.error('bridge', 'Poll error', err);
|
|
270
|
-
}
|
|
271
|
-
try {
|
|
272
|
-
const creds = (0, config_1.loadCredentials)();
|
|
273
|
-
const platformConfig = {
|
|
274
|
-
apiUrl: config.platformApiUrl ?? null,
|
|
275
|
-
instanceId: creds?.instanceId || '',
|
|
276
|
-
hmacSecret: creds?.hmacSecret || '',
|
|
277
|
-
};
|
|
278
|
-
await (0, case_monitor_1.checkForNewCases)(platformConfig);
|
|
279
|
-
}
|
|
280
|
-
catch (err) {
|
|
281
|
-
log.debug('case-monitor', `Check error: ${err instanceof Error ? err.message : String(err)}`);
|
|
282
|
-
}
|
|
283
|
-
if (!running)
|
|
284
|
-
break;
|
|
285
|
-
const interval = getBackoffInterval(config.pollIntervalMs);
|
|
286
|
-
if (interval !== config.pollIntervalMs) {
|
|
287
|
-
log.warn('bridge', `Backing off: next poll in ${Math.round(interval / 1000)}s (${consecutiveFailures} consecutive failures)`);
|
|
288
|
-
}
|
|
289
|
-
await new Promise(r => setTimeout(r, interval));
|
|
290
|
-
}
|
|
291
|
-
if (config.redactionEnabled)
|
|
292
|
-
(0, redactor_1.flush)();
|
|
293
|
-
console.log('[Shield Bridge] Stopped');
|
|
294
|
-
}
|
|
295
|
-
function checkConfiguration() {
|
|
296
|
-
if (!(0, fs_1.existsSync)(config_1.SHIELD_CONFIG_PATH)) {
|
|
297
|
-
const { SHIELD_API_URL, SHIELD_INSTANCE_ID, SHIELD_HMAC_SECRET, SHIELD_FINGERPRINT, SHIELD_SECRET } = process.env;
|
|
298
|
-
if (SHIELD_API_URL && (SHIELD_INSTANCE_ID || SHIELD_FINGERPRINT) && (SHIELD_HMAC_SECRET || SHIELD_SECRET))
|
|
299
|
-
return true;
|
|
300
|
-
console.log('🛡️ OpenClaw Shield — First Time Setup Required');
|
|
301
|
-
console.log('=================================================\n');
|
|
302
|
-
console.log(`No config found at ${config_1.SHIELD_CONFIG_PATH}`);
|
|
303
|
-
console.log('Run the setup wizard to connect to your Shield API:\n');
|
|
304
|
-
console.log(' npx shield-setup\n');
|
|
305
|
-
return false;
|
|
306
|
-
}
|
|
307
|
-
(0, config_1.injectConfigEnv)();
|
|
308
|
-
const creds = (0, config_1.loadCredentials)();
|
|
309
|
-
if (!creds.apiUrl || !creds.instanceId || !creds.hmacSecret) {
|
|
310
|
-
console.error('❌ Shield config is incomplete. Missing apiUrl, instanceId, or hmacSecret.');
|
|
311
|
-
console.error(` Config file: ${config_1.SHIELD_CONFIG_PATH}`);
|
|
312
|
-
console.error(' Run: npx shield-setup');
|
|
313
|
-
return false;
|
|
314
|
-
}
|
|
315
|
-
return true;
|
|
316
|
-
}
|
|
317
|
-
if (!checkConfiguration()) {
|
|
318
|
-
process.exit(1);
|
|
319
|
-
}
|
|
320
|
-
const config = (0, config_1.loadConfig)();
|
|
321
|
-
process.on('SIGINT', () => {
|
|
322
|
-
console.log('\n[Shield Bridge] Shutting down...');
|
|
323
|
-
if (config.redactionEnabled)
|
|
324
|
-
(0, redactor_1.flush)();
|
|
325
|
-
running = false;
|
|
326
|
-
});
|
|
327
|
-
process.on('SIGTERM', () => {
|
|
328
|
-
console.log('\n[Shield Bridge] Received SIGTERM, shutting down gracefully...');
|
|
329
|
-
if (config.redactionEnabled)
|
|
330
|
-
(0, redactor_1.flush)();
|
|
331
|
-
running = false;
|
|
332
|
-
});
|
|
333
|
-
poll().catch((err) => {
|
|
334
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
335
|
-
console.error('[Shield Bridge] Fatal error:', msg);
|
|
336
|
-
if (msg.includes('ENOTFOUND') || msg.includes('ECONNREFUSED')) {
|
|
337
|
-
console.error('\n💡 This looks like a network connectivity issue.');
|
|
338
|
-
console.error(' - Check your Shield API URL in the configuration');
|
|
339
|
-
console.error(' - Verify your internet connection');
|
|
340
|
-
console.error(' - Run: npm run setup -- --verify');
|
|
341
|
-
}
|
|
342
|
-
else if (msg.includes('401') || msg.includes('403')) {
|
|
343
|
-
console.error('\n💡 This looks like an authentication issue.');
|
|
344
|
-
console.error(' - Verify your Shield API credentials');
|
|
345
|
-
console.error(' - Run: npm run setup');
|
|
346
|
-
}
|
|
347
|
-
process.exit(1);
|
|
348
|
-
});
|
|
2
|
+
console.error('');
|
|
3
|
+
console.error('OpenClaw Shield is an OpenClaw plugin — it runs inside the OpenClaw gateway,');
|
|
4
|
+
console.error('not as a standalone process.');
|
|
5
|
+
console.error('');
|
|
6
|
+
console.error('To get started:');
|
|
7
|
+
console.error(' 1. Install OpenClaw https://openclaw.ai');
|
|
8
|
+
console.error(' 2. Install the plugin openclaw plugins install @upx-us/shield');
|
|
9
|
+
console.error(' 3. Activate openclaw shield activate <YOUR_KEY>');
|
|
10
|
+
console.error('');
|
|
11
|
+
console.error('Get your key at: https://www.upx.com/en/lp/openclaw-shield-upx');
|
|
12
|
+
console.error('');
|
|
13
|
+
process.exit(1);
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "shield",
|
|
3
3
|
"name": "OpenClaw Shield",
|
|
4
4
|
"description": "Real-time security monitoring — streams enriched, redacted security events to the Shield detection platform.",
|
|
5
|
-
"version": "0.7.
|
|
5
|
+
"version": "0.7.11",
|
|
6
6
|
"skills": [
|
|
7
7
|
"./skills"
|
|
8
8
|
],
|
|
@@ -48,6 +48,11 @@
|
|
|
48
48
|
],
|
|
49
49
|
"default": true,
|
|
50
50
|
"description": "Auto-update mode: true (auto-update patch versions), false (disabled), or 'notify-only' (log available updates without installing)."
|
|
51
|
+
},
|
|
52
|
+
"debugLog": {
|
|
53
|
+
"type": "boolean",
|
|
54
|
+
"default": false,
|
|
55
|
+
"description": "Enable verbose debug logging to ~/.openclaw/shield/logs/shield-debug.log. Use only when diagnosing connectivity issues."
|
|
51
56
|
}
|
|
52
57
|
}
|
|
53
58
|
},
|
|
@@ -74,6 +79,10 @@
|
|
|
74
79
|
"autoUpdate": {
|
|
75
80
|
"label": "Auto-update mode",
|
|
76
81
|
"description": "true = auto-install patch updates, 'notify-only' = log only, false = disabled"
|
|
82
|
+
},
|
|
83
|
+
"debugLog": {
|
|
84
|
+
"label": "Debug logging",
|
|
85
|
+
"description": "Writes detailed startup and lifecycle logs to a file for support diagnosis"
|
|
77
86
|
}
|
|
78
87
|
},
|
|
79
88
|
"clawhub": {
|
package/package.json
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@upx-us/shield",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.11",
|
|
4
4
|
"description": "Security monitoring plugin for OpenClaw agents — streams enriched security events to the Shield detection platform",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"bin": {
|
|
8
|
-
"shield-bridge": "dist/src/index.js",
|
|
9
8
|
"shield-setup": "dist/src/setup.js"
|
|
10
9
|
},
|
|
11
10
|
"files": [
|