@deeeed/metamask-harness 0.70.0 → 0.70.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/CHANGELOG.md +10 -0
- package/README.md +16 -9
- package/adapters/extension/launch-browser.cjs +16 -4
- package/adapters/shared/browser-cdp.cjs +21 -7
- package/adapters/shared/browser-resolver.cjs +22 -11
- package/dist/doctor.js +1 -1
- package/library/actions/extension/platform/cdp.mjs +35 -10
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.70.1 - 2026-10-02
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Detect forced Google Chrome sign-in (`BrowserSignin: 2`, `ForceBrowserSignin`) as managed: `plutil` returns these scalar policies only as raw values, so a Mac with only forced sign-in looked unmanaged and fell back to the managed Chrome.
|
|
10
|
+
- Disable other user extensions in the slot browser before its start page (a dapp or the wallet home) loads; a browser started without a window gets a blank background window first.
|
|
11
|
+
- Fail the launch, and stop the browser, when a slot browser started without a window cannot open its start window, instead of recording a browser with no window.
|
|
12
|
+
- Start the library autolaunch browser on macOS without a startup window, load the wallet home into its window's blank tab (so it is the page a screenshot of the window shows) or a background window, and restore the operator's front app once, by pid, only if its own browser took it. Headful browser probes also start without a startup window.
|
|
13
|
+
- Show doctor's managed-Chrome diagnostics next to any other browser warning.
|
|
14
|
+
|
|
5
15
|
## 0.70.0 - 2026-10-02
|
|
6
16
|
|
|
7
17
|
### Added
|
package/README.md
CHANGED
|
@@ -149,15 +149,22 @@ On a cold cache the probe can take up to about 80 seconds and
|
|
|
149
149
|
shows a small (about 480×360) browser window; `doctor --adapter extension`
|
|
150
150
|
runs the same probe.
|
|
151
151
|
|
|
152
|
-
On a Mac whose Google Chrome is managed by policy
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
152
|
+
On a Mac whose Google Chrome is managed by policy, `auto` uses Chrome for
|
|
153
|
+
Testing, which does not read those policies, so no forced extension can open
|
|
154
|
+
windows in the slot. Only MDM-managed preferences are read
|
|
155
|
+
(`/Library/Managed Preferences/com.google.Chrome.plist` and its per-user
|
|
156
|
+
copy under `/Library/Managed Preferences/<user>/`), not `defaults` a user set
|
|
157
|
+
themselves; force-installed extensions (`ExtensionInstallForcelist`,
|
|
158
|
+
`ExtensionSettings`) or forced sign-in (`BrowserSignin: 2`,
|
|
159
|
+
`ForceBrowserSignin`) count as managed; a policy stored as a Data or Date
|
|
160
|
+
value is treated as absent. If Chrome for Testing does not start
|
|
161
|
+
and paint there, the launch stops with `BROWSER_UNLAUNCHABLE` rather than
|
|
162
|
+
falling back to the managed Chrome; the way out is
|
|
163
|
+
`RECIPE_HARNESS_BROWSER=chrome` (accepting that forced extensions may open
|
|
164
|
+
windows that take the front), or a less loaded machine. Doctor lists the forced
|
|
165
|
+
extension ids, with names where it can read them (best effort, read-only: the
|
|
166
|
+
slot record, or the operator's own Chrome profile folders). A slot profile
|
|
167
|
+
created on the managed Chrome keeps it, with a warning to reset the profile.
|
|
161
168
|
|
|
162
169
|
A slot then keeps the kind of browser its profile was created with, until the
|
|
163
170
|
profile is reset (`mm-harness fixtures set` or `fixtures reset`):
|
|
@@ -223,7 +223,7 @@ try {
|
|
|
223
223
|
}
|
|
224
224
|
}
|
|
225
225
|
if (loadsOverCdp) cdpLoad = loadExtensionOverCdp(browserPid);
|
|
226
|
-
else if (application) openBackgroundWindow(initialUrl);
|
|
226
|
+
else if (application) openBackgroundWindow(browserPid, initialUrl);
|
|
227
227
|
owner = cdpLoad?.owner ?? ownerIdentity(browserPid);
|
|
228
228
|
for (const problem of pruneExtraHomeTabs(cdpPort, args['extension-dir'])) {
|
|
229
229
|
process.stderr.write(`[launch] home-tab pruning: ${problem}\n`);
|
|
@@ -283,14 +283,26 @@ try {
|
|
|
283
283
|
}
|
|
284
284
|
|
|
285
285
|
// The first window of a browser started without one, opened in the background.
|
|
286
|
-
|
|
286
|
+
// Without it the slot has no window at all, so a failure fails the launch:
|
|
287
|
+
// the owned browser is stopped (its markers kept if it cannot be).
|
|
288
|
+
function openBackgroundWindow(pid, url) {
|
|
287
289
|
const result = spawnSync(process.execPath, [browserResolverPath, 'open-window', '--port', String(cdpPort), '--url', url], {
|
|
288
290
|
encoding: 'utf8',
|
|
289
291
|
timeout: 30_000,
|
|
290
292
|
});
|
|
291
|
-
if (result.status
|
|
292
|
-
|
|
293
|
+
if (result.status === 0) return;
|
|
294
|
+
const openError = (result.stderr || result.error?.message || `exit ${result.status}`).trim();
|
|
295
|
+
try {
|
|
296
|
+
stopProfileProcessesSync(args.profile, { extraPids: [pid] });
|
|
297
|
+
} catch (stopError) {
|
|
298
|
+
throw new Error(
|
|
299
|
+
`Chrome started but its start window could not be opened (${openError}), and stopping it failed: ${stopError.message}. ` +
|
|
300
|
+
`The launch markers for port ${cdpPort} and ${args.profile} are kept. Next: stop pid ${pid}, then rerun.`,
|
|
301
|
+
);
|
|
293
302
|
}
|
|
303
|
+
clearDetachedLaunchUnproven(args.profile);
|
|
304
|
+
if (!activeValidationLease) clearValidationPortQuarantine(cdpPort, args.profile);
|
|
305
|
+
throw new Error(`Chrome started but its start window could not be opened: ${openError}. Next: inspect ${args['chrome-log']}, then rerun.`);
|
|
294
306
|
}
|
|
295
307
|
|
|
296
308
|
// live.sh passes the record its resolver just wrote (--browser-resolution);
|
|
@@ -428,12 +428,21 @@ async function openBackgroundWindow(send, url, timeoutMs) {
|
|
|
428
428
|
return send('Target.createTarget', { url, newWindow: true, background: true }, undefined, timeoutMs);
|
|
429
429
|
}
|
|
430
430
|
|
|
431
|
+
const isBlankPage = (target) => target.type === 'page' && (target.url === 'about:blank' || target.url.startsWith('chrome://newtab') || target.url.startsWith('chrome://new-tab-page'));
|
|
432
|
+
|
|
433
|
+
// A browser started without a window gets a blank background window, so the
|
|
434
|
+
// first window never takes the operator's focus and nothing is navigated yet.
|
|
435
|
+
async function ensureBackgroundWindow(send, remaining) {
|
|
436
|
+
const { targetInfos } = await send('Target.getTargets', {}, undefined, remaining());
|
|
437
|
+
if ((targetInfos || []).some((target) => target.type === 'page')) return;
|
|
438
|
+
await openBackgroundWindow(send, 'about:blank', remaining());
|
|
439
|
+
}
|
|
440
|
+
|
|
431
441
|
// Point the launch tab at `url` once the extension exists: the tab opened at
|
|
432
|
-
// launch would otherwise show an error page for chrome-extension:// URLs.
|
|
433
|
-
// browser started without a window gets a background window instead.
|
|
442
|
+
// launch would otherwise show an error page for chrome-extension:// URLs.
|
|
434
443
|
async function openUrl(send, url, remaining) {
|
|
435
444
|
const { targetInfos } = await send('Target.getTargets', {}, undefined, remaining());
|
|
436
|
-
const blank = targetInfos
|
|
445
|
+
const blank = (targetInfos || []).find(isBlankPage);
|
|
437
446
|
if (!blank) {
|
|
438
447
|
await openBackgroundWindow(send, url, remaining());
|
|
439
448
|
return;
|
|
@@ -489,12 +498,15 @@ async function loadUnpackedOverPort(port, extensionDir, {
|
|
|
489
498
|
assertSameOwner(owner, { exec, timeoutMs: remaining() });
|
|
490
499
|
step = 'Extensions.loadUnpacked';
|
|
491
500
|
const loaded = await loadUnpackedExtension(client.send, extensionDir, { expectedId, timeoutMs: remaining() });
|
|
492
|
-
//
|
|
493
|
-
//
|
|
494
|
-
|
|
495
|
-
|
|
501
|
+
// A browser started without a window gets a blank background window
|
|
502
|
+
// first; other extensions are disabled before the start page (a dapp, or
|
|
503
|
+
// the wallet home) loads in it.
|
|
504
|
+
step = 'opening a background window';
|
|
505
|
+
await ensureBackgroundWindow(client.send, remaining);
|
|
496
506
|
step = 'isolating the wallet from other extensions';
|
|
497
507
|
const otherExtensions = await isolateWalletExtension(client.send, loaded.id, { timeoutMs: remaining() });
|
|
508
|
+
step = 'opening the start page';
|
|
509
|
+
if (url) await openUrl(client.send, url, remaining);
|
|
498
510
|
remaining();
|
|
499
511
|
return { ...loaded, otherExtensions, owner };
|
|
500
512
|
} catch (error) {
|
|
@@ -523,6 +535,8 @@ module.exports = {
|
|
|
523
535
|
expectedExtensionId: (extensionDir) => extensionIdFromExtensionDir(extensionDir) || null,
|
|
524
536
|
loadUnpackedExtension,
|
|
525
537
|
loadUnpackedOverPort,
|
|
538
|
+
ensureBackgroundWindow,
|
|
526
539
|
isolateWalletExtension,
|
|
527
540
|
openBackgroundWindow,
|
|
541
|
+
openUrl,
|
|
528
542
|
};
|
|
@@ -106,14 +106,24 @@ function managedChromePlists(env = process.env) {
|
|
|
106
106
|
];
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
// One policy key
|
|
109
|
+
// One policy key, or undefined when absent or unreadable. `plutil -extract
|
|
110
|
+
// … json` only extracts arrays and dictionaries; a scalar (BrowserSignin: 2,
|
|
111
|
+
// ForceBrowserSignin: true) comes out with `raw`.
|
|
110
112
|
function readPolicyKey(file, key) {
|
|
113
|
+
const extract = (format) => execFileSync('plutil', ['-extract', key, format, '-o', '-', file], {
|
|
114
|
+
encoding: 'utf8',
|
|
115
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
116
|
+
timeout: 5000,
|
|
117
|
+
});
|
|
111
118
|
try {
|
|
112
|
-
return JSON.parse(
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
119
|
+
return JSON.parse(extract('json'));
|
|
120
|
+
} catch {
|
|
121
|
+
// A scalar, or the key is absent: try it as a raw value.
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
const raw = extract('raw').trim();
|
|
125
|
+
if (raw === 'true' || raw === 'false') return raw === 'true';
|
|
126
|
+
return /^-?\d+$/u.test(raw) ? Number(raw) : raw;
|
|
117
127
|
} catch {
|
|
118
128
|
return undefined;
|
|
119
129
|
}
|
|
@@ -512,11 +522,12 @@ async function probeLaunch(executable, {
|
|
|
512
522
|
let exited = null;
|
|
513
523
|
try {
|
|
514
524
|
const port = await freePort();
|
|
515
|
-
// A headful
|
|
516
|
-
// background with no startup window (Chrome
|
|
517
|
-
// one); renderCheck opens a background
|
|
518
|
-
// a window
|
|
519
|
-
|
|
525
|
+
// A headful probe starts like the slot browsers (the launchers and the
|
|
526
|
+
// library autolaunch): in the background with no startup window (Chrome
|
|
527
|
+
// activates itself when it opens one); renderCheck opens a background
|
|
528
|
+
// window. Like the launchers, it keeps a window behind the operator's apps
|
|
529
|
+
// rendering.
|
|
530
|
+
const backgroundWindow = display === HEADFUL;
|
|
520
531
|
const args = [
|
|
521
532
|
...(display === HEADLESS ? ['--headless=new'] : ['--window-size=480,360']),
|
|
522
533
|
`--user-data-dir=${profile}`,
|
package/dist/doctor.js
CHANGED
|
@@ -241,7 +241,7 @@ function extensionBrowserResolutionDoctorCheck(target) {
|
|
|
241
241
|
if (result.status === 0) {
|
|
242
242
|
const resolution = JSON.parse(result.stdout);
|
|
243
243
|
const managed = resolution.managedChrome;
|
|
244
|
-
const managedNote = managed && !resolution.warning ? ` Google Chrome is managed by policy (${managed.forcedExtensions.length} forced extension${managed.forcedExtensions.length === 1 ? "" : "s"}: ${managed.forcedExtensions.map(({ id, name }) => name ? `${id} (${name})` : id).join(", ")}${managed.signinForced ? "; browser sign-in forced" : ""}); slots use Chrome for Testing.` : "";
|
|
244
|
+
const managedNote = managed && !resolution.warning?.includes("managed by policy") ? ` Google Chrome is managed by policy (${managed.forcedExtensions.length} forced extension${managed.forcedExtensions.length === 1 ? "" : "s"}: ${managed.forcedExtensions.map(({ id, name }) => name ? `${id} (${name})` : id).join(", ")}${managed.signinForced ? "; browser sign-in forced" : ""}); slots use Chrome for Testing.` : "";
|
|
245
245
|
const probe = resolution.probe ? `; Chrome for Testing probe (${resolution.probe.launchMethod}, ${resolution.probe.display}${resolution.probe.cached ? ", cached" : ""}${resolution.probe.transient ? `, retried after ${resolution.probe.expiresAt}` : ""}): ${resolution.probe.reason}` : resolution.recordedFrom ? `; kept from the slot profile record ${resolution.recordedFrom}` : "";
|
|
246
246
|
return {
|
|
247
247
|
id: "extension-browser",
|
|
@@ -373,6 +373,26 @@ const runnerRequire = createRequire(import.meta.url);
|
|
|
373
373
|
const runnerRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../..');
|
|
374
374
|
const browserResolver = runnerRequire(path.join(runnerRoot, 'adapters/shared/browser-resolver.cjs'));
|
|
375
375
|
const { stopProfileProcessesSync } = runnerRequire(path.join(runnerRoot, 'adapters/extension/lib/validation-process-ownership.cjs'));
|
|
376
|
+
const browserCdp = runnerRequire(path.join(runnerRoot, 'adapters/shared/browser-cdp.cjs'));
|
|
377
|
+
const macosFocus = runnerRequire(path.join(runnerRoot, 'adapters/extension/lib/macos-focus.cjs'));
|
|
378
|
+
|
|
379
|
+
// On macOS a browser that opens a window at startup activates itself, so the
|
|
380
|
+
// autolaunched browser starts without one and gets its pages over CDP in the
|
|
381
|
+
// background (as the extension launcher does).
|
|
382
|
+
const startsWithoutWindow = () => process.platform === 'darwin';
|
|
383
|
+
|
|
384
|
+
// One bounded browser-level CDP operation (the shared helpers in
|
|
385
|
+
// browser-cdp.cjs never take the operator's focus).
|
|
386
|
+
async function withBrowserCdp(port, fn) {
|
|
387
|
+
const deadline = Date.now() + 15000;
|
|
388
|
+
const remaining = () => Math.max(1, deadline - Date.now());
|
|
389
|
+
const client = await browserCdp.connectBrowserCdp(port, { timeoutMs: remaining() });
|
|
390
|
+
try {
|
|
391
|
+
return await fn(client.send, remaining);
|
|
392
|
+
} finally {
|
|
393
|
+
client.close();
|
|
394
|
+
}
|
|
395
|
+
}
|
|
376
396
|
|
|
377
397
|
// Same browser choice as the live launcher (RECIPE_HARNESS_BROWSER /
|
|
378
398
|
// RECIPE_HARNESS_CHROME_BIN); branded Chrome loads the dist over CDP. This
|
|
@@ -491,6 +511,7 @@ export async function launchExistingDistRuntime(input, port) {
|
|
|
491
511
|
const profileExisted = await canAccess(path.join(profileDir, 'Local State'));
|
|
492
512
|
const browser = await resolveChromeBinary(projectRoot, profileExisted ? browserRecord : null);
|
|
493
513
|
const loadsOverCdp = browser.extensionLoading === browserResolver.CDP_LOAD_UNPACKED;
|
|
514
|
+
const previousFrontmost = macosFocus.captureMacFrontmost();
|
|
494
515
|
const stdoutFd = openSync(path.join(logsDir, 'chrome.log'), 'a');
|
|
495
516
|
const stderrFd = openSync(path.join(logsDir, 'chrome.log'), 'a');
|
|
496
517
|
let child;
|
|
@@ -508,7 +529,7 @@ export async function launchExistingDistRuntime(input, port) {
|
|
|
508
529
|
'--disable-extensions-content-verification',
|
|
509
530
|
'--disable-features=ExtensionContentVerification,DisableLoadExtensionCommandLineSwitch',
|
|
510
531
|
...browserResolver.extensionLaunchArgs(runtimeDist, browser.extensionLoading),
|
|
511
|
-
'chrome://extensions/',
|
|
532
|
+
startsWithoutWindow() ? '--no-startup-window' : 'chrome://extensions/',
|
|
512
533
|
], {
|
|
513
534
|
cwd: projectRoot,
|
|
514
535
|
detached: true,
|
|
@@ -534,7 +555,10 @@ export async function launchExistingDistRuntime(input, port) {
|
|
|
534
555
|
try {
|
|
535
556
|
await retryJsonGet(`http://127.0.0.1:${port}/json/version`, 45000);
|
|
536
557
|
const owner = browserResolver.waitForCdpOwner(port, profileDir);
|
|
558
|
+
// The CDP load opens a background window itself; otherwise open one here.
|
|
559
|
+
// Without it the browser has no window, so a failure fails the launch.
|
|
537
560
|
const cdpLoad = loadsOverCdp ? await browserResolver.loadUnpackedOverPort(port, runtimeDist, { profile: profileDir }) : null;
|
|
561
|
+
if (!loadsOverCdp && startsWithoutWindow()) await withBrowserCdp(port, browserCdp.ensureBackgroundWindow);
|
|
538
562
|
launchedBrowser = {
|
|
539
563
|
...browser,
|
|
540
564
|
launch: {
|
|
@@ -582,6 +606,8 @@ export async function launchExistingDistRuntime(input, port) {
|
|
|
582
606
|
}
|
|
583
607
|
throw error;
|
|
584
608
|
}
|
|
609
|
+
// One check, at most one restore, only if this launch's browser holds the front.
|
|
610
|
+
macosFocus.restoreFrontmostIfOurs(previousFrontmost, [launchedBrowser.launch.pid]);
|
|
585
611
|
|
|
586
612
|
await writeFile(path.join(runtimeRoot, 'runtime.json'), `${JSON.stringify({
|
|
587
613
|
port,
|
|
@@ -618,17 +644,16 @@ function extensionIdFromAnyTarget(targets) {
|
|
|
618
644
|
return null;
|
|
619
645
|
}
|
|
620
646
|
|
|
621
|
-
|
|
647
|
+
// The wallet home goes into the launch's blank tab (so it is the window's
|
|
648
|
+
// selected page, and what a native screenshot of the window captures), or a
|
|
649
|
+
// background window when there is no blank tab; never a foreground one.
|
|
650
|
+
export async function openExtensionHomePage(port, extensionId) {
|
|
622
651
|
const url = `chrome-extension://${extensionId}/home.html`;
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
}
|
|
628
|
-
if (!response.ok) {
|
|
629
|
-
throw new Error(`Failed to open extension home page ${url}: HTTP ${response.status}`);
|
|
652
|
+
try {
|
|
653
|
+
return await withBrowserCdp(port, (send, remaining) => browserCdp.openUrl(send, url, remaining));
|
|
654
|
+
} catch (error) {
|
|
655
|
+
throw new Error(`Failed to open extension home page ${url}: ${error.message}`);
|
|
630
656
|
}
|
|
631
|
-
return response.json();
|
|
632
657
|
}
|
|
633
658
|
|
|
634
659
|
async function ensureExtensionTarget(input, port) {
|