u-chan-secrets 4.0.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -6
- package/dist/cli.js +30 -23
- package/dist/client/browser-choice.js +49 -0
- package/dist/client/codex-setup.js +11 -4
- package/dist/client/setup.js +17 -5
- package/dist/core/browser.js +90 -22
- package/dist/core/config.js +13 -0
- package/dist/core/doctor.js +26 -5
- 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/doctor.js +1 -1
- package/dist/service/control.js +1 -1
- package/dist/service/doctor.js +5 -4
- 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/debug-install.bat +69 -69
- package/scripts/desktop-browser.ps1 +103 -0
- package/scripts/login-bw.bat +21 -21
- package/scripts/migrate.bat +4 -4
- 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
|
@@ -6,6 +6,7 @@ const CHALLENGE_CSS = 'iframe[src*="recaptcha"], iframe[src*="hcaptcha"], iframe
|
|
|
6
6
|
const USERNAME_CSS = '[autocomplete=username], [type=email], [name*=email i], [name*=user i], [id*=email i]';
|
|
7
7
|
const OTP_CSS = 'input[autocomplete="one-time-code"], input[name*="otp" i], input[id*="otp" i], input[name*="2fa" i], input[name="code"], input[id="code"]';
|
|
8
8
|
const visible = (l) => l.filter({ visible: true }).first();
|
|
9
|
+
const failed = (reason, code = 'unsupported_form') => ({ status: 'failed', reason, code });
|
|
9
10
|
/**
|
|
10
11
|
* Playwright puts the filled value into its error call log, so any action that
|
|
11
12
|
* handles credentials is rethrown with a fixed message and no cause.
|
|
@@ -15,7 +16,7 @@ async function guard(what, fn) {
|
|
|
15
16
|
return await fn();
|
|
16
17
|
}
|
|
17
18
|
catch {
|
|
18
|
-
throw new UserFacingError(`failed to ${what}
|
|
19
|
+
throw new UserFacingError(`failed to ${what}`, 'unsupported_form');
|
|
19
20
|
}
|
|
20
21
|
}
|
|
21
22
|
/**
|
|
@@ -31,14 +32,9 @@ export async function fillLogin(page, entry, getCreds, opts = {}) {
|
|
|
31
32
|
const url = page.url();
|
|
32
33
|
if (hostCheck(url))
|
|
33
34
|
return undefined;
|
|
34
|
-
|
|
35
|
-
try {
|
|
36
|
-
host = new URL(url).hostname;
|
|
37
|
-
}
|
|
38
|
-
catch { /* keep unknown */ }
|
|
39
|
-
return { status: 'failed', reason: `unexpected host ${host}` };
|
|
35
|
+
return failed('unexpected login host');
|
|
40
36
|
};
|
|
41
|
-
await page.goto(entry.loginUrl, { waitUntil: '
|
|
37
|
+
await guard('open approved login page', () => page.goto(entry.loginUrl, { waitUntil: 'domcontentloaded', timeout }));
|
|
42
38
|
const hostFail = badHost();
|
|
43
39
|
if (hostFail)
|
|
44
40
|
return hostFail;
|
|
@@ -47,6 +43,8 @@ export async function fillLogin(page, entry, getCreds, opts = {}) {
|
|
|
47
43
|
const passBox = visible(page.locator(sel?.password ?? 'input[type=password]'));
|
|
48
44
|
const submitBox = visible(page.locator(sel?.submit ?? 'button[type=submit], input[type=submit]'));
|
|
49
45
|
const otpBox = visible(page.locator(sel?.otp ?? OTP_CSS));
|
|
46
|
+
const needsHuman = async () => await visible(page.locator(CHALLENGE_CSS)).isVisible()
|
|
47
|
+
|| await visible(page.getByText(/(?:use|enter|verify|sign in with) (?:your |a )?passkey|check your (?:phone|device)|approve (?:this |the )?sign.?in/i)).isVisible();
|
|
50
48
|
const handleOf = async (l) => (await l.isVisible()) ? l.elementHandle({ timeout }) : null;
|
|
51
49
|
// Forms may render late; wait briefly before concluding there is none.
|
|
52
50
|
await userBox.or(passBox).first().waitFor({ state: 'visible', timeout: Math.min(timeout, 3000) }).catch(() => {
|
|
@@ -56,12 +54,14 @@ export async function fillLogin(page, entry, getCreds, opts = {}) {
|
|
|
56
54
|
const afterWaitHost = badHost();
|
|
57
55
|
if (afterWaitHost)
|
|
58
56
|
return afterWaitHost;
|
|
57
|
+
if (await needsHuman())
|
|
58
|
+
return failed(NEEDS_YOU, 'needs_human');
|
|
59
|
+
if (!hasUser && !hasPass && await otpBox.isVisible())
|
|
60
|
+
return failed(NEEDS_YOU, 'needs_human');
|
|
59
61
|
if (!hasUser && !hasPass)
|
|
60
62
|
return { status: 'already_logged_in' };
|
|
61
63
|
if (!hasUser)
|
|
62
|
-
return
|
|
63
|
-
if (await visible(page.locator(CHALLENGE_CSS)).isVisible())
|
|
64
|
-
return { status: 'failed', reason: NEEDS_YOU };
|
|
64
|
+
return failed('username field not found');
|
|
65
65
|
const creds = await getCreds();
|
|
66
66
|
// Each action: sync host check, then only the action itself (no awaits in between).
|
|
67
67
|
const submit = async (h) => {
|
|
@@ -73,7 +73,7 @@ export async function fillLogin(page, entry, getCreds, opts = {}) {
|
|
|
73
73
|
};
|
|
74
74
|
const userH = await handleOf(userBox);
|
|
75
75
|
if (!userH)
|
|
76
|
-
return
|
|
76
|
+
return failed('username field not found');
|
|
77
77
|
let passH = hasPass ? await handleOf(passBox) : null;
|
|
78
78
|
let submitH = await handleOf(submitBox);
|
|
79
79
|
let fail;
|
|
@@ -81,11 +81,11 @@ export async function fillLogin(page, entry, getCreds, opts = {}) {
|
|
|
81
81
|
// 2FA code, at most once per login so a bad seed cannot lock the account.
|
|
82
82
|
const fillOtp = async () => {
|
|
83
83
|
if (!creds.totp)
|
|
84
|
-
return
|
|
84
|
+
return failed('the page asks for a 2FA code but the Bitwarden item has no TOTP', 'needs_human');
|
|
85
85
|
const code = await freshTotpCode(creds.totp);
|
|
86
86
|
const otpH = await handleOf(otpBox);
|
|
87
87
|
if (!otpH)
|
|
88
|
-
return
|
|
88
|
+
return failed('2FA field not found');
|
|
89
89
|
const f = badHost();
|
|
90
90
|
if (f)
|
|
91
91
|
return f;
|
|
@@ -117,12 +117,12 @@ export async function fillLogin(page, entry, getCreds, opts = {}) {
|
|
|
117
117
|
await passBox.waitFor({ state: 'visible', timeout });
|
|
118
118
|
}
|
|
119
119
|
catch {
|
|
120
|
-
return
|
|
120
|
+
return await needsHuman() ? failed(NEEDS_YOU, 'needs_human') : failed('password field did not appear');
|
|
121
121
|
}
|
|
122
122
|
passH = await handleOf(passBox);
|
|
123
123
|
submitH = await handleOf(submitBox); // re-measured for step 2
|
|
124
124
|
if (!passH)
|
|
125
|
-
return
|
|
125
|
+
return failed('password field did not appear');
|
|
126
126
|
if ((fail = badHost()))
|
|
127
127
|
return fail;
|
|
128
128
|
await guard('fill password field', () => passH.fill(creds.password, { timeout }));
|
|
@@ -137,20 +137,19 @@ export async function fillLogin(page, entry, getCreds, opts = {}) {
|
|
|
137
137
|
await userBox.or(passBox).first().waitFor({ state: 'hidden', timeout }).catch(() => {
|
|
138
138
|
// Timeout: the state check below decides.
|
|
139
139
|
});
|
|
140
|
-
await page.waitForLoadState('
|
|
141
|
-
await page.waitForLoadState('networkidle', { timeout }).catch(() => { });
|
|
140
|
+
await page.waitForLoadState('domcontentloaded', { timeout: Math.min(timeout, 3000) }).catch(() => { });
|
|
142
141
|
if ((fail = badHost()))
|
|
143
142
|
return fail;
|
|
144
|
-
if (await
|
|
145
|
-
return
|
|
143
|
+
if (await needsHuman())
|
|
144
|
+
return failed(NEEDS_YOU, 'needs_human');
|
|
146
145
|
if ((await userBox.isVisible()) || (await passBox.isVisible())) {
|
|
147
146
|
const reason = otpFilled ? 'login form still present (wrong password or 2FA code?)' : 'login form still present (wrong password?)';
|
|
148
|
-
return
|
|
147
|
+
return failed(reason, otpFilled ? 'needs_human' : 'authentication_unconfirmed');
|
|
149
148
|
}
|
|
150
149
|
// Separate 2FA step after the password.
|
|
151
150
|
if (await otpBox.isVisible()) {
|
|
152
151
|
if (otpFilled)
|
|
153
|
-
return
|
|
152
|
+
return failed('2FA code rejected (wrong TOTP in Bitwarden?)', 'needs_human');
|
|
154
153
|
const otpSubmitH = await handleOf(submitBox);
|
|
155
154
|
if ((fail = await fillOtp()))
|
|
156
155
|
return fail;
|
|
@@ -159,12 +158,13 @@ export async function fillLogin(page, entry, getCreds, opts = {}) {
|
|
|
159
158
|
await otpBox.waitFor({ state: 'hidden', timeout }).catch(() => {
|
|
160
159
|
// Timeout: the check below decides.
|
|
161
160
|
});
|
|
162
|
-
await page.waitForLoadState('
|
|
163
|
-
await page.waitForLoadState('networkidle', { timeout }).catch(() => { });
|
|
161
|
+
await page.waitForLoadState('domcontentloaded', { timeout: Math.min(timeout, 3000) }).catch(() => { });
|
|
164
162
|
if ((fail = badHost()))
|
|
165
163
|
return fail;
|
|
166
164
|
if (await otpBox.isVisible())
|
|
167
|
-
return
|
|
165
|
+
return failed('2FA code rejected (wrong TOTP in Bitwarden?)', 'needs_human');
|
|
166
|
+
if (await needsHuman())
|
|
167
|
+
return failed(NEEDS_YOU, 'needs_human');
|
|
168
168
|
}
|
|
169
169
|
return { status: 'logged_in' };
|
|
170
170
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
2
|
+
/** Poll only approved evidence; page values and browser errors never become public diagnostics. */
|
|
3
|
+
export async function waitForLoginSuccess(page, success, hostCheck, signal, timeoutMs = 15000) {
|
|
4
|
+
const deadline = Date.now() + timeoutMs;
|
|
5
|
+
try {
|
|
6
|
+
do {
|
|
7
|
+
signal?.throwIfAborted();
|
|
8
|
+
if (!hostCheck(page.url()))
|
|
9
|
+
return false;
|
|
10
|
+
const matched = 'url' in success ? page.url() === success.url : await page.locator(success.selector).first().isVisible();
|
|
11
|
+
if (matched && hostCheck(page.url()))
|
|
12
|
+
return true;
|
|
13
|
+
if (Date.now() >= deadline)
|
|
14
|
+
return false;
|
|
15
|
+
await delay(Math.min(100, Math.max(0, deadline - Date.now())), undefined, { signal });
|
|
16
|
+
} while (Date.now() <= deadline);
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
if (signal?.aborted)
|
|
21
|
+
signal.throwIfAborted();
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
package/dist/core/service.js
CHANGED
|
@@ -50,9 +50,11 @@ export class CoreService {
|
|
|
50
50
|
}
|
|
51
51
|
return result;
|
|
52
52
|
}
|
|
53
|
-
login(domain) {
|
|
53
|
+
login(domain, browser = 'chrome') {
|
|
54
54
|
const secrets = [];
|
|
55
55
|
return this.guarded('login', domain, secrets, async () => {
|
|
56
|
+
if (browser !== 'chrome' && browser !== 'edge')
|
|
57
|
+
throw new UserFacingError('invalid browser selection', 'invalid_policy');
|
|
56
58
|
const entry = await this.deps.allowlist.get(domainOf(domain));
|
|
57
59
|
if (!entry)
|
|
58
60
|
throw new UserFacingError('domain is not allowlisted — use request_allow', 'grant_missing');
|
|
@@ -72,7 +74,7 @@ export class CoreService {
|
|
|
72
74
|
if (status !== 'unlocked')
|
|
73
75
|
throw new UserFacingError(status === 'locked' ? 'vault locked — run u-chan unlock' : status === 'unauthenticated' ? 'complete Bitwarden service login during setup' : 'Bitwarden is unavailable', status === 'locked' ? 'vault_locked' : status === 'unauthenticated' ? 'vault_unauthenticated' : 'vault_unavailable');
|
|
74
76
|
if (this.deps.login.preflight)
|
|
75
|
-
await within(this.deps.login.preflight(signal));
|
|
77
|
+
await within(this.deps.login.preflight(signal, browser));
|
|
76
78
|
signal.throwIfAborted();
|
|
77
79
|
await this.grants.reserve(entry.domain);
|
|
78
80
|
const getCreds = async () => {
|
|
@@ -81,7 +83,7 @@ export class CoreService {
|
|
|
81
83
|
secrets.push(c.username, c.password, ...(c.totp ? [c.totp] : []));
|
|
82
84
|
return c;
|
|
83
85
|
};
|
|
84
|
-
const out = await this.deps.login.run(entry, getCreds, { signal, beforeTransfer: async () => { signal.throwIfAborted(); this.grants.check(entry, true); } });
|
|
86
|
+
const out = await this.deps.login.run(entry, getCreds, { signal, browser, beforeTransfer: async () => { signal.throwIfAborted(); this.grants.check(entry, true); } });
|
|
85
87
|
if (out.status === 'logged_in')
|
|
86
88
|
return { result: { ok: true, message: 'logged in' }, audit: 'ok' };
|
|
87
89
|
if (out.status === 'already_logged_in')
|
|
@@ -122,7 +124,7 @@ export class CoreService {
|
|
|
122
124
|
...(entry.selectors ? [selectorsLine(entry.selectors)] : []),
|
|
123
125
|
...(entry.success ? [`Success condition: ${JSON.stringify(entry.success)}`] : []),
|
|
124
126
|
entry.remainingUses !== undefined ? 'Duration: one login attempt; a failed reserved attempt also counts.' : entry.expiresAt ? `Duration: until ${entry.expiresAt}` : 'Duration: permanent future logins.',
|
|
125
|
-
'Expiry and revocation do not log out sessions already transferred to
|
|
127
|
+
'Expiry and revocation do not log out sessions already transferred to the selected browser.',
|
|
126
128
|
]);
|
|
127
129
|
if (!approved)
|
|
128
130
|
return { result: { ok: false, message: 'denied' }, audit: 'denied' };
|
package/dist/core/types.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const RESULT_CODES = ['grant_missing', 'grant_expired', 'grant_exhausted', 'invalid_policy', 'unsupported_service', 'vault_locked', 'vault_unauthenticated', 'vault_unavailable', 'chrome_unavailable', 'unsupported_form', 'needs_human', 'authentication_unconfirmed', 'session_transfer_failed', 'internal_error', 'login_timeout', 'success_condition_failed'];
|
|
1
|
+
export const RESULT_CODES = ['grant_missing', 'grant_expired', 'grant_exhausted', 'invalid_policy', 'unsupported_service', 'vault_locked', 'vault_unauthenticated', 'vault_unavailable', 'chrome_unavailable', 'browser_unavailable', 'destination_unconfirmed', 'unsupported_form', 'needs_human', 'authentication_unconfirmed', 'session_transfer_failed', 'internal_error', 'login_timeout', 'success_condition_failed'];
|
|
2
2
|
export const isResultCode = (value) => typeof value === 'string' && RESULT_CODES.includes(value);
|
|
3
3
|
/** Message is safe to return to the AI. */
|
|
4
4
|
export class UserFacingError extends Error {
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
4
4
|
import { IpcClient } from './ipc/client.js';
|
|
5
5
|
import { defaultEndpoint } from './ipc/endpoint.js';
|
|
6
6
|
import { checkPlaywrightNow } from './core/doctor.js';
|
|
7
|
-
import { loadConfig } from './core/config.js';
|
|
7
|
+
import { loadConfig, resolveLoginBrowser } from './core/config.js';
|
|
8
8
|
import { createMcpServer, withPlaywrightStatus } from './mcp/server.js';
|
|
9
9
|
// Thin MCP client: every privileged action goes to the U-Chan service over IPC.
|
|
10
10
|
// stdout is the MCP channel: log only to stderr.
|
|
@@ -13,5 +13,5 @@ try {
|
|
|
13
13
|
version = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
14
14
|
}
|
|
15
15
|
catch { }
|
|
16
|
-
const client = new IpcClient(defaultEndpoint(process.env, process.platform));
|
|
16
|
+
const client = new IpcClient(defaultEndpoint(process.env, process.platform), { browser: () => resolveLoginBrowser(process.env, process.platform) });
|
|
17
17
|
await createMcpServer(withPlaywrightStatus(client, () => checkPlaywrightNow(process.pid, loadConfig(process.env, process.platform).cdpEndpoint)), version).connect(new StdioServerTransport());
|
package/dist/ipc/client.js
CHANGED
|
@@ -2,15 +2,18 @@ import net from 'node:net';
|
|
|
2
2
|
import { FrameReader, encode } from './protocol.js';
|
|
3
3
|
import { isResultCode } from '../core/types.js';
|
|
4
4
|
import { validOptions } from '../core/policy.js';
|
|
5
|
+
import { browserKind } from '../client/browser-choice.js';
|
|
5
6
|
const DOWN = 'U-Chan service is not running — run u-chan install';
|
|
6
7
|
/** One connection per call; the service does all privileged work. */
|
|
7
8
|
export class IpcClient {
|
|
8
9
|
path;
|
|
9
10
|
nextId = 1;
|
|
10
11
|
timeoutMs;
|
|
12
|
+
browser;
|
|
11
13
|
constructor(path, opts = {}) {
|
|
12
14
|
this.path = path;
|
|
13
15
|
this.timeoutMs = opts.timeoutMs ?? 180000;
|
|
16
|
+
this.browser = opts.browser ?? 'chrome';
|
|
14
17
|
}
|
|
15
18
|
call(method, params = {}) {
|
|
16
19
|
const id = this.nextId++;
|
|
@@ -45,7 +48,23 @@ export class IpcClient {
|
|
|
45
48
|
socket.on('close', () => finish({ ok: false, message: 'U-Chan service closed the connection' }));
|
|
46
49
|
});
|
|
47
50
|
}
|
|
48
|
-
login(domain
|
|
51
|
+
async login(domain, browser) {
|
|
52
|
+
let selected;
|
|
53
|
+
try {
|
|
54
|
+
selected = browserKind(browser ?? (typeof this.browser === 'function' ? this.browser() : this.browser));
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return { ok: false, code: 'browser_unavailable', message: 'browser selection is invalid or changed; run setup and restart the AI session before login' };
|
|
58
|
+
}
|
|
59
|
+
const response = await this.capabilities();
|
|
60
|
+
try {
|
|
61
|
+
const capability = JSON.parse(response.message);
|
|
62
|
+
if (response.ok && capability.protocol === 3 && Array.isArray(capability.features) && capability.features.includes('verified_session_transfer'))
|
|
63
|
+
return this.call('login', { domain, browser: selected });
|
|
64
|
+
}
|
|
65
|
+
catch { }
|
|
66
|
+
return { ok: false, code: 'unsupported_service', message: 'verified login requires a reachable V5 service; update the installed service before login' };
|
|
67
|
+
}
|
|
49
68
|
async requestAllow(domain, loginUrl, item, selectors, options) {
|
|
50
69
|
if (options !== undefined) {
|
|
51
70
|
if (!validOptions(options))
|
package/dist/ipc/server.js
CHANGED
|
@@ -20,7 +20,7 @@ const handlers = {
|
|
|
20
20
|
capabilities: api => api.capabilities ? api.capabilities() : Promise.resolve({ ok: false, message: 'unknown method' }),
|
|
21
21
|
diagnose: api => api.diagnose(),
|
|
22
22
|
manage: api => api.manage(),
|
|
23
|
-
login: (api, p) => (str(p.domain) ? api.login(p.domain) :
|
|
23
|
+
login: (api, p) => (!str(p.domain) || (p.browser !== undefined && p.browser !== 'chrome' && p.browser !== 'edge') ? BAD : p.browser ? api.login(p.domain, p.browser) : api.login(p.domain)),
|
|
24
24
|
request_allow: (api, p) => {
|
|
25
25
|
const sel = selectorsOf(p.selectors);
|
|
26
26
|
if (sel === BAD || !str(p.domain) || !str(p.loginUrl) || !str(p.item))
|
package/dist/mcp/server.js
CHANGED
|
@@ -14,12 +14,12 @@ const INSTRUCTIONS = [
|
|
|
14
14
|
'U-Chan Secrets logs into websites for you without exposing passwords.',
|
|
15
15
|
'Use it whenever a site needs sign-in (admin pages, dashboards, a Playwright page showing a login form).',
|
|
16
16
|
'Never ask the user for a password, and never type one into a form yourself.',
|
|
17
|
-
'Flow: list_allowed -> login(domain) -> continue with the Playwright MCP tools in the same Chrome.',
|
|
17
|
+
'Flow: list_allowed -> login(domain) -> continue with the Playwright MCP tools in the same selected dedicated Chrome or Edge browser.',
|
|
18
18
|
'If the domain is not allowlisted, ask the user which Bitwarden item to use, then call request_allow; the user approves it in a U-Chan window.',
|
|
19
19
|
'If login fails, call status: a locked vault means the user must run u-chan unlock themselves. If login says "needs you", ask the user to sign in by hand.',
|
|
20
20
|
].join('\n');
|
|
21
21
|
const PLAYWRIGHT_MSG = {
|
|
22
|
-
ok: 'shared
|
|
22
|
+
ok: 'shared browser process configuration observed; active AI session ownership unverified',
|
|
23
23
|
missing: 'missing or mismatched --cdp-endpoint (restart the AI session)',
|
|
24
24
|
not_found: 'not found',
|
|
25
25
|
unknown: 'unknown',
|
|
@@ -55,7 +55,7 @@ export function createMcpServer(core, version = '0.1.0') {
|
|
|
55
55
|
const server = new McpServer({ name: 'u-chan-secrets', version }, { instructions: INSTRUCTIONS });
|
|
56
56
|
server.registerTool('login', {
|
|
57
57
|
description: 'Use this whenever a website needs sign-in: an admin page, a dashboard, or a Playwright page that shows a login form. ' +
|
|
58
|
-
'Signs into an allowlisted domain with Bitwarden credentials inside the U-Chan service and hands only the session to the
|
|
58
|
+
'Signs into an allowlisted domain with Bitwarden credentials inside the U-Chan service and hands only the session to the selected dedicated browser; the credentials are never revealed. ' +
|
|
59
59
|
'After the result is ok, continue browsing with the Playwright MCP tools in the same browser.',
|
|
60
60
|
inputSchema: { domain: z.string() },
|
|
61
61
|
}, ({ domain }) => run(() => core.login(domain)));
|
|
@@ -67,6 +67,6 @@ export function createMcpServer(core, version = '0.1.0') {
|
|
|
67
67
|
}, ({ domain, login_url, item, selectors, grant, success }) => run(() => (grant || success ? core.requestAllow(domain, login_url, item, selectors, { ...(grant ? { grant } : {}), ...(success ? { success } : {}) }) : selectors ? core.requestAllow(domain, login_url, item, selectors) : core.requestAllow(domain, login_url, item))));
|
|
68
68
|
server.registerTool('revoke', { description: 'Remove a domain from the login allowlist.', inputSchema: { domain: z.string() } }, ({ domain }) => run(() => core.revoke(domain)));
|
|
69
69
|
server.registerTool('list_allowed', { description: 'List allowlisted domains with their Bitwarden item and hosts. Call it to check whether a site can be logged into before login or request_allow.' }, () => run(() => core.listAllowed()));
|
|
70
|
-
server.registerTool('status', { description: 'Report Bitwarden vault lock state, approval mode and whether Playwright MCP shares the
|
|
70
|
+
server.registerTool('status', { description: 'Report Bitwarden vault lock state, approval mode and whether Playwright MCP shares the selected browser. Call it first when login fails.' }, () => run(() => core.status()));
|
|
71
71
|
return server;
|
|
72
72
|
}
|
|
@@ -1,33 +1,33 @@
|
|
|
1
1
|
export function buildJxaManagementScript(req) {
|
|
2
|
-
return `ObjC.import('Cocoa');
|
|
3
|
-
ObjC.import('stdlib');
|
|
4
|
-
var alert = $.NSAlert.alloc.init;
|
|
5
|
-
alert.messageText = ${JSON.stringify(req.title)};
|
|
6
|
-
alert.informativeText = ${JSON.stringify('Phrase: ' + req.phrase)};
|
|
7
|
-
['Refresh','Previous','Next','Lock','Unlock','Revoke selected','Close'].forEach(function(t){ alert.addButtonWithTitle(t); });
|
|
8
|
-
var view = $.NSView.alloc.initWithFrame($.NSMakeRect(0,0,640,420));
|
|
9
|
-
var scroll = $.NSScrollView.alloc.initWithFrame($.NSMakeRect(0,50,640,370));
|
|
10
|
-
scroll.hasVerticalScroller = true;
|
|
11
|
-
var text = $.NSTextView.alloc.initWithFrame($.NSMakeRect(0,0,620,370));
|
|
12
|
-
text.editable = false;
|
|
13
|
-
text.string = ${JSON.stringify(req.lines.join('\n'))};
|
|
14
|
-
scroll.documentView = text;
|
|
15
|
-
view.addSubview(scroll);
|
|
16
|
-
var domains = ${JSON.stringify(req.domains ?? [])};
|
|
17
|
-
var select = $.NSPopUpButton.alloc.initWithFramePullsDown($.NSMakeRect(0,5,640,32),false);
|
|
18
|
-
domains.forEach(function(d){ select.addItemWithTitle(d); });
|
|
19
|
-
view.addSubview(select);
|
|
20
|
-
alert.accessoryView = view;
|
|
21
|
-
var response = Number(alert.runModal) - 1000;
|
|
22
|
-
var actions = ['refresh','previous','next','lock','unlock','revoke','close'];
|
|
23
|
-
var result = {action: actions[response] || 'close'};
|
|
24
|
-
if (result.action === 'revoke') {
|
|
25
|
-
var index = Number(select.indexOfSelectedItem);
|
|
26
|
-
if (index < 0 || index >= domains.length) result.action = 'refresh';
|
|
27
|
-
else result.domain = domains[index];
|
|
28
|
-
}
|
|
29
|
-
$.NSFileHandle.fileHandleWithStandardOutput.writeData(
|
|
30
|
-
$.NSString.alloc.initWithString(JSON.stringify(result)).dataUsingEncoding($.NSUTF8StringEncoding));
|
|
31
|
-
$.exit(0);
|
|
2
|
+
return `ObjC.import('Cocoa');
|
|
3
|
+
ObjC.import('stdlib');
|
|
4
|
+
var alert = $.NSAlert.alloc.init;
|
|
5
|
+
alert.messageText = ${JSON.stringify(req.title)};
|
|
6
|
+
alert.informativeText = ${JSON.stringify('Phrase: ' + req.phrase)};
|
|
7
|
+
['Refresh','Previous','Next','Lock','Unlock','Revoke selected','Close'].forEach(function(t){ alert.addButtonWithTitle(t); });
|
|
8
|
+
var view = $.NSView.alloc.initWithFrame($.NSMakeRect(0,0,640,420));
|
|
9
|
+
var scroll = $.NSScrollView.alloc.initWithFrame($.NSMakeRect(0,50,640,370));
|
|
10
|
+
scroll.hasVerticalScroller = true;
|
|
11
|
+
var text = $.NSTextView.alloc.initWithFrame($.NSMakeRect(0,0,620,370));
|
|
12
|
+
text.editable = false;
|
|
13
|
+
text.string = ${JSON.stringify(req.lines.join('\n'))};
|
|
14
|
+
scroll.documentView = text;
|
|
15
|
+
view.addSubview(scroll);
|
|
16
|
+
var domains = ${JSON.stringify(req.domains ?? [])};
|
|
17
|
+
var select = $.NSPopUpButton.alloc.initWithFramePullsDown($.NSMakeRect(0,5,640,32),false);
|
|
18
|
+
domains.forEach(function(d){ select.addItemWithTitle(d); });
|
|
19
|
+
view.addSubview(select);
|
|
20
|
+
alert.accessoryView = view;
|
|
21
|
+
var response = Number(alert.runModal) - 1000;
|
|
22
|
+
var actions = ['refresh','previous','next','lock','unlock','revoke','close'];
|
|
23
|
+
var result = {action: actions[response] || 'close'};
|
|
24
|
+
if (result.action === 'revoke') {
|
|
25
|
+
var index = Number(select.indexOfSelectedItem);
|
|
26
|
+
if (index < 0 || index >= domains.length) result.action = 'refresh';
|
|
27
|
+
else result.domain = domains[index];
|
|
28
|
+
}
|
|
29
|
+
$.NSFileHandle.fileHandleWithStandardOutput.writeData(
|
|
30
|
+
$.NSString.alloc.initWithString(JSON.stringify(result)).dataUsingEncoding($.NSUTF8StringEncoding));
|
|
31
|
+
$.exit(0);
|
|
32
32
|
`;
|
|
33
33
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
export function inspectDesktopBrowser(options, env = process.env) {
|
|
4
|
+
// The shared script derives the dedicated profile from the same user home as loadConfig.
|
|
5
|
+
const expected = options.profile.replace(/[\\/](?:chrome|edge)-profile$/, '');
|
|
6
|
+
return new Promise(resolve => execFile('powershell.exe', ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File',
|
|
7
|
+
fileURLToPath(new URL('../../../scripts/start-browser.ps1', import.meta.url)), '-Browser', options.browser, '-CheckOnly'], { env: { ...env, U_CHAN_HOME: expected, U_CHAN_CDP: options.endpoint }, timeout: 15000, windowsHide: true, maxBuffer: 65536 }, (error, stdout) => resolve(error ? 'unknown' : stdout.trim() === 'pass' ? 'pass' : stdout.trim() === 'fail' ? 'fail' : 'unknown')));
|
|
8
|
+
}
|
|
9
|
+
export function inspectServiceDesktopBrowser(endpoint, ownerSid, env = process.env, browser) {
|
|
10
|
+
if (!/^S-1-5-21-(?:[0-9]+-){3}[0-9]+$/.test(ownerSid))
|
|
11
|
+
return Promise.resolve('unknown');
|
|
12
|
+
return new Promise(resolve => execFile('powershell.exe', ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File',
|
|
13
|
+
fileURLToPath(new URL('../../../scripts/start-browser.ps1', import.meta.url)), '-CheckOnly', '-DesktopOwnerSid', ownerSid, ...(browser ? ['-Browser', browser] : [])], { env: { ...env, U_CHAN_CDP: endpoint }, timeout: 15000, windowsHide: true, maxBuffer: 65536 }, (error, stdout) => resolve(error ? 'unknown' : stdout.trim() === 'pass' ? 'pass' : stdout.trim() === 'fail' ? 'fail' : 'unknown')));
|
|
14
|
+
}
|
|
@@ -5,5 +5,5 @@ export const checkWindowsUac = () => commandState('powershell.exe', ['-NoProfile
|
|
|
5
5
|
export function checkWindowsPermissions(home) {
|
|
6
6
|
const encoded = Buffer.from(home, 'utf8').toString('base64');
|
|
7
7
|
const script = `$ErrorActionPreference='Stop'; $p=[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encoded}')); $root=Get-Item -LiteralPath $p -Force; if($root.Attributes -band [IO.FileAttributes]::ReparsePoint){'UNSAFE';exit}; $items=@($root)+@(Get-ChildItem -LiteralPath $p -Force -Recurse); foreach($i in $items){if($i.Attributes -band [IO.FileAttributes]::ReparsePoint){'UNSAFE';exit}; $acl=$i.GetAccessControl(); $owner=$acl.GetOwner([Security.Principal.SecurityIdentifier]).Value; if($owner -notin @('S-1-5-18','S-1-5-32-544')){'UNSAFE';exit}; foreach($ace in $acl.GetAccessRules($true,$true,[Security.Principal.SecurityIdentifier])){if($ace.AccessControlType -eq 'Allow' -and $ace.IdentityReference.Value -notin @('S-1-5-18','S-1-5-32-544')){'UNSAFE';exit}} }; 'SAFE'`;
|
|
8
|
-
return commandState('powershell.exe', ['-NoProfile', '-NonInteractive', '-EncodedCommand', Buffer.from(script, 'utf16le').toString('base64')], s => s.trim() === 'SAFE',
|
|
8
|
+
return commandState('powershell.exe', ['-NoProfile', '-NonInteractive', '-EncodedCommand', Buffer.from(script, 'utf16le').toString('base64')], s => s.trim() === 'SAFE', 60000);
|
|
9
9
|
}
|
package/dist/service/control.js
CHANGED
|
@@ -14,7 +14,7 @@ export class ControlService {
|
|
|
14
14
|
this.grants = d.grants ?? new GrantCoordinator(d.allowlist);
|
|
15
15
|
}
|
|
16
16
|
async capabilities() {
|
|
17
|
-
return { ok: true, message: JSON.stringify({ protocol: 3, features: ['doctor', 'success_conditions', 'grants'], serviceVersion: this.d.serviceVersion }) };
|
|
17
|
+
return { ok: true, message: JSON.stringify({ protocol: 3, features: ['doctor', 'success_conditions', 'grants', 'verified_session_transfer'], serviceVersion: this.d.serviceVersion }) };
|
|
18
18
|
}
|
|
19
19
|
async diagnose() {
|
|
20
20
|
try {
|
package/dist/service/doctor.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { lstat } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { privateBrowserPath } from '../platform/runtime-assets.js';
|
|
4
|
-
import { bounded, chromeState, safeCdpOrigin } from '../core/doctor.js';
|
|
4
|
+
import { bounded, chromeState, observeCdpBrowser, safeCdpOrigin } from '../core/doctor.js';
|
|
5
5
|
import { checkWindowsPermissions } from '../platform/windows/doctor.js';
|
|
6
6
|
import { checkDarwinPermissions } from '../platform/darwin/doctor.js';
|
|
7
7
|
export async function diagnoseService(home, vault, platform, cdpEndpoint) {
|
|
8
8
|
const paths = [join(home, 'program', 'dist', 'service', 'main.js'), join(home, 'program', 'node_modules'),
|
|
9
9
|
platform === 'win32' ? join(home, 'bin', 'node.exe') : process.execPath,
|
|
10
10
|
platform === 'win32' ? join(home, 'bin', 'bw.exe') : join(home, 'bin', 'bw')];
|
|
11
|
-
const [assets, permissions, status, browser] = await Promise.all([
|
|
11
|
+
const [assets, permissions, status, browser, browserKind] = await Promise.all([
|
|
12
12
|
bounded(async () => {
|
|
13
13
|
await privateBrowserPath(home);
|
|
14
14
|
for (const p of paths) {
|
|
@@ -18,10 +18,11 @@ export async function diagnoseService(home, vault, platform, cdpEndpoint) {
|
|
|
18
18
|
}
|
|
19
19
|
return 'pass';
|
|
20
20
|
}, 'unknown'),
|
|
21
|
-
bounded(() => platform === 'win32' ? checkWindowsPermissions(home) : checkDarwinPermissions(home), 'unknown',
|
|
21
|
+
bounded(() => platform === 'win32' ? checkWindowsPermissions(home) : checkDarwinPermissions(home), 'unknown', 65000),
|
|
22
22
|
bounded(() => vault.status(), 'unavailable'),
|
|
23
23
|
cdpEndpoint === undefined ? undefined : bounded(() => chromeState(cdpEndpoint), 'unknown'),
|
|
24
|
+
cdpEndpoint === undefined ? undefined : observeCdpBrowser(cdpEndpoint),
|
|
24
25
|
]);
|
|
25
26
|
return { assets, permissions, vault: status === 'unlocked' ? 'pass' : status === 'unavailable' ? 'unknown' : status,
|
|
26
|
-
...(cdpEndpoint === undefined ? {} : { browser, browserEndpoint: safeCdpOrigin(cdpEndpoint) }) };
|
|
27
|
+
...(cdpEndpoint === undefined ? {} : { browser, browserEndpoint: safeCdpOrigin(cdpEndpoint), browserKind }) };
|
|
27
28
|
}
|
|
@@ -3,7 +3,9 @@ import { isHostAllowed } from '../core/allowlist.js';
|
|
|
3
3
|
import { abortable } from '../core/abort.js';
|
|
4
4
|
import { filterState } from './session-transfer.js';
|
|
5
5
|
import { UserFacingError } from '../core/types.js';
|
|
6
|
-
|
|
6
|
+
import { waitForLoginSuccess } from '../core/login-success.js';
|
|
7
|
+
import { createPrivateNetwork } from './private-network.js';
|
|
8
|
+
/** Signs in inside a service-owned browser, then verifies only the approved session in the user's selected browser. */
|
|
7
9
|
export class IsolatedLogin {
|
|
8
10
|
o;
|
|
9
11
|
browser;
|
|
@@ -17,19 +19,23 @@ export class IsolatedLogin {
|
|
|
17
19
|
else
|
|
18
20
|
await this.o.target.cancel?.();
|
|
19
21
|
}
|
|
20
|
-
async preflight(signal) {
|
|
21
|
-
const check = signal ? AbortSignal.any([signal, AbortSignal.timeout(
|
|
22
|
+
async preflight(signal, browser = 'chrome') {
|
|
23
|
+
const check = signal ? AbortSignal.any([signal, AbortSignal.timeout(20000)]) : AbortSignal.timeout(20000);
|
|
22
24
|
try {
|
|
25
|
+
if (this.o.target.bindBrowser && !await abortable(this.o.target.bindBrowser(browser, check), check, async () => { await this.o.target.cancel?.(); }))
|
|
26
|
+
throw new Error('browser mismatch');
|
|
23
27
|
await abortable(this.o.target.userAgent(check), check, async () => { await this.o.target.cancel?.(); });
|
|
24
28
|
}
|
|
25
29
|
catch {
|
|
26
|
-
throw new UserFacingError(signal?.aborted ? 'login timed out; retry if needed' : 'dedicated
|
|
30
|
+
throw new UserFacingError(signal?.aborted ? 'login timed out; retry if needed' : 'selected dedicated browser is unavailable; run u-chan setup --start-browser', signal?.aborted ? 'login_timeout' : 'browser_unavailable');
|
|
27
31
|
}
|
|
28
32
|
}
|
|
29
33
|
async run(entry, getCreds, options = {}) {
|
|
30
34
|
const signal = options.signal ? AbortSignal.any([options.signal, AbortSignal.timeout(60000)]) : AbortSignal.timeout(60000);
|
|
31
35
|
const hostCheck = this.o.hostCheck ?? ((url) => isHostAllowed(entry, url));
|
|
32
36
|
let browser = this.browser, ctx;
|
|
37
|
+
let transferStarted = false;
|
|
38
|
+
let network;
|
|
33
39
|
let closing, stopping;
|
|
34
40
|
const closeBrowser = async () => {
|
|
35
41
|
const current = browser;
|
|
@@ -41,16 +47,21 @@ export class IsolatedLogin {
|
|
|
41
47
|
const stop = () => stopping ??= Promise.allSettled([
|
|
42
48
|
ctx ? closeContext() : closeBrowser(),
|
|
43
49
|
this.o.target.cancel?.(),
|
|
50
|
+
network?.close(),
|
|
44
51
|
]).then(() => { });
|
|
45
52
|
this.cancellation = stop;
|
|
46
53
|
try {
|
|
47
54
|
signal.throwIfAborted();
|
|
55
|
+
if (this.o.target.bindBrowser && !await abortable(this.o.target.bindBrowser(options.browser ?? 'chrome', signal), signal, stop))
|
|
56
|
+
throw new UserFacingError('selected dedicated browser is unavailable or mismatched', 'browser_unavailable');
|
|
48
57
|
const userAgent = await abortable(this.o.target.userAgent(signal), signal, stop);
|
|
49
58
|
if (!browser?.isConnected()) {
|
|
50
59
|
browser = await abortable(this.o.launch(), signal, stop, b => b.close());
|
|
51
60
|
this.browser = browser;
|
|
52
61
|
}
|
|
53
|
-
|
|
62
|
+
const fixtureOrigin = this.o.hostCheck && new URL(entry.loginUrl).protocol === 'http:' ? new URL(entry.loginUrl).origin : undefined;
|
|
63
|
+
network = await abortable(createPrivateNetwork(hostCheck, signal, fixtureOrigin), signal, stop, n => n.close());
|
|
64
|
+
ctx = await abortable(browser.newContext({ userAgent, serviceWorkers: 'block', proxy: network.proxy }), signal, stop, c => c.close());
|
|
54
65
|
const page = await abortable(ctx.newPage(), signal, stop, p => p.close());
|
|
55
66
|
const checkHost = () => {
|
|
56
67
|
if (!hostCheck(page.url()))
|
|
@@ -58,12 +69,14 @@ export class IsolatedLogin {
|
|
|
58
69
|
};
|
|
59
70
|
const outcome = await abortable(fillLogin(page, entry, getCreds, { timeoutMs: this.o.timeoutMs, hostCheck }), signal, stop);
|
|
60
71
|
if (outcome.status === 'failed')
|
|
61
|
-
return
|
|
72
|
+
return network.wasBlocked()
|
|
73
|
+
? { status: 'failed', code: 'unsupported_form', reason: 'an unapproved private-browser request was blocked; session was not transferred' }
|
|
74
|
+
: { ...outcome, code: outcome.code ?? 'unsupported_form' };
|
|
62
75
|
checkHost();
|
|
63
76
|
if (entry.success) {
|
|
64
77
|
let matched = false;
|
|
65
78
|
try {
|
|
66
|
-
matched =
|
|
79
|
+
matched = await abortable(waitForLoginSuccess(page, entry.success, hostCheck, signal, this.o.timeoutMs ?? 15000), signal, stop);
|
|
67
80
|
}
|
|
68
81
|
catch (e) {
|
|
69
82
|
if (signal.aborted)
|
|
@@ -80,19 +93,28 @@ export class IsolatedLogin {
|
|
|
80
93
|
signal.throwIfAborted();
|
|
81
94
|
await options.beforeTransfer?.();
|
|
82
95
|
checkHost();
|
|
96
|
+
transferStarted = true;
|
|
83
97
|
try {
|
|
84
98
|
await abortable(this.o.target.inject(state, signal), signal, stop);
|
|
85
99
|
}
|
|
86
100
|
catch (e) {
|
|
87
101
|
if (signal.aborted)
|
|
88
102
|
throw e;
|
|
89
|
-
return { status: 'failed', reason: '
|
|
103
|
+
return { status: 'failed', reason: 'session transfer did not complete; some browser session state may already exist', code: 'session_transfer_failed' };
|
|
90
104
|
}
|
|
91
|
-
|
|
105
|
+
await options.beforeTransfer?.();
|
|
106
|
+
signal.throwIfAborted();
|
|
107
|
+
if (entry.success) {
|
|
108
|
+
const verified = this.o.target.verify ? await abortable(this.o.target.verify(entry, page.url(), signal), signal, stop) : false;
|
|
109
|
+
return verified ? { status: 'logged_in' } : { status: 'unconfirmed', code: 'destination_unconfirmed', reason: 'session state was transferred, but login was not confirmed in the selected browser; inspect the site before continuing' };
|
|
110
|
+
}
|
|
111
|
+
return { status: 'unconfirmed', code: 'authentication_unconfirmed', reason: 'session state transferred to the selected browser; sign-in was not confirmed; inspect the site before continuing' };
|
|
92
112
|
}
|
|
93
113
|
catch (e) {
|
|
94
114
|
if (signal.aborted)
|
|
95
|
-
return { status: 'failed', code: 'login_timeout', reason: 'login cancelled or timed out; retry with fresh approval if needed' };
|
|
115
|
+
return { status: 'failed', code: 'login_timeout', reason: transferStarted ? 'login cancelled or timed out after transfer started; browser session state may already exist and login is unconfirmed' : 'login cancelled or timed out; retry with fresh approval if needed' };
|
|
116
|
+
if (transferStarted)
|
|
117
|
+
return { status: 'unconfirmed', code: 'destination_unconfirmed', reason: 'destination verification did not complete; browser session state may already exist and login is unconfirmed' };
|
|
96
118
|
if (e instanceof UserFacingError && e.code === 'unsupported_form')
|
|
97
119
|
return { status: 'failed', code: e.code, reason: e.message };
|
|
98
120
|
throw e;
|
|
@@ -102,6 +124,7 @@ export class IsolatedLogin {
|
|
|
102
124
|
await stop();
|
|
103
125
|
else
|
|
104
126
|
await closeContext();
|
|
127
|
+
await network?.close();
|
|
105
128
|
this.cancellation = undefined;
|
|
106
129
|
}
|
|
107
130
|
}
|
package/dist/service/main.js
CHANGED
|
@@ -25,6 +25,7 @@ import { ensureV3State } from './state.js';
|
|
|
25
25
|
import { ManagementService } from './manage.js';
|
|
26
26
|
import { privateBrowserPath } from '../platform/runtime-assets.js';
|
|
27
27
|
import { safeVersion } from '../core/doctor.js';
|
|
28
|
+
import { inspectServiceDesktopBrowser } from '../platform/windows/desktop-browser.js';
|
|
28
29
|
export function prepareSocketEndpoint(endpoint, platform = process.platform) {
|
|
29
30
|
if (platform !== 'win32') {
|
|
30
31
|
const dir = dirname(endpoint);
|
|
@@ -60,9 +61,14 @@ export async function runService(platform = process.platform, env = process.env)
|
|
|
60
61
|
const allowlist = new AllowlistStore(cfg.allowlistPath, () => stateReady ??= ensureV3State(cfg.home).catch(e => { stateReady = undefined; throw e; }));
|
|
61
62
|
const grants = new GrantCoordinator(allowlist);
|
|
62
63
|
const audit = new FileAuditLog(cfg.auditPath);
|
|
64
|
+
const endpoint = loadConfig(env, platform).cdpEndpoint;
|
|
65
|
+
const validateTarget = platform === 'win32' ? async (browser = 'chrome') => {
|
|
66
|
+
const owner = (await settings.get()).authorizedSid;
|
|
67
|
+
return typeof owner === 'string' && await inspectServiceDesktopBrowser(endpoint, owner, env, browser) === 'pass';
|
|
68
|
+
} : undefined;
|
|
63
69
|
const login = new IsolatedLogin({
|
|
64
|
-
launch: async () => chromium.launch({ headless: true, executablePath: await privateBrowserPath(cfg.home), timeout: 15000 }),
|
|
65
|
-
target: new CdpSessionTarget(new CdpBrowserProvider(
|
|
70
|
+
launch: async () => chromium.launch({ headless: true, executablePath: await privateBrowserPath(cfg.home), timeout: 15000, args: ['--disable-http2', '--disable-quic'] }),
|
|
71
|
+
target: new CdpSessionTarget(new CdpBrowserProvider(endpoint, { validate: validateTarget })),
|
|
66
72
|
});
|
|
67
73
|
const core = new CoreService({ vault, allowlist, grants, approver, audit, login, mode: () => approver.effectiveMode() });
|
|
68
74
|
let currentVersion = 'unknown';
|
|
@@ -94,7 +100,7 @@ export async function runService(platform = process.platform, env = process.env)
|
|
|
94
100
|
capabilities: () => control.capabilities(),
|
|
95
101
|
diagnose: () => control.diagnose(),
|
|
96
102
|
manage: () => control.manage(),
|
|
97
|
-
login: (domain) => core.login(domain),
|
|
103
|
+
login: (domain, browser) => core.login(domain, browser),
|
|
98
104
|
requestAllow: (domain, loginUrl, item, selectors, options) => core.requestAllow(domain, loginUrl, item, selectors, options),
|
|
99
105
|
revoke: (domain) => core.revoke(domain),
|
|
100
106
|
listAllowed: () => core.listAllowed(),
|