@hcu-lab.me/mcp-device 1.0.0 → 1.0.2
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/README.md +51 -51
- package/dist/device/gateway-config.d.ts +1 -9
- package/dist/device/gateway-config.js +7 -27
- package/dist/device/gateway-identity.d.ts +2 -11
- package/dist/device/gateway-identity.js +11 -52
- package/dist/device/official-trust.d.ts +1 -1
- package/dist/device/official-trust.js +11 -4
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +131 -131
- package/dist/device/windows-dpapi.d.ts +0 -11
- package/dist/device/windows-dpapi.js +0 -67
package/README.md
CHANGED
|
@@ -1,51 +1,51 @@
|
|
|
1
|
-
# MCP Device
|
|
2
|
-
|
|
3
|
-
`@hcu-lab.me/mcp-device` securely connects a user-owned computer to an authenticated Model Context Protocol gateway. The device makes the outbound connection; no inbound device port is required.
|
|
4
|
-
|
|
5
|
-
The gateway owns accounts, OAuth, device ownership, MCP tool contracts, routing, and usage aggregation. MCP Device owns local device identity, pairing, reconnect lifecycle, bounded execution, background registration, and local status.
|
|
6
|
-
|
|
7
|
-
## Commands
|
|
8
|
-
|
|
9
|
-
```text
|
|
10
|
-
mcp-device start in the foreground
|
|
11
|
-
mcp-device start start in the foreground
|
|
12
|
-
mcp-device login explicitly link or switch the gateway account
|
|
13
|
-
mcp-device logout unlink the gateway account from this device
|
|
14
|
-
mcp-device status show runtime, manager, connection, security, proxy, usage, and schema state
|
|
15
|
-
mcp-device install pair if needed, then install and start a background runtime
|
|
16
|
-
mcp-device stop stop the current runtime without deleting identity/account state
|
|
17
|
-
mcp-device uninstall remove background registration while preserving identity/account state
|
|
18
|
-
mcp-device --help show command help
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
`md` is an alias of `mcp-device` on POSIX shells. On Windows, use `mcp-device`; bare `md` has a shell collision with built-in directory-creation commands/aliases and is not the supported invocation.
|
|
22
|
-
|
|
23
|
-
On Windows, `install` uses the current-user background registration. On Linux, every `install` asks whether to use `systemd --user` or an already configured PM2 installation. MCP Device does not install PM2, run `pm2 startup`, or use `sudo` automatically.
|
|
24
|
-
|
|
25
|
-
## First run and trust
|
|
26
|
-
|
|
27
|
-
The official gateway is `https://mcp-v2.hcu-lab.me/mcp`. The official package is designed to pin an independently distributed application CA and require protocol v2 before the first network connection. If that CA is absent, MCP Device fails closed rather than learning trust from the gateway it is about to contact or falling back to protocol v1.
|
|
28
|
-
|
|
29
|
-
For a custom gateway, configure the gateway URL and provision its application CA through an independent trusted channel before enabling protocol v2. Proxy selection is captured during interactive provisioning and reused by the background runtime; proxy credentials are protected with Windows DPAPI on Windows.
|
|
30
|
-
|
|
31
|
-
## Local state and migration
|
|
32
|
-
|
|
33
|
-
Canonical state is stored under `~/.mcp-device/`. Existing `~/.hcu-device/` identity/config/status state is migrated non-destructively under serialized runtime ownership. A conflicting canonical and legacy identity fails closed rather than creating a second device identity.
|
|
34
|
-
|
|
35
|
-
The device identity is Ed25519. On Windows, the private key is protected with DPAPI for the current user. The gateway stores the public identity and account ownership.
|
|
36
|
-
|
|
37
|
-
## Development
|
|
38
|
-
|
|
39
|
-
```powershell
|
|
40
|
-
git clone https://github.com/Hiroshimeow/agent-mcp-device.git
|
|
41
|
-
cd agent-mcp-device
|
|
42
|
-
npm ci
|
|
43
|
-
npm run build
|
|
44
|
-
node dist/mcp-device.js --help
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
For a custom development gateway, set `MCP_GATEWAY_URL`. Supply `MCP_GATEWAY_APP_CA_PATH` from an independent trusted source when using protocol v2. Non-loopback plaintext gateway URLs are rejected.
|
|
48
|
-
|
|
49
|
-
## Upstream execution engine
|
|
50
|
-
|
|
51
|
-
The local execution engine is derived from [Desktop Commander MCP](https://github.com/wonderwhy-er/DesktopCommanderMCP). Desktop Commander is MIT licensed; its copyright and MIT license are preserved in this repository's `LICENSE`. The MCP Device product identity, gateway protocol, account/device lifecycle, security layer, and packaging are maintained separately in this repository.
|
|
1
|
+
# MCP Device
|
|
2
|
+
|
|
3
|
+
`@hcu-lab.me/mcp-device` securely connects a user-owned computer to an authenticated Model Context Protocol gateway. The device makes the outbound connection; no inbound device port is required.
|
|
4
|
+
|
|
5
|
+
The gateway owns accounts, OAuth, device ownership, MCP tool contracts, routing, and usage aggregation. MCP Device owns local device identity, pairing, reconnect lifecycle, bounded execution, background registration, and local status.
|
|
6
|
+
|
|
7
|
+
## Commands
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
mcp-device start in the foreground
|
|
11
|
+
mcp-device start start in the foreground
|
|
12
|
+
mcp-device login explicitly link or switch the gateway account
|
|
13
|
+
mcp-device logout unlink the gateway account from this device
|
|
14
|
+
mcp-device status show runtime, manager, connection, security, proxy, usage, and schema state
|
|
15
|
+
mcp-device install pair if needed, then install and start a background runtime
|
|
16
|
+
mcp-device stop stop the current runtime without deleting identity/account state
|
|
17
|
+
mcp-device uninstall remove background registration while preserving identity/account state
|
|
18
|
+
mcp-device --help show command help
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`md` is an alias of `mcp-device` on POSIX shells. On Windows, use `mcp-device`; bare `md` has a shell collision with built-in directory-creation commands/aliases and is not the supported invocation.
|
|
22
|
+
|
|
23
|
+
On Windows, `install` uses the current-user background registration. On Linux, every `install` asks whether to use `systemd --user` or an already configured PM2 installation. MCP Device does not install PM2, run `pm2 startup`, or use `sudo` automatically.
|
|
24
|
+
|
|
25
|
+
## First run and trust
|
|
26
|
+
|
|
27
|
+
The official gateway is `https://mcp-v2.hcu-lab.me/mcp`. The official package is designed to pin an independently distributed application CA and require protocol v2 before the first network connection. If that CA is absent, MCP Device fails closed rather than learning trust from the gateway it is about to contact or falling back to protocol v1.
|
|
28
|
+
|
|
29
|
+
For a custom gateway, configure the gateway URL and provision its application CA through an independent trusted channel before enabling protocol v2. Proxy selection is captured during interactive provisioning and reused by the background runtime; proxy credentials are protected with Windows DPAPI on Windows.
|
|
30
|
+
|
|
31
|
+
## Local state and migration
|
|
32
|
+
|
|
33
|
+
Canonical state is stored under `~/.mcp-device/`. Existing `~/.hcu-device/` identity/config/status state is migrated non-destructively under serialized runtime ownership. A conflicting canonical and legacy identity fails closed rather than creating a second device identity.
|
|
34
|
+
|
|
35
|
+
The device identity is Ed25519. On Windows, the private key is protected with DPAPI for the current user. The gateway stores the public identity and account ownership.
|
|
36
|
+
|
|
37
|
+
## Development
|
|
38
|
+
|
|
39
|
+
```powershell
|
|
40
|
+
git clone https://github.com/Hiroshimeow/agent-mcp-device.git
|
|
41
|
+
cd agent-mcp-device
|
|
42
|
+
npm ci
|
|
43
|
+
npm run build
|
|
44
|
+
node dist/mcp-device.js --help
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
For a custom development gateway, set `MCP_GATEWAY_URL`. Supply `MCP_GATEWAY_APP_CA_PATH` from an independent trusted source when using protocol v2. Non-loopback plaintext gateway URLs are rejected.
|
|
48
|
+
|
|
49
|
+
## Upstream execution engine
|
|
50
|
+
|
|
51
|
+
The local execution engine is derived from [Desktop Commander MCP](https://github.com/wonderwhy-er/DesktopCommanderMCP). Desktop Commander is MIT licensed; its copyright and MIT license are preserved in this repository's `LICENSE`. The MCP Device product identity, gateway protocol, account/device lifecycle, security layer, and packaging are maintained separately in this repository.
|
|
@@ -11,17 +11,10 @@ export interface GatewayDeviceConfig {
|
|
|
11
11
|
appCaPem: string | null;
|
|
12
12
|
securityProtocolFloor: number;
|
|
13
13
|
}
|
|
14
|
-
type SecretProtect = (value: Buffer) => Promise<string>;
|
|
15
|
-
type SecretUnprotect = (value: string) => Promise<Buffer>;
|
|
16
14
|
export declare class GatewayDeviceConfigStore {
|
|
17
15
|
readonly configPath: string;
|
|
18
|
-
|
|
19
|
-
private protectSecret;
|
|
20
|
-
private unprotectSecret;
|
|
21
|
-
constructor(configPath?: string, options?: {
|
|
16
|
+
constructor(configPath?: string, _options?: {
|
|
22
17
|
platform?: NodeJS.Platform | string;
|
|
23
|
-
protectSecret?: SecretProtect;
|
|
24
|
-
unprotectSecret?: SecretUnprotect;
|
|
25
18
|
});
|
|
26
19
|
load(): Promise<GatewayDeviceConfig>;
|
|
27
20
|
update(patch: Partial<GatewayDeviceConfig>): Promise<GatewayDeviceConfig>;
|
|
@@ -37,4 +30,3 @@ export declare function captureGatewayConfigFromEnvironment(store: GatewayDevice
|
|
|
37
30
|
export declare function createGatewayProxyAgent(config: GatewayDeviceConfig): {
|
|
38
31
|
agent: ProxyAgent;
|
|
39
32
|
};
|
|
40
|
-
export {};
|
|
@@ -4,7 +4,6 @@ import { createRequire } from 'module';
|
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { ProxyAgent } from 'proxy-agent';
|
|
6
6
|
import { deviceStatePaths } from './device-state.js';
|
|
7
|
-
import { protectWindowsSecret, unprotectWindowsSecret } from './windows-dpapi.js';
|
|
8
7
|
const require = createRequire(import.meta.url);
|
|
9
8
|
const { getProxyForUrl } = require('proxy-from-env');
|
|
10
9
|
const PROXY_ENV_NAMES = ['HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'ALL_PROXY', 'http_proxy', 'https_proxy', 'no_proxy', 'all_proxy'];
|
|
@@ -61,11 +60,8 @@ function normalizeFloor(value) {
|
|
|
61
60
|
return Math.max(1, parsed);
|
|
62
61
|
}
|
|
63
62
|
export class GatewayDeviceConfigStore {
|
|
64
|
-
constructor(configPath = defaultConfigPath(),
|
|
63
|
+
constructor(configPath = defaultConfigPath(), _options = {}) {
|
|
65
64
|
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
65
|
}
|
|
70
66
|
async load() {
|
|
71
67
|
let parsed;
|
|
@@ -79,16 +75,10 @@ export class GatewayDeviceConfigStore {
|
|
|
79
75
|
}
|
|
80
76
|
if (!parsed || typeof parsed !== 'object')
|
|
81
77
|
throw new Error('Invalid gateway config record.');
|
|
82
|
-
|
|
83
|
-
|
|
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);
|
|
78
|
+
if (parsed.proxy?.mode === 'configured' && parsed.proxy?.protected) {
|
|
79
|
+
throw new Error('Legacy protected proxy configuration is unsupported in MCP Device 1.0.2+. Re-run `mcp-device login` with your proxy environment configured.');
|
|
91
80
|
}
|
|
81
|
+
const proxy = normalizedProxy(parsed.proxy);
|
|
92
82
|
const appCaPem = normalizeAppCa(parsed.appCaPem);
|
|
93
83
|
const requestedFloor = normalizeFloor(parsed.securityProtocolFloor);
|
|
94
84
|
return {
|
|
@@ -118,19 +108,9 @@ export class GatewayDeviceConfigStore {
|
|
|
118
108
|
return next;
|
|
119
109
|
}
|
|
120
110
|
async persist(config) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
}
|
|
111
|
+
const proxy = config.proxy.mode === 'configured' && config.proxy.url
|
|
112
|
+
? { mode: 'configured', url: config.proxy.url }
|
|
113
|
+
: { mode: 'direct' };
|
|
134
114
|
const persisted = {
|
|
135
115
|
version: 1,
|
|
136
116
|
gatewayUrl: config.gatewayUrl,
|
|
@@ -5,18 +5,11 @@ export interface GatewayIdentityRecord {
|
|
|
5
5
|
enrolled: boolean;
|
|
6
6
|
}
|
|
7
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
8
|
export declare class GatewayDeviceIdentity {
|
|
11
9
|
readonly identityPath: string;
|
|
12
10
|
private record?;
|
|
13
|
-
|
|
14
|
-
private protectSecret;
|
|
15
|
-
private unprotectSecret;
|
|
16
|
-
constructor(identityPath?: string, options?: {
|
|
11
|
+
constructor(identityPath?: string, _options?: {
|
|
17
12
|
platform?: NodeJS.Platform | string;
|
|
18
|
-
protectSecret?: SecretProtect;
|
|
19
|
-
unprotectSecret?: SecretUnprotect;
|
|
20
13
|
});
|
|
21
14
|
loadOrCreate(): Promise<GatewayIdentityRecord>;
|
|
22
15
|
markEnrolled(): Promise<void>;
|
|
@@ -27,9 +20,7 @@ export declare class GatewayDeviceIdentity {
|
|
|
27
20
|
exporter: Buffer | string;
|
|
28
21
|
grant?: string | null;
|
|
29
22
|
}): Promise<string>;
|
|
30
|
-
private
|
|
31
|
-
private protectAndVerify;
|
|
23
|
+
private archiveLegacyProtectedIdentity;
|
|
32
24
|
private persist;
|
|
33
25
|
private persistSerialized;
|
|
34
26
|
}
|
|
35
|
-
export {};
|
|
@@ -3,7 +3,6 @@ import fs from 'fs/promises';
|
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { deviceStatePaths } from './device-state.js';
|
|
6
|
-
import { protectWindowsSecret, unprotectWindowsSecret } from './windows-dpapi.js';
|
|
7
6
|
export function deviceIdentityNeedsPairing(record) {
|
|
8
7
|
return record.enrolled !== true;
|
|
9
8
|
}
|
|
@@ -45,26 +44,22 @@ function validateRecord(value) {
|
|
|
45
44
|
throw new Error('Gateway identity private key does not match the stored public key');
|
|
46
45
|
return { deviceId, publicKeyPem: normalizedPublic, privateKeyPem, enrolled: value.enrolled === true };
|
|
47
46
|
}
|
|
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
47
|
export class GatewayDeviceIdentity {
|
|
55
|
-
constructor(identityPath = defaultIdentityPath(),
|
|
48
|
+
constructor(identityPath = defaultIdentityPath(), _options = {}) {
|
|
56
49
|
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
50
|
}
|
|
61
51
|
async loadOrCreate() {
|
|
62
52
|
if (this.record)
|
|
63
53
|
return { ...this.record };
|
|
64
54
|
try {
|
|
65
55
|
const parsed = JSON.parse(await fs.readFile(this.identityPath, 'utf8'));
|
|
66
|
-
|
|
67
|
-
|
|
56
|
+
if (parsed?.privateKey?.scheme) {
|
|
57
|
+
await this.archiveLegacyProtectedIdentity();
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
this.record = validateRecord(parsed);
|
|
61
|
+
return { ...this.record };
|
|
62
|
+
}
|
|
68
63
|
}
|
|
69
64
|
catch (error) {
|
|
70
65
|
if (error?.code !== 'ENOENT')
|
|
@@ -120,49 +115,13 @@ export class GatewayDeviceIdentity {
|
|
|
120
115
|
]);
|
|
121
116
|
return sign(null, challenge, current.privateKeyPem).toString('base64');
|
|
122
117
|
}
|
|
123
|
-
async
|
|
124
|
-
|
|
125
|
-
|
|
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;
|
|
118
|
+
async archiveLegacyProtectedIdentity() {
|
|
119
|
+
const archivePath = `${this.identityPath}.legacy-protected.${Date.now()}`;
|
|
120
|
+
await fs.rename(this.identityPath, archivePath);
|
|
151
121
|
}
|
|
152
122
|
async persist() {
|
|
153
123
|
if (!this.record)
|
|
154
124
|
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
125
|
await this.persistSerialized(this.record);
|
|
167
126
|
}
|
|
168
127
|
async persistSerialized(value) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { GatewayDeviceConfigStore, type GatewayDeviceConfig } from './gateway-config.js';
|
|
2
|
-
export declare const OFFICIAL_GATEWAY_URL = "https://
|
|
2
|
+
export declare const OFFICIAL_GATEWAY_URL = "https://device.hcu-lab.me";
|
|
3
3
|
export declare function isOfficialGatewayUrl(value: string | null | undefined): boolean;
|
|
4
4
|
export declare function bootstrapOfficialGatewayTrust(store: GatewayDeviceConfigStore, options?: {
|
|
5
5
|
gatewayUrl?: string | null;
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { X509Certificate } from 'crypto';
|
|
2
2
|
import fs from 'fs/promises';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
|
-
export const OFFICIAL_GATEWAY_URL = 'https://
|
|
4
|
+
export const OFFICIAL_GATEWAY_URL = 'https://device.hcu-lab.me';
|
|
5
5
|
function normalizedUrl(value) {
|
|
6
6
|
return new URL(value).toString();
|
|
7
7
|
}
|
|
8
|
+
function isOfficialGatewayHost(hostname) {
|
|
9
|
+
const host = hostname.toLowerCase();
|
|
10
|
+
return host === 'hcu-lab.me' || host.endsWith('.hcu-lab.me');
|
|
11
|
+
}
|
|
8
12
|
function defaultOfficialCaPath() {
|
|
9
13
|
return fileURLToPath(new URL('../data/official-app-ca.pem', import.meta.url));
|
|
10
14
|
}
|
|
@@ -12,7 +16,8 @@ export function isOfficialGatewayUrl(value) {
|
|
|
12
16
|
if (!value)
|
|
13
17
|
return false;
|
|
14
18
|
try {
|
|
15
|
-
|
|
19
|
+
const url = new URL(value);
|
|
20
|
+
return url.protocol === 'https:' && isOfficialGatewayHost(url.hostname);
|
|
16
21
|
}
|
|
17
22
|
catch {
|
|
18
23
|
return false;
|
|
@@ -28,7 +33,9 @@ export async function bootstrapOfficialGatewayTrust(store, options = {}) {
|
|
|
28
33
|
return await store.update({ gatewayUrl });
|
|
29
34
|
}
|
|
30
35
|
if (current.appCaPem && current.securityProtocolFloor >= 2 && isOfficialGatewayUrl(current.gatewayUrl || gatewayUrl)) {
|
|
31
|
-
|
|
36
|
+
if (current.gatewayUrl && normalizedUrl(current.gatewayUrl) === normalizedUrl(gatewayUrl))
|
|
37
|
+
return current;
|
|
38
|
+
return await store.update({ gatewayUrl });
|
|
32
39
|
}
|
|
33
40
|
const caPath = options.caPath || defaultOfficialCaPath();
|
|
34
41
|
let appCaPem;
|
|
@@ -42,7 +49,7 @@ export async function bootstrapOfficialGatewayTrust(store, options = {}) {
|
|
|
42
49
|
throw new Error(`Official MCP Device application CA is not provisioned in this package; refusing network bootstrap: ${error?.message || error}`);
|
|
43
50
|
}
|
|
44
51
|
return await store.update({
|
|
45
|
-
gatewayUrl
|
|
52
|
+
gatewayUrl,
|
|
46
53
|
appCaPem,
|
|
47
54
|
securityProtocolFloor: 2
|
|
48
55
|
});
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.2
|
|
1
|
+
export declare const VERSION = "1.0.2";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.2
|
|
1
|
+
export const VERSION = '1.0.2';
|
package/package.json
CHANGED
|
@@ -1,131 +1,131 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@hcu-lab.me/mcp-device",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "MCP Device securely connects a user-owned computer to an authenticated Model Context Protocol gateway.",
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"author": "Hirohimeow",
|
|
7
|
-
"homepage": "https://github.com/Hiroshimeow/agent-mcp-device",
|
|
8
|
-
"repository": {
|
|
9
|
-
"type": "git",
|
|
10
|
-
"url": "https://github.com/Hiroshimeow/agent-mcp-device.git"
|
|
11
|
-
},
|
|
12
|
-
"bugs": "https://github.com/Hiroshimeow/agent-mcp-device/issues",
|
|
13
|
-
"type": "module",
|
|
14
|
-
"engines": {
|
|
15
|
-
"node": ">=18.0.0"
|
|
16
|
-
},
|
|
17
|
-
"bin": {
|
|
18
|
-
"mcp-device": "dist/mcp-device.js",
|
|
19
|
-
"md": "dist/mcp-device.js"
|
|
20
|
-
},
|
|
21
|
-
"files": [
|
|
22
|
-
"dist"
|
|
23
|
-
],
|
|
24
|
-
"scripts": {
|
|
25
|
-
"device:install": "cd src/device && npm install",
|
|
26
|
-
"device:start": "tsx src/device/device.ts",
|
|
27
|
-
"device:start:dev": "nodemon --watch src/device --exec tsx src/device/device.ts",
|
|
28
|
-
"sync-version": "node scripts/sync-version.js",
|
|
29
|
-
"bump": "node scripts/sync-version.js --bump",
|
|
30
|
-
"bump:minor": "node scripts/sync-version.js --bump --minor",
|
|
31
|
-
"bump:major": "node scripts/sync-version.js --bump --major",
|
|
32
|
-
"build": "shx rm -rf dist/remote-device && shx rm -f dist/device/desktop-commander-integration.js dist/device/desktop-commander-integration.d.ts && tsc && shx chmod +x dist/*.js && shx mkdir -p dist/data && shx cp src/data/onboarding-prompts.json dist/data/ && node scripts/copy-official-ca.cjs && node scripts/build-ui-runtime.cjs",
|
|
33
|
-
"watch": "tsc --watch",
|
|
34
|
-
"start": "node dist/mcp-device.js",
|
|
35
|
-
"start:debug": "node --inspect-brk=9229 dist/mcp-device.js",
|
|
36
|
-
"prepack": "node scripts/verify-production-trust.cjs && shx rm -rf dist/remote-device && shx rm -f dist/device/desktop-commander-integration.js dist/device/desktop-commander-integration.d.ts dist/hcu-device.js dist/hcu-device.d.ts dist/track-installation.js dist/setup-claude-server.js dist/uninstall-claude-server.js dist/npm-scripts/setup.js dist/npm-scripts/setup.d.ts dist/npm-scripts/uninstall.js dist/npm-scripts/uninstall.d.ts dist/npm-scripts/verify-ripgrep.js dist/npm-scripts/verify-ripgrep.d.ts",
|
|
37
|
-
"prepare": "npm run build",
|
|
38
|
-
"clean": "shx rm -rf dist",
|
|
39
|
-
"test": "npm run build && node test/run-all-tests.js",
|
|
40
|
-
"test:integration": "npm run build && node test/integration/run-all-integration-tests.js",
|
|
41
|
-
"test:debug": "node --inspect test/run-all-tests.js",
|
|
42
|
-
"link:local": "npm run build && npm link",
|
|
43
|
-
"unlink:local": "npm unlink",
|
|
44
|
-
"inspector": "npx @modelcontextprotocol/inspector dist/index.js",
|
|
45
|
-
"logs:view": "npm run build && node scripts/view-fuzzy-logs.js",
|
|
46
|
-
"logs:analyze": "npm run build && node scripts/analyze-fuzzy-logs.js",
|
|
47
|
-
"logs:clear": "npm run build && node scripts/clear-fuzzy-logs.js",
|
|
48
|
-
"logs:export": "npm run build && node scripts/export-fuzzy-logs.js",
|
|
49
|
-
"count-tokens": "npm run build && node scripts/count-tokens.js",
|
|
50
|
-
"count-tokens:json": "npm run build && node scripts/count-tokens.js --json"
|
|
51
|
-
},
|
|
52
|
-
"keywords": [
|
|
53
|
-
"mcp",
|
|
54
|
-
"model-context-protocol",
|
|
55
|
-
"terminal",
|
|
56
|
-
"claude",
|
|
57
|
-
"ai",
|
|
58
|
-
"command-line",
|
|
59
|
-
"process-management",
|
|
60
|
-
"file-editing",
|
|
61
|
-
"code-editing",
|
|
62
|
-
"diff",
|
|
63
|
-
"patch",
|
|
64
|
-
"block-editing",
|
|
65
|
-
"file-system",
|
|
66
|
-
"text-manipulation",
|
|
67
|
-
"code-modification",
|
|
68
|
-
"surgical-edits",
|
|
69
|
-
"file-operations"
|
|
70
|
-
],
|
|
71
|
-
"dependencies": {
|
|
72
|
-
"@modelcontextprotocol/sdk": "^1.9.0",
|
|
73
|
-
"@opendocsg/pdf2md": "^0.2.2",
|
|
74
|
-
"@tiptap/core": "^3.31.3",
|
|
75
|
-
"@tiptap/extension-image": "^3.31.3",
|
|
76
|
-
"@tiptap/extension-table": "^3.31.3",
|
|
77
|
-
"@tiptap/extension-table-cell": "^3.31.3",
|
|
78
|
-
"@tiptap/extension-table-header": "^3.31.3",
|
|
79
|
-
"@tiptap/extension-table-row": "^3.31.3",
|
|
80
|
-
"@tiptap/pm": "^3.31.3",
|
|
81
|
-
"@tiptap/starter-kit": "^3.31.3",
|
|
82
|
-
"@vscode/ripgrep": "^1.15.9",
|
|
83
|
-
"cross-fetch": "^4.1.0",
|
|
84
|
-
"exceljs": "^4.4.0",
|
|
85
|
-
"fastest-levenshtein": "^1.0.16",
|
|
86
|
-
"file-type": "^21.1.1",
|
|
87
|
-
"highlight.js": "^11.11.1",
|
|
88
|
-
"isbinaryfile": "^5.0.4",
|
|
89
|
-
"markdown-it": "^14.1.0",
|
|
90
|
-
"md-to-pdf": "^5.2.5",
|
|
91
|
-
"open": "^10.2.0",
|
|
92
|
-
"pdf-lib": "^1.17.1",
|
|
93
|
-
"pizzip": "^3.2.0",
|
|
94
|
-
"proxy-agent": "6.5.0",
|
|
95
|
-
"proxy-from-env": "2.1.0",
|
|
96
|
-
"qrcode-terminal": "^0.12.0",
|
|
97
|
-
"remark": "^15.0.1",
|
|
98
|
-
"remark-gfm": "^4.0.1",
|
|
99
|
-
"remark-parse": "^11.0.0",
|
|
100
|
-
"sharp": "^0.35.4",
|
|
101
|
-
"tiptap-markdown": "^0.9.0",
|
|
102
|
-
"unified": "^11.0.5",
|
|
103
|
-
"unpdf": "^1.4.0",
|
|
104
|
-
"ws": "^8.18.3",
|
|
105
|
-
"zod": "^3.24.1",
|
|
106
|
-
"zod-to-json-schema": "^3.23.5"
|
|
107
|
-
},
|
|
108
|
-
"optionalDependencies": {
|
|
109
|
-
"caffeinate": "^1.0.1"
|
|
110
|
-
},
|
|
111
|
-
"devDependencies": {
|
|
112
|
-
"@modelcontextprotocol/ext-apps": "^1.0.1",
|
|
113
|
-
"@types/node": "^20.17.24",
|
|
114
|
-
"@types/ws": "^8.18.1",
|
|
115
|
-
"commander": "^13.1.0",
|
|
116
|
-
"esbuild": "^0.27.2",
|
|
117
|
-
"js-tiktoken": "^1.0.21",
|
|
118
|
-
"jsdom": "^24.1.3",
|
|
119
|
-
"nodemon": "^3.0.2",
|
|
120
|
-
"shx": "^0.3.4",
|
|
121
|
-
"ts-node": "^10.9.2",
|
|
122
|
-
"tsx": "^4.21.0",
|
|
123
|
-
"typescript": "^5.3.3"
|
|
124
|
-
},
|
|
125
|
-
"overrides": {
|
|
126
|
-
"@mapbox/node-pre-gyp": "^2.0.3",
|
|
127
|
-
"exceljs": {
|
|
128
|
-
"uuid": "^11.1.1"
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@hcu-lab.me/mcp-device",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "MCP Device securely connects a user-owned computer to an authenticated Model Context Protocol gateway.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Hirohimeow",
|
|
7
|
+
"homepage": "https://github.com/Hiroshimeow/agent-mcp-device",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/Hiroshimeow/agent-mcp-device.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": "https://github.com/Hiroshimeow/agent-mcp-device/issues",
|
|
13
|
+
"type": "module",
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18.0.0"
|
|
16
|
+
},
|
|
17
|
+
"bin": {
|
|
18
|
+
"mcp-device": "dist/mcp-device.js",
|
|
19
|
+
"md": "dist/mcp-device.js"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"device:install": "cd src/device && npm install",
|
|
26
|
+
"device:start": "tsx src/device/device.ts",
|
|
27
|
+
"device:start:dev": "nodemon --watch src/device --exec tsx src/device/device.ts",
|
|
28
|
+
"sync-version": "node scripts/sync-version.js",
|
|
29
|
+
"bump": "node scripts/sync-version.js --bump",
|
|
30
|
+
"bump:minor": "node scripts/sync-version.js --bump --minor",
|
|
31
|
+
"bump:major": "node scripts/sync-version.js --bump --major",
|
|
32
|
+
"build": "shx rm -rf dist/remote-device && shx rm -f dist/device/desktop-commander-integration.js dist/device/desktop-commander-integration.d.ts dist/device/windows-dpapi.js dist/device/windows-dpapi.d.ts && tsc && shx chmod +x dist/*.js && shx mkdir -p dist/data && shx cp src/data/onboarding-prompts.json dist/data/ && node scripts/copy-official-ca.cjs && node scripts/build-ui-runtime.cjs",
|
|
33
|
+
"watch": "tsc --watch",
|
|
34
|
+
"start": "node dist/mcp-device.js",
|
|
35
|
+
"start:debug": "node --inspect-brk=9229 dist/mcp-device.js",
|
|
36
|
+
"prepack": "node scripts/verify-production-trust.cjs && shx rm -rf dist/remote-device && shx rm -f dist/device/desktop-commander-integration.js dist/device/desktop-commander-integration.d.ts dist/device/windows-dpapi.js dist/device/windows-dpapi.d.ts dist/hcu-device.js dist/hcu-device.d.ts dist/track-installation.js dist/setup-claude-server.js dist/uninstall-claude-server.js dist/npm-scripts/setup.js dist/npm-scripts/setup.d.ts dist/npm-scripts/uninstall.js dist/npm-scripts/uninstall.d.ts dist/npm-scripts/verify-ripgrep.js dist/npm-scripts/verify-ripgrep.d.ts",
|
|
37
|
+
"prepare": "npm run build",
|
|
38
|
+
"clean": "shx rm -rf dist",
|
|
39
|
+
"test": "npm run build && node test/run-all-tests.js",
|
|
40
|
+
"test:integration": "npm run build && node test/integration/run-all-integration-tests.js",
|
|
41
|
+
"test:debug": "node --inspect test/run-all-tests.js",
|
|
42
|
+
"link:local": "npm run build && npm link",
|
|
43
|
+
"unlink:local": "npm unlink",
|
|
44
|
+
"inspector": "npx @modelcontextprotocol/inspector dist/index.js",
|
|
45
|
+
"logs:view": "npm run build && node scripts/view-fuzzy-logs.js",
|
|
46
|
+
"logs:analyze": "npm run build && node scripts/analyze-fuzzy-logs.js",
|
|
47
|
+
"logs:clear": "npm run build && node scripts/clear-fuzzy-logs.js",
|
|
48
|
+
"logs:export": "npm run build && node scripts/export-fuzzy-logs.js",
|
|
49
|
+
"count-tokens": "npm run build && node scripts/count-tokens.js",
|
|
50
|
+
"count-tokens:json": "npm run build && node scripts/count-tokens.js --json"
|
|
51
|
+
},
|
|
52
|
+
"keywords": [
|
|
53
|
+
"mcp",
|
|
54
|
+
"model-context-protocol",
|
|
55
|
+
"terminal",
|
|
56
|
+
"claude",
|
|
57
|
+
"ai",
|
|
58
|
+
"command-line",
|
|
59
|
+
"process-management",
|
|
60
|
+
"file-editing",
|
|
61
|
+
"code-editing",
|
|
62
|
+
"diff",
|
|
63
|
+
"patch",
|
|
64
|
+
"block-editing",
|
|
65
|
+
"file-system",
|
|
66
|
+
"text-manipulation",
|
|
67
|
+
"code-modification",
|
|
68
|
+
"surgical-edits",
|
|
69
|
+
"file-operations"
|
|
70
|
+
],
|
|
71
|
+
"dependencies": {
|
|
72
|
+
"@modelcontextprotocol/sdk": "^1.9.0",
|
|
73
|
+
"@opendocsg/pdf2md": "^0.2.2",
|
|
74
|
+
"@tiptap/core": "^3.31.3",
|
|
75
|
+
"@tiptap/extension-image": "^3.31.3",
|
|
76
|
+
"@tiptap/extension-table": "^3.31.3",
|
|
77
|
+
"@tiptap/extension-table-cell": "^3.31.3",
|
|
78
|
+
"@tiptap/extension-table-header": "^3.31.3",
|
|
79
|
+
"@tiptap/extension-table-row": "^3.31.3",
|
|
80
|
+
"@tiptap/pm": "^3.31.3",
|
|
81
|
+
"@tiptap/starter-kit": "^3.31.3",
|
|
82
|
+
"@vscode/ripgrep": "^1.15.9",
|
|
83
|
+
"cross-fetch": "^4.1.0",
|
|
84
|
+
"exceljs": "^4.4.0",
|
|
85
|
+
"fastest-levenshtein": "^1.0.16",
|
|
86
|
+
"file-type": "^21.1.1",
|
|
87
|
+
"highlight.js": "^11.11.1",
|
|
88
|
+
"isbinaryfile": "^5.0.4",
|
|
89
|
+
"markdown-it": "^14.1.0",
|
|
90
|
+
"md-to-pdf": "^5.2.5",
|
|
91
|
+
"open": "^10.2.0",
|
|
92
|
+
"pdf-lib": "^1.17.1",
|
|
93
|
+
"pizzip": "^3.2.0",
|
|
94
|
+
"proxy-agent": "6.5.0",
|
|
95
|
+
"proxy-from-env": "2.1.0",
|
|
96
|
+
"qrcode-terminal": "^0.12.0",
|
|
97
|
+
"remark": "^15.0.1",
|
|
98
|
+
"remark-gfm": "^4.0.1",
|
|
99
|
+
"remark-parse": "^11.0.0",
|
|
100
|
+
"sharp": "^0.35.4",
|
|
101
|
+
"tiptap-markdown": "^0.9.0",
|
|
102
|
+
"unified": "^11.0.5",
|
|
103
|
+
"unpdf": "^1.4.0",
|
|
104
|
+
"ws": "^8.18.3",
|
|
105
|
+
"zod": "^3.24.1",
|
|
106
|
+
"zod-to-json-schema": "^3.23.5"
|
|
107
|
+
},
|
|
108
|
+
"optionalDependencies": {
|
|
109
|
+
"caffeinate": "^1.0.1"
|
|
110
|
+
},
|
|
111
|
+
"devDependencies": {
|
|
112
|
+
"@modelcontextprotocol/ext-apps": "^1.0.1",
|
|
113
|
+
"@types/node": "^20.17.24",
|
|
114
|
+
"@types/ws": "^8.18.1",
|
|
115
|
+
"commander": "^13.1.0",
|
|
116
|
+
"esbuild": "^0.27.2",
|
|
117
|
+
"js-tiktoken": "^1.0.21",
|
|
118
|
+
"jsdom": "^24.1.3",
|
|
119
|
+
"nodemon": "^3.0.2",
|
|
120
|
+
"shx": "^0.3.4",
|
|
121
|
+
"ts-node": "^10.9.2",
|
|
122
|
+
"tsx": "^4.21.0",
|
|
123
|
+
"typescript": "^5.3.3"
|
|
124
|
+
},
|
|
125
|
+
"overrides": {
|
|
126
|
+
"@mapbox/node-pre-gyp": "^2.0.3",
|
|
127
|
+
"exceljs": {
|
|
128
|
+
"uuid": "^11.1.1"
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
type DpapiLabel = 'identity' | 'proxy';
|
|
2
|
-
type PowerShellRunner = (script: string, input: string) => Promise<string>;
|
|
3
|
-
export declare function protectWindowsSecret(secret: Buffer | Uint8Array | string, label: DpapiLabel, options?: {
|
|
4
|
-
platform?: NodeJS.Platform | string;
|
|
5
|
-
runner?: PowerShellRunner;
|
|
6
|
-
}): Promise<string>;
|
|
7
|
-
export declare function unprotectWindowsSecret(blob: string, label: DpapiLabel, options?: {
|
|
8
|
-
platform?: NodeJS.Platform | string;
|
|
9
|
-
runner?: PowerShellRunner;
|
|
10
|
-
}): Promise<Buffer>;
|
|
11
|
-
export {};
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'child_process';
|
|
2
|
-
const ENTROPY = {
|
|
3
|
-
identity: 'hcu-device-identity-private-key-v1',
|
|
4
|
-
proxy: 'hcu-device-proxy-url-v1'
|
|
5
|
-
};
|
|
6
|
-
function assertLabel(label) {
|
|
7
|
-
if (!(label in ENTROPY))
|
|
8
|
-
throw new Error('Unsupported DPAPI secret label.');
|
|
9
|
-
}
|
|
10
|
-
async function runPowerShell(script, input) {
|
|
11
|
-
return await new Promise((resolve, reject) => {
|
|
12
|
-
const child = spawn('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script], {
|
|
13
|
-
windowsHide: true,
|
|
14
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
15
|
-
});
|
|
16
|
-
const stdout = [];
|
|
17
|
-
const stderr = [];
|
|
18
|
-
child.stdout.on('data', chunk => stdout.push(Buffer.from(chunk)));
|
|
19
|
-
child.stderr.on('data', chunk => stderr.push(Buffer.from(chunk)));
|
|
20
|
-
child.once('error', reject);
|
|
21
|
-
child.once('close', code => {
|
|
22
|
-
if (code !== 0) {
|
|
23
|
-
reject(new Error(Buffer.concat(stderr).toString('utf8').trim() || `DPAPI PowerShell failed with exit code ${code}`));
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
resolve(Buffer.concat(stdout).toString('utf8').trim());
|
|
27
|
-
});
|
|
28
|
-
child.stdin.end(input, 'utf8');
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
function dpapiScript(action, label) {
|
|
32
|
-
const entropy = Buffer.from(ENTROPY[label], 'utf8').toString('base64');
|
|
33
|
-
return [
|
|
34
|
-
"$ErrorActionPreference = 'Stop'",
|
|
35
|
-
'Add-Type -AssemblyName System.Security',
|
|
36
|
-
'$inputB64 = [Console]::In.ReadToEnd().Trim()',
|
|
37
|
-
'$data = [Convert]::FromBase64String($inputB64)',
|
|
38
|
-
`$entropy = [Convert]::FromBase64String('${entropy}')`,
|
|
39
|
-
'$scope = [System.Security.Cryptography.DataProtectionScope]::CurrentUser',
|
|
40
|
-
`$output = [System.Security.Cryptography.ProtectedData]::${action}($data, $entropy, $scope)`,
|
|
41
|
-
'[Console]::Out.Write([Convert]::ToBase64String($output))'
|
|
42
|
-
].join('; ');
|
|
43
|
-
}
|
|
44
|
-
export async function protectWindowsSecret(secret, label, options = {}) {
|
|
45
|
-
const platform = options.platform || process.platform;
|
|
46
|
-
if (platform !== 'win32')
|
|
47
|
-
throw new Error('Windows DPAPI is only available on Windows.');
|
|
48
|
-
assertLabel(label);
|
|
49
|
-
const bytes = typeof secret === 'string' ? Buffer.from(secret, 'utf8') : Buffer.from(secret);
|
|
50
|
-
const output = await (options.runner || runPowerShell)(dpapiScript('Protect', label), bytes.toString('base64'));
|
|
51
|
-
if (!output)
|
|
52
|
-
throw new Error('Windows DPAPI returned an empty protected value.');
|
|
53
|
-
return output;
|
|
54
|
-
}
|
|
55
|
-
export async function unprotectWindowsSecret(blob, label, options = {}) {
|
|
56
|
-
const platform = options.platform || process.platform;
|
|
57
|
-
if (platform !== 'win32')
|
|
58
|
-
throw new Error('Windows DPAPI is only available on Windows.');
|
|
59
|
-
assertLabel(label);
|
|
60
|
-
const input = String(blob || '').trim();
|
|
61
|
-
if (!input)
|
|
62
|
-
throw new Error('Windows DPAPI protected value is empty.');
|
|
63
|
-
const output = await (options.runner || runPowerShell)(dpapiScript('Unprotect', label), input);
|
|
64
|
-
if (!output)
|
|
65
|
-
throw new Error('Windows DPAPI returned an empty unprotected value.');
|
|
66
|
-
return Buffer.from(output, 'base64');
|
|
67
|
-
}
|