u-chan-secrets 3.0.2 → 4.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.
@@ -1,148 +1,148 @@
1
- export const SERVICE_HOST_CSHARP = `using System;
2
- using System.Diagnostics;
3
- using System.ServiceProcess;
4
- using System.Threading;
5
-
6
- namespace UChan.Platform.Windows.Assets
7
- {
8
- public class UChanService : ServiceBase
9
- {
10
- private Process _process;
11
- private readonly string[] _args;
12
- private volatile bool _stopping;
13
- private string _targetExe;
14
- private string _targetArgs;
15
-
16
- public UChanService(string[] args)
17
- {
18
- ServiceName = "UChanSecrets";
19
- _args = args;
20
- }
21
-
22
- protected override void OnStart(string[] args)
23
- {
24
- _stopping = false;
25
- string[] effectiveArgs = (args != null && args.Length > 0) ? args : _args;
26
- string targetExe = null;
27
- string targetArgs = "";
28
-
29
- if (effectiveArgs != null && effectiveArgs.Length > 0)
30
- {
31
- targetExe = effectiveArgs[0];
32
- if (effectiveArgs.Length > 1)
33
- {
34
- string[] rest = new string[effectiveArgs.Length - 1];
35
- Array.Copy(effectiveArgs, 1, rest, 0, rest.Length);
36
- targetArgs = string.Join(" ", rest);
37
- }
38
- }
39
- else
40
- {
41
- string baseDir = AppDomain.CurrentDomain.BaseDirectory;
42
- string targetFile = System.IO.Path.Combine(baseDir, "service-target.txt");
43
- if (System.IO.File.Exists(targetFile))
44
- {
45
- string[] lines = System.IO.File.ReadAllLines(targetFile);
46
- if (lines.Length > 0)
47
- {
48
- targetExe = lines[0].Trim();
49
- if (lines.Length > 1)
50
- {
51
- targetArgs = lines[1].Trim();
52
- }
53
- }
54
- }
55
- }
56
-
57
- if (string.IsNullOrEmpty(targetExe))
58
- {
59
- throw new InvalidOperationException("No target executable specified for UChanSecrets service.");
60
- }
61
-
62
- _targetExe = targetExe;
63
- _targetArgs = targetArgs;
64
- StartChild();
65
- }
66
-
67
- private void StartChild()
68
- {
69
- if (_stopping) return;
70
- ProcessStartInfo psi = new ProcessStartInfo();
71
- psi.FileName = _targetExe;
72
- psi.Arguments = _targetArgs;
73
- psi.UseShellExecute = false;
74
- psi.CreateNoWindow = true;
75
-
76
- string baseDir = AppDomain.CurrentDomain.BaseDirectory;
77
-
78
- string logFile = System.IO.Path.GetFullPath(System.IO.Path.Combine(baseDir, "..", "service.log"));
79
- psi.RedirectStandardOutput = true;
80
- psi.RedirectStandardError = true;
81
-
82
- Process proc = new Process();
83
- proc.StartInfo = psi;
84
- proc.EnableRaisingEvents = true;
85
- proc.OutputDataReceived += (s, e) => {
86
- if (!string.IsNullOrEmpty(e.Data)) {
87
- try { System.IO.File.AppendAllText(logFile, string.Format("[{0:s}] {1}", DateTime.Now, e.Data) + Environment.NewLine); } catch {}
88
- }
89
- };
90
- proc.ErrorDataReceived += (s, e) => {
91
- if (!string.IsNullOrEmpty(e.Data)) {
92
- try { System.IO.File.AppendAllText(logFile, string.Format("[{0:s}] [ERR] {1}", DateTime.Now, e.Data) + Environment.NewLine); } catch {}
93
- }
94
- };
95
- proc.Exited += OnChildExited;
96
- proc.Start();
97
- proc.BeginOutputReadLine();
98
- proc.BeginErrorReadLine();
99
- _process = proc;
100
- }
101
-
102
- private void OnChildExited(object sender, EventArgs e)
103
- {
104
- if (_stopping) return;
105
- Thread.Sleep(500);
106
- if (_stopping) return;
107
- try
108
- {
109
- StartChild();
110
- }
111
- catch
112
- {
113
- }
114
- }
115
-
116
- protected override void OnStop()
117
- {
118
- _stopping = true;
119
- if (_process != null && !_process.HasExited)
120
- {
121
- try
122
- {
123
- Process killer = Process.Start("taskkill.exe", string.Format("/F /T /PID {0}", _process.Id));
124
- if (killer != null)
125
- {
126
- killer.WaitForExit(5000);
127
- }
128
- }
129
- catch
130
- {
131
- try
132
- {
133
- _process.Kill();
134
- }
135
- catch
136
- {
137
- }
138
- }
139
- }
140
- }
141
-
142
- public static void Main(string[] args)
143
- {
144
- ServiceBase.Run(new UChanService(args));
145
- }
146
- }
147
- }
1
+ export const SERVICE_HOST_CSHARP = `using System;
2
+ using System.Diagnostics;
3
+ using System.ServiceProcess;
4
+ using System.Threading;
5
+
6
+ namespace UChan.Platform.Windows.Assets
7
+ {
8
+ public class UChanService : ServiceBase
9
+ {
10
+ private Process _process;
11
+ private readonly string[] _args;
12
+ private volatile bool _stopping;
13
+ private string _targetExe;
14
+ private string _targetArgs;
15
+
16
+ public UChanService(string[] args)
17
+ {
18
+ ServiceName = "UChanSecrets";
19
+ _args = args;
20
+ }
21
+
22
+ protected override void OnStart(string[] args)
23
+ {
24
+ _stopping = false;
25
+ string[] effectiveArgs = (args != null && args.Length > 0) ? args : _args;
26
+ string targetExe = null;
27
+ string targetArgs = "";
28
+
29
+ if (effectiveArgs != null && effectiveArgs.Length > 0)
30
+ {
31
+ targetExe = effectiveArgs[0];
32
+ if (effectiveArgs.Length > 1)
33
+ {
34
+ string[] rest = new string[effectiveArgs.Length - 1];
35
+ Array.Copy(effectiveArgs, 1, rest, 0, rest.Length);
36
+ targetArgs = string.Join(" ", rest);
37
+ }
38
+ }
39
+ else
40
+ {
41
+ string baseDir = AppDomain.CurrentDomain.BaseDirectory;
42
+ string targetFile = System.IO.Path.Combine(baseDir, "service-target.txt");
43
+ if (System.IO.File.Exists(targetFile))
44
+ {
45
+ string[] lines = System.IO.File.ReadAllLines(targetFile);
46
+ if (lines.Length > 0)
47
+ {
48
+ targetExe = lines[0].Trim();
49
+ if (lines.Length > 1)
50
+ {
51
+ targetArgs = lines[1].Trim();
52
+ }
53
+ }
54
+ }
55
+ }
56
+
57
+ if (string.IsNullOrEmpty(targetExe))
58
+ {
59
+ throw new InvalidOperationException("No target executable specified for UChanSecrets service.");
60
+ }
61
+
62
+ _targetExe = targetExe;
63
+ _targetArgs = targetArgs;
64
+ StartChild();
65
+ }
66
+
67
+ private void StartChild()
68
+ {
69
+ if (_stopping) return;
70
+ ProcessStartInfo psi = new ProcessStartInfo();
71
+ psi.FileName = _targetExe;
72
+ psi.Arguments = _targetArgs;
73
+ psi.UseShellExecute = false;
74
+ psi.CreateNoWindow = true;
75
+
76
+ string baseDir = AppDomain.CurrentDomain.BaseDirectory;
77
+
78
+ string logFile = System.IO.Path.GetFullPath(System.IO.Path.Combine(baseDir, "..", "service.log"));
79
+ psi.RedirectStandardOutput = true;
80
+ psi.RedirectStandardError = true;
81
+
82
+ Process proc = new Process();
83
+ proc.StartInfo = psi;
84
+ proc.EnableRaisingEvents = true;
85
+ proc.OutputDataReceived += (s, e) => {
86
+ if (!string.IsNullOrEmpty(e.Data)) {
87
+ try { System.IO.File.AppendAllText(logFile, string.Format("[{0:s}] {1}", DateTime.Now, e.Data) + Environment.NewLine); } catch {}
88
+ }
89
+ };
90
+ proc.ErrorDataReceived += (s, e) => {
91
+ if (!string.IsNullOrEmpty(e.Data)) {
92
+ try { System.IO.File.AppendAllText(logFile, string.Format("[{0:s}] [ERR] {1}", DateTime.Now, e.Data) + Environment.NewLine); } catch {}
93
+ }
94
+ };
95
+ proc.Exited += OnChildExited;
96
+ proc.Start();
97
+ proc.BeginOutputReadLine();
98
+ proc.BeginErrorReadLine();
99
+ _process = proc;
100
+ }
101
+
102
+ private void OnChildExited(object sender, EventArgs e)
103
+ {
104
+ if (_stopping) return;
105
+ Thread.Sleep(500);
106
+ if (_stopping) return;
107
+ try
108
+ {
109
+ StartChild();
110
+ }
111
+ catch
112
+ {
113
+ }
114
+ }
115
+
116
+ protected override void OnStop()
117
+ {
118
+ _stopping = true;
119
+ if (_process != null && !_process.HasExited)
120
+ {
121
+ try
122
+ {
123
+ Process killer = Process.Start("taskkill.exe", string.Format("/F /T /PID {0}", _process.Id));
124
+ if (killer != null)
125
+ {
126
+ killer.WaitForExit(5000);
127
+ }
128
+ }
129
+ catch
130
+ {
131
+ try
132
+ {
133
+ _process.Kill();
134
+ }
135
+ catch
136
+ {
137
+ }
138
+ }
139
+ }
140
+ }
141
+
142
+ public static void Main(string[] args)
143
+ {
144
+ ServiceBase.Run(new UChanService(args));
145
+ }
146
+ }
147
+ }
148
148
  `;
@@ -1,5 +1,7 @@
1
1
  import { commandState } from '../../core/doctor.js';
2
2
  export const checkWindowsRegistration = () => commandState('sc.exe', ['query', 'u-chan-secrets'], s => /SERVICE_NAME:\s*u-chan-secrets/i.test(s));
3
+ export const checkWindowsUac = () => commandState('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command',
4
+ "$p=Get-ItemProperty -LiteralPath 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System'; if($p.EnableLUA -eq 1 -and $p.ConsentPromptBehaviorAdmin -eq 2 -and $p.PromptOnSecureDesktop -eq 1){'SAFE'}else{'UNSAFE'}"], s => s.trim() === 'SAFE');
3
5
  export function checkWindowsPermissions(home) {
4
6
  const encoded = Buffer.from(home, 'utf8').toString('base64');
5
7
  const script = `$ErrorActionPreference='Stop'; $p=[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encoded}')); $root=Get-Item -LiteralPath $p -Force; if($root.Attributes -band [IO.FileAttributes]::ReparsePoint){'UNSAFE';exit}; $items=@($root)+@(Get-ChildItem -LiteralPath $p -Force -Recurse); foreach($i in $items){if($i.Attributes -band [IO.FileAttributes]::ReparsePoint){'UNSAFE';exit}; $acl=$i.GetAccessControl(); $owner=$acl.GetOwner([Security.Principal.SecurityIdentifier]).Value; if($owner -notin @('S-1-5-18','S-1-5-32-544')){'UNSAFE';exit}; foreach($ace in $acl.GetAccessRules($true,$true,[Security.Principal.SecurityIdentifier])){if($ace.AccessControlType -eq 'Allow' -and $ace.IdentityReference.Value -notin @('S-1-5-18','S-1-5-32-544')){'UNSAFE';exit}} }; 'SAFE'`;
