@hcu-lab.me/mcp-device 1.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/LICENSE +21 -0
- package/README.md +51 -0
- package/dist/bootstrap.d.ts +1 -0
- package/dist/bootstrap.js +22 -0
- package/dist/command-manager.d.ts +8 -0
- package/dist/command-manager.js +241 -0
- package/dist/config-field-definitions.d.ts +41 -0
- package/dist/config-field-definitions.js +37 -0
- package/dist/config-manager.d.ts +102 -0
- package/dist/config-manager.js +291 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.js +10 -0
- package/dist/custom-stdio.d.ts +51 -0
- package/dist/custom-stdio.js +358 -0
- package/dist/data/official-app-ca.pem +12 -0
- package/dist/data/onboarding-prompts.json +70 -0
- package/dist/device/device-state.d.ts +13 -0
- package/dist/device/device-state.js +100 -0
- package/dist/device/device-status.d.ts +81 -0
- package/dist/device/device-status.js +156 -0
- package/dist/device/device.d.ts +16 -0
- package/dist/device/device.js +190 -0
- package/dist/device/execution-engine.d.ts +144 -0
- package/dist/device/execution-engine.js +195 -0
- package/dist/device/gateway-channel.d.ts +48 -0
- package/dist/device/gateway-channel.js +384 -0
- package/dist/device/gateway-config.d.ts +40 -0
- package/dist/device/gateway-config.js +207 -0
- package/dist/device/gateway-identity.d.ts +35 -0
- package/dist/device/gateway-identity.js +178 -0
- package/dist/device/gateway-pairing.d.ts +25 -0
- package/dist/device/gateway-pairing.js +105 -0
- package/dist/device/gateway-secure-transport.d.ts +25 -0
- package/dist/device/gateway-secure-transport.js +117 -0
- package/dist/device/gateway-tool-adapter.d.ts +17 -0
- package/dist/device/gateway-tool-adapter.js +284 -0
- package/dist/device/gateway-url-policy.d.ts +3 -0
- package/dist/device/gateway-url-policy.js +43 -0
- package/dist/device/linux-service.d.ts +70 -0
- package/dist/device/linux-service.js +268 -0
- package/dist/device/official-trust.d.ts +7 -0
- package/dist/device/official-trust.js +49 -0
- package/dist/device/project-inspection.d.ts +1 -0
- package/dist/device/project-inspection.js +180 -0
- package/dist/device/runtime-owner.d.ts +41 -0
- package/dist/device/runtime-owner.js +177 -0
- package/dist/device/windows-dpapi.d.ts +11 -0
- package/dist/device/windows-dpapi.js +67 -0
- package/dist/device/windows-service.d.ts +56 -0
- package/dist/device/windows-service.js +238 -0
- package/dist/error-handlers.d.ts +7 -0
- package/dist/error-handlers.js +15 -0
- package/dist/handlers/edit-search-handlers.d.ts +6 -0
- package/dist/handlers/edit-search-handlers.js +6 -0
- package/dist/handlers/filesystem-handlers.d.ts +38 -0
- package/dist/handlers/filesystem-handlers.js +440 -0
- package/dist/handlers/history-handlers.d.ts +12 -0
- package/dist/handlers/history-handlers.js +67 -0
- package/dist/handlers/index.d.ts +6 -0
- package/dist/handlers/index.js +7 -0
- package/dist/handlers/process-handlers.d.ts +9 -0
- package/dist/handlers/process-handlers.js +15 -0
- package/dist/handlers/search-handlers.d.ts +17 -0
- package/dist/handlers/search-handlers.js +224 -0
- package/dist/handlers/terminal-handlers.d.ts +21 -0
- package/dist/handlers/terminal-handlers.js +35 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +138 -0
- package/dist/mcp-device.d.ts +2 -0
- package/dist/mcp-device.js +7 -0
- package/dist/npm-scripts/remote.d.ts +3 -0
- package/dist/npm-scripts/remote.js +314 -0
- package/dist/search-manager.d.ts +147 -0
- package/dist/search-manager.js +828 -0
- package/dist/server.d.ts +46 -0
- package/dist/server.js +1563 -0
- package/dist/terminal-manager.d.ts +104 -0
- package/dist/terminal-manager.js +635 -0
- package/dist/tools/config.d.ts +96 -0
- package/dist/tools/config.js +260 -0
- package/dist/tools/edit.d.ts +38 -0
- package/dist/tools/edit.js +435 -0
- package/dist/tools/feedback.d.ts +8 -0
- package/dist/tools/feedback.js +132 -0
- package/dist/tools/filesystem.d.ts +79 -0
- package/dist/tools/filesystem.js +947 -0
- package/dist/tools/fuzzySearch.d.ts +12 -0
- package/dist/tools/fuzzySearch.js +74 -0
- package/dist/tools/fuzzySearchCore.d.ts +52 -0
- package/dist/tools/fuzzySearchCore.js +125 -0
- package/dist/tools/improved-process-tools.d.ts +24 -0
- package/dist/tools/improved-process-tools.js +627 -0
- package/dist/tools/mime-types.d.ts +3 -0
- package/dist/tools/mime-types.js +28 -0
- package/dist/tools/pdf/extract-images.d.ts +34 -0
- package/dist/tools/pdf/extract-images.js +132 -0
- package/dist/tools/pdf/index.d.ts +6 -0
- package/dist/tools/pdf/index.js +3 -0
- package/dist/tools/pdf/lib/pdf2md.d.ts +36 -0
- package/dist/tools/pdf/lib/pdf2md.js +76 -0
- package/dist/tools/pdf/manipulations.d.ts +13 -0
- package/dist/tools/pdf/manipulations.js +99 -0
- package/dist/tools/pdf/markdown.d.ts +26 -0
- package/dist/tools/pdf/markdown.js +275 -0
- package/dist/tools/pdf/utils.d.ts +12 -0
- package/dist/tools/pdf/utils.js +34 -0
- package/dist/tools/process.d.ts +3 -0
- package/dist/tools/process.js +56 -0
- package/dist/tools/prompts.d.ts +28 -0
- package/dist/tools/prompts.js +259 -0
- package/dist/tools/schemas.d.ts +454 -0
- package/dist/tools/schemas.js +242 -0
- package/dist/tools/usage.d.ts +5 -0
- package/dist/tools/usage.js +24 -0
- package/dist/types.d.ts +83 -0
- package/dist/types.js +1 -0
- package/dist/ui/config-editor/config-editor-runtime.js +150 -0
- package/dist/ui/config-editor/index.html +13 -0
- package/dist/ui/config-editor/src/app.d.ts +43 -0
- package/dist/ui/config-editor/src/app.js +831 -0
- package/dist/ui/config-editor/src/array-modal.d.ts +19 -0
- package/dist/ui/config-editor/src/array-modal.js +185 -0
- package/dist/ui/config-editor/src/main.d.ts +1 -0
- package/dist/ui/config-editor/src/main.js +2 -0
- package/dist/ui/config-editor/styles.css +587 -0
- package/dist/ui/contracts.d.ts +14 -0
- package/dist/ui/contracts.js +21 -0
- package/dist/ui/file-preview/index.html +16 -0
- package/dist/ui/file-preview/preview-runtime.js +469 -0
- package/dist/ui/file-preview/shared/preview-file-types.d.ts +5 -0
- package/dist/ui/file-preview/shared/preview-file-types.js +59 -0
- package/dist/ui/file-preview/src/app.d.ts +4 -0
- package/dist/ui/file-preview/src/app.js +680 -0
- package/dist/ui/file-preview/src/components/code-viewer.d.ts +6 -0
- package/dist/ui/file-preview/src/components/code-viewer.js +73 -0
- package/dist/ui/file-preview/src/components/highlighting.d.ts +2 -0
- package/dist/ui/file-preview/src/components/highlighting.js +54 -0
- package/dist/ui/file-preview/src/components/html-renderer.d.ts +5 -0
- package/dist/ui/file-preview/src/components/html-renderer.js +47 -0
- package/dist/ui/file-preview/src/components/markdown-renderer.d.ts +1 -0
- package/dist/ui/file-preview/src/components/markdown-renderer.js +59 -0
- package/dist/ui/file-preview/src/directory-controller.d.ts +8 -0
- package/dist/ui/file-preview/src/directory-controller.js +240 -0
- package/dist/ui/file-preview/src/document-layout.d.ts +20 -0
- package/dist/ui/file-preview/src/document-layout.js +109 -0
- package/dist/ui/file-preview/src/document-outline.d.ts +17 -0
- package/dist/ui/file-preview/src/document-outline.js +97 -0
- package/dist/ui/file-preview/src/document-workspace.d.ts +19 -0
- package/dist/ui/file-preview/src/document-workspace.js +33 -0
- package/dist/ui/file-preview/src/file-type-handlers.d.ts +10 -0
- package/dist/ui/file-preview/src/file-type-handlers.js +109 -0
- package/dist/ui/file-preview/src/host/external-actions.d.ts +8 -0
- package/dist/ui/file-preview/src/host/external-actions.js +55 -0
- package/dist/ui/file-preview/src/host/selection-context.d.ts +9 -0
- package/dist/ui/file-preview/src/host/selection-context.js +106 -0
- package/dist/ui/file-preview/src/image-preview.d.ts +3 -0
- package/dist/ui/file-preview/src/image-preview.js +21 -0
- package/dist/ui/file-preview/src/main.d.ts +1 -0
- package/dist/ui/file-preview/src/main.js +5 -0
- package/dist/ui/file-preview/src/markdown/conflict-dialog.d.ts +40 -0
- package/dist/ui/file-preview/src/markdown/conflict-dialog.js +163 -0
- package/dist/ui/file-preview/src/markdown/controller.d.ts +44 -0
- package/dist/ui/file-preview/src/markdown/controller.js +1043 -0
- package/dist/ui/file-preview/src/markdown/editor.d.ts +131 -0
- package/dist/ui/file-preview/src/markdown/editor.js +1479 -0
- package/dist/ui/file-preview/src/markdown/linking.d.ts +16 -0
- package/dist/ui/file-preview/src/markdown/linking.js +228 -0
- package/dist/ui/file-preview/src/markdown/outline.d.ts +2 -0
- package/dist/ui/file-preview/src/markdown/outline.js +16 -0
- package/dist/ui/file-preview/src/markdown/parser.d.ts +30 -0
- package/dist/ui/file-preview/src/markdown/parser.js +38 -0
- package/dist/ui/file-preview/src/markdown/preview.d.ts +1 -0
- package/dist/ui/file-preview/src/markdown/preview.js +20 -0
- package/dist/ui/file-preview/src/markdown/slugify.d.ts +3 -0
- package/dist/ui/file-preview/src/markdown/slugify.js +31 -0
- package/dist/ui/file-preview/src/markdown/utils.d.ts +1 -0
- package/dist/ui/file-preview/src/markdown/utils.js +15 -0
- package/dist/ui/file-preview/src/model.d.ts +35 -0
- package/dist/ui/file-preview/src/model.js +1 -0
- package/dist/ui/file-preview/src/panel-actions.d.ts +17 -0
- package/dist/ui/file-preview/src/panel-actions.js +182 -0
- package/dist/ui/file-preview/src/path-utils.d.ts +6 -0
- package/dist/ui/file-preview/src/path-utils.js +64 -0
- package/dist/ui/file-preview/src/payload-utils.d.ts +11 -0
- package/dist/ui/file-preview/src/payload-utils.js +119 -0
- package/dist/ui/file-preview/src/types.d.ts +1 -0
- package/dist/ui/file-preview/src/types.js +1 -0
- package/dist/ui/file-preview/styles.css +1631 -0
- package/dist/ui/resources.d.ts +28 -0
- package/dist/ui/resources.js +86 -0
- package/dist/ui/shared/compact-row.d.ts +11 -0
- package/dist/ui/shared/compact-row.js +18 -0
- package/dist/ui/shared/escape-html.d.ts +4 -0
- package/dist/ui/shared/escape-html.js +11 -0
- package/dist/ui/shared/host-context.d.ts +15 -0
- package/dist/ui/shared/host-context.js +51 -0
- package/dist/ui/shared/tool-bridge.d.ts +30 -0
- package/dist/ui/shared/tool-bridge.js +137 -0
- package/dist/ui/shared/tool-shell.d.ts +25 -0
- package/dist/ui/shared/tool-shell.js +107 -0
- package/dist/ui/shared/ui-event-tracker.d.ts +9 -0
- package/dist/ui/shared/ui-event-tracker.js +27 -0
- package/dist/ui/shared/widget-state.d.ts +33 -0
- package/dist/ui/shared/widget-state.js +158 -0
- package/dist/utils/ab-test.d.ts +12 -0
- package/dist/utils/ab-test.js +104 -0
- package/dist/utils/capture.d.ts +35 -0
- package/dist/utils/capture.js +481 -0
- package/dist/utils/dockerPrompt.d.ts +17 -0
- package/dist/utils/dockerPrompt.js +53 -0
- package/dist/utils/feature-flags.d.ts +59 -0
- package/dist/utils/feature-flags.js +215 -0
- package/dist/utils/files/base.d.ts +173 -0
- package/dist/utils/files/base.js +5 -0
- package/dist/utils/files/binary.d.ts +21 -0
- package/dist/utils/files/binary.js +65 -0
- package/dist/utils/files/docx.d.ts +47 -0
- package/dist/utils/files/docx.js +679 -0
- package/dist/utils/files/excel.d.ts +24 -0
- package/dist/utils/files/excel.js +437 -0
- package/dist/utils/files/factory.d.ts +41 -0
- package/dist/utils/files/factory.js +113 -0
- package/dist/utils/files/image.d.ts +21 -0
- package/dist/utils/files/image.js +78 -0
- package/dist/utils/files/index.d.ts +10 -0
- package/dist/utils/files/index.js +13 -0
- package/dist/utils/files/pdf.d.ts +32 -0
- package/dist/utils/files/pdf.js +142 -0
- package/dist/utils/files/text.d.ts +63 -0
- package/dist/utils/files/text.js +370 -0
- package/dist/utils/fuzzySearchLogger.d.ts +30 -0
- package/dist/utils/fuzzySearchLogger.js +126 -0
- package/dist/utils/lineEndingHandler.d.ts +21 -0
- package/dist/utils/lineEndingHandler.js +77 -0
- package/dist/utils/logger.d.ts +32 -0
- package/dist/utils/logger.js +72 -0
- package/dist/utils/mcp-ui-ab-test.d.ts +13 -0
- package/dist/utils/mcp-ui-ab-test.js +62 -0
- package/dist/utils/open-browser.d.ts +9 -0
- package/dist/utils/open-browser.js +42 -0
- package/dist/utils/process-detection.d.ts +23 -0
- package/dist/utils/process-detection.js +150 -0
- package/dist/utils/ripgrep-resolver.d.ts +9 -0
- package/dist/utils/ripgrep-resolver.js +74 -0
- package/dist/utils/system-info.d.ts +71 -0
- package/dist/utils/system-info.js +712 -0
- package/dist/utils/toolHistory.d.ts +86 -0
- package/dist/utils/toolHistory.js +257 -0
- package/dist/utils/trackTools.d.ts +6 -0
- package/dist/utils/trackTools.js +54 -0
- package/dist/utils/unsupportedParams.d.ts +24 -0
- package/dist/utils/unsupportedParams.js +66 -0
- package/dist/utils/usageTracker.d.ts +133 -0
- package/dist/utils/usageTracker.js +477 -0
- package/dist/utils/welcome-onboarding.d.ts +11 -0
- package/dist/utils/welcome-onboarding.js +99 -0
- package/dist/utils/withTimeout.d.ts +28 -0
- package/dist/utils/withTimeout.js +85 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/package.json +131 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs/promises';
|
|
4
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
5
|
+
import { StdioClientTransport, getDefaultEnvironment } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import { captureRemote } from '../utils/capture.js';
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
10
|
+
export class DesktopCommanderIntegration {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.mcpClient = null;
|
|
13
|
+
this.mcpTransport = null;
|
|
14
|
+
this.isReady = false;
|
|
15
|
+
}
|
|
16
|
+
async initialize() {
|
|
17
|
+
console.debug('[DEBUG] DesktopCommanderIntegration.initialize() called');
|
|
18
|
+
const config = await this.resolveMcpConfig();
|
|
19
|
+
if (!config) {
|
|
20
|
+
console.debug('[DEBUG] No MCP config found');
|
|
21
|
+
throw new Error('Desktop Commander MCP not found. Please install it globally via `npm install -g @wonderwhy-er/desktop-commander` or build the local project.');
|
|
22
|
+
}
|
|
23
|
+
console.log(` - â³ Connecting to Local Desktop Commander MCP using: ${config.command} ${config.args.join(' ')}`);
|
|
24
|
+
console.debug('[DEBUG] MCP config:', JSON.stringify(config, null, 2));
|
|
25
|
+
try {
|
|
26
|
+
console.debug('[DEBUG] Creating StdioClientTransport');
|
|
27
|
+
// MCP_DEVICE_REMOTE tells the spawned server it is serving remote
|
|
28
|
+
// services, so it suppresses local-only behavior like opening the
|
|
29
|
+
// welcome page in a browser the remote user would never see.
|
|
30
|
+
this.mcpTransport = new StdioClientTransport({
|
|
31
|
+
...config,
|
|
32
|
+
env: {
|
|
33
|
+
...getDefaultEnvironment(),
|
|
34
|
+
...config.env,
|
|
35
|
+
MCP_DEVICE_REMOTE: 'true',
|
|
36
|
+
DESKTOP_COMMANDER_DISABLE_TELEMETRY: 'true'
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
// Create MCP client
|
|
40
|
+
console.debug('[DEBUG] Creating MCP Client');
|
|
41
|
+
this.mcpClient = new Client({
|
|
42
|
+
name: "desktop-commander-client",
|
|
43
|
+
version: "1.0.0"
|
|
44
|
+
}, {
|
|
45
|
+
capabilities: {}
|
|
46
|
+
});
|
|
47
|
+
// Connect to Desktop Commander
|
|
48
|
+
console.debug('[DEBUG] Connecting MCP client to transport');
|
|
49
|
+
await this.mcpClient.connect(this.mcpTransport);
|
|
50
|
+
this.isReady = true;
|
|
51
|
+
console.log(' - 🔌 Connected to Desktop Commander MCP');
|
|
52
|
+
console.debug('[DEBUG] Desktop Commander MCP connection successful');
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
console.error(' - ⌠Failed to connect to Desktop Commander MCP:', error);
|
|
56
|
+
console.debug('[DEBUG] MCP connection error:', error);
|
|
57
|
+
await captureRemote('desktop_integration_init_failed', { error });
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async resolveMcpConfig() {
|
|
62
|
+
console.debug('[DEBUG] Resolving MCP config...');
|
|
63
|
+
// Option 1: Development/Local Build
|
|
64
|
+
// Adjusting path resolution since we are now in src/device and dist is in root/dist
|
|
65
|
+
// Original: path.resolve(__dirname, '../../dist/index.js')
|
|
66
|
+
const devPath = path.resolve(__dirname, '../../dist/index.js');
|
|
67
|
+
console.debug('[DEBUG] Checking local dev path:', devPath);
|
|
68
|
+
try {
|
|
69
|
+
await fs.access(devPath);
|
|
70
|
+
console.debug(' - 🔠Found local MCP server at:', devPath);
|
|
71
|
+
return {
|
|
72
|
+
command: process.execPath, // Use the current node executable
|
|
73
|
+
args: [devPath],
|
|
74
|
+
cwd: path.dirname(devPath)
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
console.debug('[DEBUG] Local dev path not found, trying global installation');
|
|
79
|
+
// Local file not found, continue...
|
|
80
|
+
}
|
|
81
|
+
// Option 2: Global Installation
|
|
82
|
+
const commandName = 'desktop-commander';
|
|
83
|
+
console.debug('[DEBUG] Checking for global command:', commandName);
|
|
84
|
+
try {
|
|
85
|
+
await new Promise((resolve, reject) => {
|
|
86
|
+
// Use platform-appropriate command to check if the command exists in PATH
|
|
87
|
+
// We can't run it directly as it's an stdio MCP server that waits for input
|
|
88
|
+
const whichCommand = process.platform === 'win32' ? 'where' : 'which';
|
|
89
|
+
console.debug('[DEBUG] Using platform command:', whichCommand, 'on platform:', process.platform);
|
|
90
|
+
const check = spawn(whichCommand, [commandName], { windowsHide: true }); // Prevent visible console windows on Windows
|
|
91
|
+
check.on('error', (err) => {
|
|
92
|
+
console.debug('[DEBUG] Spawn error for', whichCommand, ':', err.message);
|
|
93
|
+
reject(err);
|
|
94
|
+
});
|
|
95
|
+
check.on('close', (code) => {
|
|
96
|
+
console.debug('[DEBUG]', whichCommand, 'exited with code:', code);
|
|
97
|
+
return code === 0 ? resolve() : reject(new Error('Command not found'));
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
console.debug(' - Found global desktop-commander CLI');
|
|
101
|
+
return {
|
|
102
|
+
command: commandName,
|
|
103
|
+
args: []
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
console.debug('[DEBUG] Global command not found:', err);
|
|
108
|
+
// Global command not found
|
|
109
|
+
}
|
|
110
|
+
console.debug('[DEBUG] No MCP config resolved');
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
async callClientTool(toolName, args, metadata) {
|
|
114
|
+
if (!this.isReady || !this.mcpClient) {
|
|
115
|
+
console.debug('[DEBUG] callClientTool() failed - not ready or no client');
|
|
116
|
+
throw new Error('DesktopIntegration not initialized');
|
|
117
|
+
}
|
|
118
|
+
// Proxy other tools to MCP server
|
|
119
|
+
try {
|
|
120
|
+
console.debug('[DEBUG] Calling MCP tool:', toolName, 'args:', JSON.stringify(args).substring(0, 100));
|
|
121
|
+
const result = await this.mcpClient.callTool({
|
|
122
|
+
name: toolName,
|
|
123
|
+
arguments: args,
|
|
124
|
+
_meta: { remote: true, ...metadata || {} }
|
|
125
|
+
});
|
|
126
|
+
console.debug('[DEBUG] Tool call successful:', toolName);
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
console.error(`Error executing tool ${toolName}:`, error);
|
|
131
|
+
console.debug('[DEBUG] Tool call error details:', error);
|
|
132
|
+
await captureRemote('desktop_integration_tool_call_failed', { error, toolName });
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
async listClientTools() {
|
|
137
|
+
if (!this.mcpClient)
|
|
138
|
+
return { tools: [] };
|
|
139
|
+
try {
|
|
140
|
+
// List tools from MCP server
|
|
141
|
+
const mcpTools = await this.mcpClient.listTools();
|
|
142
|
+
// Merge tools
|
|
143
|
+
return {
|
|
144
|
+
tools: mcpTools.tools || []
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
console.error('Error fetching capabilities:', error);
|
|
149
|
+
await captureRemote('desktop_integration_list_tools_failed', { error });
|
|
150
|
+
// Fallback to local tools
|
|
151
|
+
return {
|
|
152
|
+
tools: []
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async shutdown() {
|
|
157
|
+
console.debug('[DEBUG] DesktopCommanderIntegration.shutdown() called');
|
|
158
|
+
const closeWithTimeout = async (operation, name, timeoutMs = 3000) => {
|
|
159
|
+
return Promise.race([
|
|
160
|
+
operation(),
|
|
161
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`${name} timeout after ${timeoutMs}ms`)), timeoutMs))
|
|
162
|
+
]);
|
|
163
|
+
};
|
|
164
|
+
if (this.mcpClient) {
|
|
165
|
+
try {
|
|
166
|
+
console.log(' → Closing MCP client...');
|
|
167
|
+
console.debug('[DEBUG] Calling mcpClient.close() with timeout');
|
|
168
|
+
await closeWithTimeout(() => this.mcpClient.close(), 'MCP client close');
|
|
169
|
+
console.log(' ✓ MCP client closed');
|
|
170
|
+
}
|
|
171
|
+
catch (e) {
|
|
172
|
+
console.warn(' âš ï¸ MCP client close timeout or error:', e.message);
|
|
173
|
+
console.debug('[DEBUG] MCP client close error:', e);
|
|
174
|
+
await captureRemote('desktop_integration_shutdown_error', { error: e, component: 'client' });
|
|
175
|
+
}
|
|
176
|
+
this.mcpClient = null;
|
|
177
|
+
}
|
|
178
|
+
if (this.mcpTransport) {
|
|
179
|
+
try {
|
|
180
|
+
console.log(' → Closing MCP transport...');
|
|
181
|
+
console.debug('[DEBUG] Calling mcpTransport.close() with timeout');
|
|
182
|
+
await closeWithTimeout(() => this.mcpTransport.close(), 'MCP transport close');
|
|
183
|
+
console.log(' ✓ MCP transport closed');
|
|
184
|
+
}
|
|
185
|
+
catch (e) {
|
|
186
|
+
console.warn(' âš ï¸ MCP transport close timeout or error:', e.message);
|
|
187
|
+
console.debug('[DEBUG] MCP transport close error:', e);
|
|
188
|
+
await captureRemote('desktop_integration_shutdown_error', { error: e, component: 'transport' });
|
|
189
|
+
}
|
|
190
|
+
this.mcpTransport = null;
|
|
191
|
+
}
|
|
192
|
+
this.isReady = false;
|
|
193
|
+
console.debug('[DEBUG] Desktop Commander integration shutdown complete');
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Agent } from 'http';
|
|
2
|
+
import { GatewayDeviceIdentity } from './gateway-identity.js';
|
|
3
|
+
import { GatewayToolAdapter } from './gateway-tool-adapter.js';
|
|
4
|
+
export interface GatewayChannelOptions {
|
|
5
|
+
gatewayUrl: string;
|
|
6
|
+
enrollmentToken?: string;
|
|
7
|
+
pairingGrant?: string;
|
|
8
|
+
identity?: GatewayDeviceIdentity;
|
|
9
|
+
proxyAgent?: Agent;
|
|
10
|
+
securityProtocolFloor?: number;
|
|
11
|
+
appCaPem?: string;
|
|
12
|
+
adapter: GatewayToolAdapter;
|
|
13
|
+
agentVersion?: string;
|
|
14
|
+
onStatus?: (payload: any) => void | Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
export declare class GatewayDeviceChannel {
|
|
17
|
+
private options;
|
|
18
|
+
private socket?;
|
|
19
|
+
private heartbeat?;
|
|
20
|
+
private shuttingDown;
|
|
21
|
+
private identity;
|
|
22
|
+
private readyResolve?;
|
|
23
|
+
private readyReject?;
|
|
24
|
+
private reconnectTimer?;
|
|
25
|
+
private reconnectAttempt;
|
|
26
|
+
private authenticatedDeviceId?;
|
|
27
|
+
private connectionEpoch?;
|
|
28
|
+
private accountLogoutResolve?;
|
|
29
|
+
private accountLogoutReject?;
|
|
30
|
+
private messageQueue;
|
|
31
|
+
private protocolVersion;
|
|
32
|
+
private secureSocket?;
|
|
33
|
+
private authMode;
|
|
34
|
+
constructor(options: GatewayChannelOptions);
|
|
35
|
+
start(): Promise<void>;
|
|
36
|
+
private startV2Transport;
|
|
37
|
+
stop(): Promise<void>;
|
|
38
|
+
private sendHello;
|
|
39
|
+
private sendHelloV2;
|
|
40
|
+
private handleMessage;
|
|
41
|
+
logoutAccount(timeoutMs?: number): Promise<any>;
|
|
42
|
+
private handleToolCall;
|
|
43
|
+
private scheduleReconnect;
|
|
44
|
+
private startHeartbeat;
|
|
45
|
+
private stopHeartbeat;
|
|
46
|
+
private send;
|
|
47
|
+
private fail;
|
|
48
|
+
}
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import WebSocket from 'ws';
|
|
3
|
+
import { GatewayDeviceIdentity } from './gateway-identity.js';
|
|
4
|
+
import { DEVICE_INNER_TLS_SUBPROTOCOL, createClientInnerTls, createJsonFrameParser, encodeJsonFrame, exportDeviceAuthKeyingMaterial } from './gateway-secure-transport.js';
|
|
5
|
+
import { GATEWAY_CAPABILITIES } from './gateway-tool-adapter.js';
|
|
6
|
+
import { gatewaySocketUrl } from './gateway-url-policy.js';
|
|
7
|
+
const PROTOCOL_VERSION = 1;
|
|
8
|
+
const PROTOCOL_VERSION_V2 = 2;
|
|
9
|
+
const HEARTBEAT_MS = 20000;
|
|
10
|
+
const MAX_OUTBOUND_MESSAGE_BYTES = 56 * 1024;
|
|
11
|
+
export class GatewayDeviceChannel {
|
|
12
|
+
constructor(options) {
|
|
13
|
+
this.options = options;
|
|
14
|
+
this.shuttingDown = false;
|
|
15
|
+
this.reconnectAttempt = 0;
|
|
16
|
+
this.messageQueue = Promise.resolve();
|
|
17
|
+
this.protocolVersion = PROTOCOL_VERSION;
|
|
18
|
+
this.authMode = 'reconnect';
|
|
19
|
+
this.identity = options.identity || new GatewayDeviceIdentity();
|
|
20
|
+
}
|
|
21
|
+
async start() {
|
|
22
|
+
this.authenticatedDeviceId = undefined;
|
|
23
|
+
this.connectionEpoch = undefined;
|
|
24
|
+
this.messageQueue = Promise.resolve();
|
|
25
|
+
const record = await this.identity.loadOrCreate();
|
|
26
|
+
this.protocolVersion = Number(this.options.securityProtocolFloor || 1) >= 2 ? PROTOCOL_VERSION_V2 : PROTOCOL_VERSION;
|
|
27
|
+
if (this.protocolVersion === PROTOCOL_VERSION_V2 && !String(this.options.appCaPem || '').trim()) {
|
|
28
|
+
throw new Error('Protocol v2 requires a provisioned gateway application CA.');
|
|
29
|
+
}
|
|
30
|
+
const credential = this.options.pairingGrant || (!record.enrolled ? this.options.enrollmentToken : undefined);
|
|
31
|
+
const webSocketOptions = this.options.proxyAgent ? { agent: this.options.proxyAgent } : {};
|
|
32
|
+
const socket = this.protocolVersion === PROTOCOL_VERSION_V2
|
|
33
|
+
? new WebSocket(gatewaySocketUrl(this.options.gatewayUrl), DEVICE_INNER_TLS_SUBPROTOCOL, webSocketOptions)
|
|
34
|
+
: new WebSocket(gatewaySocketUrl(this.options.gatewayUrl), {
|
|
35
|
+
...(credential ? { headers: { authorization: `Bearer ${credential}` } } : {}),
|
|
36
|
+
...webSocketOptions
|
|
37
|
+
});
|
|
38
|
+
this.socket = socket;
|
|
39
|
+
const ready = new Promise((resolve, reject) => {
|
|
40
|
+
this.readyResolve = resolve;
|
|
41
|
+
this.readyReject = reject;
|
|
42
|
+
});
|
|
43
|
+
if (this.protocolVersion === PROTOCOL_VERSION_V2) {
|
|
44
|
+
socket.once('open', () => this.startV2Transport().catch(error => this.fail(error)));
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
socket.once('open', () => this.sendHello().catch(error => this.fail(error)));
|
|
48
|
+
socket.on('message', raw => {
|
|
49
|
+
this.messageQueue = this.messageQueue
|
|
50
|
+
.then(() => this.handleMessage(raw.toString()))
|
|
51
|
+
.catch(error => this.fail(error));
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
socket.once('error', error => this.fail(error));
|
|
55
|
+
socket.once('close', (code, reason) => {
|
|
56
|
+
this.stopHeartbeat();
|
|
57
|
+
if (!this.shuttingDown && this.readyReject) {
|
|
58
|
+
this.fail(new Error(`Gateway connection closed (${code}): ${reason.toString()}`));
|
|
59
|
+
}
|
|
60
|
+
else if (!this.shuttingDown) {
|
|
61
|
+
this.scheduleReconnect();
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
await ready;
|
|
65
|
+
}
|
|
66
|
+
async startV2Transport() {
|
|
67
|
+
const socket = this.socket;
|
|
68
|
+
if (!socket || socket.readyState !== WebSocket.OPEN)
|
|
69
|
+
throw new Error('Gateway socket is not open');
|
|
70
|
+
if (socket.protocol !== DEVICE_INNER_TLS_SUBPROTOCOL) {
|
|
71
|
+
throw new Error('Gateway did not negotiate the required v2 inner TLS subprotocol.');
|
|
72
|
+
}
|
|
73
|
+
const url = new URL(gatewaySocketUrl(this.options.gatewayUrl));
|
|
74
|
+
const secure = createClientInnerTls(socket, {
|
|
75
|
+
ca: String(this.options.appCaPem || ''),
|
|
76
|
+
servername: url.hostname
|
|
77
|
+
});
|
|
78
|
+
this.secureSocket = secure;
|
|
79
|
+
const parser = createJsonFrameParser({
|
|
80
|
+
onMessage: message => {
|
|
81
|
+
this.messageQueue = this.messageQueue
|
|
82
|
+
.then(() => this.handleMessage(message))
|
|
83
|
+
.catch(error => this.fail(error));
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
secure.on('data', chunk => {
|
|
87
|
+
try {
|
|
88
|
+
parser.push(chunk);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
this.fail(error instanceof Error ? error : new Error(String(error)));
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
secure.once('error', error => this.fail(error));
|
|
95
|
+
await new Promise((resolve, reject) => {
|
|
96
|
+
secure.once('secureConnect', resolve);
|
|
97
|
+
secure.once('error', reject);
|
|
98
|
+
});
|
|
99
|
+
await this.sendHelloV2();
|
|
100
|
+
}
|
|
101
|
+
async stop() {
|
|
102
|
+
this.shuttingDown = true;
|
|
103
|
+
this.stopHeartbeat();
|
|
104
|
+
if (this.reconnectTimer)
|
|
105
|
+
clearTimeout(this.reconnectTimer);
|
|
106
|
+
this.reconnectTimer = undefined;
|
|
107
|
+
const secure = this.secureSocket;
|
|
108
|
+
this.secureSocket = undefined;
|
|
109
|
+
if (secure && !secure.destroyed)
|
|
110
|
+
secure.destroy();
|
|
111
|
+
const socket = this.socket;
|
|
112
|
+
this.socket = undefined;
|
|
113
|
+
if (!socket || socket.readyState === WebSocket.CLOSED)
|
|
114
|
+
return;
|
|
115
|
+
await new Promise(resolve => {
|
|
116
|
+
const timer = setTimeout(() => {
|
|
117
|
+
try {
|
|
118
|
+
socket.terminate();
|
|
119
|
+
}
|
|
120
|
+
catch { }
|
|
121
|
+
resolve();
|
|
122
|
+
}, 1500);
|
|
123
|
+
socket.once('close', () => {
|
|
124
|
+
clearTimeout(timer);
|
|
125
|
+
resolve();
|
|
126
|
+
});
|
|
127
|
+
socket.close(1000, 'device shutdown');
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
async sendHello() {
|
|
131
|
+
const record = await this.identity.loadOrCreate();
|
|
132
|
+
const pairing = record.enrolled && Boolean(this.options.pairingGrant);
|
|
133
|
+
const enrolling = !record.enrolled && Boolean(this.options.pairingGrant || this.options.enrollmentToken);
|
|
134
|
+
this.send({
|
|
135
|
+
protocol_version: PROTOCOL_VERSION,
|
|
136
|
+
type: pairing ? 'pair_hello' : enrolling ? 'enroll_hello' : 'auth_hello',
|
|
137
|
+
device_id: record.deviceId,
|
|
138
|
+
timestamp: Date.now(),
|
|
139
|
+
payload: {
|
|
140
|
+
agent_version: this.options.agentVersion || 'mcp-device-1',
|
|
141
|
+
hostname: os.hostname(),
|
|
142
|
+
platform: process.platform,
|
|
143
|
+
arch: process.arch,
|
|
144
|
+
path_style: process.platform === 'win32' ? 'windows' : 'posix',
|
|
145
|
+
capabilities: [...GATEWAY_CAPABILITIES],
|
|
146
|
+
...(enrolling || pairing ? { public_key_pem: record.publicKeyPem } : {})
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
async sendHelloV2() {
|
|
151
|
+
const record = await this.identity.loadOrCreate();
|
|
152
|
+
const pairing = record.enrolled && Boolean(this.options.pairingGrant);
|
|
153
|
+
const enrolling = !record.enrolled && Boolean(this.options.pairingGrant || this.options.enrollmentToken);
|
|
154
|
+
this.authMode = pairing ? 'pair' : enrolling ? 'enroll' : 'reconnect';
|
|
155
|
+
const grant = pairing || enrolling ? String(this.options.pairingGrant || this.options.enrollmentToken || '') : '';
|
|
156
|
+
this.send({
|
|
157
|
+
protocol_version: PROTOCOL_VERSION_V2,
|
|
158
|
+
type: pairing ? 'pair_hello' : enrolling ? 'enroll_hello' : 'auth_hello',
|
|
159
|
+
device_id: record.deviceId,
|
|
160
|
+
timestamp: Date.now(),
|
|
161
|
+
payload: {
|
|
162
|
+
agent_version: this.options.agentVersion || 'mcp-device-1',
|
|
163
|
+
hostname: os.hostname(),
|
|
164
|
+
platform: process.platform,
|
|
165
|
+
arch: process.arch,
|
|
166
|
+
path_style: process.platform === 'win32' ? 'windows' : 'posix',
|
|
167
|
+
capabilities: [...GATEWAY_CAPABILITIES],
|
|
168
|
+
...(pairing || enrolling ? { public_key_pem: record.publicKeyPem, enrollment_grant: grant } : {})
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
async handleMessage(raw) {
|
|
173
|
+
const message = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
174
|
+
if (message?.protocol_version !== this.protocolVersion)
|
|
175
|
+
throw new Error('Unsupported gateway protocol version');
|
|
176
|
+
if (['auth_challenge', 'auth_ok', 'tool_call'].includes(String(message?.type || ''))) {
|
|
177
|
+
const record = await this.identity.loadOrCreate();
|
|
178
|
+
if (String(message?.device_id || '') !== record.deviceId)
|
|
179
|
+
throw new Error('Gateway message device_id mismatch');
|
|
180
|
+
}
|
|
181
|
+
if (message.type === 'auth_challenge') {
|
|
182
|
+
const nonce = String(message.payload?.nonce || '');
|
|
183
|
+
if (!nonce)
|
|
184
|
+
throw new Error('Gateway authentication challenge is missing a nonce');
|
|
185
|
+
const record = await this.identity.loadOrCreate();
|
|
186
|
+
if (this.protocolVersion === PROTOCOL_VERSION_V2) {
|
|
187
|
+
const mode = String(message.payload?.mode || '');
|
|
188
|
+
if (mode !== this.authMode)
|
|
189
|
+
throw new Error('Gateway authentication challenge mode mismatch');
|
|
190
|
+
const nonceBytes = Buffer.from(nonce, 'base64url');
|
|
191
|
+
if (nonceBytes.length !== 32)
|
|
192
|
+
throw new Error('Gateway v2 authentication challenge nonce must be exactly 32 bytes');
|
|
193
|
+
if (!this.secureSocket)
|
|
194
|
+
throw new Error('Gateway v2 inner TLS socket is unavailable');
|
|
195
|
+
const exporter = exportDeviceAuthKeyingMaterial(this.secureSocket, record.deviceId, this.authMode);
|
|
196
|
+
const grant = this.authMode === 'reconnect' ? null : String(this.options.pairingGrant || this.options.enrollmentToken || '');
|
|
197
|
+
this.send({
|
|
198
|
+
protocol_version: PROTOCOL_VERSION_V2,
|
|
199
|
+
type: 'auth_response',
|
|
200
|
+
device_id: record.deviceId,
|
|
201
|
+
timestamp: Date.now(),
|
|
202
|
+
payload: {
|
|
203
|
+
signature: await this.identity.signChallengeV2({ mode: this.authMode, nonce: nonceBytes, exporter, grant })
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
this.send({
|
|
209
|
+
protocol_version: PROTOCOL_VERSION,
|
|
210
|
+
type: 'auth_response',
|
|
211
|
+
device_id: record.deviceId,
|
|
212
|
+
timestamp: Date.now(),
|
|
213
|
+
payload: { signature: await this.identity.signChallenge(nonce) }
|
|
214
|
+
});
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (message.type === 'auth_ok') {
|
|
218
|
+
if (message.connection_epoch === undefined || message.connection_epoch === null || String(message.connection_epoch).trim() === '')
|
|
219
|
+
throw new Error('Gateway auth_ok is missing connection_epoch');
|
|
220
|
+
const record = await this.identity.loadOrCreate();
|
|
221
|
+
this.authenticatedDeviceId = record.deviceId;
|
|
222
|
+
this.connectionEpoch = message.connection_epoch;
|
|
223
|
+
await this.identity.markEnrolled();
|
|
224
|
+
this.options.pairingGrant = undefined;
|
|
225
|
+
if (this.options.onStatus && message.payload)
|
|
226
|
+
await this.options.onStatus(message.payload);
|
|
227
|
+
this.startHeartbeat();
|
|
228
|
+
this.reconnectAttempt = 0;
|
|
229
|
+
this.readyResolve?.();
|
|
230
|
+
this.readyResolve = undefined;
|
|
231
|
+
this.readyReject = undefined;
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (message.type === 'status_snapshot') {
|
|
235
|
+
if (this.connectionEpoch === undefined || this.authenticatedDeviceId === undefined)
|
|
236
|
+
return;
|
|
237
|
+
if (String(message.connection_epoch) !== String(this.connectionEpoch))
|
|
238
|
+
return;
|
|
239
|
+
if (this.options.onStatus && message.payload)
|
|
240
|
+
await this.options.onStatus(message.payload);
|
|
241
|
+
if (message.payload?.account?.connected === false && this.accountLogoutResolve) {
|
|
242
|
+
this.accountLogoutResolve(message.payload);
|
|
243
|
+
this.accountLogoutResolve = undefined;
|
|
244
|
+
this.accountLogoutReject = undefined;
|
|
245
|
+
}
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (message.type !== 'tool_call')
|
|
249
|
+
return;
|
|
250
|
+
if (this.connectionEpoch === undefined || this.authenticatedDeviceId === undefined)
|
|
251
|
+
throw new Error('Gateway tool_call arrived before authentication');
|
|
252
|
+
if (String(message.connection_epoch) !== String(this.connectionEpoch))
|
|
253
|
+
throw new Error('Gateway tool_call connection_epoch mismatch');
|
|
254
|
+
await this.handleToolCall(message);
|
|
255
|
+
}
|
|
256
|
+
async logoutAccount(timeoutMs = 5000) {
|
|
257
|
+
if (!this.socket || this.socket.readyState !== WebSocket.OPEN || this.connectionEpoch === undefined || !this.authenticatedDeviceId) {
|
|
258
|
+
throw new Error('Gateway device is not authenticated.');
|
|
259
|
+
}
|
|
260
|
+
if (this.accountLogoutResolve)
|
|
261
|
+
throw new Error('Account logout is already pending.');
|
|
262
|
+
const result = new Promise((resolve, reject) => {
|
|
263
|
+
this.accountLogoutResolve = resolve;
|
|
264
|
+
this.accountLogoutReject = reject;
|
|
265
|
+
});
|
|
266
|
+
this.send({
|
|
267
|
+
protocol_version: this.protocolVersion,
|
|
268
|
+
type: 'account_logout',
|
|
269
|
+
device_id: this.authenticatedDeviceId,
|
|
270
|
+
connection_epoch: this.connectionEpoch,
|
|
271
|
+
timestamp: Date.now(),
|
|
272
|
+
payload: {}
|
|
273
|
+
});
|
|
274
|
+
const timer = setTimeout(() => {
|
|
275
|
+
if (!this.accountLogoutReject)
|
|
276
|
+
return;
|
|
277
|
+
const reject = this.accountLogoutReject;
|
|
278
|
+
this.accountLogoutResolve = undefined;
|
|
279
|
+
this.accountLogoutReject = undefined;
|
|
280
|
+
reject(new Error('Timed out waiting for account logout status.'));
|
|
281
|
+
}, timeoutMs);
|
|
282
|
+
try {
|
|
283
|
+
return await result;
|
|
284
|
+
}
|
|
285
|
+
finally {
|
|
286
|
+
clearTimeout(timer);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
async handleToolCall(message) {
|
|
290
|
+
if (this.connectionEpoch === undefined || !this.authenticatedDeviceId)
|
|
291
|
+
throw new Error('Gateway tool_call arrived before authentication');
|
|
292
|
+
const requestId = String(message.request_id || '');
|
|
293
|
+
const tool = String(message.payload?.tool || '');
|
|
294
|
+
try {
|
|
295
|
+
const result = await this.options.adapter.call(tool, message.payload?.arguments || {});
|
|
296
|
+
this.send({
|
|
297
|
+
protocol_version: this.protocolVersion,
|
|
298
|
+
type: 'tool_result',
|
|
299
|
+
request_id: requestId,
|
|
300
|
+
device_id: this.authenticatedDeviceId,
|
|
301
|
+
connection_epoch: this.connectionEpoch,
|
|
302
|
+
timestamp: Date.now(),
|
|
303
|
+
payload: result
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
this.send({
|
|
308
|
+
protocol_version: this.protocolVersion,
|
|
309
|
+
type: 'tool_error',
|
|
310
|
+
request_id: requestId,
|
|
311
|
+
device_id: this.authenticatedDeviceId,
|
|
312
|
+
connection_epoch: this.connectionEpoch,
|
|
313
|
+
timestamp: Date.now(),
|
|
314
|
+
payload: {
|
|
315
|
+
message: String(error?.message || error),
|
|
316
|
+
code: String(error?.code || 'REMOTE_DEVICE_ERROR').slice(0, 64)
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
scheduleReconnect() {
|
|
322
|
+
if (this.shuttingDown || this.reconnectTimer)
|
|
323
|
+
return;
|
|
324
|
+
const delay = Math.min(30000, 1000 * 2 ** Math.min(this.reconnectAttempt, 5));
|
|
325
|
+
this.reconnectAttempt += 1;
|
|
326
|
+
this.reconnectTimer = setTimeout(() => {
|
|
327
|
+
this.reconnectTimer = undefined;
|
|
328
|
+
this.start().catch(() => this.scheduleReconnect());
|
|
329
|
+
}, delay);
|
|
330
|
+
this.reconnectTimer.unref?.();
|
|
331
|
+
}
|
|
332
|
+
startHeartbeat() {
|
|
333
|
+
this.stopHeartbeat();
|
|
334
|
+
this.heartbeat = setInterval(async () => {
|
|
335
|
+
const record = await this.identity.loadOrCreate();
|
|
336
|
+
this.send({
|
|
337
|
+
protocol_version: this.protocolVersion,
|
|
338
|
+
type: 'heartbeat',
|
|
339
|
+
device_id: record.deviceId,
|
|
340
|
+
timestamp: Date.now(),
|
|
341
|
+
payload: {}
|
|
342
|
+
});
|
|
343
|
+
}, HEARTBEAT_MS);
|
|
344
|
+
this.heartbeat.unref?.();
|
|
345
|
+
}
|
|
346
|
+
stopHeartbeat() {
|
|
347
|
+
if (this.heartbeat)
|
|
348
|
+
clearInterval(this.heartbeat);
|
|
349
|
+
this.heartbeat = undefined;
|
|
350
|
+
}
|
|
351
|
+
send(message) {
|
|
352
|
+
if (!this.socket || this.socket.readyState !== WebSocket.OPEN)
|
|
353
|
+
throw new Error('Gateway socket is not open');
|
|
354
|
+
const encoded = JSON.stringify(message);
|
|
355
|
+
if (Buffer.byteLength(encoded, 'utf8') > MAX_OUTBOUND_MESSAGE_BYTES) {
|
|
356
|
+
const error = new Error('Gateway device response exceeds the outbound message limit');
|
|
357
|
+
error.code = 'DEVICE_OUTPUT_TOO_LARGE';
|
|
358
|
+
throw error;
|
|
359
|
+
}
|
|
360
|
+
if (this.protocolVersion === PROTOCOL_VERSION_V2) {
|
|
361
|
+
if (!this.secureSocket || this.secureSocket.destroyed)
|
|
362
|
+
throw new Error('Gateway v2 inner TLS socket is not open');
|
|
363
|
+
this.secureSocket.write(encodeJsonFrame(message));
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
this.socket.send(encoded);
|
|
367
|
+
}
|
|
368
|
+
fail(error) {
|
|
369
|
+
const reject = this.readyReject;
|
|
370
|
+
this.readyResolve = undefined;
|
|
371
|
+
this.readyReject = undefined;
|
|
372
|
+
if (reject) {
|
|
373
|
+
reject(error);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
const socket = this.socket;
|
|
377
|
+
if (socket && socket.readyState !== WebSocket.CLOSED) {
|
|
378
|
+
try {
|
|
379
|
+
socket.terminate();
|
|
380
|
+
}
|
|
381
|
+
catch { }
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ProxyAgent } from 'proxy-agent';
|
|
2
|
+
export interface GatewayProxyConfig {
|
|
3
|
+
mode: 'direct' | 'configured';
|
|
4
|
+
url: string | null;
|
|
5
|
+
}
|
|
6
|
+
export interface GatewayDeviceConfig {
|
|
7
|
+
version: 1;
|
|
8
|
+
gatewayUrl: string | null;
|
|
9
|
+
allowedRoots: string[];
|
|
10
|
+
proxy: GatewayProxyConfig;
|
|
11
|
+
appCaPem: string | null;
|
|
12
|
+
securityProtocolFloor: number;
|
|
13
|
+
}
|
|
14
|
+
type SecretProtect = (value: Buffer) => Promise<string>;
|
|
15
|
+
type SecretUnprotect = (value: string) => Promise<Buffer>;
|
|
16
|
+
export declare class GatewayDeviceConfigStore {
|
|
17
|
+
readonly configPath: string;
|
|
18
|
+
private platform;
|
|
19
|
+
private protectSecret;
|
|
20
|
+
private unprotectSecret;
|
|
21
|
+
constructor(configPath?: string, options?: {
|
|
22
|
+
platform?: NodeJS.Platform | string;
|
|
23
|
+
protectSecret?: SecretProtect;
|
|
24
|
+
unprotectSecret?: SecretUnprotect;
|
|
25
|
+
});
|
|
26
|
+
load(): Promise<GatewayDeviceConfig>;
|
|
27
|
+
update(patch: Partial<GatewayDeviceConfig>): Promise<GatewayDeviceConfig>;
|
|
28
|
+
private persist;
|
|
29
|
+
}
|
|
30
|
+
export declare function resolveProxyDecisionFromEnvironment(gatewayUrl: string): {
|
|
31
|
+
explicit: boolean;
|
|
32
|
+
proxyUrl: string | null;
|
|
33
|
+
};
|
|
34
|
+
export declare function captureGatewayConfigFromEnvironment(store: GatewayDeviceConfigStore, options?: {
|
|
35
|
+
fallbackGatewayUrl?: string | null;
|
|
36
|
+
}): Promise<GatewayDeviceConfig>;
|
|
37
|
+
export declare function createGatewayProxyAgent(config: GatewayDeviceConfig): {
|
|
38
|
+
agent: ProxyAgent;
|
|
39
|
+
};
|
|
40
|
+
export {};
|