u-chan-secrets 5.0.0 → 5.0.1
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 +15 -0
- package/dist/cli.js +2 -0
- package/dist/client/setup.js +9 -4
- package/dist/client/windows-command.js +54 -0
- package/dist/platform/windows/install.js +4 -0
- package/dist/service/doctor.js +3 -0
- package/package.json +1 -1
- package/scripts/debug-install.bat +69 -69
- package/scripts/desktop-browser.ps1 +110 -103
- package/scripts/login-bw.bat +21 -21
- package/scripts/migrate.bat +4 -4
- package/scripts/start-browser.ps1 +33 -33
- package/scripts/windows-security/desktop-matrix.mjs +54 -54
package/README.md
CHANGED
|
@@ -20,6 +20,19 @@ Windows Hello and Touch ID remain [research only](docs/superpowers/research/2026
|
|
|
20
20
|
|
|
21
21
|
## What V5 adds
|
|
22
22
|
|
|
23
|
+
### Updating an existing Windows V5 installation to 5.0.1
|
|
24
|
+
|
|
25
|
+
Run these commands from a normal, non-administrator PowerShell terminal:
|
|
26
|
+
|
|
27
|
+
```powershell
|
|
28
|
+
npm.cmd install --global u-chan-secrets@5.0.1
|
|
29
|
+
u-chan.cmd update
|
|
30
|
+
u-chan.cmd setup --apply
|
|
31
|
+
u-chan doctor
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The signed service update restarts U-Chan and clears its in-memory vault unlock. Run `u-chan unlock` afterward and enter the master password only in the protected prompt. The explicit `.cmd` spelling bootstraps setup when npm's PowerShell launcher is blocked; afterward the short `u-chan` command works. Restart the AI session if setup requests it. Existing browser choice, grants, and vault data remain local; a site's success condition must be approved separately on each installation. Older versions with changed dependencies or native helpers may require the trusted administrator reinstall described below.
|
|
35
|
+
|
|
23
36
|
Choose your browser from a normal Windows terminal:
|
|
24
37
|
|
|
25
38
|
```powershell
|
|
@@ -60,6 +73,8 @@ Setup reports `configured` or `needs_attention`, plus `sharing` and `restartRequ
|
|
|
60
73
|
|
|
61
74
|
Automatic registration supports Codex on Windows only. Configure other clients manually using their documented MCP commands. macOS runtime remains unverified. `doctor` compares the actual CLI and installed service versions, both browser targets, and Windows UAC policy; missing metadata remains unknown. `manage` shows the installed service version and browser status.
|
|
62
75
|
|
|
76
|
+
On Windows, `setup --apply` also makes the short `u-chan` command work when PowerShell blocks scripts. It moves the recognized npm-generated `u-chan.ps1` launcher into the npm directory's `#0 backup`, leaving `u-chan.cmd` available for PowerShell to resolve automatically. Execution policy and PowerShell profiles are unchanged. Inspection without `--apply` is read-only, and custom launchers are preserved. If the first setup command is blocked, run `u-chan.cmd setup --apply`. Rerun setup after an npm installation or update recreates the PowerShell launcher. Shell results and the backup path appear in the setup JSON report.
|
|
77
|
+
|
|
63
78
|
Propose a site grant with an explicit success condition:
|
|
64
79
|
|
|
65
80
|
```sh
|
package/dist/cli.js
CHANGED
|
@@ -15,6 +15,7 @@ import { configureCodex, isElevated } from './client/codex-setup.js';
|
|
|
15
15
|
import { runSetup, setupExitCode } from './client/setup.js';
|
|
16
16
|
import { parseAllowArgs } from './client/allow.js';
|
|
17
17
|
import { saveBrowserChoice } from './client/browser-choice.js';
|
|
18
|
+
import { configureWindowsCommand } from './client/windows-command.js';
|
|
18
19
|
const USAGE = 'usage: u-chan install [--mode <easy|hardcore>] [--phrase <phrase>] | uninstall | unlock | lock | mode <easy|hardcore> | import-v1 [path] | audit [limit] | update | doctor [--json] | manage';
|
|
19
20
|
/** import-v1 shows one approval window per entry, each open for up to 2 minutes, one after another. */
|
|
20
21
|
export const IMPORT_PROMPT_MS = 120000;
|
|
@@ -110,6 +111,7 @@ export async function main(argv, d) {
|
|
|
110
111
|
const cfg = loadConfig(diagnosticEnv, d.platform), endpoint = cfg.cdpEndpoint;
|
|
111
112
|
const r = d.setup ? await d.setup(options) : await runSetup(options, {
|
|
112
113
|
platform: d.platform, endpoint, browser: cfg.browser, elevated: isElevated, doctor,
|
|
114
|
+
configureShell: apply => configureWindowsCommand(apply, d.env.PATH ?? d.env.Path ?? ''),
|
|
113
115
|
selectBrowser: choice => saveBrowserChoice(cfg.browserChoicePath, choice),
|
|
114
116
|
configure: opts => configureCodex({ ...opts, browser: cfg.browser, endpoint, ipcEndpoint: defaultEndpoint(d.env, d.platform), nodePath: process.execPath, entryPath: fileURLToPath(new URL('./index.js', import.meta.url)), configDir: d.env.CODEX_HOME ?? join(homedir(), '.codex') }),
|
|
115
117
|
startBrowser: () => new Promise(resolve => {
|
package/dist/client/setup.js
CHANGED
|
@@ -19,6 +19,11 @@ export async function runSetup(o, d) {
|
|
|
19
19
|
r.client = 'manual';
|
|
20
20
|
return r;
|
|
21
21
|
}
|
|
22
|
+
if (d.configureShell) {
|
|
23
|
+
r.shell = await d.configureShell(o.apply);
|
|
24
|
+
r.changed = r.shell.changed;
|
|
25
|
+
action('configure_shell', r.shell.message, !r.shell.ready);
|
|
26
|
+
}
|
|
22
27
|
if (r.checks.some(c => c.id === 'browser_target' && c.state === 'fail')) {
|
|
23
28
|
r.sharing = 'mismatch';
|
|
24
29
|
action('configure_mcp', 'Align the desktop and service loopback browser targets before applying setup.');
|
|
@@ -26,9 +31,9 @@ export async function runSetup(o, d) {
|
|
|
26
31
|
}
|
|
27
32
|
const configured = await d.configure({ apply: o.apply, replaceExisting: o.replaceExisting, ...(o.browser ? { browser: o.browser } : {}) });
|
|
28
33
|
r.client = configured.state;
|
|
29
|
-
r.changed
|
|
34
|
+
r.changed ||= configured.changed;
|
|
30
35
|
r.backupPath = configured.backupPath;
|
|
31
|
-
r.restartRequired =
|
|
36
|
+
r.restartRequired = configured.changed;
|
|
32
37
|
if (o.browser && o.apply && r.client === 'ready') {
|
|
33
38
|
if (!d.selectBrowser)
|
|
34
39
|
throw new Error('browser selection unavailable');
|
|
@@ -48,7 +53,7 @@ export async function runSetup(o, d) {
|
|
|
48
53
|
const pw = r.checks.find(c => c.id === 'playwright');
|
|
49
54
|
const mismatch = r.checks.some(c => c.id === 'browser_target' && c.state === 'fail');
|
|
50
55
|
r.sharing = mismatch || pw?.state === 'fail' ? 'mismatch' : pw?.state === 'pass' ? 'process_configured' : 'unobserved';
|
|
51
|
-
r.restartRequired
|
|
56
|
+
r.restartRequired ||= pw?.state === 'fail';
|
|
52
57
|
const ids = { registration: 'install_service', service: 'install_service', ipc: 'install_service',
|
|
53
58
|
version: 'update_service', assets: 'repair_permissions', permissions: 'repair_permissions', service_browser: 'start_browser', chrome: 'start_browser',
|
|
54
59
|
browser_target: 'configure_mcp', browser_identity: 'start_browser', playwright: 'configure_mcp', uac: 'review_uac' };
|
|
@@ -65,7 +70,7 @@ export async function runSetup(o, d) {
|
|
|
65
70
|
action('restart_client', 'Restart the AI session to load saved MCP registrations.', false);
|
|
66
71
|
action('configure_mcp', 'Active AI-session browser ownership is unverified; check the current client browser connection.', false);
|
|
67
72
|
const requiredOk = r.checks.length > 0 && r.checks.filter(c => c.required).every(c => c.state === 'pass');
|
|
68
|
-
if (requiredOk && r.client === 'ready' && started && !mismatch)
|
|
73
|
+
if (requiredOk && r.client === 'ready' && started && !mismatch && r.shell?.ready !== false)
|
|
69
74
|
r.state = 'configured';
|
|
70
75
|
return r;
|
|
71
76
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { lstat, mkdir, readFile, rename, realpath } from 'node:fs/promises';
|
|
2
|
+
import { delimiter, join, resolve } from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
/** Let PowerShell resolve the npm .cmd launcher without changing execution policy or loading a profile. */
|
|
5
|
+
export async function configureWindowsCommand(apply, searchPath) {
|
|
6
|
+
const result = (ready, message) => ({ ready, changed: false, message });
|
|
7
|
+
try {
|
|
8
|
+
for (const entry of searchPath.split(delimiter).filter(Boolean)) {
|
|
9
|
+
const directory = resolve(entry.replace(/^"|"$/g, ''));
|
|
10
|
+
const script = join(directory, 'u-chan.ps1'), command = join(directory, 'u-chan.cmd');
|
|
11
|
+
const exists = async (file) => {
|
|
12
|
+
try {
|
|
13
|
+
return await lstat(file);
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
if (error.code === 'ENOENT')
|
|
17
|
+
return undefined;
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
const ps = await exists(script), cmd = await exists(command);
|
|
22
|
+
if (!ps && !cmd)
|
|
23
|
+
continue;
|
|
24
|
+
if (!cmd?.isFile() || cmd.isSymbolicLink() || (ps && (!ps.isFile() || ps.isSymbolicLink()))) {
|
|
25
|
+
return result(false, 'Custom or redirected u-chan launcher preserved; use u-chan.cmd unlock.');
|
|
26
|
+
}
|
|
27
|
+
const batch = await readFile(command, 'utf8');
|
|
28
|
+
if (!batch.includes('"%dp0%\\node_modules\\u-chan-secrets\\dist\\cli.js" %*')) {
|
|
29
|
+
return result(false, 'Unrecognized u-chan launcher preserved; inspect command installation manually.');
|
|
30
|
+
}
|
|
31
|
+
if (!ps)
|
|
32
|
+
return result(true, 'PowerShell can use the u-chan.cmd launcher as u-chan.');
|
|
33
|
+
const source = await readFile(script, 'utf8');
|
|
34
|
+
if (!source.startsWith('#!/usr/bin/env pwsh') || !source.includes('"$basedir/node_modules/u-chan-secrets/dist/cli.js" $args') || !source.trimEnd().endsWith('exit $ret')) {
|
|
35
|
+
return result(false, 'Custom PowerShell launcher preserved; use u-chan.cmd unlock.');
|
|
36
|
+
}
|
|
37
|
+
if (!apply)
|
|
38
|
+
return result(false, 'Run u-chan.cmd setup --apply to make u-chan work without PowerShell scripts.');
|
|
39
|
+
if ((await realpath(directory)).toLowerCase() !== directory.toLowerCase())
|
|
40
|
+
throw new Error('Redirected launcher directory.');
|
|
41
|
+
const backupDirectory = join(directory, '#0 backup');
|
|
42
|
+
await mkdir(backupDirectory, { recursive: true });
|
|
43
|
+
if ((await lstat(backupDirectory)).isSymbolicLink() || (await realpath(backupDirectory)).toLowerCase() !== backupDirectory.toLowerCase())
|
|
44
|
+
throw new Error('Redirected backup directory.');
|
|
45
|
+
const backupPath = join(backupDirectory, `u-chan-${randomUUID()}.ps1`);
|
|
46
|
+
await rename(script, backupPath);
|
|
47
|
+
return { ready: true, changed: true, backupPath, message: 'PowerShell now uses u-chan.cmd as u-chan; original .ps1 launcher retained in #0 backup.' };
|
|
48
|
+
}
|
|
49
|
+
return result(true, 'No npm u-chan launcher found on PATH; source checkout commands remain unchanged.');
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return result(false, 'Could not configure the PowerShell command; use u-chan.cmd unlock and inspect launcher permissions.');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -92,6 +92,10 @@ export async function installWindowsService(opts = {}) {
|
|
|
92
92
|
const pkgRoot = opts.packageRoot ?? fileURLToPath(new URL('../../../', import.meta.url));
|
|
93
93
|
await fs.cp(path.join(pkgRoot, 'dist'), path.join(stagedProgram, 'dist'), { recursive: true });
|
|
94
94
|
await fs.cp(path.join(pkgRoot, 'package.json'), path.join(stagedProgram, 'package.json'));
|
|
95
|
+
await fs.mkdir(path.join(stagedProgram, 'scripts'));
|
|
96
|
+
for (const file of ['start-browser.ps1', 'desktop-browser.ps1']) {
|
|
97
|
+
await fs.copyFile(path.join(pkgRoot, 'scripts', file), path.join(stagedProgram, 'scripts', file));
|
|
98
|
+
}
|
|
95
99
|
const srcNm = path.join(pkgRoot, 'node_modules');
|
|
96
100
|
const destNm = path.join(stagedProgram, 'node_modules');
|
|
97
101
|
{
|
package/dist/service/doctor.js
CHANGED
|
@@ -8,6 +8,9 @@ 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
|
+
if (platform === 'win32')
|
|
12
|
+
for (const file of ['start-browser.ps1', 'desktop-browser.ps1'])
|
|
13
|
+
paths.push(join(home, 'program', 'scripts', file));
|
|
11
14
|
const [assets, permissions, status, browser, browserKind] = await Promise.all([
|
|
12
15
|
bounded(async () => {
|
|
13
16
|
await privateBrowserPath(home);
|
package/package.json
CHANGED
|
@@ -1,69 +1,69 @@
|
|
|
1
|
-
@echo off
|
|
2
|
-
setlocal
|
|
3
|
-
set ROOT=%~dp0..
|
|
4
|
-
|
|
5
|
-
fltmc.exe >nul 2>&1
|
|
6
|
-
if %errorlevel% neq 0 (
|
|
7
|
-
echo [*] Requesting Administrator elevation...
|
|
8
|
-
powershell -NoProfile -Command "Start-Process -FilePath '%~f0' -Verb RunAs"
|
|
9
|
-
exit /b
|
|
10
|
-
)
|
|
11
|
-
|
|
12
|
-
cd /d "%ROOT%"
|
|
13
|
-
echo ========================================================
|
|
14
|
-
echo U-Chan Secrets V2 Installation & Setup
|
|
15
|
-
echo ========================================================
|
|
16
|
-
echo.
|
|
17
|
-
echo [*] Step 1: Stopping previous service (if any)...
|
|
18
|
-
taskkill /F /IM prompt-helper.exe >nul 2>&1
|
|
19
|
-
taskkill /F /IM auth-helper.exe >nul 2>&1
|
|
20
|
-
call node dist\cli.js uninstall
|
|
21
|
-
echo.
|
|
22
|
-
echo [*] Step 2: Installing U-Chan Windows Service...
|
|
23
|
-
call node dist\cli.js install
|
|
24
|
-
if %errorlevel% neq 0 (
|
|
25
|
-
echo.
|
|
26
|
-
echo [X] Installation failed with exit code %errorlevel%.
|
|
27
|
-
pause
|
|
28
|
-
exit /b %errorlevel%
|
|
29
|
-
)
|
|
30
|
-
icacls C:\ProgramData\u-chan-secrets\bin /grant "Users:(OI)(CI)RX" >nul 2>&1
|
|
31
|
-
echo.
|
|
32
|
-
echo [*] Step 3: Checking service status...
|
|
33
|
-
sc.exe query u-chan-secrets
|
|
34
|
-
echo.
|
|
35
|
-
|
|
36
|
-
echo [*] Step 4: Bitwarden Vault Login...
|
|
37
|
-
set BITWARDENCLI_APPDATA_DIR=C:\ProgramData\u-chan-secrets\bw-data
|
|
38
|
-
set BW_BIN=C:\ProgramData\u-chan-secrets\bin\bw.exe
|
|
39
|
-
if not exist "%BW_BIN%" (
|
|
40
|
-
for /f "delims=" %%i in ('where.exe bw.exe 2^>nul') do set BW_BIN=%%i
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
if exist "%BW_BIN%" (
|
|
44
|
-
"%BW_BIN%" status > "%TEMP%\bw_stat.json" 2>&1
|
|
45
|
-
findstr /C:"\"unauthenticated\"" "%TEMP%\bw_stat.json" >nul 2>&1
|
|
46
|
-
if %errorlevel% equ 0 (
|
|
47
|
-
echo [!] Bitwarden is not logged in for the privileged service.
|
|
48
|
-
echo [*] Please log in to your Bitwarden vault below:
|
|
49
|
-
"%BW_BIN%" login
|
|
50
|
-
) else (
|
|
51
|
-
echo [+] Bitwarden vault is already logged in.
|
|
52
|
-
)
|
|
53
|
-
del "%TEMP%\bw_stat.json" >nul 2>&1
|
|
54
|
-
) else (
|
|
55
|
-
echo [!] Warning: bw.exe not found.
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
echo.
|
|
59
|
-
copy /y C:\ProgramData\u-chan-secrets\service.log "%ROOT%\service-copy.log" >nul 2>&1
|
|
60
|
-
copy /y C:\ProgramData\u-chan-secrets\settings.json "%ROOT%\settings-copy.json" >nul 2>&1
|
|
61
|
-
echo ========================================================
|
|
62
|
-
echo [+] Service and Bitwarden configuration complete!
|
|
63
|
-
echo.
|
|
64
|
-
echo Next: Close this window, open your normal terminal, and run:
|
|
65
|
-
echo u-chan import-v1
|
|
66
|
-
echo u-chan unlock
|
|
67
|
-
echo ========================================================
|
|
68
|
-
echo.
|
|
69
|
-
pause
|
|
1
|
+
@echo off
|
|
2
|
+
setlocal
|
|
3
|
+
set ROOT=%~dp0..
|
|
4
|
+
|
|
5
|
+
fltmc.exe >nul 2>&1
|
|
6
|
+
if %errorlevel% neq 0 (
|
|
7
|
+
echo [*] Requesting Administrator elevation...
|
|
8
|
+
powershell -NoProfile -Command "Start-Process -FilePath '%~f0' -Verb RunAs"
|
|
9
|
+
exit /b
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
cd /d "%ROOT%"
|
|
13
|
+
echo ========================================================
|
|
14
|
+
echo U-Chan Secrets V2 Installation & Setup
|
|
15
|
+
echo ========================================================
|
|
16
|
+
echo.
|
|
17
|
+
echo [*] Step 1: Stopping previous service (if any)...
|
|
18
|
+
taskkill /F /IM prompt-helper.exe >nul 2>&1
|
|
19
|
+
taskkill /F /IM auth-helper.exe >nul 2>&1
|
|
20
|
+
call node dist\cli.js uninstall
|
|
21
|
+
echo.
|
|
22
|
+
echo [*] Step 2: Installing U-Chan Windows Service...
|
|
23
|
+
call node dist\cli.js install
|
|
24
|
+
if %errorlevel% neq 0 (
|
|
25
|
+
echo.
|
|
26
|
+
echo [X] Installation failed with exit code %errorlevel%.
|
|
27
|
+
pause
|
|
28
|
+
exit /b %errorlevel%
|
|
29
|
+
)
|
|
30
|
+
icacls C:\ProgramData\u-chan-secrets\bin /grant "Users:(OI)(CI)RX" >nul 2>&1
|
|
31
|
+
echo.
|
|
32
|
+
echo [*] Step 3: Checking service status...
|
|
33
|
+
sc.exe query u-chan-secrets
|
|
34
|
+
echo.
|
|
35
|
+
|
|
36
|
+
echo [*] Step 4: Bitwarden Vault Login...
|
|
37
|
+
set BITWARDENCLI_APPDATA_DIR=C:\ProgramData\u-chan-secrets\bw-data
|
|
38
|
+
set BW_BIN=C:\ProgramData\u-chan-secrets\bin\bw.exe
|
|
39
|
+
if not exist "%BW_BIN%" (
|
|
40
|
+
for /f "delims=" %%i in ('where.exe bw.exe 2^>nul') do set BW_BIN=%%i
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
if exist "%BW_BIN%" (
|
|
44
|
+
"%BW_BIN%" status > "%TEMP%\bw_stat.json" 2>&1
|
|
45
|
+
findstr /C:"\"unauthenticated\"" "%TEMP%\bw_stat.json" >nul 2>&1
|
|
46
|
+
if %errorlevel% equ 0 (
|
|
47
|
+
echo [!] Bitwarden is not logged in for the privileged service.
|
|
48
|
+
echo [*] Please log in to your Bitwarden vault below:
|
|
49
|
+
"%BW_BIN%" login
|
|
50
|
+
) else (
|
|
51
|
+
echo [+] Bitwarden vault is already logged in.
|
|
52
|
+
)
|
|
53
|
+
del "%TEMP%\bw_stat.json" >nul 2>&1
|
|
54
|
+
) else (
|
|
55
|
+
echo [!] Warning: bw.exe not found.
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
echo.
|
|
59
|
+
copy /y C:\ProgramData\u-chan-secrets\service.log "%ROOT%\service-copy.log" >nul 2>&1
|
|
60
|
+
copy /y C:\ProgramData\u-chan-secrets\settings.json "%ROOT%\settings-copy.json" >nul 2>&1
|
|
61
|
+
echo ========================================================
|
|
62
|
+
echo [+] Service and Bitwarden configuration complete!
|
|
63
|
+
echo.
|
|
64
|
+
echo Next: Close this window, open your normal terminal, and run:
|
|
65
|
+
echo u-chan import-v1
|
|
66
|
+
echo u-chan unlock
|
|
67
|
+
echo ========================================================
|
|
68
|
+
echo.
|
|
69
|
+
pause
|
|
@@ -1,103 +1,110 @@
|
|
|
1
|
-
# Shared user-side Chrome/Edge launcher and read-only process policy. No elevation or vault access.
|
|
2
|
-
function Split-UChanWindowsCommandLine([string]$CommandLine) {
|
|
3
|
-
if (-not ('UChanWindowsCommandLine' -as [type])) {
|
|
4
|
-
Add-Type -TypeDefinition @'
|
|
5
|
-
using System;
|
|
6
|
-
using System.Runtime.InteropServices;
|
|
7
|
-
public static class UChanWindowsCommandLine {
|
|
8
|
-
[DllImport("shell32.dll", SetLastError=true, CharSet=CharSet.Unicode)] static extern IntPtr CommandLineToArgvW(string value, out int count);
|
|
9
|
-
[DllImport("kernel32.dll")] static extern IntPtr LocalFree(IntPtr value);
|
|
10
|
-
public static string[] Split(string value) {
|
|
11
|
-
int count; IntPtr memory=CommandLineToArgvW(value,out count);
|
|
12
|
-
if(memory==IntPtr.Zero) throw new InvalidOperationException("Command line is unavailable");
|
|
13
|
-
try { string[] result=new string[count]; for(int i=0;i<count;i++)result[i]=Marshal.PtrToStringUni(Marshal.ReadIntPtr(memory,i*IntPtr.Size)); return result; }
|
|
14
|
-
finally { LocalFree(memory); }
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
'@
|
|
18
|
-
}
|
|
19
|
-
return [UChanWindowsCommandLine]::Split($CommandLine)
|
|
20
|
-
}
|
|
21
|
-
function Test-UChanBrowserProcess($Observation, [string]$Executable, [string]$Profile, [string]$OwnerSid, [int]$Port) {
|
|
22
|
-
if (-not $Observation.ExecutablePath -or -not $Observation.OwnerSid -or -not $Observation.CommandLine -or -not $Observation.Addresses) { return 'unknown' }
|
|
23
|
-
if ($Observation.ExecutablePath -ine $Executable -or $Observation.OwnerSid -ne $OwnerSid) { return 'fail' }
|
|
24
|
-
foreach ($address in $Observation.Addresses) { if ($address -notin @('127.0.0.1','::1')) { return 'fail' } }
|
|
25
|
-
try { $tokens = @(Split-UChanWindowsCommandLine $Observation.CommandLine) } catch { return 'unknown' }
|
|
26
|
-
if ($tokens.Count -lt 1 -or $tokens[0] -ine $Executable) { return 'fail' }
|
|
27
|
-
$profiles = @($tokens | Where-Object { $_ -imatch '^--user-data-dir(?:=|$)' })
|
|
28
|
-
if ($profiles.Count -ne 1 -or $profiles[0] -inotmatch '^--user-data-dir=.+$') { return 'fail' }
|
|
29
|
-
$actual = $profiles[0].Substring('--user-data-dir='.Length)
|
|
30
|
-
try { if ([IO.Path]::GetFullPath($actual).TrimEnd('\') -ine [IO.Path]::GetFullPath($Profile).TrimEnd('\')) { return 'fail' } } catch { return 'fail' }
|
|
31
|
-
$ports = @($tokens | Where-Object { $_ -imatch '^--remote-debugging-port(?:=|$)' })
|
|
32
|
-
if ($ports.Count -ne 1 -or $ports[0] -inotmatch '^--remote-debugging-port=[0-9]+$' -or $ports[0].Substring('--remote-debugging-port='.Length) -ne [string]$Port) { return 'fail' }
|
|
33
|
-
return 'pass'
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function Get-UChanBrowserConfiguration([string]$Browser, [string]$HomeOverride, [string]$OwnerOverride, [string]$LocalOverride) {
|
|
37
|
-
$userHome = if ($HomeOverride) { $HomeOverride } elseif ($env:U_CHAN_HOME) { $env:U_CHAN_HOME } else { Join-Path $env:APPDATA 'u-chan-secrets' }
|
|
38
|
-
if (-not $Browser) { $Browser = $env:U_CHAN_BROWSER }
|
|
39
|
-
if (-not $Browser) {
|
|
40
|
-
$choice = Join-Path $userHome 'browser.json'
|
|
41
|
-
if (Test-Path -LiteralPath $choice) {
|
|
42
|
-
$item = Get-Item -LiteralPath $choice -Force
|
|
43
|
-
if ($item.PSIsContainer -or ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) { throw 'Browser preference path is redirected.' }
|
|
44
|
-
$saved = Get-Content -LiteralPath $choice -Raw | ConvertFrom-Json
|
|
45
|
-
if (@($saved.PSObject.Properties).Count -ne 1) { throw 'Invalid browser preference.' }
|
|
46
|
-
$Browser = $saved.browser
|
|
47
|
-
if (-not $Browser) { throw 'Invalid browser preference.' }
|
|
48
|
-
} else { $Browser = 'chrome' }
|
|
49
|
-
}
|
|
50
|
-
if ($Browser -cnotin @('chrome','edge')) { throw 'Choose chrome or edge.' }
|
|
51
|
-
$profile = Join-Path $userHome ($Browser + '-profile')
|
|
52
|
-
$check = [IO.Path]::GetFullPath($profile)
|
|
53
|
-
while ($check) {
|
|
54
|
-
if ((Test-Path -LiteralPath $check) -and ((Get-Item -LiteralPath $check -Force).Attributes -band [IO.FileAttributes]::ReparsePoint)) { throw 'Dedicated browser profile is redirected.' }
|
|
55
|
-
$check = [IO.Path]::GetDirectoryName($check)
|
|
56
|
-
}
|
|
57
|
-
$cdp = [Uri]$(if ($env:U_CHAN_CDP) { $env:U_CHAN_CDP } else { 'http://127.0.0.1:9222' })
|
|
58
|
-
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) { throw 'Use an HTTP loopback CDP origin without credentials, path, query or fragment.' }
|
|
59
|
-
$suffix = if ($Browser -eq 'edge') { 'Microsoft\Edge\Application\msedge.exe' } else { 'Google\Chrome\Application\chrome.exe' }
|
|
60
|
-
$local = if ($LocalOverride) { $LocalOverride } else { $env:LOCALAPPDATA }
|
|
61
|
-
$candidates = @(foreach ($base in @($env:ProgramFiles, ${env:ProgramFiles(x86)}, $local)) { if ($base) { Join-Path $base $suffix } })
|
|
62
|
-
$executables = @($candidates | Where-Object { Test-Path -LiteralPath $_ -PathType Leaf })
|
|
63
|
-
$owner = if ($OwnerOverride) { $OwnerOverride } else { [Security.Principal.WindowsIdentity]::GetCurrent().User.Value }
|
|
64
|
-
return @{ Browser=$Browser; Profile=$profile; Endpoint=$cdp; Executables=$executables; OwnerSid=$owner }
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
# Privileged checks derive the desktop home from the installed owner's SID, never SYSTEM's APPDATA or user preference files.
|
|
68
|
-
function Get-UChanOwnerBrowserConfiguration([string]$OwnerSid, [string]$Browser) {
|
|
69
|
-
if ($OwnerSid -notmatch '^S-1-5-21-(?:[0-9]+-){3}[0-9]+$') { throw 'Installed desktop owner is invalid.' }
|
|
70
|
-
$profile = (Get-ItemProperty -LiteralPath ('HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\'+$OwnerSid) -ErrorAction Stop).ProfileImagePath
|
|
71
|
-
if (-not $profile) { throw 'Installed desktop owner profile is unavailable.' }
|
|
72
|
-
$profile = [Environment]::ExpandEnvironmentVariables($profile)
|
|
73
|
-
$roaming = Join-Path $profile 'AppData\Roaming'
|
|
74
|
-
$local = Join-Path $profile 'AppData\Local'
|
|
75
|
-
$shell = Get-
|
|
76
|
-
if ($shell
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
$
|
|
92
|
-
if (-
|
|
93
|
-
|
|
94
|
-
$
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
$
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
1
|
+
# Shared user-side Chrome/Edge launcher and read-only process policy. No elevation or vault access.
|
|
2
|
+
function Split-UChanWindowsCommandLine([string]$CommandLine) {
|
|
3
|
+
if (-not ('UChanWindowsCommandLine' -as [type])) {
|
|
4
|
+
Add-Type -TypeDefinition @'
|
|
5
|
+
using System;
|
|
6
|
+
using System.Runtime.InteropServices;
|
|
7
|
+
public static class UChanWindowsCommandLine {
|
|
8
|
+
[DllImport("shell32.dll", SetLastError=true, CharSet=CharSet.Unicode)] static extern IntPtr CommandLineToArgvW(string value, out int count);
|
|
9
|
+
[DllImport("kernel32.dll")] static extern IntPtr LocalFree(IntPtr value);
|
|
10
|
+
public static string[] Split(string value) {
|
|
11
|
+
int count; IntPtr memory=CommandLineToArgvW(value,out count);
|
|
12
|
+
if(memory==IntPtr.Zero) throw new InvalidOperationException("Command line is unavailable");
|
|
13
|
+
try { string[] result=new string[count]; for(int i=0;i<count;i++)result[i]=Marshal.PtrToStringUni(Marshal.ReadIntPtr(memory,i*IntPtr.Size)); return result; }
|
|
14
|
+
finally { LocalFree(memory); }
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
'@
|
|
18
|
+
}
|
|
19
|
+
return [UChanWindowsCommandLine]::Split($CommandLine)
|
|
20
|
+
}
|
|
21
|
+
function Test-UChanBrowserProcess($Observation, [string]$Executable, [string]$Profile, [string]$OwnerSid, [int]$Port) {
|
|
22
|
+
if (-not $Observation.ExecutablePath -or -not $Observation.OwnerSid -or -not $Observation.CommandLine -or -not $Observation.Addresses) { return 'unknown' }
|
|
23
|
+
if ($Observation.ExecutablePath -ine $Executable -or $Observation.OwnerSid -ne $OwnerSid) { return 'fail' }
|
|
24
|
+
foreach ($address in $Observation.Addresses) { if ($address -notin @('127.0.0.1','::1')) { return 'fail' } }
|
|
25
|
+
try { $tokens = @(Split-UChanWindowsCommandLine $Observation.CommandLine) } catch { return 'unknown' }
|
|
26
|
+
if ($tokens.Count -lt 1 -or $tokens[0] -ine $Executable) { return 'fail' }
|
|
27
|
+
$profiles = @($tokens | Where-Object { $_ -imatch '^--user-data-dir(?:=|$)' })
|
|
28
|
+
if ($profiles.Count -ne 1 -or $profiles[0] -inotmatch '^--user-data-dir=.+$') { return 'fail' }
|
|
29
|
+
$actual = $profiles[0].Substring('--user-data-dir='.Length)
|
|
30
|
+
try { if ([IO.Path]::GetFullPath($actual).TrimEnd('\') -ine [IO.Path]::GetFullPath($Profile).TrimEnd('\')) { return 'fail' } } catch { return 'fail' }
|
|
31
|
+
$ports = @($tokens | Where-Object { $_ -imatch '^--remote-debugging-port(?:=|$)' })
|
|
32
|
+
if ($ports.Count -ne 1 -or $ports[0] -inotmatch '^--remote-debugging-port=[0-9]+$' -or $ports[0].Substring('--remote-debugging-port='.Length) -ne [string]$Port) { return 'fail' }
|
|
33
|
+
return 'pass'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function Get-UChanBrowserConfiguration([string]$Browser, [string]$HomeOverride, [string]$OwnerOverride, [string]$LocalOverride) {
|
|
37
|
+
$userHome = if ($HomeOverride) { $HomeOverride } elseif ($env:U_CHAN_HOME) { $env:U_CHAN_HOME } else { Join-Path $env:APPDATA 'u-chan-secrets' }
|
|
38
|
+
if (-not $Browser) { $Browser = $env:U_CHAN_BROWSER }
|
|
39
|
+
if (-not $Browser) {
|
|
40
|
+
$choice = Join-Path $userHome 'browser.json'
|
|
41
|
+
if (Test-Path -LiteralPath $choice) {
|
|
42
|
+
$item = Get-Item -LiteralPath $choice -Force
|
|
43
|
+
if ($item.PSIsContainer -or ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) { throw 'Browser preference path is redirected.' }
|
|
44
|
+
$saved = Get-Content -LiteralPath $choice -Raw | ConvertFrom-Json
|
|
45
|
+
if (@($saved.PSObject.Properties).Count -ne 1) { throw 'Invalid browser preference.' }
|
|
46
|
+
$Browser = $saved.browser
|
|
47
|
+
if (-not $Browser) { throw 'Invalid browser preference.' }
|
|
48
|
+
} else { $Browser = 'chrome' }
|
|
49
|
+
}
|
|
50
|
+
if ($Browser -cnotin @('chrome','edge')) { throw 'Choose chrome or edge.' }
|
|
51
|
+
$profile = Join-Path $userHome ($Browser + '-profile')
|
|
52
|
+
$check = [IO.Path]::GetFullPath($profile)
|
|
53
|
+
while ($check) {
|
|
54
|
+
if ((Test-Path -LiteralPath $check) -and ((Get-Item -LiteralPath $check -Force).Attributes -band [IO.FileAttributes]::ReparsePoint)) { throw 'Dedicated browser profile is redirected.' }
|
|
55
|
+
$check = [IO.Path]::GetDirectoryName($check)
|
|
56
|
+
}
|
|
57
|
+
$cdp = [Uri]$(if ($env:U_CHAN_CDP) { $env:U_CHAN_CDP } else { 'http://127.0.0.1:9222' })
|
|
58
|
+
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) { throw 'Use an HTTP loopback CDP origin without credentials, path, query or fragment.' }
|
|
59
|
+
$suffix = if ($Browser -eq 'edge') { 'Microsoft\Edge\Application\msedge.exe' } else { 'Google\Chrome\Application\chrome.exe' }
|
|
60
|
+
$local = if ($LocalOverride) { $LocalOverride } else { $env:LOCALAPPDATA }
|
|
61
|
+
$candidates = @(foreach ($base in @($env:ProgramFiles, ${env:ProgramFiles(x86)}, $local)) { if ($base) { Join-Path $base $suffix } })
|
|
62
|
+
$executables = @($candidates | Where-Object { Test-Path -LiteralPath $_ -PathType Leaf })
|
|
63
|
+
$owner = if ($OwnerOverride) { $OwnerOverride } else { [Security.Principal.WindowsIdentity]::GetCurrent().User.Value }
|
|
64
|
+
return @{ Browser=$Browser; Profile=$profile; Endpoint=$cdp; Executables=$executables; OwnerSid=$owner }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# Privileged checks derive the desktop home from the installed owner's SID, never SYSTEM's APPDATA or user preference files.
|
|
68
|
+
function Get-UChanOwnerBrowserConfiguration([string]$OwnerSid, [string]$Browser) {
|
|
69
|
+
if ($OwnerSid -notmatch '^S-1-5-21-(?:[0-9]+-){3}[0-9]+$') { throw 'Installed desktop owner is invalid.' }
|
|
70
|
+
$profile = (Get-ItemProperty -LiteralPath ('HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\'+$OwnerSid) -ErrorAction Stop).ProfileImagePath
|
|
71
|
+
if (-not $profile) { throw 'Installed desktop owner profile is unavailable.' }
|
|
72
|
+
$profile = [Environment]::ExpandEnvironmentVariables($profile)
|
|
73
|
+
$roaming = Join-Path $profile 'AppData\Roaming'
|
|
74
|
+
$local = Join-Path $profile 'AppData\Local'
|
|
75
|
+
$shell = Get-Item -LiteralPath ('Registry::HKEY_USERS\'+$OwnerSid+'\Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders') -ErrorAction SilentlyContinue
|
|
76
|
+
if ($shell) {
|
|
77
|
+
# Read REG_EXPAND_SZ literally; Registry providers otherwise expand USERPROFILE as SYSTEM before substitution.
|
|
78
|
+
try {
|
|
79
|
+
$rawRoaming = $shell.GetValue('AppData', $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
|
|
80
|
+
$rawLocal = $shell.GetValue('Local AppData', $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
|
|
81
|
+
} finally { $shell.Close() }
|
|
82
|
+
if ($rawRoaming) { $roaming = [Environment]::ExpandEnvironmentVariables(($rawRoaming -ireplace '%USERPROFILE%', $profile)) }
|
|
83
|
+
if ($rawLocal) { $local = [Environment]::ExpandEnvironmentVariables(($rawLocal -ireplace '%USERPROFILE%', $profile)) }
|
|
84
|
+
}
|
|
85
|
+
$desktopHome = if ($env:U_CHAN_DESKTOP_HOME) { $env:U_CHAN_DESKTOP_HOME } else { Join-Path $roaming 'u-chan-secrets' }
|
|
86
|
+
return Get-UChanBrowserConfiguration $Browser $desktopHome $OwnerSid $local
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function Get-UChanBrowserState($Configuration) {
|
|
90
|
+
try {
|
|
91
|
+
$listeners = @(Get-NetTCPConnection -State Listen -ErrorAction Stop | Where-Object LocalPort -eq $Configuration.Endpoint.Port)
|
|
92
|
+
if ($listeners.Count -eq 0) { return 'missing' }
|
|
93
|
+
$owners = @($listeners.OwningProcess | Select-Object -Unique)
|
|
94
|
+
if ($owners.Count -ne 1) { return 'fail' }
|
|
95
|
+
$process = Get-CimInstance Win32_Process -Filter ("ProcessId=" + $owners[0]) -ErrorAction Stop
|
|
96
|
+
if (-not $process) { return 'unknown' }
|
|
97
|
+
$owner = Invoke-CimMethod -InputObject $process -MethodName GetOwnerSid -ErrorAction Stop
|
|
98
|
+
$observation = @{ ExecutablePath=$process.ExecutablePath; OwnerSid=$owner.Sid; CommandLine=$process.CommandLine; Addresses=@($listeners.LocalAddress) }
|
|
99
|
+
if (-not $process.ExecutablePath) { return 'unknown' }
|
|
100
|
+
if ($process.ExecutablePath -notin $Configuration.Executables) { return 'fail' }
|
|
101
|
+
$policy = Test-UChanBrowserProcess $observation $process.ExecutablePath $Configuration.Profile $Configuration.OwnerSid $Configuration.Endpoint.Port
|
|
102
|
+
if ($policy -ne 'pass') { return $policy }
|
|
103
|
+
$version = Invoke-RestMethod -Uri ([Uri]::new($Configuration.Endpoint,'/json/version')) -TimeoutSec 2 -ErrorAction Stop
|
|
104
|
+
$socket = [Uri]$version.webSocketDebuggerUrl
|
|
105
|
+
if ($socket.Scheme -ne 'ws' -or $socket.Host -notin @('127.0.0.1','localhost','[::1]','::1') -or $socket.Port -ne $Configuration.Endpoint.Port -or $socket.UserInfo) { return 'fail' }
|
|
106
|
+
$edge = $version.'User-Agent' -match '(?:Edg|Edge)/'
|
|
107
|
+
if (($Configuration.Browser -eq 'edge') -ne $edge) { return 'fail' }
|
|
108
|
+
return 'pass'
|
|
109
|
+
} catch { return 'unknown' }
|
|
110
|
+
}
|
package/scripts/login-bw.bat
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
@echo off
|
|
2
|
-
setlocal
|
|
3
|
-
set ROOT=%~dp0..
|
|
4
|
-
|
|
5
|
-
fltmc.exe >nul 2>&1
|
|
6
|
-
if %errorlevel% neq 0 (
|
|
7
|
-
echo [*] Requesting Administrator elevation...
|
|
8
|
-
powershell -NoProfile -Command "Start-Process -FilePath '%~f0' -Verb RunAs"
|
|
9
|
-
exit /b
|
|
10
|
-
)
|
|
11
|
-
|
|
12
|
-
set BITWARDENCLI_APPDATA_DIR=C:\ProgramData\u-chan-secrets\bw-data
|
|
13
|
-
set BW_BIN=C:\ProgramData\u-chan-secrets\bin\bw.exe
|
|
14
|
-
if not exist "%BW_BIN%" (
|
|
15
|
-
for /f "delims=" %%i in ('where.exe bw.exe 2^>nul') do set BW_BIN=%%i
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
echo [*] Logging in to Bitwarden for U-Chan Service Store...
|
|
19
|
-
"%BW_BIN%" login
|
|
20
|
-
echo.
|
|
21
|
-
pause
|
|
1
|
+
@echo off
|
|
2
|
+
setlocal
|
|
3
|
+
set ROOT=%~dp0..
|
|
4
|
+
|
|
5
|
+
fltmc.exe >nul 2>&1
|
|
6
|
+
if %errorlevel% neq 0 (
|
|
7
|
+
echo [*] Requesting Administrator elevation...
|
|
8
|
+
powershell -NoProfile -Command "Start-Process -FilePath '%~f0' -Verb RunAs"
|
|
9
|
+
exit /b
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
set BITWARDENCLI_APPDATA_DIR=C:\ProgramData\u-chan-secrets\bw-data
|
|
13
|
+
set BW_BIN=C:\ProgramData\u-chan-secrets\bin\bw.exe
|
|
14
|
+
if not exist "%BW_BIN%" (
|
|
15
|
+
for /f "delims=" %%i in ('where.exe bw.exe 2^>nul') do set BW_BIN=%%i
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
echo [*] Logging in to Bitwarden for U-Chan Service Store...
|
|
19
|
+
"%BW_BIN%" login
|
|
20
|
+
echo.
|
|
21
|
+
pause
|
package/scripts/migrate.bat
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
@echo off
|
|
2
|
-
setlocal
|
|
3
|
-
call "%~dp0debug-install.bat" %*
|
|
4
|
-
|
|
1
|
+
@echo off
|
|
2
|
+
setlocal
|
|
3
|
+
call "%~dp0debug-install.bat" %*
|
|
4
|
+
|
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
param([ValidateSet('chrome','edge')][string]$Browser, [switch]$Background, [switch]$CheckOnly, [string]$DesktopOwnerSid)
|
|
2
|
-
$ErrorActionPreference='Stop'
|
|
3
|
-
. (Join-Path $PSScriptRoot 'desktop-browser.ps1')
|
|
4
|
-
if ($DesktopOwnerSid) {
|
|
5
|
-
if (-not $CheckOnly) { throw 'An owner-specific inspection cannot launch a desktop browser.' }
|
|
6
|
-
$observed = 'unknown'
|
|
7
|
-
$kinds = if ($Browser) { @($Browser) } else { @('chrome','edge') }
|
|
8
|
-
foreach ($kind in $kinds) {
|
|
9
|
-
$configuration = Get-UChanOwnerBrowserConfiguration $DesktopOwnerSid $kind
|
|
10
|
-
$candidate = Get-UChanBrowserState $configuration
|
|
11
|
-
if ($candidate -eq 'pass') { 'pass'; return }
|
|
12
|
-
if ($candidate -eq 'fail' -or $candidate -eq 'missing') { $observed = 'fail' }
|
|
13
|
-
}
|
|
14
|
-
$observed; return
|
|
15
|
-
}
|
|
16
|
-
$configuration=Get-UChanBrowserConfiguration $Browser
|
|
17
|
-
$state=Get-UChanBrowserState $configuration
|
|
18
|
-
if ($CheckOnly) { if ($state -eq 'missing') { 'fail' } else { $state }; return }
|
|
19
|
-
if ($state -eq 'pass') { Write-Host ($configuration.Browser + ' dedicated browser is ready.'); return }
|
|
20
|
-
if ($state -ne 'missing') { throw 'The browser endpoint is occupied, mismatched, or unverifiable. Close only the previous dedicated U-Chan browser before switching; keep personal windows open.' }
|
|
21
|
-
if ($configuration.Executables.Count -eq 0) { throw ($configuration.Browser + ' is not installed. Install it or explicitly select the other supported browser.') }
|
|
22
|
-
New-Item -ItemType Directory -Force -Path $configuration.Profile | Out-Null
|
|
23
|
-
$arguments=@(('--remote-debugging-port='+$configuration.Endpoint.Port), '--remote-debugging-address=127.0.0.1', ('--user-data-dir="'+$configuration.Profile+'"'), '--no-first-run')
|
|
24
|
-
if ($Background) { $arguments += '--no-startup-window' }
|
|
25
|
-
Start-Process -FilePath $configuration.Executables[0] -ArgumentList $arguments -WindowStyle Hidden
|
|
26
|
-
$deadline=[DateTime]::UtcNow.AddSeconds(15)
|
|
27
|
-
do {
|
|
28
|
-
Start-Sleep -Milliseconds 300
|
|
29
|
-
$state=Get-UChanBrowserState $configuration
|
|
30
|
-
if ($state -eq 'pass') { Write-Host ($configuration.Browser + ' dedicated browser started.'); return }
|
|
31
|
-
if ($state -eq 'fail') { break }
|
|
32
|
-
} while ([DateTime]::UtcNow -lt $deadline)
|
|
33
|
-
throw 'Selected dedicated browser did not become verifiably ready. Check browser policy and run setup again.'
|
|
1
|
+
param([ValidateSet('chrome','edge')][string]$Browser, [switch]$Background, [switch]$CheckOnly, [string]$DesktopOwnerSid)
|
|
2
|
+
$ErrorActionPreference='Stop'
|
|
3
|
+
. (Join-Path $PSScriptRoot 'desktop-browser.ps1')
|
|
4
|
+
if ($DesktopOwnerSid) {
|
|
5
|
+
if (-not $CheckOnly) { throw 'An owner-specific inspection cannot launch a desktop browser.' }
|
|
6
|
+
$observed = 'unknown'
|
|
7
|
+
$kinds = if ($Browser) { @($Browser) } else { @('chrome','edge') }
|
|
8
|
+
foreach ($kind in $kinds) {
|
|
9
|
+
$configuration = Get-UChanOwnerBrowserConfiguration $DesktopOwnerSid $kind
|
|
10
|
+
$candidate = Get-UChanBrowserState $configuration
|
|
11
|
+
if ($candidate -eq 'pass') { 'pass'; return }
|
|
12
|
+
if ($candidate -eq 'fail' -or $candidate -eq 'missing') { $observed = 'fail' }
|
|
13
|
+
}
|
|
14
|
+
$observed; return
|
|
15
|
+
}
|
|
16
|
+
$configuration=Get-UChanBrowserConfiguration $Browser
|
|
17
|
+
$state=Get-UChanBrowserState $configuration
|
|
18
|
+
if ($CheckOnly) { if ($state -eq 'missing') { 'fail' } else { $state }; return }
|
|
19
|
+
if ($state -eq 'pass') { Write-Host ($configuration.Browser + ' dedicated browser is ready.'); return }
|
|
20
|
+
if ($state -ne 'missing') { throw 'The browser endpoint is occupied, mismatched, or unverifiable. Close only the previous dedicated U-Chan browser before switching; keep personal windows open.' }
|
|
21
|
+
if ($configuration.Executables.Count -eq 0) { throw ($configuration.Browser + ' is not installed. Install it or explicitly select the other supported browser.') }
|
|
22
|
+
New-Item -ItemType Directory -Force -Path $configuration.Profile | Out-Null
|
|
23
|
+
$arguments=@(('--remote-debugging-port='+$configuration.Endpoint.Port), '--remote-debugging-address=127.0.0.1', ('--user-data-dir="'+$configuration.Profile+'"'), '--no-first-run')
|
|
24
|
+
if ($Background) { $arguments += '--no-startup-window' }
|
|
25
|
+
Start-Process -FilePath $configuration.Executables[0] -ArgumentList $arguments -WindowStyle Hidden
|
|
26
|
+
$deadline=[DateTime]::UtcNow.AddSeconds(15)
|
|
27
|
+
do {
|
|
28
|
+
Start-Sleep -Milliseconds 300
|
|
29
|
+
$state=Get-UChanBrowserState $configuration
|
|
30
|
+
if ($state -eq 'pass') { Write-Host ($configuration.Browser + ' dedicated browser started.'); return }
|
|
31
|
+
if ($state -eq 'fail') { break }
|
|
32
|
+
} while ([DateTime]::UtcNow -lt $deadline)
|
|
33
|
+
throw 'Selected dedicated browser did not become verifiably ready. Check browser policy and run setup again.'
|
|
@@ -1,54 +1,54 @@
|
|
|
1
|
-
// User-side half of the disposable Windows browser matrix. The administrator runs the SYSTEM fixture separately.
|
|
2
|
-
import fs from 'node:fs/promises'
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
import os from 'node:os'
|
|
5
|
-
import { fileURLToPath } from 'node:url'
|
|
6
|
-
import { createServer } from 'node:net'
|
|
7
|
-
import { execFile } from 'node:child_process'
|
|
8
|
-
import { promisify } from 'node:util'
|
|
9
|
-
import { chromium } from 'playwright'
|
|
10
|
-
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
|
11
|
-
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
|
12
|
-
import { isElevated } from '../../dist/client/codex-setup.js'
|
|
13
|
-
import { inspectDesktopBrowser } from '../../dist/platform/windows/desktop-browser.js'
|
|
14
|
-
const [command, browser, runId] = process.argv.slice(2)
|
|
15
|
-
if (process.platform !== 'win32' || command !== '--prepare' || !['chrome','edge'].includes(browser) || !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(runId ?? '')) throw new Error('Use --prepare chrome|edge <fresh UUID> on Windows')
|
|
16
|
-
if (await isElevated()) throw new Error('Prepare must run as the non-elevated desktop owner')
|
|
17
|
-
const root = path.join(os.tmpdir(), 'u-chan-v5-matrix-' + runId), home = path.join(root, browser)
|
|
18
|
-
await fs.mkdir(root) // A fresh UUID must never reuse another fixture or personal profile.
|
|
19
|
-
await fs.mkdir(home)
|
|
20
|
-
const port = await new Promise((resolve, reject) => { const server = createServer(); server.once('error', reject); server.listen(0, '127.0.0.1', () => { const selected = server.address().port; server.close(() => resolve(selected)) }) })
|
|
21
|
-
const endpoint = 'http://127.0.0.1:' + port, environment = { ...process.env, U_CHAN_HOME: home, U_CHAN_CDP: endpoint, U_CHAN_BROWSER: browser }
|
|
22
|
-
const report = { runId, browser, ok: false }, exec = promisify(execFile)
|
|
23
|
-
let connection
|
|
24
|
-
try {
|
|
25
|
-
await exec('powershell.exe', ['-NoProfile','-NonInteractive','-ExecutionPolicy','Bypass','-File',fileURLToPath(new URL('../start-browser.ps1',import.meta.url)),'-Browser',browser,'-Background'], { env: environment, windowsHide: true, timeout: 30000 })
|
|
26
|
-
report.desktopIdentity = await inspectDesktopBrowser({ endpoint, browser, profile: path.join(home,browser+'-profile') },environment)
|
|
27
|
-
if (report.desktopIdentity !== 'pass') throw new Error('Disposable browser identity failed')
|
|
28
|
-
connection = await chromium.connectOverCDP(endpoint)
|
|
29
|
-
await fs.writeFile(path.join(root,'prepared.json'),JSON.stringify({runId,browser,home,endpoint}),{flag:'wx'})
|
|
30
|
-
console.log(JSON.stringify({prepared:true,runId,browser,endpoint}))
|
|
31
|
-
const receipt = path.join(process.env.ProgramData ?? 'C:\\ProgramData','UChanSecurityChecks',runId,'report.json'), deadline=Date.now()+600000
|
|
32
|
-
while(Date.now()<deadline&&!await fs.stat(receipt).catch(()=>null))await new Promise(resolve=>setTimeout(resolve,500))
|
|
33
|
-
const native = JSON.parse(await fs.readFile(receipt,'utf8'))
|
|
34
|
-
report.systemFixturePassed = native.ok === true && native.fixtureBrowser === browser && native.desktopIdentity === 'pass'
|
|
35
|
-
const client = new Client({name:'u-chan-browser-matrix',version:'1.0.0'})
|
|
36
|
-
const transport = new StdioClientTransport({command:process.execPath,args:[path.join(path.dirname(process.execPath),'node_modules/npm/bin/npx-cli.js'),'@playwright/mcp@latest','--cdp-endpoint',endpoint],stderr:'pipe'})
|
|
37
|
-
try {
|
|
38
|
-
await client.connect(transport)
|
|
39
|
-
report.playwrightTools=(await client.listTools()).tools.length
|
|
40
|
-
report.sharedBrowserListing=!(await client.callTool({name:'browser_tabs',arguments:{action:'list'}})).isError
|
|
41
|
-
} finally {await client.close().catch(()=>{});await transport.close().catch(()=>{})}
|
|
42
|
-
report.ok=report.systemFixturePassed&&report.playwrightTools>0&&report.sharedBrowserListing
|
|
43
|
-
}catch(error){report.errorType=error.name}
|
|
44
|
-
finally{
|
|
45
|
-
// Only this verified disposable instance may be closed; never enumerate or terminate other browsers.
|
|
46
|
-
if(connection&&await inspectDesktopBrowser({endpoint,browser,profile:path.join(home,browser+'-profile')},environment)==='pass') {
|
|
47
|
-
const session=await connection.newBrowserCDPSession().catch(()=>null)
|
|
48
|
-
await session?.send('Browser.close').catch(()=>{})
|
|
49
|
-
}
|
|
50
|
-
await connection?.close().catch(()=>{})
|
|
51
|
-
await fs.writeFile(path.join(root,'complete.json'),JSON.stringify(report,null,2),{flag:'wx'})
|
|
52
|
-
}
|
|
53
|
-
console.log(JSON.stringify(report))
|
|
54
|
-
if(!report.ok)process.exitCode=1
|
|
1
|
+
// User-side half of the disposable Windows browser matrix. The administrator runs the SYSTEM fixture separately.
|
|
2
|
+
import fs from 'node:fs/promises'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import os from 'node:os'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
import { createServer } from 'node:net'
|
|
7
|
+
import { execFile } from 'node:child_process'
|
|
8
|
+
import { promisify } from 'node:util'
|
|
9
|
+
import { chromium } from 'playwright'
|
|
10
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
|
11
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
|
12
|
+
import { isElevated } from '../../dist/client/codex-setup.js'
|
|
13
|
+
import { inspectDesktopBrowser } from '../../dist/platform/windows/desktop-browser.js'
|
|
14
|
+
const [command, browser, runId] = process.argv.slice(2)
|
|
15
|
+
if (process.platform !== 'win32' || command !== '--prepare' || !['chrome','edge'].includes(browser) || !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(runId ?? '')) throw new Error('Use --prepare chrome|edge <fresh UUID> on Windows')
|
|
16
|
+
if (await isElevated()) throw new Error('Prepare must run as the non-elevated desktop owner')
|
|
17
|
+
const root = path.join(os.tmpdir(), 'u-chan-v5-matrix-' + runId), home = path.join(root, browser)
|
|
18
|
+
await fs.mkdir(root) // A fresh UUID must never reuse another fixture or personal profile.
|
|
19
|
+
await fs.mkdir(home)
|
|
20
|
+
const port = await new Promise((resolve, reject) => { const server = createServer(); server.once('error', reject); server.listen(0, '127.0.0.1', () => { const selected = server.address().port; server.close(() => resolve(selected)) }) })
|
|
21
|
+
const endpoint = 'http://127.0.0.1:' + port, environment = { ...process.env, U_CHAN_HOME: home, U_CHAN_CDP: endpoint, U_CHAN_BROWSER: browser }
|
|
22
|
+
const report = { runId, browser, ok: false }, exec = promisify(execFile)
|
|
23
|
+
let connection
|
|
24
|
+
try {
|
|
25
|
+
await exec('powershell.exe', ['-NoProfile','-NonInteractive','-ExecutionPolicy','Bypass','-File',fileURLToPath(new URL('../start-browser.ps1',import.meta.url)),'-Browser',browser,'-Background'], { env: environment, windowsHide: true, timeout: 30000 })
|
|
26
|
+
report.desktopIdentity = await inspectDesktopBrowser({ endpoint, browser, profile: path.join(home,browser+'-profile') },environment)
|
|
27
|
+
if (report.desktopIdentity !== 'pass') throw new Error('Disposable browser identity failed')
|
|
28
|
+
connection = await chromium.connectOverCDP(endpoint)
|
|
29
|
+
await fs.writeFile(path.join(root,'prepared.json'),JSON.stringify({runId,browser,home,endpoint}),{flag:'wx'})
|
|
30
|
+
console.log(JSON.stringify({prepared:true,runId,browser,endpoint}))
|
|
31
|
+
const receipt = path.join(process.env.ProgramData ?? 'C:\\ProgramData','UChanSecurityChecks',runId,'report.json'), deadline=Date.now()+600000
|
|
32
|
+
while(Date.now()<deadline&&!await fs.stat(receipt).catch(()=>null))await new Promise(resolve=>setTimeout(resolve,500))
|
|
33
|
+
const native = JSON.parse(await fs.readFile(receipt,'utf8'))
|
|
34
|
+
report.systemFixturePassed = native.ok === true && native.fixtureBrowser === browser && native.desktopIdentity === 'pass'
|
|
35
|
+
const client = new Client({name:'u-chan-browser-matrix',version:'1.0.0'})
|
|
36
|
+
const transport = new StdioClientTransport({command:process.execPath,args:[path.join(path.dirname(process.execPath),'node_modules/npm/bin/npx-cli.js'),'@playwright/mcp@latest','--cdp-endpoint',endpoint],stderr:'pipe'})
|
|
37
|
+
try {
|
|
38
|
+
await client.connect(transport)
|
|
39
|
+
report.playwrightTools=(await client.listTools()).tools.length
|
|
40
|
+
report.sharedBrowserListing=!(await client.callTool({name:'browser_tabs',arguments:{action:'list'}})).isError
|
|
41
|
+
} finally {await client.close().catch(()=>{});await transport.close().catch(()=>{})}
|
|
42
|
+
report.ok=report.systemFixturePassed&&report.playwrightTools>0&&report.sharedBrowserListing
|
|
43
|
+
}catch(error){report.errorType=error.name}
|
|
44
|
+
finally{
|
|
45
|
+
// Only this verified disposable instance may be closed; never enumerate or terminate other browsers.
|
|
46
|
+
if(connection&&await inspectDesktopBrowser({endpoint,browser,profile:path.join(home,browser+'-profile')},environment)==='pass') {
|
|
47
|
+
const session=await connection.newBrowserCDPSession().catch(()=>null)
|
|
48
|
+
await session?.send('Browser.close').catch(()=>{})
|
|
49
|
+
}
|
|
50
|
+
await connection?.close().catch(()=>{})
|
|
51
|
+
await fs.writeFile(path.join(root,'complete.json'),JSON.stringify(report,null,2),{flag:'wx'})
|
|
52
|
+
}
|
|
53
|
+
console.log(JSON.stringify(report))
|
|
54
|
+
if(!report.ok)process.exitCode=1
|