@@ -14,7 +14,7 @@ export class ControlService {
14
14
  this.grants = d.grants ?? new GrantCoordinator(d.allowlist);
15
15
  }
16
16
  async capabilities() {
17
- return { ok: true, message: JSON.stringify({ protocol: 3, features: ['doctor', 'success_conditions', 'grants'] }) };
17
+ return { ok: true, message: JSON.stringify({ protocol: 3, features: ['doctor', 'success_conditions', 'grants'], serviceVersion: this.d.serviceVersion }) };
18
18
  }
19
19
  async diagnose() {
20
20
  try {
@@ -1,14 +1,14 @@
1
1
  import { lstat } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
3
  import { privateBrowserPath } from '../platform/runtime-assets.js';
4
- import { bounded } from '../core/doctor.js';
4
+ import { bounded, chromeState, safeCdpOrigin } from '../core/doctor.js';
5
5
  import { checkWindowsPermissions } from '../platform/windows/doctor.js';
6
6
  import { checkDarwinPermissions } from '../platform/darwin/doctor.js';
7
- export async function diagnoseService(home, vault, platform) {
7
+ export async function diagnoseService(home, vault, platform, cdpEndpoint) {
8
8
  const paths = [join(home, 'program', 'dist', 'service', 'main.js'), join(home, 'program', 'node_modules'),
9
9
  platform === 'win32' ? join(home, 'bin', 'node.exe') : process.execPath,
10
10
  platform === 'win32' ? join(home, 'bin', 'bw.exe') : join(home, 'bin', 'bw')];
11
- const [assets, permissions, status] = await Promise.all([
11
+ const [assets, permissions, status, browser] = await Promise.all([
12
12
  bounded(async () => {
13
13
  await privateBrowserPath(home);
14
14
  for (const p of paths) {
@@ -20,6 +20,8 @@ export async function diagnoseService(home, vault, platform) {
20
20
  }, 'unknown'),
21
21
  bounded(() => platform === 'win32' ? checkWindowsPermissions(home) : checkDarwinPermissions(home), 'unknown', 20000),
22
22
  bounded(() => vault.status(), 'unavailable'),
23
+ cdpEndpoint === undefined ? undefined : bounded(() => chromeState(cdpEndpoint), 'unknown'),
23
24
  ]);
24
- return { assets, permissions, vault: status === 'unlocked' ? 'pass' : status === 'unavailable' ? 'unknown' : status };
25
+ return { assets, permissions, vault: status === 'unlocked' ? 'pass' : status === 'unavailable' ? 'unknown' : status,
26
+ ...(cdpEndpoint === undefined ? {} : { browser, browserEndpoint: safeCdpOrigin(cdpEndpoint) }) };
25
27
  }
@@ -24,6 +24,7 @@ import { GrantCoordinator } from './grants.js';
24
24
  import { ensureV3State } from './state.js';
25
25
  import { ManagementService } from './manage.js';
26
26
  import { privateBrowserPath } from '../platform/runtime-assets.js';
27
+ import { safeVersion } from '../core/doctor.js';
27
28
  export function prepareSocketEndpoint(endpoint, platform = process.platform) {
28
29
  if (platform !== 'win32') {
29
30
  const dir = dirname(endpoint);
@@ -64,9 +65,9 @@ export async function runService(platform = process.platform, env = process.env)
64
65
  target: new CdpSessionTarget(new CdpBrowserProvider(loadConfig(env, platform).cdpEndpoint)),
65
66
  });
66
67
  const core = new CoreService({ vault, allowlist, grants, approver, audit, login, mode: () => approver.effectiveMode() });
67
- let currentVersion = '3.0.0';
68
+ let currentVersion = 'unknown';
68
69
  try {
69
- currentVersion = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8')).version;
70
+ currentVersion = safeVersion(JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8')).version) ?? 'unknown';
70
71
  }
71
72
  catch { }
72
73
  const verifier = new NpmPackageVerifier();
@@ -82,8 +83,8 @@ export async function runService(platform = process.platform, env = process.env)
82
83
  stagingDir: cfg.stagingDir,
83
84
  currentVersion,
84
85
  });
85
- const diagnose = () => diagnoseService(cfg.home, vault, platform);
86
- const control = new ControlService({ vault, approver, limiter, settings, allowlist, grants, audit, platform, updater, diagnose, manage: () => manager.open() });
86
+ const diagnose = async () => ({ ...await diagnoseService(cfg.home, vault, platform, loadConfig(env, platform).cdpEndpoint), serviceVersion: safeVersion(currentVersion) });
87
+ const control = new ControlService({ vault, approver, limiter, settings, allowlist, grants, audit, platform, updater, diagnose, serviceVersion: safeVersion(currentVersion), manage: () => manager.open() });
87
88
  const manager = new ManagementService({
88
89
  store: allowlist, grants, vault, audit, diagnose, phrase: async () => (await settings.get()).phrase,
89
90
  show: req => adapters.prompt.manage ? adapters.prompt.manage(req) : Promise.reject(new Error('unsupported')),
@@ -1,6 +1,6 @@
1
1
  import { performance } from 'node:perf_hooks';
2
2
  import { clean, normalizeDomain } from '../core/allowlist.js';
3
- import { bounded } from '../core/doctor.js';
3
+ import { bounded, safeVersion, SERVICE_DIAGNOSTIC_TIMEOUT_MS } from '../core/doctor.js';
4
4
  export class ManagementService {
5
5
  d;
6
6
  opened = false;
@@ -19,12 +19,19 @@ export class ManagementService {
19
19
  const snapshot = await this.d.grants.run(async () => {
20
20
  const [entries, status, events, checks] = await Promise.all([
21
21
  this.d.store.list(), bounded(() => this.d.vault.status(), 'unavailable'),
22
- bounded(() => this.d.audit.readTail(10), []), bounded(this.d.diagnose, null),
22
+ bounded(() => this.d.audit.readTail(10), []), bounded(this.d.diagnose, null, SERVICE_DIAGNOSTIC_TIMEOUT_MS),
23
23
  ]);
24
24
  const all = entries.filter(e => e.domain === normalizeDomain(e.domain));
25
25
  page = Math.min(page, Math.max(0, Math.ceil(all.length / 20) - 1));
26
26
  const grants = all.slice(page * 20, page * 20 + 20);
27
27
  const lines = ['Vault: ' + status, lastAction, 'Grant page ' + (page + 1) + ': ' + grants.length + ' of ' + entries.length + ' shown. Use Previous/Next for more.'];
28
+ lines.push('Service version: ' + (safeVersion(checks?.serviceVersion) ?? 'unknown'), 'Service browser: ' + (['pass', 'fail'].includes(checks?.browser ?? '') ? checks?.browser : 'unknown'));
29
+ if (status === 'locked')
30
+ lines.push('Next step: use Unlock to approve a protected vault unlock.');
31
+ if (checks?.assets !== 'pass' || checks?.permissions !== 'pass')
32
+ lines.push('Next step: run u-chan doctor; repair from a trusted administrator terminal.');
33
+ if (checks?.browser !== 'pass')
34
+ lines.push('Next step: start dedicated Chrome and run u-chan setup.');
28
35
  for (const e of grants) {
29
36
  let label = e.expiresAt ? 'until ' + e.expiresAt : e.remainingUses !== undefined ? e.remainingUses + ' attempts remaining' : 'permanent';
30
37
  try {
@@ -1,8 +1,9 @@
1
1
  import crypto from 'node:crypto';
2
- export const DEFAULT_NPM_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
3
- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1Olb3/MAFFxXKHiIkQO5cJ3Yhl5i
4
- 6UPp+IhuteBJbuHcA5UogKo0EWtlWwW6KSaKoTNEYL7JlCQiVnkhBktUgg==
2
+ export const DEFAULT_NPM_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
3
+ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEY6Ya7W++7aUPzvMTrezH6Ycx3c+H
4
+ OKYCcNGybJZSCJq/fd7Qa8uuAKtdIkUQtQiEKERhAmE5lMMJhP8OkDOa2g==
5
5
  -----END PUBLIC KEY-----`;
6
+ export const DEFAULT_NPM_KEY_ID = 'SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U';
6
7
  export function parseSemver(v) {
7
8
  const m = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(v.trim());
8
9
  if (!m)
@@ -91,14 +92,14 @@ export class NpmPackageVerifier {
91
92
  packageName;
92
93
  registryUrl;
93
94
  npmPublicKey;
94
- requireSignature;
95
+ npmKeyId;
95
96
  expectedRepo;
96
97
  fetcher;
97
98
  constructor(opts = {}) {
98
99
  this.packageName = opts.packageName ?? 'u-chan-secrets';
99
100
  this.registryUrl = (opts.registryUrl ?? 'https://registry.npmjs.org').replace(/\/$/, '');
100
101
  this.npmPublicKey = opts.npmPublicKey ?? DEFAULT_NPM_PUBLIC_KEY;
101
- this.requireSignature = opts.npmPublicKey !== undefined;
102
+ this.npmKeyId = opts.npmPublicKey === undefined ? DEFAULT_NPM_KEY_ID : undefined;
102
103
  this.expectedRepo = opts.expectedRepo ?? 'https://github.com/anchuwithdeadline/u-chan-secrets';
103
104
  this.fetcher = opts.fetcher ?? globalThis.fetch;
104
105
  }
@@ -145,17 +146,18 @@ export class NpmPackageVerifier {
145
146
  }
146
147
  // 2. ECDSA signature check
147
148
  const hasSignatures = Array.isArray(dist.signatures) && dist.signatures.length > 0;
148
- const hasAttestations = Boolean(dist.attestations?.url);
149
- if (!hasSignatures && (this.requireSignature || !hasAttestations)) {
149
+ if (!hasSignatures) {
150
150
  return { ok: false, error: 'registry signature verification failed' };
151
151
  }
152
- if (hasSignatures && this.npmPublicKey) {
153
- const signedStr = `${this.packageName}@${targetVersion}:${dist.shasum}`;
152
+ if (hasSignatures) {
153
+ const signedStr = `${this.packageName}@${targetVersion}:${dist.integrity}`;
154
154
  const signedData = Buffer.from(signedStr, 'utf8');
155
155
  let valid = false;
156
156
  for (const s of dist.signatures) {
157
+ if (this.npmKeyId && s.keyid !== this.npmKeyId)
158
+ continue;
157
159
  try {
158
- if (crypto.verify(null, signedData, this.npmPublicKey, Buffer.from(s.sig, 'base64'))) {
160
+ if (crypto.verify('sha256', signedData, this.npmPublicKey, Buffer.from(s.sig, 'base64'))) {
159
161
  valid = true;
160
162
  break;
161
163
  }
@@ -167,6 +169,7 @@ export class NpmPackageVerifier {
167
169
  }
168
170
  }
169
171
  // 3. Provenance check
172
+ // Repository metadata is an additional policy check, never a substitute for a valid signature.
170
173
  if (dist.attestations?.url) {
171
174
  const attRes = await this.fetcher(dist.attestations.url);
172
175
  if (!attRes.ok) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u-chan-secrets",
3
- "version": "3.0.2",
3
+ "version": "4.0.0",
4
4
  "description": "U-Chan Secrets: local MCP server for Bitwarden-backed logins",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,7 +1,11 @@
1
+ param([switch]$Background)
1
2
  $ErrorActionPreference = 'Stop'
2
3
  $home_ = if ($env:U_CHAN_HOME) { $env:U_CHAN_HOME } else { Join-Path $env:APPDATA 'u-chan-secrets' }
3
4
  $profileDir = Join-Path $home_ 'chrome-profile' # dedicated profile, never the default Chrome profile
4
5
  $cdp = [Uri]$(if ($env:U_CHAN_CDP) { $env:U_CHAN_CDP } else { 'http://127.0.0.1:9222' })
6
+ if ($cdp.Scheme -ne 'http' -or $cdp.Host -notin @('127.0.0.1', 'localhost', '[::1]', '::1') -or $cdp.UserInfo -or $cdp.AbsolutePath -ne '/' -or $cdp.Query -or $cdp.Fragment) {
7
+ throw 'Use an HTTP loopback CDP origin without user information, path, query or fragment.'
8
+ }
5
9
  $port = $cdp.Port
6
10
  $bindHost = $cdp.Host
7
11
 
@@ -24,10 +28,12 @@ $chrome = $candidates | Where-Object { $_ -and (Test-Path $_) } | Select-Object
24
28
  if (-not $chrome) { throw 'chrome.exe not found' }
25
29
 
26
30
  New-Item -ItemType Directory -Force -Path $profileDir | Out-Null
27
- Start-Process -FilePath $chrome -ArgumentList @(
31
+ $chromeArgs = @(
28
32
  "--remote-debugging-port=$port",
29
33
  "--remote-debugging-address=$bindHost",
30
- "--user-data-dir=$profileDir",
34
+ "--user-data-dir=`"$profileDir`"",
31
35
  '--no-first-run'
32
36
  )
37
+ if ($Background) { $chromeArgs += '--no-startup-window' }
38
+ Start-Process -FilePath $chrome -ArgumentList $chromeArgs -WindowStyle Hidden
33
39
  Write-Host "Chrome started (profile: $profileDir, CDP: http://${bindHost}:$port)"