@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,207 @@
|
|
|
1
|
+
import { X509Certificate } from 'crypto';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import { createRequire } from 'module';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { ProxyAgent } from 'proxy-agent';
|
|
6
|
+
import { deviceStatePaths } from './device-state.js';
|
|
7
|
+
import { protectWindowsSecret, unprotectWindowsSecret } from './windows-dpapi.js';
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
const { getProxyForUrl } = require('proxy-from-env');
|
|
10
|
+
const PROXY_ENV_NAMES = ['HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'ALL_PROXY', 'http_proxy', 'https_proxy', 'no_proxy', 'all_proxy'];
|
|
11
|
+
function defaultConfigPath() {
|
|
12
|
+
return deviceStatePaths().config;
|
|
13
|
+
}
|
|
14
|
+
function emptyConfig() {
|
|
15
|
+
return {
|
|
16
|
+
version: 1,
|
|
17
|
+
gatewayUrl: null,
|
|
18
|
+
allowedRoots: [],
|
|
19
|
+
proxy: { mode: 'direct', url: null },
|
|
20
|
+
appCaPem: null,
|
|
21
|
+
securityProtocolFloor: 1
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function normalizeGatewayUrl(value) {
|
|
25
|
+
const text = String(value || '').trim();
|
|
26
|
+
if (!text)
|
|
27
|
+
return null;
|
|
28
|
+
const parsed = new URL(text);
|
|
29
|
+
if (!['http:', 'https:', 'ws:', 'wss:'].includes(parsed.protocol))
|
|
30
|
+
throw new Error('Gateway URL must use http(s) or ws(s).');
|
|
31
|
+
return parsed.toString();
|
|
32
|
+
}
|
|
33
|
+
function normalizeAllowedRoots(value) {
|
|
34
|
+
if (!Array.isArray(value))
|
|
35
|
+
return [];
|
|
36
|
+
return [...new Set(value.map(item => String(item || '').trim()).filter(Boolean))];
|
|
37
|
+
}
|
|
38
|
+
function normalizeAppCa(value) {
|
|
39
|
+
const text = String(value || '').trim();
|
|
40
|
+
if (!text)
|
|
41
|
+
return null;
|
|
42
|
+
new X509Certificate(text);
|
|
43
|
+
return `${text}\n`;
|
|
44
|
+
}
|
|
45
|
+
function normalizedProxy(value) {
|
|
46
|
+
if (!value || value.mode === 'direct')
|
|
47
|
+
return { mode: 'direct', url: null };
|
|
48
|
+
if (value.mode !== 'configured')
|
|
49
|
+
throw new Error('Invalid persisted gateway proxy mode.');
|
|
50
|
+
const url = String(value.url || '').trim();
|
|
51
|
+
if (!url)
|
|
52
|
+
throw new Error('Configured gateway proxy URL is missing.');
|
|
53
|
+
const parsed = new URL(url);
|
|
54
|
+
if (!['http:', 'https:', 'socks:', 'socks4:', 'socks5:'].includes(parsed.protocol)) {
|
|
55
|
+
throw new Error('Unsupported gateway proxy URL scheme.');
|
|
56
|
+
}
|
|
57
|
+
return { mode: 'configured', url: parsed.toString() };
|
|
58
|
+
}
|
|
59
|
+
function normalizeFloor(value) {
|
|
60
|
+
const parsed = Math.trunc(Number(value) || 1);
|
|
61
|
+
return Math.max(1, parsed);
|
|
62
|
+
}
|
|
63
|
+
export class GatewayDeviceConfigStore {
|
|
64
|
+
constructor(configPath = defaultConfigPath(), options = {}) {
|
|
65
|
+
this.configPath = path.resolve(configPath);
|
|
66
|
+
this.platform = options.platform || process.platform;
|
|
67
|
+
this.protectSecret = options.protectSecret || (value => protectWindowsSecret(value, 'proxy', { platform: this.platform }));
|
|
68
|
+
this.unprotectSecret = options.unprotectSecret || (value => unprotectWindowsSecret(value, 'proxy', { platform: this.platform }));
|
|
69
|
+
}
|
|
70
|
+
async load() {
|
|
71
|
+
let parsed;
|
|
72
|
+
try {
|
|
73
|
+
parsed = JSON.parse(await fs.readFile(this.configPath, 'utf8'));
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (error?.code === 'ENOENT')
|
|
77
|
+
return emptyConfig();
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
if (!parsed || typeof parsed !== 'object')
|
|
81
|
+
throw new Error('Invalid gateway config record.');
|
|
82
|
+
let proxy;
|
|
83
|
+
if (parsed.proxy?.mode === 'configured' && parsed.proxy?.protected?.scheme === 'dpapi-current-user-v1') {
|
|
84
|
+
if (this.platform !== 'win32')
|
|
85
|
+
throw new Error('DPAPI-protected proxy configuration requires Windows.');
|
|
86
|
+
const restored = await this.unprotectSecret(String(parsed.proxy.protected.blob || ''));
|
|
87
|
+
proxy = normalizedProxy({ mode: 'configured', url: restored.toString('utf8') });
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
proxy = normalizedProxy(parsed.proxy);
|
|
91
|
+
}
|
|
92
|
+
const appCaPem = normalizeAppCa(parsed.appCaPem);
|
|
93
|
+
const requestedFloor = normalizeFloor(parsed.securityProtocolFloor);
|
|
94
|
+
return {
|
|
95
|
+
version: 1,
|
|
96
|
+
gatewayUrl: normalizeGatewayUrl(parsed.gatewayUrl),
|
|
97
|
+
allowedRoots: normalizeAllowedRoots(parsed.allowedRoots),
|
|
98
|
+
proxy,
|
|
99
|
+
appCaPem,
|
|
100
|
+
securityProtocolFloor: Math.max(requestedFloor, appCaPem ? 2 : 1)
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
async update(patch) {
|
|
104
|
+
const current = await this.load();
|
|
105
|
+
const appCaPem = patch.appCaPem === undefined ? current.appCaPem : normalizeAppCa(patch.appCaPem);
|
|
106
|
+
const requestedFloor = patch.securityProtocolFloor === undefined
|
|
107
|
+
? current.securityProtocolFloor
|
|
108
|
+
: normalizeFloor(patch.securityProtocolFloor);
|
|
109
|
+
const next = {
|
|
110
|
+
version: 1,
|
|
111
|
+
gatewayUrl: patch.gatewayUrl === undefined ? current.gatewayUrl : normalizeGatewayUrl(patch.gatewayUrl),
|
|
112
|
+
allowedRoots: patch.allowedRoots === undefined ? current.allowedRoots : normalizeAllowedRoots(patch.allowedRoots),
|
|
113
|
+
proxy: patch.proxy === undefined ? current.proxy : normalizedProxy(patch.proxy),
|
|
114
|
+
appCaPem,
|
|
115
|
+
securityProtocolFloor: Math.max(current.securityProtocolFloor, requestedFloor, appCaPem ? 2 : 1)
|
|
116
|
+
};
|
|
117
|
+
await this.persist(next);
|
|
118
|
+
return next;
|
|
119
|
+
}
|
|
120
|
+
async persist(config) {
|
|
121
|
+
let proxy = { mode: 'direct' };
|
|
122
|
+
if (config.proxy.mode === 'configured' && config.proxy.url) {
|
|
123
|
+
if (this.platform === 'win32') {
|
|
124
|
+
const blob = await this.protectSecret(Buffer.from(config.proxy.url, 'utf8'));
|
|
125
|
+
const restored = await this.unprotectSecret(blob);
|
|
126
|
+
if (restored.toString('utf8') !== config.proxy.url)
|
|
127
|
+
throw new Error('DPAPI proxy verification failed.');
|
|
128
|
+
proxy = { mode: 'configured', protected: { scheme: 'dpapi-current-user-v1', blob } };
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
proxy = { mode: 'configured', url: config.proxy.url };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const persisted = {
|
|
135
|
+
version: 1,
|
|
136
|
+
gatewayUrl: config.gatewayUrl,
|
|
137
|
+
allowedRoots: config.allowedRoots,
|
|
138
|
+
proxy,
|
|
139
|
+
appCaPem: config.appCaPem,
|
|
140
|
+
securityProtocolFloor: config.securityProtocolFloor
|
|
141
|
+
};
|
|
142
|
+
await fs.mkdir(path.dirname(this.configPath), { recursive: true });
|
|
143
|
+
const temporary = `${this.configPath}.${process.pid}.${Date.now()}.tmp`;
|
|
144
|
+
await fs.writeFile(temporary, JSON.stringify(persisted, null, 2), { mode: 0o600 });
|
|
145
|
+
await fs.rename(temporary, this.configPath);
|
|
146
|
+
try {
|
|
147
|
+
await fs.chmod(this.configPath, 0o600);
|
|
148
|
+
}
|
|
149
|
+
catch { /* Windows ACLs inherit from the profile. */ }
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function proxyResolutionUrl(raw) {
|
|
153
|
+
const url = new URL(raw);
|
|
154
|
+
if (url.protocol === 'ws:')
|
|
155
|
+
url.protocol = 'http:';
|
|
156
|
+
if (url.protocol === 'wss:')
|
|
157
|
+
url.protocol = 'https:';
|
|
158
|
+
url.pathname = '/';
|
|
159
|
+
url.search = '';
|
|
160
|
+
url.hash = '';
|
|
161
|
+
return url.toString();
|
|
162
|
+
}
|
|
163
|
+
export function resolveProxyDecisionFromEnvironment(gatewayUrl) {
|
|
164
|
+
const explicit = PROXY_ENV_NAMES.some(name => process.env[name] !== undefined);
|
|
165
|
+
if (!explicit)
|
|
166
|
+
return { explicit: false, proxyUrl: null };
|
|
167
|
+
const resolved = String(getProxyForUrl(proxyResolutionUrl(gatewayUrl)) || '').trim();
|
|
168
|
+
return { explicit: true, proxyUrl: resolved || null };
|
|
169
|
+
}
|
|
170
|
+
function parseAllowedRootsEnvironment(value) {
|
|
171
|
+
let parsed;
|
|
172
|
+
try {
|
|
173
|
+
parsed = JSON.parse(value);
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
throw new Error('MCP_GATEWAY_ALLOWED_ROOTS must be a JSON array of absolute paths');
|
|
177
|
+
}
|
|
178
|
+
if (!Array.isArray(parsed))
|
|
179
|
+
throw new Error('MCP_GATEWAY_ALLOWED_ROOTS must be a JSON array of absolute paths');
|
|
180
|
+
return normalizeAllowedRoots(parsed);
|
|
181
|
+
}
|
|
182
|
+
export async function captureGatewayConfigFromEnvironment(store, options = {}) {
|
|
183
|
+
const current = await store.load();
|
|
184
|
+
const gatewayUrl = String(process.env.MCP_GATEWAY_URL || current.gatewayUrl || options.fallbackGatewayUrl || '').trim();
|
|
185
|
+
if (!gatewayUrl)
|
|
186
|
+
throw new Error('MCP_GATEWAY_URL is required for the first login or install.');
|
|
187
|
+
const patch = { gatewayUrl };
|
|
188
|
+
if (process.env.MCP_GATEWAY_ALLOWED_ROOTS !== undefined) {
|
|
189
|
+
patch.allowedRoots = parseAllowedRootsEnvironment(String(process.env.MCP_GATEWAY_ALLOWED_ROOTS || ''));
|
|
190
|
+
}
|
|
191
|
+
const proxy = resolveProxyDecisionFromEnvironment(gatewayUrl);
|
|
192
|
+
if (proxy.explicit) {
|
|
193
|
+
patch.proxy = proxy.proxyUrl
|
|
194
|
+
? { mode: 'configured', url: proxy.proxyUrl }
|
|
195
|
+
: { mode: 'direct', url: null };
|
|
196
|
+
}
|
|
197
|
+
const appCaPath = String(process.env.MCP_GATEWAY_APP_CA_PATH || '').trim();
|
|
198
|
+
if (appCaPath)
|
|
199
|
+
patch.appCaPem = await fs.readFile(path.resolve(appCaPath), 'utf8');
|
|
200
|
+
return await store.update(patch);
|
|
201
|
+
}
|
|
202
|
+
export function createGatewayProxyAgent(config) {
|
|
203
|
+
const proxyUrl = config.proxy.mode === 'configured' ? String(config.proxy.url || '') : '';
|
|
204
|
+
return {
|
|
205
|
+
agent: new ProxyAgent({ getProxyForUrl: () => proxyUrl })
|
|
206
|
+
};
|
|
207
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface GatewayIdentityRecord {
|
|
2
|
+
deviceId: string;
|
|
3
|
+
publicKeyPem: string;
|
|
4
|
+
privateKeyPem: string;
|
|
5
|
+
enrolled: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function deviceIdentityNeedsPairing(record: Pick<GatewayIdentityRecord, 'enrolled'>): boolean;
|
|
8
|
+
type SecretProtect = (value: Buffer) => Promise<string>;
|
|
9
|
+
type SecretUnprotect = (value: string) => Promise<Buffer>;
|
|
10
|
+
export declare class GatewayDeviceIdentity {
|
|
11
|
+
readonly identityPath: string;
|
|
12
|
+
private record?;
|
|
13
|
+
private platform;
|
|
14
|
+
private protectSecret;
|
|
15
|
+
private unprotectSecret;
|
|
16
|
+
constructor(identityPath?: string, options?: {
|
|
17
|
+
platform?: NodeJS.Platform | string;
|
|
18
|
+
protectSecret?: SecretProtect;
|
|
19
|
+
unprotectSecret?: SecretUnprotect;
|
|
20
|
+
});
|
|
21
|
+
loadOrCreate(): Promise<GatewayIdentityRecord>;
|
|
22
|
+
markEnrolled(): Promise<void>;
|
|
23
|
+
signChallenge(nonce: string): Promise<string>;
|
|
24
|
+
signChallengeV2(options: {
|
|
25
|
+
mode: 'reconnect' | 'enroll' | 'pair';
|
|
26
|
+
nonce: Buffer | string;
|
|
27
|
+
exporter: Buffer | string;
|
|
28
|
+
grant?: string | null;
|
|
29
|
+
}): Promise<string>;
|
|
30
|
+
private loadPersisted;
|
|
31
|
+
private protectAndVerify;
|
|
32
|
+
private persist;
|
|
33
|
+
private persistSerialized;
|
|
34
|
+
}
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, randomUUID, sign } from 'crypto';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { deviceStatePaths } from './device-state.js';
|
|
6
|
+
import { protectWindowsSecret, unprotectWindowsSecret } from './windows-dpapi.js';
|
|
7
|
+
export function deviceIdentityNeedsPairing(record) {
|
|
8
|
+
return record.enrolled !== true;
|
|
9
|
+
}
|
|
10
|
+
function defaultIdentityPath() {
|
|
11
|
+
const secureRoot = deviceStatePaths().root;
|
|
12
|
+
const fallback = deviceStatePaths().identity;
|
|
13
|
+
const configured = String(process.env.MCP_GATEWAY_DEVICE_IDENTITY_PATH || '').trim();
|
|
14
|
+
if (!configured)
|
|
15
|
+
return fallback;
|
|
16
|
+
const resolved = path.resolve(configured);
|
|
17
|
+
if (process.platform === 'win32') {
|
|
18
|
+
const relative = path.relative(secureRoot, resolved);
|
|
19
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
20
|
+
throw new Error('MCP_GATEWAY_DEVICE_IDENTITY_PATH must stay inside ~/.mcp-device on Windows');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return resolved;
|
|
24
|
+
}
|
|
25
|
+
function newDeviceId() {
|
|
26
|
+
const host = os.hostname().toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 54) || 'device';
|
|
27
|
+
return `${host}-${randomUUID().slice(0, 8)}`;
|
|
28
|
+
}
|
|
29
|
+
function validateRecord(value) {
|
|
30
|
+
if (!value || typeof value !== 'object')
|
|
31
|
+
throw new Error('Invalid gateway identity record');
|
|
32
|
+
const deviceId = String(value.deviceId || '').trim();
|
|
33
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(deviceId))
|
|
34
|
+
throw new Error('Invalid persisted device ID');
|
|
35
|
+
const publicKeyPem = String(value.publicKeyPem || '');
|
|
36
|
+
const privateKeyPem = String(value.privateKeyPem || '');
|
|
37
|
+
const publicKey = createPublicKey(publicKeyPem);
|
|
38
|
+
const privateKey = createPrivateKey(privateKeyPem);
|
|
39
|
+
if (publicKey.asymmetricKeyType !== 'ed25519' || privateKey.asymmetricKeyType !== 'ed25519') {
|
|
40
|
+
throw new Error('Gateway identity must use Ed25519 keys');
|
|
41
|
+
}
|
|
42
|
+
const normalizedPublic = publicKey.export({ type: 'spki', format: 'pem' }).toString();
|
|
43
|
+
const derivedPublic = createPublicKey(privateKey).export({ type: 'spki', format: 'pem' }).toString();
|
|
44
|
+
if (normalizedPublic !== derivedPublic)
|
|
45
|
+
throw new Error('Gateway identity private key does not match the stored public key');
|
|
46
|
+
return { deviceId, publicKeyPem: normalizedPublic, privateKeyPem, enrolled: value.enrolled === true };
|
|
47
|
+
}
|
|
48
|
+
function privateKeyDer(privateKeyPem) {
|
|
49
|
+
return createPrivateKey(privateKeyPem).export({ type: 'pkcs8', format: 'der' });
|
|
50
|
+
}
|
|
51
|
+
function privateKeyPemFromDer(value) {
|
|
52
|
+
return createPrivateKey({ key: value, type: 'pkcs8', format: 'der' }).export({ type: 'pkcs8', format: 'pem' }).toString();
|
|
53
|
+
}
|
|
54
|
+
export class GatewayDeviceIdentity {
|
|
55
|
+
constructor(identityPath = defaultIdentityPath(), options = {}) {
|
|
56
|
+
this.identityPath = identityPath;
|
|
57
|
+
this.platform = options.platform || process.platform;
|
|
58
|
+
this.protectSecret = options.protectSecret || (value => protectWindowsSecret(value, 'identity', { platform: this.platform }));
|
|
59
|
+
this.unprotectSecret = options.unprotectSecret || (value => unprotectWindowsSecret(value, 'identity', { platform: this.platform }));
|
|
60
|
+
}
|
|
61
|
+
async loadOrCreate() {
|
|
62
|
+
if (this.record)
|
|
63
|
+
return { ...this.record };
|
|
64
|
+
try {
|
|
65
|
+
const parsed = JSON.parse(await fs.readFile(this.identityPath, 'utf8'));
|
|
66
|
+
this.record = await this.loadPersisted(parsed);
|
|
67
|
+
return { ...this.record };
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
if (error?.code !== 'ENOENT')
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
const { publicKey, privateKey } = generateKeyPairSync('ed25519');
|
|
74
|
+
this.record = validateRecord({
|
|
75
|
+
deviceId: String(process.env.MCP_DEVICE_ID || '').trim() || newDeviceId(),
|
|
76
|
+
publicKeyPem: publicKey.export({ type: 'spki', format: 'pem' }).toString(),
|
|
77
|
+
privateKeyPem: privateKey.export({ type: 'pkcs8', format: 'pem' }).toString(),
|
|
78
|
+
enrolled: false
|
|
79
|
+
});
|
|
80
|
+
await this.persist();
|
|
81
|
+
return { ...this.record };
|
|
82
|
+
}
|
|
83
|
+
async markEnrolled() {
|
|
84
|
+
const current = await this.loadOrCreate();
|
|
85
|
+
if (current.enrolled)
|
|
86
|
+
return;
|
|
87
|
+
this.record = { ...current, enrolled: true };
|
|
88
|
+
await this.persist();
|
|
89
|
+
}
|
|
90
|
+
async signChallenge(nonce) {
|
|
91
|
+
const current = await this.loadOrCreate();
|
|
92
|
+
const challenge = Buffer.from(`mcp-device-auth-v1\n${current.deviceId}\n${nonce}`, 'utf8');
|
|
93
|
+
return sign(null, challenge, current.privateKeyPem).toString('base64');
|
|
94
|
+
}
|
|
95
|
+
async signChallengeV2(options) {
|
|
96
|
+
const current = await this.loadOrCreate();
|
|
97
|
+
const modeByte = { reconnect: 0, enroll: 1, pair: 2 }[options.mode];
|
|
98
|
+
const nonce = Buffer.isBuffer(options.nonce) ? options.nonce : Buffer.from(String(options.nonce || ''), 'base64url');
|
|
99
|
+
const exporter = Buffer.isBuffer(options.exporter) ? options.exporter : Buffer.from(String(options.exporter || ''), 'base64url');
|
|
100
|
+
if (nonce.length !== 32)
|
|
101
|
+
throw new Error('gateway nonce must be exactly 32 bytes.');
|
|
102
|
+
if (exporter.length !== 32)
|
|
103
|
+
throw new Error('TLS exporter must be exactly 32 bytes.');
|
|
104
|
+
const deviceBytes = Buffer.from(current.deviceId, 'utf8');
|
|
105
|
+
const deviceLength = Buffer.allocUnsafe(2);
|
|
106
|
+
deviceLength.writeUInt16BE(deviceBytes.length, 0);
|
|
107
|
+
const publicKeyDer = createPublicKey(current.publicKeyPem).export({ type: 'spki', format: 'der' });
|
|
108
|
+
const grantDigest = options.mode === 'reconnect'
|
|
109
|
+
? Buffer.alloc(32)
|
|
110
|
+
: createHash('sha256').update(String(options.grant || ''), 'utf8').digest();
|
|
111
|
+
const challenge = Buffer.concat([
|
|
112
|
+
Buffer.from('hcu-mcp-device-auth-v2\0', 'ascii'),
|
|
113
|
+
Buffer.from([2, modeByte]),
|
|
114
|
+
deviceLength,
|
|
115
|
+
deviceBytes,
|
|
116
|
+
nonce,
|
|
117
|
+
exporter,
|
|
118
|
+
createHash('sha256').update(publicKeyDer).digest(),
|
|
119
|
+
grantDigest
|
|
120
|
+
]);
|
|
121
|
+
return sign(null, challenge, current.privateKeyPem).toString('base64');
|
|
122
|
+
}
|
|
123
|
+
async loadPersisted(value) {
|
|
124
|
+
if (value?.privateKeyPem) {
|
|
125
|
+
const legacy = validateRecord(value);
|
|
126
|
+
if (this.platform !== 'win32')
|
|
127
|
+
return legacy;
|
|
128
|
+
const blob = await this.protectAndVerify(legacy);
|
|
129
|
+
await this.persistSerialized({
|
|
130
|
+
version: 2,
|
|
131
|
+
deviceId: legacy.deviceId,
|
|
132
|
+
publicKeyPem: legacy.publicKeyPem,
|
|
133
|
+
privateKey: { scheme: 'dpapi-current-user-v1', blob },
|
|
134
|
+
enrolled: legacy.enrolled
|
|
135
|
+
});
|
|
136
|
+
return legacy;
|
|
137
|
+
}
|
|
138
|
+
if (value?.privateKey?.scheme !== 'dpapi-current-user-v1' || !value.privateKey.blob) {
|
|
139
|
+
throw new Error('Invalid gateway identity protected private key');
|
|
140
|
+
}
|
|
141
|
+
if (this.platform !== 'win32')
|
|
142
|
+
throw new Error('DPAPI-protected gateway identity requires Windows');
|
|
143
|
+
const privateKeyPem = privateKeyPemFromDer(await this.unprotectSecret(String(value.privateKey.blob)));
|
|
144
|
+
return validateRecord({ ...value, privateKeyPem });
|
|
145
|
+
}
|
|
146
|
+
async protectAndVerify(record) {
|
|
147
|
+
const blob = await this.protectSecret(privateKeyDer(record.privateKeyPem));
|
|
148
|
+
const restoredPem = privateKeyPemFromDer(await this.unprotectSecret(blob));
|
|
149
|
+
validateRecord({ ...record, privateKeyPem: restoredPem });
|
|
150
|
+
return blob;
|
|
151
|
+
}
|
|
152
|
+
async persist() {
|
|
153
|
+
if (!this.record)
|
|
154
|
+
throw new Error('Gateway identity is not initialized');
|
|
155
|
+
if (this.platform === 'win32') {
|
|
156
|
+
const blob = await this.protectAndVerify(this.record);
|
|
157
|
+
await this.persistSerialized({
|
|
158
|
+
version: 2,
|
|
159
|
+
deviceId: this.record.deviceId,
|
|
160
|
+
publicKeyPem: this.record.publicKeyPem,
|
|
161
|
+
privateKey: { scheme: 'dpapi-current-user-v1', blob },
|
|
162
|
+
enrolled: this.record.enrolled
|
|
163
|
+
});
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
await this.persistSerialized(this.record);
|
|
167
|
+
}
|
|
168
|
+
async persistSerialized(value) {
|
|
169
|
+
await fs.mkdir(path.dirname(this.identityPath), { recursive: true });
|
|
170
|
+
const temporary = `${this.identityPath}.${process.pid}.${Date.now()}.tmp`;
|
|
171
|
+
await fs.writeFile(temporary, JSON.stringify(value, null, 2), { mode: 0o600 });
|
|
172
|
+
await fs.rename(temporary, this.identityPath);
|
|
173
|
+
try {
|
|
174
|
+
await fs.chmod(this.identityPath, 0o600);
|
|
175
|
+
}
|
|
176
|
+
catch { /* Windows ACLs are inherited from the user profile. */ }
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Agent } from 'http';
|
|
2
|
+
import fetch from 'cross-fetch';
|
|
3
|
+
import { GatewayDeviceIdentity } from './gateway-identity.js';
|
|
4
|
+
type AccountStatus = {
|
|
5
|
+
connected: boolean;
|
|
6
|
+
label: string | null;
|
|
7
|
+
};
|
|
8
|
+
export interface GatewayPairingResult {
|
|
9
|
+
enrollmentGrant: string;
|
|
10
|
+
deviceId: string;
|
|
11
|
+
account: AccountStatus;
|
|
12
|
+
}
|
|
13
|
+
export interface GatewayPairingOptions {
|
|
14
|
+
gatewayUrl: string;
|
|
15
|
+
identity: GatewayDeviceIdentity;
|
|
16
|
+
deviceName?: string;
|
|
17
|
+
fetchFn?: typeof fetch;
|
|
18
|
+
proxyAgent?: Agent;
|
|
19
|
+
openBrowser?: (url: string) => Promise<unknown>;
|
|
20
|
+
renderQr?: (value: string) => void;
|
|
21
|
+
sleep?: (ms: number) => Promise<void>;
|
|
22
|
+
log?: (value: string) => void;
|
|
23
|
+
}
|
|
24
|
+
export declare function pairGatewayDevice(options: GatewayPairingOptions): Promise<GatewayPairingResult>;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { createHash, randomBytes } from 'crypto';
|
|
2
|
+
import { createRequire } from 'module';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import open from 'open';
|
|
5
|
+
import fetch from 'cross-fetch';
|
|
6
|
+
import { gatewayHttpBase } from './gateway-url-policy.js';
|
|
7
|
+
const CLIENT_ID = 'mcp-device';
|
|
8
|
+
const SCOPE = 'mcp:tools';
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
10
|
+
function pkce() {
|
|
11
|
+
const verifier = randomBytes(32).toString('base64url');
|
|
12
|
+
const challenge = createHash('sha256').update(verifier).digest('base64url');
|
|
13
|
+
return { verifier, challenge };
|
|
14
|
+
}
|
|
15
|
+
function defaultQrRenderer(value) {
|
|
16
|
+
const qr = require('qrcode-terminal');
|
|
17
|
+
qr.generate(value, { small: true }, output => process.stdout.write(`${output}\n`));
|
|
18
|
+
}
|
|
19
|
+
async function jsonResponse(response) {
|
|
20
|
+
let payload = null;
|
|
21
|
+
try {
|
|
22
|
+
payload = await response.json();
|
|
23
|
+
}
|
|
24
|
+
catch { /* handled below */ }
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
const code = String(payload?.error || `http_${response.status}`);
|
|
27
|
+
const error = new Error(String(payload?.error_description || code));
|
|
28
|
+
error.code = code;
|
|
29
|
+
error.status = response.status;
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
return payload;
|
|
33
|
+
}
|
|
34
|
+
export async function pairGatewayDevice(options) {
|
|
35
|
+
const baseUrl = gatewayHttpBase(options.gatewayUrl);
|
|
36
|
+
const fetchFn = options.fetchFn || fetch;
|
|
37
|
+
const openBrowser = options.openBrowser || (url => open(url));
|
|
38
|
+
const renderQr = options.renderQr || defaultQrRenderer;
|
|
39
|
+
const sleep = options.sleep || (ms => new Promise(resolve => setTimeout(resolve, ms)));
|
|
40
|
+
const log = options.log || (value => console.log(value));
|
|
41
|
+
const identity = await options.identity.loadOrCreate();
|
|
42
|
+
const verifier = pkce();
|
|
43
|
+
const deviceName = String(options.deviceName || os.hostname() || identity.deviceId).trim().slice(0, 128) || identity.deviceId;
|
|
44
|
+
const start = await jsonResponse(await fetchFn(`${baseUrl}/device/start`, {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
headers: { 'content-type': 'application/json' },
|
|
47
|
+
...(options.proxyAgent ? { agent: options.proxyAgent } : {}),
|
|
48
|
+
body: JSON.stringify({
|
|
49
|
+
client_id: CLIENT_ID,
|
|
50
|
+
scope: SCOPE,
|
|
51
|
+
device_id: identity.deviceId,
|
|
52
|
+
device_name: deviceName,
|
|
53
|
+
public_key_pem: identity.publicKeyPem,
|
|
54
|
+
code_challenge: verifier.challenge,
|
|
55
|
+
code_challenge_method: 'S256'
|
|
56
|
+
})
|
|
57
|
+
}));
|
|
58
|
+
const verificationUrl = String(start.verification_uri_complete || start.verification_uri || '').trim();
|
|
59
|
+
if (!verificationUrl || !start.device_code || !start.user_code)
|
|
60
|
+
throw new Error('Gateway returned an incomplete device pairing response.');
|
|
61
|
+
log(`Gateway: ${baseUrl}`);
|
|
62
|
+
log(`Pair this device: ${verificationUrl}`);
|
|
63
|
+
log(`Code: ${start.user_code}`);
|
|
64
|
+
renderQr(verificationUrl);
|
|
65
|
+
try {
|
|
66
|
+
await openBrowser(verificationUrl);
|
|
67
|
+
}
|
|
68
|
+
catch { /* URL + QR remain usable */ }
|
|
69
|
+
const intervalMs = Math.max(1000, Math.min(30000, Number(start.interval || 2) * 1000));
|
|
70
|
+
const deadline = Date.now() + Math.max(1, Number(start.expires_in || 600)) * 1000;
|
|
71
|
+
while (Date.now() < deadline) {
|
|
72
|
+
try {
|
|
73
|
+
const payload = await jsonResponse(await fetchFn(`${baseUrl}/device/poll`, {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: { 'content-type': 'application/json' },
|
|
76
|
+
...(options.proxyAgent ? { agent: options.proxyAgent } : {}),
|
|
77
|
+
body: JSON.stringify({
|
|
78
|
+
device_code: start.device_code,
|
|
79
|
+
client_id: CLIENT_ID,
|
|
80
|
+
code_verifier: verifier.verifier
|
|
81
|
+
})
|
|
82
|
+
}));
|
|
83
|
+
const enrollmentGrant = String(payload?.enrollment_grant || '').trim();
|
|
84
|
+
const deviceId = String(payload?.device_id || identity.deviceId).trim();
|
|
85
|
+
if (!enrollmentGrant)
|
|
86
|
+
throw new Error('Gateway approval did not return an enrollment grant.');
|
|
87
|
+
if (deviceId !== identity.deviceId)
|
|
88
|
+
throw new Error('Gateway pairing response device_id mismatch.');
|
|
89
|
+
return {
|
|
90
|
+
enrollmentGrant,
|
|
91
|
+
deviceId,
|
|
92
|
+
account: {
|
|
93
|
+
connected: payload?.account?.connected === true,
|
|
94
|
+
label: payload?.account?.label ? String(payload.account.label) : null
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
if (error?.code !== 'authorization_pending')
|
|
100
|
+
throw error;
|
|
101
|
+
await sleep(intervalMs);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
throw new Error('Device pairing code expired before approval.');
|
|
105
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import tls from 'tls';
|
|
2
|
+
import { Duplex } from 'stream';
|
|
3
|
+
import WebSocket from 'ws';
|
|
4
|
+
export declare const DEVICE_INNER_TLS_SUBPROTOCOL = "hcu-device-v2-inner-tls";
|
|
5
|
+
export declare const DEVICE_MAX_MESSAGE_BYTES: number;
|
|
6
|
+
export declare class WebSocketDuplex extends Duplex {
|
|
7
|
+
private ws;
|
|
8
|
+
constructor(ws: WebSocket);
|
|
9
|
+
_read(): void;
|
|
10
|
+
_write(chunk: Buffer, _encoding: BufferEncoding, callback: (error?: Error | null) => void): void;
|
|
11
|
+
_destroy(error: Error | null, callback: (error?: Error | null) => void): void;
|
|
12
|
+
}
|
|
13
|
+
export declare function createWebSocketDuplex(ws: WebSocket): WebSocketDuplex;
|
|
14
|
+
export declare function createClientInnerTls(ws: WebSocket, options: {
|
|
15
|
+
ca: string | Buffer;
|
|
16
|
+
servername: string;
|
|
17
|
+
}): tls.TLSSocket;
|
|
18
|
+
export declare function exportDeviceAuthKeyingMaterial(socket: tls.TLSSocket, deviceId: string, mode: string): Buffer;
|
|
19
|
+
export declare function encodeJsonFrame(message: any, maxMessageBytes?: number): Buffer;
|
|
20
|
+
export declare function createJsonFrameParser(options: {
|
|
21
|
+
maxMessageBytes?: number;
|
|
22
|
+
onMessage: (message: any) => void;
|
|
23
|
+
}): {
|
|
24
|
+
push(value: Buffer | Uint8Array): void;
|
|
25
|
+
};
|