u-chan-secrets 4.0.1 → 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 +45 -6
- package/dist/cli.js +32 -23
- package/dist/client/browser-choice.js +49 -0
- package/dist/client/codex-setup.js +11 -4
- package/dist/client/setup.js +25 -8
- package/dist/client/windows-command.js +54 -0
- package/dist/core/browser.js +90 -22
- package/dist/core/config.js +13 -0
- package/dist/core/doctor.js +25 -4
- package/dist/core/login-filler.js +25 -25
- package/dist/core/login-success.js +24 -0
- package/dist/core/service.js +6 -4
- package/dist/core/types.js +1 -1
- package/dist/index.js +2 -2
- package/dist/ipc/client.js +20 -1
- package/dist/ipc/server.js +1 -1
- package/dist/mcp/server.js +4 -4
- package/dist/platform/darwin/manage-script.js +30 -30
- package/dist/platform/windows/desktop-browser.js +14 -0
- package/dist/platform/windows/install.js +4 -0
- package/dist/service/control.js +1 -1
- package/dist/service/doctor.js +7 -3
- package/dist/service/isolated-login.js +33 -10
- package/dist/service/main.js +9 -3
- package/dist/service/manage.js +3 -2
- package/dist/service/private-network.js +97 -0
- package/package.json +1 -1
- package/scripts/desktop-browser.ps1 +110 -0
- package/scripts/start-browser.ps1 +33 -0
- package/scripts/start-chrome.ps1 +4 -38
- package/scripts/verify-windows-adversarial.mjs +11 -1
- package/scripts/windows-security/browser-worker.mjs +50 -10
- package/scripts/windows-security/desktop-matrix.mjs +54 -0
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
# U-Chan Secrets
|
|
1
|
+
# U-Chan Secrets V5
|
|
2
2
|
|
|
3
|
-
A local MCP server that signs into approved websites with Bitwarden credentials. A privileged service performs credential entry in an isolated browser and transfers filtered session state into a dedicated
|
|
3
|
+
A local MCP server that signs into approved websites with Bitwarden credentials. A privileged service performs credential entry in an isolated browser and transfers filtered session state into a dedicated Chrome or Edge profile.
|
|
4
4
|
|
|
5
|
-
Version `
|
|
5
|
+
Version `5.0.0` adds selectable Chrome/Edge desktop destinations and checks approved login evidence before and after session transfer. Chrome remains the default when no browser has been selected. Protocol and protected-state schema remain version 3. Source integration, publication, and installed verification are separate events.
|
|
6
6
|
|
|
7
|
-
**Verification:** Windows/macOS build and test CI is separate from installed-service security verification. Native process identity, input isolation, permissions, and installation rehearsals remain [release gates](docs/attack-checklist.md). See [validation evidence](docs/superpowers/research/2026-10-
|
|
7
|
+
**Verification:** Windows/macOS build and test CI is separate from installed-service security verification. Native process identity, input isolation, permissions, and installation rehearsals remain [release gates](docs/attack-checklist.md). See [V5 validation evidence](docs/superpowers/research/2026-10-30-v5-validation.md) and [SECURITY.md](SECURITY.md).
|
|
8
8
|
|
|
9
9
|
The [Windows V4 rollout](docs/superpowers/research/2026-10-29-v4-rollout.md) records matching service/CLI versions, preserved protected state, real setup and fresh MCP/browser checks. It builds on the tightened V3.0.3 [native closeout](docs/superpowers/research/2026-10-29-v3-windows-closeout.md). macOS runtime remains unverified; source merge, installed verification, and package publication are recorded separately.
|
|
10
10
|
|
|
@@ -18,6 +18,43 @@ The [Windows V4 rollout](docs/superpowers/research/2026-10-29-v4-rollout.md) rec
|
|
|
18
18
|
|
|
19
19
|
Windows Hello and Touch ID remain [research only](docs/superpowers/research/2026-10-29-native-approval.md). Linux, general SSO/OAuth, passkeys, and automated CAPTCHA solving are outside V3.
|
|
20
20
|
|
|
21
|
+
## What V5 adds
|
|
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
|
+
|
|
36
|
+
Choose your browser from a normal Windows terminal:
|
|
37
|
+
|
|
38
|
+
```powershell
|
|
39
|
+
u-chan setup --browser edge --json
|
|
40
|
+
u-chan setup --browser edge --apply --replace-existing --start-browser
|
|
41
|
+
u-chan setup --browser chrome --apply --replace-existing --start-browser
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The first command inspects the proposed choice. `--apply` saves it in user-side `browser.json` and registers the matching MCP environment through the official Codex CLI. Later commands without `--browser` keep the saved choice; a new installation defaults to Chrome. Browser and Codex configuration backups are retained under their respective `#0 backup` directories. Each user's choice is independent.
|
|
45
|
+
|
|
46
|
+
Chrome and Edge use separate dedicated profiles. Only one browser owns the configured U-Chan CDP endpoint at a time. When switching, close the previous **dedicated U-Chan instance** and rerun setup; keep your personal browser windows open. U-Chan refuses wrong, unknown, personal-profile, or non-loopback listeners instead of silently attaching, selecting another port/browser, or terminating processes. It does not install browsers or change enterprise debugging policy. The legacy `scripts/start-chrome.ps1` explicitly selects Chrome; `scripts/start-browser.ps1 -Browser edge` selects Edge.
|
|
47
|
+
|
|
48
|
+
The service independently checks the installed owner's SID and dedicated profile before accessing the Windows destination. It derives that user's AppData paths from the profile registry. Advanced installations with a custom desktop profile root must align the trusted service environment's `U_CHAN_DESKTOP_HOME` with the user-side home; changing only a user-side environment variable does not redirect privileged session transfer.
|
|
49
|
+
|
|
50
|
+
An approved exact URL or visible selector must match in both the private login context and a new verification tab in the destination browser before `login` returns success. Existing user tabs are left alone. Missing evidence returns `authentication_unconfirmed`; private success with unverified desktop state returns `destination_unconfirmed`. Transfer failure/cancellation can leave partial session state and does not log the browser out. Expired or consumed grants are not refunded.
|
|
51
|
+
|
|
52
|
+
The private credential-bearing context uses a per-attempt HTTP/CONNECT proxy to block network destinations outside approved hosts, including redirect chains and worker/frame requests. Service workers are disabled. Private Chromium disables HTTP/2 and QUIC so connections cannot coalesce across approved and unapproved origins; HTTP/2-only sites may be unsupported. TLS remains end-to-end with normal browser certificate validation. Some sites requiring third-party resources or external identity providers also remain unsupported; U-Chan does not silently broaden host approval. CAPTCHA, passkey, push approval, or unavailable/rejected TOTP returns `needs_human` with manual sign-in guidance. V5 does not expose or resume the private challenge context, repeat credential submissions, or ask for passwords/codes in AI chat.
|
|
53
|
+
|
|
54
|
+
A V5 client requires the service's `verified_session_transfer` capability before login and binds the request to its selected browser. Update both CLI and service; an older service is refused before a login attempt is sent. An old client without a browser field retains its legacy Chrome destination. A stale MCP browser environment that conflicts with the saved choice is refused until the AI session restarts. Existing grants and vault data remain compatible. Native macOS runtime and ownership by an already-running AI session remain unverified.
|
|
55
|
+
|
|
56
|
+
Other MCP clients may set `U_CHAN_BROWSER=edge` (or `chrome`) and the matching loopback `U_CHAN_CDP` in their own registration, then launch the matching dedicated browser. Automatic registration remains Windows/Codex only; setting a desktop environment variable does not change the privileged service's endpoint.
|
|
57
|
+
|
|
21
58
|
## What V4 adds
|
|
22
59
|
|
|
23
60
|
Use a normal, non-administrator terminal after installing the matching trusted service:
|
|
@@ -30,12 +67,14 @@ u-chan setup --client codex --apply --replace-existing --start-browser
|
|
|
30
67
|
|
|
31
68
|
The first command only inspects. `--apply` adds missing Codex MCP registrations through the official Codex CLI. `--replace-existing` also permits replacement of recognized simple registrations. Existing configuration is backed up under the Codex configuration directory's `#0 backup` before mutation. Compatible enabled registrations retain custom environment, working directory, tool filters, timeouts, and other settings. Incompatible custom registrations, conflicting target overrides, disabled entries, and HTTP transports require manual configuration and remain untouched. Partial failures retain completed changes and the backup; setup never restores an obsolete whole configuration over concurrent edits.
|
|
32
69
|
|
|
33
|
-
`--start-browser` explicitly starts the dedicated
|
|
70
|
+
`--start-browser` explicitly starts the selected dedicated browser profile in the background when its endpoint is unhealthy. No startup task is added. Setup refuses elevated execution and known service/desktop browser-target mismatches. It never installs the privileged service or changes UAC. Run `u-chan install` separately from a trusted administrator terminal.
|
|
34
71
|
|
|
35
72
|
Setup reports `configured` or `needs_attention`, plus `sharing` and `restartRequired`. `process_configured` means matching process arguments were observed; it does not verify active AI-session ownership. Unobservable sharing remains `unobserved`. Restart the AI session after registration changes. Exit codes: 1 means attention required, 2 means configured with restart required, and 0 means configured without a known restart requirement. A locked vault remains a valid installed state.
|
|
36
73
|
|
|
37
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.
|
|
38
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
|
+
|
|
39
78
|
Propose a site grant with an explicit success condition:
|
|
40
79
|
|
|
41
80
|
```sh
|
|
@@ -160,7 +199,7 @@ In Windows PowerShell, use `u-chan.cmd` if execution policy blocks npm's `.ps1`
|
|
|
160
199
|
| Command | Purpose |
|
|
161
200
|
|---|---|
|
|
162
201
|
| `doctor [--json]` | Read-only diagnostic report and safe next actions |
|
|
163
|
-
| `setup [--client codex] [--apply] [--replace-existing] [--start-browser] [--json]` | Windows desktop inspection and explicit Codex/browser setup |
|
|
202
|
+
| `setup [--client codex] [--browser chrome\|edge] [--apply] [--replace-existing] [--start-browser] [--json]` | Windows desktop inspection and explicit Codex/browser setup |
|
|
164
203
|
| `allow <domain> --login-url <https-url> --item <label> [--once\|--minutes N\|--permanent] [--success-url URL\|--success-selector SELECTOR]` | Propose a scoped grant through protected approval |
|
|
165
204
|
| `manage` | Native status, grant pages, recent activity, lock/unlock, and revocation |
|
|
166
205
|
| `unlock` | Request the protected master-password prompt |
|
package/dist/cli.js
CHANGED
|
@@ -14,6 +14,8 @@ import { join } from 'node:path';
|
|
|
14
14
|
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
|
+
import { saveBrowserChoice } from './client/browser-choice.js';
|
|
18
|
+
import { configureWindowsCommand } from './client/windows-command.js';
|
|
17
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';
|
|
18
20
|
/** import-v1 shows one approval window per entry, each open for up to 2 minutes, one after another. */
|
|
19
21
|
export const IMPORT_PROMPT_MS = 120000;
|
|
@@ -28,10 +30,16 @@ function defaultV1Path(env, platform) {
|
|
|
28
30
|
export async function main(argv, d) {
|
|
29
31
|
const [cmd, arg, ...rest] = argv;
|
|
30
32
|
let call;
|
|
33
|
+
let diagnosticEnv = d.env;
|
|
31
34
|
const doctor = d.doctor ?? (() => runDoctor({
|
|
32
35
|
platform: d.platform,
|
|
33
36
|
clientVersion: d.clientVersion ?? 'unknown',
|
|
34
|
-
clientEndpoint: loadConfig(
|
|
37
|
+
clientEndpoint: loadConfig(diagnosticEnv, d.platform).cdpEndpoint,
|
|
38
|
+
browser: loadConfig(diagnosticEnv, d.platform).browser,
|
|
39
|
+
browserIdentity: d.platform === 'win32' ? async () => {
|
|
40
|
+
const cfg = loadConfig(diagnosticEnv, d.platform);
|
|
41
|
+
return (await import('./platform/windows/desktop-browser.js')).inspectDesktopBrowser({ endpoint: cfg.cdpEndpoint, browser: cfg.browser, profile: cfg.browserProfile }, diagnosticEnv);
|
|
42
|
+
} : undefined,
|
|
35
43
|
elevation: d.platform === 'win32' ? async () => (await import('./platform/windows/doctor.js')).checkWindowsUac() : undefined,
|
|
36
44
|
registration: async () => d.platform === 'win32' ? (await import('./platform/windows/doctor.js')).checkWindowsRegistration() : d.platform === 'darwin' ? (await import('./platform/darwin/doctor.js')).checkDarwinRegistration() : 'fail',
|
|
37
45
|
service: async () => {
|
|
@@ -43,11 +51,11 @@ export async function main(argv, d) {
|
|
|
43
51
|
throw new Error('invalid diagnostics');
|
|
44
52
|
return s;
|
|
45
53
|
},
|
|
46
|
-
chrome: () => chromeState(loadConfig(
|
|
54
|
+
chrome: () => chromeState(loadConfig(diagnosticEnv, d.platform).cdpEndpoint),
|
|
47
55
|
playwright: async () => {
|
|
48
56
|
if (d.platform !== 'win32')
|
|
49
57
|
return 'unknown';
|
|
50
|
-
const state = await checkPlaywrightNow(process.pid, loadConfig(
|
|
58
|
+
const state = await checkPlaywrightNow(process.pid, loadConfig(diagnosticEnv, d.platform).cdpEndpoint);
|
|
51
59
|
return state === 'ok' ? 'pass' : state === 'missing' ? 'fail' : 'unknown';
|
|
52
60
|
},
|
|
53
61
|
}));
|
|
@@ -71,9 +79,10 @@ export async function main(argv, d) {
|
|
|
71
79
|
if (cmd === 'setup') {
|
|
72
80
|
const args = argv.slice(1), seen = new Set();
|
|
73
81
|
let valid = true;
|
|
82
|
+
let browser;
|
|
74
83
|
for (let i = 0; i < args.length; i++) {
|
|
75
84
|
const flag = args[i];
|
|
76
|
-
if (seen.has(flag) || !['--client', '--apply', '--replace-existing', '--start-browser', '--json'].includes(flag)) {
|
|
85
|
+
if (seen.has(flag) || !['--client', '--browser', '--apply', '--replace-existing', '--start-browser', '--json'].includes(flag)) {
|
|
77
86
|
valid = false;
|
|
78
87
|
break;
|
|
79
88
|
}
|
|
@@ -82,31 +91,31 @@ export async function main(argv, d) {
|
|
|
82
91
|
valid = false;
|
|
83
92
|
break;
|
|
84
93
|
}
|
|
94
|
+
if (flag === '--browser') {
|
|
95
|
+
const value = args[++i];
|
|
96
|
+
if (value !== 'chrome' && value !== 'edge') {
|
|
97
|
+
valid = false;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
;
|
|
101
|
+
browser = value;
|
|
102
|
+
}
|
|
85
103
|
}
|
|
86
104
|
if (!valid || (seen.has('--replace-existing') && !seen.has('--apply'))) {
|
|
87
|
-
d.err('usage: u-chan setup [--client codex] [--apply] [--replace-existing] [--start-browser] [--json]');
|
|
105
|
+
d.err('usage: u-chan setup [--client codex] [--browser chrome|edge] [--apply] [--replace-existing] [--start-browser] [--json]');
|
|
88
106
|
return 1;
|
|
89
107
|
}
|
|
90
|
-
const options = { apply: seen.has('--apply'), replaceExisting: seen.has('--replace-existing'), startBrowser: seen.has('--start-browser') };
|
|
91
|
-
|
|
108
|
+
const options = { apply: seen.has('--apply'), replaceExisting: seen.has('--replace-existing'), startBrowser: seen.has('--start-browser'), ...(browser ? { browser } : {}) };
|
|
109
|
+
if (browser)
|
|
110
|
+
diagnosticEnv = { ...d.env, U_CHAN_BROWSER: browser };
|
|
111
|
+
const cfg = loadConfig(diagnosticEnv, d.platform), endpoint = cfg.cdpEndpoint;
|
|
92
112
|
const r = d.setup ? await d.setup(options) : await runSetup(options, {
|
|
93
|
-
platform: d.platform, endpoint, elevated: isElevated, doctor,
|
|
94
|
-
|
|
113
|
+
platform: d.platform, endpoint, browser: cfg.browser, elevated: isElevated, doctor,
|
|
114
|
+
configureShell: apply => configureWindowsCommand(apply, d.env.PATH ?? d.env.Path ?? ''),
|
|
115
|
+
selectBrowser: choice => saveBrowserChoice(cfg.browserChoicePath, choice),
|
|
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') }),
|
|
95
117
|
startBrowser: () => new Promise(resolve => {
|
|
96
|
-
execFile('powershell.exe', ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', fileURLToPath(new URL('../scripts/start-
|
|
97
|
-
if (err) {
|
|
98
|
-
resolve(false);
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
for (let i = 0; i < 5; i++) {
|
|
102
|
-
if (await chromeState(endpoint, 1000) === 'pass') {
|
|
103
|
-
resolve(true);
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
await new Promise(done => setTimeout(done, 300));
|
|
107
|
-
}
|
|
108
|
-
resolve(false);
|
|
109
|
-
});
|
|
118
|
+
execFile('powershell.exe', ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', fileURLToPath(new URL('../scripts/start-browser.ps1', import.meta.url)), '-Browser', cfg.browser, '-Background'], { timeout: 24000, windowsHide: true, env: diagnosticEnv, maxBuffer: 65536 }, err => resolve(!err));
|
|
110
119
|
}),
|
|
111
120
|
});
|
|
112
121
|
if (seen.has('--json'))
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { lstatSync, readFileSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { mkdir, rename, writeFile, lstat, realpath } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
export function browserKind(value) {
|
|
6
|
+
if (value !== 'chrome' && value !== 'edge')
|
|
7
|
+
throw new Error('Invalid browser selection; choose chrome or edge.');
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
export function readBrowserChoice(file) {
|
|
11
|
+
try {
|
|
12
|
+
const info = lstatSync(file), parent = resolve(dirname(file));
|
|
13
|
+
if (!info.isFile() || info.isSymbolicLink() || realpathSync(parent).toLowerCase() !== parent.toLowerCase())
|
|
14
|
+
throw new Error('redirected');
|
|
15
|
+
const value = JSON.parse(readFileSync(file, 'utf8'));
|
|
16
|
+
if (!value || Array.isArray(value) || Object.keys(value).length !== 1)
|
|
17
|
+
throw new Error('invalid');
|
|
18
|
+
return browserKind(value.browser);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (error.code === 'ENOENT')
|
|
22
|
+
return undefined;
|
|
23
|
+
throw new Error('Browser preference could not be read safely; inspect browser.json.');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export async function saveBrowserChoice(file, choice) {
|
|
27
|
+
browserKind(choice);
|
|
28
|
+
const previous = readBrowserChoice(file);
|
|
29
|
+
if (previous === choice)
|
|
30
|
+
return { changed: false };
|
|
31
|
+
const parent = resolve(dirname(file));
|
|
32
|
+
await mkdir(parent, { recursive: true, mode: 0o700 });
|
|
33
|
+
if ((await lstat(parent)).isSymbolicLink() || (await realpath(parent)).toLowerCase() !== parent.toLowerCase())
|
|
34
|
+
throw new Error('Redirected browser preference directory.');
|
|
35
|
+
let backupPath;
|
|
36
|
+
if (previous !== undefined) {
|
|
37
|
+
const backup = join(parent, '#0 backup');
|
|
38
|
+
await mkdir(backup, { mode: 0o700 }).catch(error => { if (error.code !== 'EEXIST')
|
|
39
|
+
throw error; });
|
|
40
|
+
if ((await lstat(backup)).isSymbolicLink() || (await realpath(backup)).toLowerCase() !== backup.toLowerCase())
|
|
41
|
+
throw new Error('Redirected browser backup directory.');
|
|
42
|
+
backupPath = join(backup, `browser-${randomUUID()}.json`);
|
|
43
|
+
await writeFile(backupPath, readFileSync(file), { flag: 'wx', mode: 0o600 });
|
|
44
|
+
}
|
|
45
|
+
const temporary = join(parent, `browser-${randomUUID()}.tmp`);
|
|
46
|
+
await writeFile(temporary, JSON.stringify({ browser: choice }) + '\n', { flag: 'wx', mode: 0o600 });
|
|
47
|
+
await rename(temporary, file);
|
|
48
|
+
return { changed: true, ...(backupPath ? { backupPath } : {}) };
|
|
49
|
+
}
|
|
@@ -5,6 +5,7 @@ import { basename, join, resolve } from 'node:path';
|
|
|
5
5
|
import { randomUUID } from 'node:crypto';
|
|
6
6
|
import { safeCdpOrigin } from '../core/doctor.js';
|
|
7
7
|
import { defaultEndpoint } from '../ipc/endpoint.js';
|
|
8
|
+
import { browserKind } from './browser-choice.js';
|
|
8
9
|
/** Fail closed when privilege inspection is unavailable. Never execute user-owned clients as administrator. */
|
|
9
10
|
export async function isElevated() {
|
|
10
11
|
if (process.platform !== 'win32')
|
|
@@ -27,7 +28,9 @@ function simple(entry, name) {
|
|
|
27
28
|
return false;
|
|
28
29
|
if (t.type !== 'stdio' || typeof t.command !== 'string' || !Array.isArray(t.args) || !t.args.every((a) => typeof a === 'string'))
|
|
29
30
|
return false;
|
|
30
|
-
if (t.cwd != null || (t.
|
|
31
|
+
if (t.cwd != null || (t.env_vars != null && (!Array.isArray(t.env_vars) || t.env_vars.length > 0)))
|
|
32
|
+
return false;
|
|
33
|
+
if (t.env != null && (typeof t.env !== 'object' || Array.isArray(t.env) || Object.entries(t.env).some(([key, value]) => name !== 'u_chan_secrets' || (key === 'U_CHAN_BROWSER' ? value !== 'chrome' && value !== 'edge' : key === 'U_CHAN_CDP' ? !safeCdpOrigin(value) : true))))
|
|
31
34
|
return false;
|
|
32
35
|
const command = basename(t.command.replaceAll('\\', '/')).toLowerCase();
|
|
33
36
|
if (name === 'u_chan_secrets')
|
|
@@ -72,6 +75,8 @@ export async function configureCodex(o, injected) {
|
|
|
72
75
|
try {
|
|
73
76
|
if (!safeCdpOrigin(o.endpoint) || (o.replaceExisting && !o.apply))
|
|
74
77
|
return result('manual', 'Use a safe loopback browser target and explicit apply flags.');
|
|
78
|
+
if (o.browser !== undefined)
|
|
79
|
+
browserKind(o.browser);
|
|
75
80
|
if (await d.elevated())
|
|
76
81
|
return result('unavailable', 'Run setup from a non-administrator desktop terminal.');
|
|
77
82
|
const desired = [
|
|
@@ -95,11 +100,12 @@ export async function configureCodex(o, injected) {
|
|
|
95
100
|
const name = key.toUpperCase();
|
|
96
101
|
return name === 'U_CHAN_CDP' ? safeCdpOrigin(env[key]) !== safeCdpOrigin(o.endpoint)
|
|
97
102
|
: name === 'U_CHAN_IPC' && env[key] !== ipc;
|
|
98
|
-
})) || (Array.isArray(inherited) && inherited.some(key => typeof key === 'string' && ['U_CHAN_CDP', 'U_CHAN_IPC'].includes(key.toUpperCase())))) {
|
|
103
|
+
})) || (Array.isArray(inherited) && inherited.some(key => typeof key === 'string' && ['U_CHAN_CDP', 'U_CHAN_IPC', 'U_CHAN_BROWSER'].includes(key.toUpperCase())))) {
|
|
99
104
|
return result('manual', 'Custom target environment preserved; align browser and service targets manually.');
|
|
100
105
|
}
|
|
101
106
|
if (current?.enabled === true && current.transport?.type === 'stdio'
|
|
102
|
-
&& current.transport.command === entry.command && JSON.stringify(current.transport.args) === JSON.stringify(entry.args)
|
|
107
|
+
&& current.transport.command === entry.command && JSON.stringify(current.transport.args) === JSON.stringify(entry.args)
|
|
108
|
+
&& (entry.name !== 'u_chan_secrets' || o.browser === undefined || (env?.U_CHAN_BROWSER === o.browser && safeCdpOrigin(env?.U_CHAN_CDP) === safeCdpOrigin(o.endpoint))))
|
|
103
109
|
continue;
|
|
104
110
|
if (!simple(current, entry.name))
|
|
105
111
|
return result('manual', 'Custom Codex settings preserved; configure this registration manually.');
|
|
@@ -127,7 +133,8 @@ export async function configureCodex(o, injected) {
|
|
|
127
133
|
return result('unavailable', 'Codex change failed; inspect configuration before retrying.');
|
|
128
134
|
changed = true;
|
|
129
135
|
}
|
|
130
|
-
const
|
|
136
|
+
const browserEnv = entry.name === 'u_chan_secrets' && o.browser ? ['--env', `U_CHAN_BROWSER=${o.browser}`, '--env', `U_CHAN_CDP=${safeCdpOrigin(o.endpoint)}`] : [];
|
|
137
|
+
const added = await d.exec(['mcp', 'add', entry.name, ...browserEnv, '--', entry.command, ...entry.args]);
|
|
131
138
|
if (added.code !== 0)
|
|
132
139
|
return result('unavailable', 'Codex change failed; completed changes and backup were retained.');
|
|
133
140
|
changed = true;
|
package/dist/client/setup.js
CHANGED
|
@@ -2,6 +2,8 @@ import { bounded, safeCdpOrigin } from '../core/doctor.js';
|
|
|
2
2
|
export async function runSetup(o, d) {
|
|
3
3
|
const r = { version: 1, state: 'needs_attention', sharing: 'unobserved', restartRequired: false, checks: [], actions: [], client: 'unavailable', changed: false };
|
|
4
4
|
const action = (id, message, required = true) => r.actions.push({ id, message, required });
|
|
5
|
+
if (d.browser || o.browser)
|
|
6
|
+
r.browser = o.browser ?? d.browser;
|
|
5
7
|
try {
|
|
6
8
|
if (await bounded(d.elevated, true)) {
|
|
7
9
|
action('configure_mcp', 'Run setup from a non-administrator desktop terminal.');
|
|
@@ -17,29 +19,44 @@ export async function runSetup(o, d) {
|
|
|
17
19
|
r.client = 'manual';
|
|
18
20
|
return r;
|
|
19
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
|
+
}
|
|
20
27
|
if (r.checks.some(c => c.id === 'browser_target' && c.state === 'fail')) {
|
|
21
28
|
r.sharing = 'mismatch';
|
|
22
29
|
action('configure_mcp', 'Align the desktop and service loopback browser targets before applying setup.');
|
|
23
30
|
return r;
|
|
24
31
|
}
|
|
25
|
-
const configured = await d.configure({ apply: o.apply, replaceExisting: o.replaceExisting });
|
|
32
|
+
const configured = await d.configure({ apply: o.apply, replaceExisting: o.replaceExisting, ...(o.browser ? { browser: o.browser } : {}) });
|
|
26
33
|
r.client = configured.state;
|
|
27
|
-
r.changed
|
|
34
|
+
r.changed ||= configured.changed;
|
|
28
35
|
r.backupPath = configured.backupPath;
|
|
36
|
+
r.restartRequired = configured.changed;
|
|
37
|
+
if (o.browser && o.apply && r.client === 'ready') {
|
|
38
|
+
if (!d.selectBrowser)
|
|
39
|
+
throw new Error('browser selection unavailable');
|
|
40
|
+
const selected = await d.selectBrowser(o.browser);
|
|
41
|
+
r.browserChanged = selected.changed;
|
|
42
|
+
r.browserBackupPath = selected.backupPath;
|
|
43
|
+
r.changed ||= selected.changed;
|
|
44
|
+
r.restartRequired ||= selected.changed;
|
|
45
|
+
}
|
|
29
46
|
let attemptedStart = false, started = true;
|
|
30
|
-
if (o.startBrowser && !r.checks.some(c => c.id === 'chrome' && c.state === 'pass')) {
|
|
47
|
+
if (o.startBrowser && (!r.checks.some(c => c.id === 'chrome' && c.state === 'pass') || r.checks.some(c => c.id === 'browser_identity' && c.state !== 'pass'))) {
|
|
31
48
|
attemptedStart = true;
|
|
32
|
-
started = await bounded(d.startBrowser, false,
|
|
49
|
+
started = await bounded(d.startBrowser, false, 25000);
|
|
33
50
|
}
|
|
34
51
|
if (r.changed || attemptedStart)
|
|
35
52
|
r.checks = (await d.doctor()).checks;
|
|
36
53
|
const pw = r.checks.find(c => c.id === 'playwright');
|
|
37
54
|
const mismatch = r.checks.some(c => c.id === 'browser_target' && c.state === 'fail');
|
|
38
55
|
r.sharing = mismatch || pw?.state === 'fail' ? 'mismatch' : pw?.state === 'pass' ? 'process_configured' : 'unobserved';
|
|
39
|
-
r.restartRequired
|
|
56
|
+
r.restartRequired ||= pw?.state === 'fail';
|
|
40
57
|
const ids = { registration: 'install_service', service: 'install_service', ipc: 'install_service',
|
|
41
58
|
version: 'update_service', assets: 'repair_permissions', permissions: 'repair_permissions', service_browser: 'start_browser', chrome: 'start_browser',
|
|
42
|
-
browser_target: 'configure_mcp', playwright: 'configure_mcp', uac: 'review_uac' };
|
|
59
|
+
browser_target: 'configure_mcp', browser_identity: 'start_browser', playwright: 'configure_mcp', uac: 'review_uac' };
|
|
43
60
|
for (const c of r.checks)
|
|
44
61
|
if (c.nextAction) {
|
|
45
62
|
const id = c.id === 'vault' ? c.state === 'pass' ? 'unlock_vault' : 'authenticate_vault' : ids[c.id] ?? 'configure_mcp';
|
|
@@ -48,12 +65,12 @@ export async function runSetup(o, d) {
|
|
|
48
65
|
if (r.client !== 'ready')
|
|
49
66
|
action('configure_mcp', 'Inspect Codex configuration; apply missing registrations explicitly and preserve custom settings.');
|
|
50
67
|
if (!started)
|
|
51
|
-
action('start_browser', '
|
|
68
|
+
action('start_browser', 'Selected dedicated browser is unavailable or mismatched. Close only the previous dedicated U-Chan instance before switching, then run setup again.');
|
|
52
69
|
if (r.restartRequired)
|
|
53
70
|
action('restart_client', 'Restart the AI session to load saved MCP registrations.', false);
|
|
54
71
|
action('configure_mcp', 'Active AI-session browser ownership is unverified; check the current client browser connection.', false);
|
|
55
72
|
const requiredOk = r.checks.length > 0 && r.checks.filter(c => c.required).every(c => c.state === 'pass');
|
|
56
|
-
if (requiredOk && r.client === 'ready' && started && !mismatch)
|
|
73
|
+
if (requiredOk && r.client === 'ready' && started && !mismatch && r.shell?.ready !== false)
|
|
57
74
|
r.state = 'configured';
|
|
58
75
|
return r;
|
|
59
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
|
+
}
|
package/dist/core/browser.js
CHANGED
|
@@ -1,8 +1,27 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
2
|
import { chromium } from 'playwright';
|
|
3
3
|
import { UserFacingError } from './types.js';
|
|
4
|
+
import { isHostAllowed } from './allowlist.js';
|
|
5
|
+
import { waitForLoginSuccess } from './login-success.js';
|
|
6
|
+
import { bounded } from './doctor.js';
|
|
4
7
|
import { abortable } from './abort.js';
|
|
5
8
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
9
|
+
/** Request-stage CDP interception checks each real main-frame redirect without rewriting responses. */
|
|
10
|
+
async function restrictPageNavigation(page, allowed, signal) {
|
|
11
|
+
const session = await page.context().newCDPSession(page);
|
|
12
|
+
const { frameTree } = await session.send('Page.getFrameTree');
|
|
13
|
+
session.on('Fetch.requestPaused', event => {
|
|
14
|
+
let permit = false;
|
|
15
|
+
try {
|
|
16
|
+
permit = event.frameId !== frameTree.frame.id || (!signal?.aborted && allowed(event.request.url));
|
|
17
|
+
}
|
|
18
|
+
catch { }
|
|
19
|
+
void session.send(permit ? 'Fetch.continueRequest' : 'Fetch.failRequest', permit ? { requestId: event.requestId } : { requestId: event.requestId, errorReason: 'BlockedByClient' }).catch(() => { });
|
|
20
|
+
});
|
|
21
|
+
// Bypass service workers only on this owned tab; existing registrations and user tabs are unchanged.
|
|
22
|
+
await session.send('Network.setBypassServiceWorker', { bypass: true });
|
|
23
|
+
await session.send('Fetch.enable', { patterns: [{ resourceType: 'Document', requestStage: 'Request' }] });
|
|
24
|
+
}
|
|
6
25
|
/** Runs scripts/start-chrome.ps1 (idempotent: it exits early if the port is already open). */
|
|
7
26
|
export const startChromeScript = (scriptPath) => () => new Promise((resolve, reject) => {
|
|
8
27
|
execFile('powershell.exe', ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', scriptPath], { timeout: 30000, windowsHide: true }, (err) => (err ? reject(err) : resolve()));
|
|
@@ -14,11 +33,18 @@ export class CdpBrowserProvider {
|
|
|
14
33
|
disconnecting;
|
|
15
34
|
browser;
|
|
16
35
|
connecting;
|
|
36
|
+
ownedPages = new Set();
|
|
37
|
+
openingPages = new Set();
|
|
17
38
|
constructor(endpoint, opts = {}) {
|
|
18
39
|
this.endpoint = endpoint;
|
|
19
40
|
this.opts = opts;
|
|
20
41
|
}
|
|
21
|
-
async getBrowser(autoLaunch) {
|
|
42
|
+
async getBrowser(autoLaunch, selectedBrowser) {
|
|
43
|
+
if (this.opts.validate && !await this.opts.validate(selectedBrowser))
|
|
44
|
+
throw new UserFacingError('selected dedicated browser identity could not be verified', 'browser_unavailable');
|
|
45
|
+
// Validated service operations reconnect so a browser switch cannot keep using an old cached destination.
|
|
46
|
+
if (this.opts.validate)
|
|
47
|
+
await this.disconnect();
|
|
22
48
|
if (this.browser?.isConnected())
|
|
23
49
|
return this.browser;
|
|
24
50
|
this.connecting ??= this.connect(autoLaunch)
|
|
@@ -34,9 +60,9 @@ export class CdpBrowserProvider {
|
|
|
34
60
|
}
|
|
35
61
|
catch {
|
|
36
62
|
if (!autoLaunch || !this.opts.launch)
|
|
37
|
-
throw new UserFacingError('
|
|
63
|
+
throw new UserFacingError('selected dedicated browser is not running — run u-chan setup --start-browser', 'browser_unavailable');
|
|
38
64
|
}
|
|
39
|
-
const fail = () => new UserFacingError('could not start
|
|
65
|
+
const fail = () => new UserFacingError('could not start the selected dedicated browser — run u-chan setup --start-browser', 'browser_unavailable');
|
|
40
66
|
await this.opts.launch().catch(() => {
|
|
41
67
|
throw fail();
|
|
42
68
|
});
|
|
@@ -53,32 +79,44 @@ export class CdpBrowserProvider {
|
|
|
53
79
|
}
|
|
54
80
|
disconnect() {
|
|
55
81
|
return this.disconnecting ??= (async () => {
|
|
82
|
+
await Promise.allSettled([...this.openingPages].map(opening => bounded(() => opening, undefined, 2000)));
|
|
83
|
+
await Promise.allSettled([...this.ownedPages].map(page => bounded(() => page.close(), undefined, 2000)));
|
|
84
|
+
this.ownedPages.clear();
|
|
56
85
|
const browser = this.browser ?? await this.connecting?.catch(() => undefined);
|
|
57
86
|
this.browser = undefined;
|
|
58
87
|
// For connectOverCDP, close disconnects this transport, not the user's Chrome process.
|
|
59
88
|
await browser?.close().catch(() => { });
|
|
60
89
|
})().finally(() => { this.disconnecting = undefined; });
|
|
61
90
|
}
|
|
62
|
-
async withPage(fn, signal) {
|
|
63
|
-
const browser = await abortable(this.getBrowser(true), signal, () => this.disconnect(), b => b.close());
|
|
91
|
+
async withPage(fn, signal, selectedBrowser) {
|
|
92
|
+
const browser = await abortable(this.getBrowser(true, selectedBrowser), signal, () => this.disconnect(), b => b.close());
|
|
64
93
|
const context = browser.contexts()[0];
|
|
65
94
|
if (!context)
|
|
66
|
-
throw new UserFacingError('
|
|
67
|
-
const
|
|
95
|
+
throw new UserFacingError('selected dedicated browser has no default context — run u-chan setup --start-browser', 'browser_unavailable');
|
|
96
|
+
const opening = context.newPage().then(page => { this.ownedPages.add(page); return page; });
|
|
97
|
+
this.openingPages.add(opening);
|
|
98
|
+
let page;
|
|
99
|
+
try {
|
|
100
|
+
page = await abortable(opening, signal, () => this.disconnect(), p => p.close());
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
this.openingPages.delete(opening);
|
|
104
|
+
}
|
|
68
105
|
page.setDefaultTimeout(5000);
|
|
69
106
|
try {
|
|
70
107
|
signal?.throwIfAborted();
|
|
71
108
|
return await abortable(fn(page), signal, () => this.disconnect());
|
|
72
109
|
}
|
|
73
110
|
finally {
|
|
74
|
-
await
|
|
111
|
+
await bounded(() => page.close(), undefined, 2000);
|
|
112
|
+
this.ownedPages.delete(page);
|
|
75
113
|
}
|
|
76
114
|
}
|
|
77
|
-
async withContext(fn, signal) {
|
|
78
|
-
const browser = await abortable(this.getBrowser(true), signal, () => this.disconnect(), b => b.close());
|
|
115
|
+
async withContext(fn, signal, selectedBrowser) {
|
|
116
|
+
const browser = await abortable(this.getBrowser(true, selectedBrowser), signal, () => this.disconnect(), b => b.close());
|
|
79
117
|
const context = browser.contexts()[0];
|
|
80
118
|
if (!context)
|
|
81
|
-
throw new UserFacingError('
|
|
119
|
+
throw new UserFacingError('selected dedicated browser has no default context — run u-chan setup --start-browser', 'browser_unavailable');
|
|
82
120
|
signal?.throwIfAborted();
|
|
83
121
|
return abortable(fn(context), signal, () => this.disconnect());
|
|
84
122
|
}
|
|
@@ -92,16 +130,45 @@ export class CdpBrowserProvider {
|
|
|
92
130
|
}
|
|
93
131
|
}
|
|
94
132
|
}
|
|
95
|
-
/** Receives
|
|
133
|
+
/** Receives filtered session state and checks approved evidence in the user's dedicated browser over CDP. */
|
|
96
134
|
export class CdpSessionTarget {
|
|
97
135
|
provider;
|
|
98
|
-
|
|
136
|
+
options;
|
|
137
|
+
selectedBrowser = 'chrome';
|
|
138
|
+
constructor(provider, options = {}) {
|
|
99
139
|
this.provider = provider;
|
|
140
|
+
this.options = options;
|
|
141
|
+
}
|
|
142
|
+
async bindBrowser(browser) {
|
|
143
|
+
if (browser !== 'chrome' && browser !== 'edge')
|
|
144
|
+
return false;
|
|
145
|
+
this.selectedBrowser = browser;
|
|
146
|
+
return true; // The immediately following provider operation validates this exact kind against the native process.
|
|
100
147
|
}
|
|
101
148
|
userAgent(signal) {
|
|
102
|
-
return this.provider.withPage((page) => page.evaluate(() => navigator.userAgent), signal);
|
|
149
|
+
return this.provider.withPage((page) => page.evaluate(() => navigator.userAgent), signal, this.selectedBrowser);
|
|
103
150
|
}
|
|
104
151
|
cancel() { return this.provider.disconnect(); }
|
|
152
|
+
async verify(entry, finalUrl, signal) {
|
|
153
|
+
if (!entry.success)
|
|
154
|
+
return false;
|
|
155
|
+
const allowed = (url) => this.options.hostCheck ? this.options.hostCheck(entry, url) : isHostAllowed(entry, url);
|
|
156
|
+
const url = 'url' in entry.success ? entry.success.url : finalUrl;
|
|
157
|
+
if (!allowed(url))
|
|
158
|
+
return false;
|
|
159
|
+
try {
|
|
160
|
+
return await this.provider.withPage(async (page) => {
|
|
161
|
+
await restrictPageNavigation(page, allowed, signal);
|
|
162
|
+
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 10000 });
|
|
163
|
+
return waitForLoginSuccess(page, entry.success, allowed, signal, this.options.verifyTimeoutMs ?? 15000);
|
|
164
|
+
}, signal, this.selectedBrowser);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
if (signal?.aborted)
|
|
168
|
+
signal.throwIfAborted();
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
105
172
|
inject(state, signal) {
|
|
106
173
|
return this.provider.withContext(async (context) => {
|
|
107
174
|
signal?.throwIfAborted();
|
|
@@ -110,9 +177,13 @@ export class CdpSessionTarget {
|
|
|
110
177
|
signal?.throwIfAborted();
|
|
111
178
|
if (!o.localStorage.length)
|
|
112
179
|
continue;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
180
|
+
await this.provider.withPage(async (page) => {
|
|
181
|
+
await restrictPageNavigation(page, url => { try {
|
|
182
|
+
return new URL(url).origin === o.origin;
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return false;
|
|
186
|
+
} }, signal);
|
|
116
187
|
await page.goto(o.origin, { waitUntil: 'commit' });
|
|
117
188
|
signal?.throwIfAborted();
|
|
118
189
|
await page.evaluate(({ origin, items }) => {
|
|
@@ -121,11 +192,8 @@ export class CdpSessionTarget {
|
|
|
121
192
|
for (const i of items)
|
|
122
193
|
localStorage.setItem(i.name, i.value);
|
|
123
194
|
}, { origin: o.origin, items: o.localStorage });
|
|
124
|
-
}
|
|
125
|
-
finally {
|
|
126
|
-
await page.close().catch(() => { });
|
|
127
|
-
}
|
|
195
|
+
}, signal, this.selectedBrowser);
|
|
128
196
|
}
|
|
129
|
-
}, signal);
|
|
197
|
+
}, signal, this.selectedBrowser);
|
|
130
198
|
}
|
|
131
199
|
}
|