@hcu-lab.me/mcp-device 1.0.1 → 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.
@@ -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
- private platform;
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(), options = {}) {
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
- 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);
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
- 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
- }
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
- private platform;
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 loadPersisted;
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(), options = {}) {
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
- this.record = await this.loadPersisted(parsed);
67
- return { ...this.record };
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 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;
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) {
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.2.46";
1
+ export declare const VERSION = "1.0.2";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.2.46';
1
+ export const VERSION = '1.0.2';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hcu-lab.me/mcp-device",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "MCP Device securely connects a user-owned computer to an authenticated Model Context Protocol gateway.",
5
5
  "license": "MIT",
6
6
  "author": "Hirohimeow",
@@ -29,11 +29,11 @@
29
29
  "bump": "node scripts/sync-version.js --bump",
30
30
  "bump:minor": "node scripts/sync-version.js --bump --minor",
31
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",
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
33
  "watch": "tsc --watch",
34
34
  "start": "node dist/mcp-device.js",
35
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",
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
37
  "prepare": "npm run build",
38
38
  "clean": "shx rm -rf dist",
39
39
  "test": "npm run build && node test/run-all-tests.js",
@@ -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
- }