Adds the server/app half of a remote-diagnostics system so Claude (via a local MCP gateway, added separately) can read a server's full state to diagnose problems: logs, errors, queue/app state, redacted config, system health. All read-only; security review folded in as hard requirements. Architecture (design-verified): - A SECOND, independent RemoteServer instance (diagnosticMode) on its own port (default 9110, bind 127.0.0.1) with NO capture/input callbacks, so screen capture and sendInputEvent are structurally unreachable from the diagnostic path. The existing screen-share server (port 9100) is byte-for-byte unchanged. - lib/remote-server.js: a `host` bind option, a post-auth `diag-request` branch that delegates to onDiagnosticRequest and replies with a reqId-correlated `diag-response`, a `diagnosticMode` guard (capture never spawns), a timing-safe token compare (length-guarded), and getLastAccess(). - lib/diagnostics-agent.js: handle(op,args) enforces a HARDCODED read-only op whitelist as the sole authority — no write/exec op, no run_health_check. - lib/diagnostics-collectors.js: pure, dependency-injected collectors (server_health one-shot hub, read_log, list_errors, get_queue_state, get_history, get_config_redacted, get_system_info, list_logs, get_app_events, get_rotation_state, get_health) reusing support-bundle + stats. Security (all mandatory, implemented): - Redaction gates every off-box payload. support-bundle now adds webhookUrl + diagToken to CRED_KEYS, and exports redactLogText (value-scrub of live secret strings + pattern-scrub of Discord webhooks / Bearer / api_key= / cookies / sess ids) + valueScrub + collectSecretValues. read_log takes a logical NAME (no path traversal); doodstream-debug.log is excluded from the readable set (it logs live api-key-bearing HTML). grep is length-capped (ReDoS guard). - diagnostics config subtree (enabled/port/token/label/codeIssuedAt/bindAddress) defaults OFF, bind 127.0.0.1; deep-merge makes it migration-free. The server-owned token is preserved against renderer clobber in both save-global-settings handlers; the diagnostics:* IPC is the only mutator. - app.requestSingleInstanceLock() (also the approved fix #6) so a relaunch can't EADDRINUSE-kill the agent and two instances can't clobber the config. main.js: buildDiagnosticCode (mhu1_ base64url, no host embedded), start/stop + auto-start-on-launch + stop-on-quit, the four diagnostics:* IPC handlers. renderer: a "Diagnose-Zugriff" settings subtab (toggle, port, bind-address, copyable connection code + regenerate, status, read-only security note). 382 tests pass (incl. 11 new: collectors+redaction, agent whitelist, and a live RemoteServer protocol test proving auth->diag-response correlation, that a diagnostic client never triggers the capture window, and brute-force lockout). Lint clean (0 errors). Smoke boots identically to baseline. The standalone MCP gateway package + operator setup docs land in a follow-up commit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
163 lines
7.7 KiB
JavaScript
163 lines
7.7 KiB
JavaScript
const { contextBridge, ipcRenderer, webUtils } = require('electron');
|
|
|
|
contextBridge.exposeInMainWorld('api', {
|
|
// Config
|
|
getConfig: () => ipcRenderer.invoke('get-config'),
|
|
saveConfig: (config) => ipcRenderer.invoke('save-config', config),
|
|
getHistory: () => ipcRenderer.invoke('get-history'),
|
|
clearHistory: () => ipcRenderer.invoke('clear-history'),
|
|
pruneHistory: (retention, opts) => ipcRenderer.invoke('prune-history', { retention, dryRun: !!(opts && opts.dryRun) }),
|
|
exportHistory: (format) => ipcRenderer.invoke('export-history', format),
|
|
saveTextFile: (defaultName, content, filters) => ipcRenderer.invoke('save-text-file', defaultName, content, filters),
|
|
|
|
// Hoster settings
|
|
getHosterSettings: () => ipcRenderer.invoke('get-hoster-settings'),
|
|
saveHosterSettings: (settings) => ipcRenderer.invoke('save-hoster-settings', settings),
|
|
|
|
// Global settings
|
|
getGlobalSettings: () => ipcRenderer.invoke('get-global-settings'),
|
|
saveGlobalSettings: (settings) => ipcRenderer.invoke('save-global-settings', settings),
|
|
saveGlobalSettingsSync: (settings) => ipcRenderer.sendSync('save-global-settings-sync', settings),
|
|
|
|
// Always on top
|
|
setAlwaysOnTop: (value) => ipcRenderer.invoke('set-always-on-top', value),
|
|
getAlwaysOnTop: () => ipcRenderer.invoke('get-always-on-top'),
|
|
|
|
// Shutdown after finish
|
|
setShutdownAfterFinish: (mode) => ipcRenderer.invoke('set-shutdown-after-finish', mode),
|
|
getShutdownAfterFinish: () => ipcRenderer.invoke('get-shutdown-after-finish'),
|
|
cancelShutdown: () => ipcRenderer.invoke('cancel-shutdown'),
|
|
|
|
// File selection
|
|
selectFiles: () => ipcRenderer.invoke('select-files'),
|
|
selectFolder: () => ipcRenderer.invoke('select-folder'),
|
|
selectFolderWithSizes: () => ipcRenderer.invoke('select-folder-with-sizes'),
|
|
resolveFolderFiles: (folderPath) => ipcRenderer.invoke('resolve-folder-files', folderPath),
|
|
getFileSizes: (paths) => ipcRenderer.invoke('get-file-sizes', paths),
|
|
|
|
// Upload control
|
|
startUpload: (payload) => ipcRenderer.invoke('start-upload', payload),
|
|
cancelUpload: () => ipcRenderer.invoke('cancel-upload'),
|
|
cancelSelectedJobs: (jobIds) => ipcRenderer.invoke('cancel-selected-jobs', jobIds),
|
|
addJobsToBatch: (payload) => ipcRenderer.invoke('add-jobs-to-batch', payload),
|
|
finishAfterActive: () => ipcRenderer.invoke('finish-after-active'),
|
|
runHealthCheck: (payload) => ipcRenderer.invoke('run-health-check', payload),
|
|
validateCredentials: (payload) => ipcRenderer.invoke('validate-credentials', payload),
|
|
|
|
// Log import
|
|
readOwnUploadLog: () => ipcRenderer.invoke('read-own-upload-log'),
|
|
importUploadLog: () => ipcRenderer.invoke('import-upload-log'),
|
|
|
|
// Clipboard
|
|
copyToClipboard: (text) => ipcRenderer.invoke('copy-to-clipboard', text),
|
|
|
|
// Updates
|
|
checkForUpdate: () => ipcRenderer.invoke('app:check-updates'),
|
|
installUpdate: () => ipcRenderer.invoke('app:install-update'),
|
|
abortUpdate: () => ipcRenderer.invoke('app:abort-update'),
|
|
getVersion: () => ipcRenderer.invoke('app:get-version'),
|
|
restartApp: () => ipcRenderer.invoke('app:restart'),
|
|
quitApp: () => ipcRenderer.invoke('app:quit'),
|
|
onUpdateAvailable: (callback) => {
|
|
ipcRenderer.on('app:update-available', (_event, data) => callback(data));
|
|
},
|
|
onUpdateProgress: (callback) => {
|
|
ipcRenderer.on('app:update-progress', (_event, data) => callback(data));
|
|
},
|
|
|
|
// Backup
|
|
exportBackup: () => ipcRenderer.invoke('export-backup'),
|
|
importBackup: (legacyPassword) => ipcRenderer.invoke('import-backup', legacyPassword),
|
|
|
|
// Folder Monitor
|
|
folderMonitorStart: (settings) => ipcRenderer.invoke('folder-monitor:start', settings),
|
|
folderMonitorStop: () => ipcRenderer.invoke('folder-monitor:stop'),
|
|
folderMonitorStatus: () => ipcRenderer.invoke('folder-monitor:status'),
|
|
folderMonitorSelectFolder: () => ipcRenderer.invoke('folder-monitor:select-folder'),
|
|
onFolderMonitorNewFiles: (callback) => {
|
|
ipcRenderer.on('folder-monitor:new-files', (_event, data) => callback(data));
|
|
},
|
|
|
|
// Account switched event
|
|
onAccountSwitched: (callback) => {
|
|
ipcRenderer.on('account-switched', (_event, data) => callback(data));
|
|
},
|
|
|
|
// Drop Target
|
|
showDropTarget: () => ipcRenderer.invoke('show-drop-target'),
|
|
hideDropTarget: () => ipcRenderer.invoke('hide-drop-target'),
|
|
onDropTargetFiles: (callback) => {
|
|
ipcRenderer.on('drop-target:files', (_event, paths) => callback(paths));
|
|
},
|
|
|
|
// Debug
|
|
debugTestUpload: () => ipcRenderer.invoke('debug-test-upload'),
|
|
debugLog: (msg) => ipcRenderer.invoke('debug-log', msg),
|
|
|
|
// Events (main -> renderer)
|
|
onUploadProgress: (callback) => {
|
|
ipcRenderer.on('upload-progress', (_event, data) => callback(data));
|
|
},
|
|
onUploadProgressBatch: (callback) => {
|
|
ipcRenderer.on('upload-progress-batch', (_event, batch) => callback(batch));
|
|
},
|
|
onUploadBatchDone: (callback) => {
|
|
ipcRenderer.on('upload-batch-done', (_event, data) => callback(data));
|
|
},
|
|
onUploadStats: (callback) => {
|
|
ipcRenderer.on('upload-stats', (_event, data) => callback(data));
|
|
},
|
|
onShutdownCountdown: (callback) => {
|
|
ipcRenderer.on('shutdown-countdown', (_event, data) => callback(data));
|
|
},
|
|
onUploadLogFallback: (callback) => {
|
|
ipcRenderer.on('upload-log-fallback', (_event, data) => callback(data));
|
|
},
|
|
onAccountRotationLog: (callback) => {
|
|
ipcRenderer.on('account-rotation-log', (_event, data) => callback(data));
|
|
},
|
|
openLogFolder: () => ipcRenderer.invoke('open-log-folder'),
|
|
getJobLog: (jobId) => ipcRenderer.invoke('get-job-log', jobId),
|
|
getSessionFailedAccounts: () => ipcRenderer.invoke('get-session-failed-accounts'),
|
|
resetSessionFailedAccount: (payload) => ipcRenderer.invoke('reset-session-failed-account', payload),
|
|
resetAllSessionFailedAccounts: () => ipcRenderer.invoke('reset-all-session-failed-accounts'),
|
|
getLogPaths: () => ipcRenderer.invoke('get-log-paths'),
|
|
testWebhook: (payload) => ipcRenderer.invoke('test-webhook', payload),
|
|
revealLogFile: (target) => ipcRenderer.invoke('reveal-log-file', target),
|
|
setLogVerbose: (enabled) => ipcRenderer.invoke('set-log-verbose', enabled),
|
|
createSupportBundle: () => ipcRenderer.invoke('create-support-bundle'),
|
|
getAppInfo: () => ipcRenderer.invoke('get-app-info'),
|
|
onLogPathAutoUpdated: (callback) => {
|
|
ipcRenderer.on('log-path-auto-updated', (_event, data) => callback(data));
|
|
},
|
|
// Remote Control
|
|
remoteGetSettings: () => ipcRenderer.invoke('remote:get-settings'),
|
|
remoteSaveSettings: (settings) => ipcRenderer.invoke('remote:save-settings', settings),
|
|
remoteGenerateToken: () => ipcRenderer.invoke('remote:generate-token'),
|
|
remoteStatus: () => ipcRenderer.invoke('remote:status'),
|
|
onRemoteClientCount: (callback) => {
|
|
ipcRenderer.on('remote:client-count', (_event, count) => callback(count));
|
|
},
|
|
|
|
// Remote Diagnostics (read-only)
|
|
diagnosticsGetSettings: () => ipcRenderer.invoke('diagnostics:get-settings'),
|
|
diagnosticsSaveSettings: (settings) => ipcRenderer.invoke('diagnostics:save-settings', settings),
|
|
diagnosticsRegenerate: () => ipcRenderer.invoke('diagnostics:regenerate'),
|
|
diagnosticsStatus: () => ipcRenderer.invoke('diagnostics:status'),
|
|
|
|
// File path from drag & drop (Electron 33+ compatible)
|
|
getPathForFile: (file) => webUtils.getPathForFile(file),
|
|
removeAllListeners: () => {
|
|
ipcRenderer.removeAllListeners('upload-progress');
|
|
ipcRenderer.removeAllListeners('upload-batch-done');
|
|
ipcRenderer.removeAllListeners('upload-stats');
|
|
ipcRenderer.removeAllListeners('app:update-available');
|
|
ipcRenderer.removeAllListeners('app:update-progress');
|
|
ipcRenderer.removeAllListeners('shutdown-countdown');
|
|
ipcRenderer.removeAllListeners('folder-monitor:new-files');
|
|
ipcRenderer.removeAllListeners('drop-target:files');
|
|
ipcRenderer.removeAllListeners('account-switched');
|
|
ipcRenderer.removeAllListeners('remote:client-count');
|
|
}
|
|
});
|