@deeeed/metamask-harness 0.83.0 → 0.85.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.
Files changed (97) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/README.md +8 -1
  3. package/adapters/extension/console-tail.mjs +11 -7
  4. package/adapters/extension/ensure-browser.sh +17 -18
  5. package/adapters/manifest.json +14 -110
  6. package/adapters/mobile/start-console-forwarder.sh +8 -0
  7. package/adapters/shared/resolve-slot-ports-core.mjs +48 -6
  8. package/adapters/shared/session-lock.mjs +43 -0
  9. package/adapters/web-dapp/metamask-signer.mjs +212 -0
  10. package/dist/adapters/extension/actions.js +5 -0
  11. package/dist/adapters/extension/console-capture.js +18 -0
  12. package/dist/adapters/extension/fixtures.js +2 -0
  13. package/dist/adapters/extension/overlay.js +5 -4
  14. package/dist/adapters/extension/run.js +4 -2
  15. package/dist/adapters/extension/runtime.js +73 -18
  16. package/dist/adapters/mobile/devices.js +5 -1
  17. package/dist/adapters/mobile/launch.js +19 -2
  18. package/dist/adapters/mobile/provision.js +4 -2
  19. package/dist/adapters/mobile/run.js +3 -2
  20. package/dist/adapters/mobile/surface.js +1 -2
  21. package/dist/adapters/mobile/verify.js +15 -7
  22. package/dist/adapters/plugin-surface.js +14 -42
  23. package/dist/adapters/session-lock.js +23 -0
  24. package/dist/adapters/web-dapp/actions.js +7 -5
  25. package/dist/adapters/web-dapp/console-capture.js +2 -20
  26. package/dist/adapters/web-dapp/run.js +25 -24
  27. package/dist/adapters/web-dapp/surface.js +32 -108
  28. package/dist/adapters.js +5 -3
  29. package/dist/command-contract.js +1351 -643
  30. package/dist/commands/check.js +5 -7
  31. package/dist/commands/debug.js +2 -2
  32. package/dist/commands/doctor.js +3 -4
  33. package/dist/commands/domain.js +4 -3
  34. package/dist/commands/fixtures.js +3 -3
  35. package/dist/commands/help.js +6 -5
  36. package/dist/commands/launch/device.js +1 -1
  37. package/dist/commands/launch/extension.js +9 -7
  38. package/dist/commands/logs.js +2 -2
  39. package/dist/commands/prepare.js +4 -4
  40. package/dist/commands/provision.js +2 -2
  41. package/dist/commands/review.js +8 -10
  42. package/dist/commands/runtime-launch.js +2 -0
  43. package/dist/commands/status.js +4 -2
  44. package/dist/commands/task-init.js +2 -3
  45. package/dist/console-classifier.js +3 -2
  46. package/dist/doctor.js +11 -4
  47. package/dist/mm-harness-cli.js +241 -1210
  48. package/dist/paths.js +9 -4
  49. package/dist/recipe-security.js +1 -1
  50. package/dist/review/knowledge.js +10 -2
  51. package/dist/runner.js +18 -3
  52. package/docs/CONTRIBUTING.md +14 -4
  53. package/docs/RECIPES.md +14 -1
  54. package/docs/cli-contract.md +8 -7
  55. package/docs/feature-guard.md +1 -1
  56. package/library/actions/extension/app/assert_no_console_errors.mjs +95 -0
  57. package/library/actions/extension/platform/cdp.mjs +26 -0
  58. package/library/actions/mobile/app/assert_no_console_errors.mjs +129 -0
  59. package/library/actions/shared/console-assert.mjs +174 -0
  60. package/library/actions/shared/console-collector.mjs +11 -4
  61. package/library/actions/shared/console-findings.mjs +108 -7
  62. package/library/actions/web-dapp/app/assert_no_console_errors.mjs +10 -54
  63. package/library/actions/web-dapp/app/launch.mjs +1 -1
  64. package/library/actions/web-dapp/platform/adapter-web-lib.mjs +18 -0
  65. package/library/actions/web-dapp/platform/hud.mjs +3 -10
  66. package/library/actions/web-dapp/platform/page.mjs +63 -18
  67. package/library/actions/web-dapp/platform/runtime.mjs +30 -179
  68. package/library/diagnostics/console-allowlist.json +13 -0
  69. package/library/manifests/extension.action-manifest.json +36 -0
  70. package/library/manifests/mobile.action-manifest.json +36 -0
  71. package/library/manifests/web-dapp.action-manifest.json +1 -1
  72. package/library/recipes/core/health/check.recipe.json +40 -0
  73. package/library/recipes/extension/health/check.recipe.json +71 -0
  74. package/library/recipes/mobile/health/check.recipe.json +71 -0
  75. package/package.json +9 -9
  76. package/scripts/completions.sh +1 -1
  77. package/site/assets/help-recipes.json +2 -1
  78. package/adapters/web-dapp/cleanup.mjs +0 -69
  79. package/adapters/web-dapp/inject.mjs +0 -75
  80. package/adapters/web-dapp/launch.mjs +0 -631
  81. package/adapters/web-dapp/lib/blocked-hosts.mjs +0 -24
  82. package/adapters/web-dapp/lib/cdp-client.mjs +0 -84
  83. package/adapters/web-dapp/lib/display.mjs +0 -115
  84. package/adapters/web-dapp/lib/hud.mjs +0 -148
  85. package/adapters/web-dapp/lib/policy-fence.mjs +0 -119
  86. package/adapters/web-dapp/lib/policy.mjs +0 -84
  87. package/adapters/web-dapp/lib/processes.mjs +0 -187
  88. package/adapters/web-dapp/lib/readiness.mjs +0 -272
  89. package/adapters/web-dapp/stop.mjs +0 -38
  90. package/adapters/web-dapp/verify.mjs +0 -51
  91. package/adapters/web-dapp/wallet-host.mjs +0 -520
  92. package/dist/adapters/web-dapp/doctor-checks.js +0 -33
  93. package/dist/adapters/web-dapp/launch.js +0 -87
  94. package/dist/adapters/web-dapp/overlay.js +0 -21
  95. package/dist/adapters/web-dapp/readiness.js +0 -10
  96. package/dist/adapters/web-dapp/stop.js +0 -20
  97. package/dist/cli.js +0 -202
@@ -1,187 +0,0 @@
1
- // Stop the web-dapp slot browser and wallet host by their pid files and prove
2
- // the slot CDP port is released. A pid is signalled only when its command line
3
- // carries this slot's owner marker for that process kind. A pid file naming a
4
- // dead process is removed; one naming a live process without the marker is
5
- // kept and reported, never signalled.
6
-
7
- import { execFileSync } from 'node:child_process';
8
- import { createHash } from 'node:crypto';
9
- import { existsSync, readFileSync, rmSync } from 'node:fs';
10
- import net from 'node:net';
11
- import path from 'node:path';
12
-
13
- import { webDappPolicy, webDappRuntimeDir } from '../../../library/actions/web-dapp/platform/runtime.mjs';
14
-
15
- export function pidAlive(pid) {
16
- if (!Number.isInteger(Number(pid)) || Number(pid) <= 0) return false;
17
- try {
18
- process.kill(Number(pid), 0);
19
- return true;
20
- } catch (error) {
21
- return error.code === 'EPERM';
22
- }
23
- }
24
-
25
- function groupAlive(pgid) {
26
- try {
27
- process.kill(-pgid, 0);
28
- return true;
29
- } catch (error) {
30
- return error.code === 'EPERM';
31
- }
32
- }
33
-
34
- function readPid(file) {
35
- if (!existsSync(file)) return null;
36
- const pid = Number(readFileSync(file, 'utf8').trim());
37
- return Number.isInteger(pid) && pid > 0 ? pid : null;
38
- }
39
-
40
- function psField(pid, field) {
41
- try {
42
- return execFileSync('ps', ['-ww', '-o', `${field}=`, '-p', String(pid)], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
43
- } catch {
44
- return '';
45
- }
46
- }
47
-
48
- export function processCommand(pid) {
49
- return pidAlive(pid) ? psField(pid, 'command') : '';
50
- }
51
-
52
- // Each process the slot starts carries `--mm-harness-owner=<kind>-<id>`, where
53
- // <id> is derived from the canonical runtime dir. Ownership is that exact
54
- // argument, compared as a whole whitespace-free token, so a runtime dir that
55
- // shares a prefix with this one (".../terminal backup", ".../terminal2")
56
- // never matches.
57
- export const OWNER_FLAG = '--mm-harness-owner';
58
- export const OWNER_KINDS = Object.freeze(['browser', 'wallet-host', 'console-collector']);
59
-
60
- export function ownerMarker(kind, runtimeDir) {
61
- if (!OWNER_KINDS.includes(kind)) throw new Error(`unknown web-dapp process kind ${kind}`);
62
- const id = createHash('sha256').update(path.resolve(runtimeDir)).digest('hex').slice(0, 32);
63
- return `${OWNER_FLAG}=${kind}-${id}`;
64
- }
65
-
66
- export function ownsProcess(kind, pid, runtimeDir, command = processCommand(pid)) {
67
- if (!command) return false;
68
- return command.split(/\s+/u).includes(ownerMarker(kind, runtimeDir));
69
- }
70
-
71
- export function portFree(port) {
72
- return new Promise((resolve) => {
73
- const socket = net.connect({ host: '127.0.0.1', port }, () => {
74
- socket.destroy();
75
- resolve(false);
76
- });
77
- socket.on('error', () => resolve(true));
78
- });
79
- }
80
-
81
- // Browsers and hosts are spawned detached, each leading its own process group.
82
- // When the pid leads its group the whole group is signalled and waited for,
83
- // so children that outlive the leader (renderers, a child ignoring SIGTERM)
84
- // are killed too; otherwise only the pid.
85
- export async function terminate(pid, graceMs = 8000) {
86
- if (!pidAlive(pid)) return false;
87
- const group = Number(psField(pid, 'pgid')) === pid;
88
- const alive = () => (group ? groupAlive(pid) : pidAlive(pid));
89
- const signal = (name) => {
90
- try {
91
- process.kill(group ? -pid : pid, name);
92
- } catch {
93
- // Already gone.
94
- }
95
- };
96
- signal('SIGTERM');
97
- const deadline = Date.now() + graceMs;
98
- while (alive() && Date.now() < deadline) await new Promise((resolve) => setTimeout(resolve, 200));
99
- if (alive()) {
100
- signal('SIGKILL');
101
- const killDeadline = Date.now() + 5000;
102
- while (alive() && Date.now() < killDeadline) await new Promise((resolve) => setTimeout(resolve, 100));
103
- }
104
- return true;
105
- }
106
-
107
- // The console collector a run started (src/adapters/web-dapp/console-capture.ts)
108
- // outlives browser relaunches inside the run; stop and cleanup end it.
109
- export async function stopConsoleCollector(target, { log = () => {}, runtimeDir } = {}) {
110
- const runtime = runtimeDir ?? webDappRuntimeDir(target);
111
- const pidFile = path.join(runtime, 'console-tail.pid');
112
- const pid = readPid(pidFile);
113
- if (!pid) return { stopped: null };
114
- if (ownsProcess('console-collector', pid, runtime)) {
115
- await terminate(pid, 3000);
116
- rmSync(pidFile, { force: true });
117
- log(`stopped console collector pid ${pid}`);
118
- return { stopped: pid };
119
- }
120
- if (pidAlive(pid)) {
121
- log(`console collector pid ${pid} is alive but is not this slot's collector; left it alone`);
122
- return { stopped: null, foreign: pid };
123
- }
124
- rmSync(pidFile, { force: true });
125
- return { stopped: null, stale: pid };
126
- }
127
-
128
- // Every live process that proves it is a browser on this slot's profiles,
129
- // found by command line: a browser started through LaunchServices has no
130
- // child handle, and its pid is only known once it owns the CDP port.
131
- export function ownedBrowserPids(runtimeDir) {
132
- const runtime = path.resolve(runtimeDir);
133
- let lines = '';
134
- try {
135
- // `--` ends pgrep's options: the pattern itself starts with dashes.
136
- lines = execFileSync('pgrep', ['-f', '--', ownerMarker('browser', runtime)], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
137
- } catch {
138
- return [];
139
- }
140
- return lines.split('\n').map(Number).filter((pid) => pid > 0 && ownsProcess('browser', pid, runtime));
141
- }
142
-
143
- export function cdpListenerPids(port) {
144
- try {
145
- return execFileSync('lsof', ['-nP', `-iTCP:${port}`, '-sTCP:LISTEN', '-t'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })
146
- .split('\n').map(Number).filter((pid) => pid > 0);
147
- } catch {
148
- return [];
149
- }
150
- }
151
-
152
- export async function stopWebDappBrowser(target, { cdpPort, log = () => {}, runtimeDir } = {}) {
153
- const runtime = runtimeDir ?? webDappRuntimeDir(target);
154
- const stopped = [];
155
- const stale = [];
156
- const foreign = [];
157
- for (const name of ['wallet-host', 'browser']) {
158
- const pidFile = path.join(runtime, `${name}.pid`);
159
- const pid = readPid(pidFile);
160
- if (pid && ownsProcess(name, pid, runtime)) {
161
- if (await terminate(pid)) {
162
- stopped.push({ name, pid });
163
- log(`stopped ${name} pid ${pid}`);
164
- }
165
- rmSync(pidFile, { force: true });
166
- } else if (pid && pidAlive(pid)) {
167
- foreign.push({ name, pid });
168
- log(`${name} pid ${pid} is alive but is not this slot's ${name}; left it and its pid file alone`);
169
- } else {
170
- if (pid) stale.push({ name, pid });
171
- rmSync(pidFile, { force: true });
172
- }
173
- }
174
- rmSync(path.join(runtime, 'wallet-host.ready'), { force: true });
175
- rmSync(path.join(runtime, 'browser.json'), { force: true });
176
- let free = null;
177
- if (cdpPort) {
178
- const deadline = Date.now() + 10000;
179
- free = await portFree(cdpPort);
180
- while (!free && Date.now() < deadline) {
181
- await new Promise((resolve) => setTimeout(resolve, 250));
182
- free = await portFree(cdpPort);
183
- }
184
- if (!free) throw new Error(`CDP port ${cdpPort} is still in use after stopping the ${webDappPolicy().adapterId} browser; another process owns it.`);
185
- }
186
- return { stopped, stale, foreign, cdpPortFree: free };
187
- }
@@ -1,272 +0,0 @@
1
- // Read-only readiness of a web-dapp slot for recipe runs: checkout shape,
2
- // dependencies, the slot dev server, forced testnet, wallet fixture, browser,
3
- // MetaMask extension copy and the per-slot CDP port. Never launches anything
4
- // except the optional short-lived browser start probe.
5
-
6
- import { execFileSync } from 'node:child_process';
7
- import { existsSync, readFileSync } from 'node:fs';
8
- import path from 'node:path';
9
-
10
- import { canonicalPath, fixtureEntry, loadWalletFixture, readBrowserState, viemAccountFromEntry, webDappPolicy, webDappRuntimeDir } from '../../../library/actions/web-dapp/platform/runtime.mjs';
11
- import { ownsProcess, pidAlive, portFree } from './processes.mjs';
12
-
13
- function check(id, ok, detail, { required = true, status } = {}) {
14
- return { id, status: status ?? (ok ? 'pass' : 'fail'), required, detail };
15
- }
16
-
17
- async function httpStatus(url, timeoutMs = 20000) {
18
- try {
19
- const response = await fetch(url, { signal: AbortSignal.timeout(timeoutMs), redirect: 'manual' });
20
- return response.status;
21
- } catch (error) {
22
- return error?.name === 'TimeoutError' ? 'timeout' : 'unreachable';
23
- }
24
- }
25
-
26
- // Diagnostic only. Whether the slot's dev server looks configured for testnet:
27
- // the farm starts `next dev` (temp/farmslot/next-dev.pid) with the policy's
28
- // testnet variable set to true (the Terminal's
29
- // NEXT_PUBLIC_HYPERLIQUID_FORCE_TESTNET). Process inspection cannot prove
30
- // what the browser talks to (macOS reports a process's initial environment,
31
- // and lsof cannot see other users' sockets), so this only warns; the browser
32
- // is held to testnet by the network and wallet layers (the venue policy's
33
- // blocked hosts, the wallet host). Each listener on the app port must descend
34
- // from the pid-file process and run from the checkout (its cwd); every process on the way from
35
- // a listener up to the pid-file process is read exactly (argv and environment
36
- // entries from sysctl KERN_PROCARGS2) and must carry the testnet variable. An
37
- // unreadable process makes the result unknown.
38
- export const DEV_SERVER_PID_FILE = 'temp/farmslot/next-dev.pid';
39
-
40
- function run(command, args) {
41
- try {
42
- return execFileSync(command, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
43
- } catch (error) {
44
- return typeof error?.stdout === 'string' ? error.stdout : '';
45
- }
46
- }
47
-
48
- const PROCARGS_HELPER = `
49
- import ctypes, ctypes.util, json, struct, sys
50
- libc = ctypes.CDLL(ctypes.util.find_library("c"), use_errno=True)
51
- pid = int(sys.argv[1])
52
- argmax = ctypes.c_int(0)
53
- size = ctypes.c_size_t(ctypes.sizeof(argmax))
54
- if libc.sysctl((ctypes.c_int * 2)(1, 8), 2, ctypes.byref(argmax), ctypes.byref(size), None, 0) != 0:
55
- print("null"); sys.exit(0)
56
- buf = ctypes.create_string_buffer(argmax.value)
57
- size = ctypes.c_size_t(argmax.value)
58
- if libc.sysctl((ctypes.c_int * 3)(1, 49, pid), 3, buf, ctypes.byref(size), None, 0) != 0:
59
- print("null"); sys.exit(0)
60
- raw = buf.raw[:size.value]
61
- argc = struct.unpack("i", raw[:4])[0]
62
- rest = raw[4:]
63
- rest = rest[rest.index(b"\\0"):].lstrip(b"\\0")
64
- parts = rest.split(b"\\0")
65
- env = []
66
- for part in parts[argc:]:
67
- if not part: break
68
- env.append(part.decode("utf-8", "replace"))
69
- print(json.dumps({"argv": [p.decode("utf-8", "replace") for p in parts[:argc]], "env": env}))
70
- `;
71
-
72
- export const systemProbe = Object.freeze({
73
- listeners: (port) => run('lsof', ['-nP', `-iTCP:${port}`, '-sTCP:LISTEN', '-t']).split('\n').map(Number).filter((pid) => pid > 0),
74
- parents: () => new Map(run('ps', ['-A', '-o', 'pid=,ppid=']).split('\n')
75
- .map((line) => line.trim().split(/\s+/u).map(Number))
76
- .filter(([pid, ppid]) => pid > 0 && ppid >= 0)),
77
- cwd: (pid) => (run('lsof', ['-a', '-p', String(pid), '-d', 'cwd', '-Fn']).split('\n').find((line) => line.startsWith('n')) ?? '').slice(1) || null,
78
- // { argv: string[], env: string[] } with exact element boundaries, or null.
79
- procArgs: (pid) => {
80
- const python = usablePython();
81
- if (!python) return null;
82
- try {
83
- return JSON.parse(execFileSync(python, ['-c', PROCARGS_HELPER, String(pid)], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 15000 }).trim());
84
- } catch {
85
- return null;
86
- }
87
- },
88
- });
89
-
90
- // macOS's /usr/bin/python3 is a stub until the Command Line Tools are
91
- // installed, and running it opens an install dialog that takes keyboard
92
- // focus. Use it only when xcode-select reports a developer directory; any
93
- // other python3 on PATH is a real install.
94
- export function pythonFor({ found, platform = process.platform, developerDir }) {
95
- if (!found) return null;
96
- if (platform === 'darwin' && found === '/usr/bin/python3' && !developerDir()) return null;
97
- return found;
98
- }
99
-
100
- let pythonChoice;
101
- function usablePython() {
102
- if (pythonChoice === undefined) {
103
- pythonChoice = pythonFor({ found: run('/usr/bin/which', ['python3']).trim(), developerDir: () => run('/usr/bin/xcode-select', ['-p']).trim() });
104
- }
105
- return pythonChoice;
106
- }
107
-
108
- export function environmentValue(env, name) {
109
- const entry = env.find((item) => item.startsWith(`${name}=`));
110
- return entry === undefined ? undefined : entry.slice(name.length + 1);
111
- }
112
-
113
- // `next dev` / `next start`: the argument after the next binary.
114
- export function nextServerMode(argv) {
115
- for (let index = 0; index < argv.length - 1; index += 1) {
116
- const element = argv[index];
117
- if (element === 'next' || element.endsWith('/next') || element.endsWith('/next/dist/bin/next')) {
118
- if (argv[index + 1] === 'dev' || argv[index + 1] === 'start') return argv[index + 1];
119
- }
120
- }
121
- return null;
122
- }
123
-
124
- function chainTo(listener, root, parents) {
125
- const chain = [];
126
- let current = listener;
127
- for (let hops = 0; current && hops < 64; hops += 1) {
128
- chain.push(current);
129
- if (current === root) return chain;
130
- current = parents.get(current);
131
- }
132
- return null;
133
- }
134
-
135
- export function devServerTestnetDiagnostic(target, { appPort, variable, probe = systemProbe } = {}) {
136
- const checkout = canonicalPath(target);
137
- const unknown = (detail) => ({ known: false, forced: false, detail });
138
- const pidFile = path.join(checkout, DEV_SERVER_PID_FILE);
139
- if (!existsSync(pidFile)) return unknown(`no ${DEV_SERVER_PID_FILE}`);
140
- const root = Number(readFileSync(pidFile, 'utf8').trim());
141
- if (!pidAlive(root)) return unknown(`next dev pid ${root} is not running`);
142
- if (!appPort) return unknown('no app port to check');
143
- const listeners = probe.listeners(appPort);
144
- if (listeners.length === 0) return unknown(`nothing (visible to this user) listens on app port ${appPort}`);
145
- const parents = probe.parents();
146
- let mode = null;
147
- for (const listener of listeners) {
148
- const chain = chainTo(listener, root, parents);
149
- if (!chain) return { known: true, forced: false, detail: `app port ${appPort} is served by pid ${listener}, outside next dev pid ${root}` };
150
- const cwd = probe.cwd(listener);
151
- if (!cwd || canonicalPath(cwd) !== checkout) {
152
- return { known: true, forced: false, detail: `pid ${listener} on app port ${appPort} runs from ${cwd ?? 'an unknown directory'}, not ${checkout}` };
153
- }
154
- for (const pid of chain) {
155
- const args = probe.procArgs(pid);
156
- if (!args) return unknown(`cannot read the arguments and environment of pid ${pid}`);
157
- // A process that rewrote its title (next-server) overwrote the memory
158
- // its arguments and environment are read from.
159
- if (args.env.length === 0) return unknown(`pid ${pid} (${args.argv[0] ?? '?'}) rewrote its process title; its environment is not readable`);
160
- const processMode = nextServerMode(args.argv);
161
- if (processMode === 'start') return { known: true, forced: false, detail: `pid ${pid} runs next start; its environment cannot prove what the build inlined` };
162
- if (processMode === 'dev') mode = 'dev';
163
- const value = environmentValue(args.env, variable);
164
- if (value !== 'true') {
165
- return { known: true, forced: false, detail: `pid ${pid} (between app port ${appPort} and next dev pid ${root}) has ${variable}=${value ?? '<unset>'}` };
166
- }
167
- }
168
- }
169
- if (mode !== 'dev') return { known: true, forced: false, detail: `no process serving app port ${appPort} runs next dev` };
170
- return { known: true, forced: true, detail: `every process from app port ${appPort}'s listeners (cwd ${checkout}) up to next dev pid ${root} has ${variable}=true` };
171
- }
172
-
173
- export const MAINNET_CONFIRMATION = 'REAL_FUNDS';
174
-
175
- // The launch only checks the requested network: mainnet needs the real-funds
176
- // confirmation. Testnet is enforced inside the browser (the venue policy's blocked hosts).
177
- export function assertLaunchNetwork(_target, { network, mainnetConfirmation } = {}) {
178
- const requested = network == null || network === '' ? 'testnet' : String(network);
179
- if (requested !== 'testnet' && requested !== 'mainnet') {
180
- throw new Error(`network must be testnet or mainnet, got ${JSON.stringify(network)}.`);
181
- }
182
- if (requested === 'mainnet' && mainnetConfirmation !== MAINNET_CONFIRMATION) {
183
- throw new Error(`network=mainnet needs mainnet_confirmation=${MAINNET_CONFIRMATION}; refusing to launch the ${webDappPolicy().adapterId} browser.`);
184
- }
185
- return requested;
186
- }
187
-
188
- // "a, b and c".
189
- function listing(names) {
190
- return names.length > 1 ? `${names.slice(0, -1).join(', ')} and ${names.at(-1)}` : names.join('');
191
- }
192
-
193
- // `policy`: the venue policy (checkout, dependencies, startPath, testnetVariable);
194
- // by default the one RECIPE_WEB_DAPP_POLICY names.
195
- export async function webDappReadiness({
196
- policy = webDappPolicy(),
197
- target,
198
- appPort,
199
- cdpPort,
200
- signer = 'extension',
201
- account = 'dev1',
202
- env = process.env,
203
- probeBrowser = undefined,
204
- }) {
205
- const checks = [];
206
- const shape = policy.checkout.matches(target);
207
- checks.push(check('checkout', shape, shape ? policy.checkout.label : `${target} is not a ${policy.checkout.name} checkout`));
208
- const deps = policy.dependencies.filter((name) => !existsSync(path.join(target, 'node_modules', name, 'package.json')));
209
- checks.push(check('dependencies', deps.length === 0, deps.length === 0 ? `${listing(policy.dependencies)} installed` : `missing: ${deps.join(', ')} (run npm ci on Node 22)`));
210
-
211
- if (appPort) {
212
- const status = await httpStatus(`http://127.0.0.1:${appPort}${policy.startPath}`);
213
- checks.push(check('app-dev-server', status === 200, `GET http://localhost:${appPort}${policy.startPath} -> ${status}`));
214
- } else {
215
- checks.push(check('app-dev-server', false, 'no dev-server port; pass --watcher-port <port> or set TERMINAL_APP_PORT'));
216
- }
217
- // Advisory: the browser enforces testnet itself (the venue policy's blocked hosts).
218
- const testnet = devServerTestnetDiagnostic(target, { appPort, variable: policy.testnetVariable });
219
- checks.push(check('testnet-forced', testnet.known && testnet.forced, testnet.detail, {
220
- required: false,
221
- status: testnet.known && testnet.forced ? 'pass' : 'warn',
222
- }));
223
-
224
- try {
225
- const { file, fixture } = await loadWalletFixture(target, env);
226
- const address = viemAccountFromEntry(fixtureEntry(fixture, account)).address;
227
- checks.push(check('wallet-fixture', true, `${path.basename(file)}: account ${account} -> ${address}`));
228
- } catch (error) {
229
- checks.push(check('wallet-fixture', false, error.message.split('\n')[0]));
230
- }
231
-
232
- if (probeBrowser) {
233
- try {
234
- const browser = await probeBrowser(env);
235
- checks.push(check('browser', true, `${browser.version ?? browser.bin} (${browser.source})`));
236
- } catch (error) {
237
- checks.push(check('browser', false, error.message.split('\n')[0]));
238
- }
239
- }
240
-
241
- const extensionDir = path.resolve(env.TERMINAL_EXTENSION_DIR || path.join(target, 'temp/metamask-extension/chrome'));
242
- const manifestPath = path.join(extensionDir, 'manifest.json');
243
- const extensionRequired = signer === 'extension';
244
- if (existsSync(manifestPath)) {
245
- const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
246
- checks.push(check('metamask-extension', true, `MetaMask ${manifest.version} at ${extensionDir}`, { required: extensionRequired }));
247
- } else {
248
- checks.push(check('metamask-extension', false, `no extension dist at ${extensionDir}`, { required: extensionRequired }));
249
- }
250
- const checkout = env.TERMINAL_EXTENSION_CHECKOUT;
251
- const checkoutOk = Boolean(checkout) && existsSync(path.join(checkout, 'node_modules/@metamask/browser-passworder'));
252
- checks.push(check('extension-fixture-tooling', checkoutOk, checkoutOk
253
- ? `extension checkout ${checkout} has fixture-state dependencies`
254
- : 'TERMINAL_EXTENSION_CHECKOUT unset or not installed (needed to import the wallet into the extension)', { required: extensionRequired }));
255
-
256
- const state = await readBrowserState(target);
257
- const runtime = webDappRuntimeDir(target);
258
- const live = Boolean(state && ownsProcess('browser', state.browserPid, runtime) && ownsProcess('wallet-host', state.hostPid, runtime));
259
- if (cdpPort) {
260
- const free = await portFree(cdpPort);
261
- const owned = live && state.cdpPort === cdpPort;
262
- checks.push(check('cdp-port', free || owned, free ? `CDP port ${cdpPort} is free` : owned ? `CDP port ${cdpPort} held by this slot's browser` : `CDP port ${cdpPort} is held by another process`));
263
- } else {
264
- checks.push(check('cdp-port', false, 'no CDP port; pass --cdp-port <port>'));
265
- }
266
- checks.push(check('browser-live', live, live
267
- ? `browser pid ${state.browserPid} (${state.signer}, ${state.account?.name}) and wallet host pid ${state.hostPid}`
268
- : 'no slot browser running (metamask.app.launch starts it)', { required: false }));
269
-
270
- const failed = checks.filter((item) => item.required && item.status === 'fail').map((item) => item.id);
271
- return { status: failed.length === 0 ? 'pass' : 'fail', checks, failed, browser: live ? state : null };
272
- }
@@ -1,38 +0,0 @@
1
- #!/usr/bin/env node
2
- // stop.mjs — stop the web-dapp slot browser, wallet host and console collector, verify the CDP port
3
- // is free. Idempotent: nothing running is success.
4
- //
5
- // Inputs: --target <checkout> [--cdp-port <port>] [--json]
6
- // Outputs: JSON summary on stdout. Exit 0 stopped/nothing to stop; 1 port still owned.
7
- // Never touches: the Next.js dev server (Farmslot owns it), browser profiles.
8
-
9
- import path from 'node:path';
10
- import { fileURLToPath } from 'node:url';
11
-
12
- import { webDappPolicy } from '../../library/actions/web-dapp/platform/runtime.mjs';
13
- import { stopConsoleCollector, stopWebDappBrowser } from './lib/processes.mjs';
14
-
15
- const usage = 'Usage: stop.mjs --target <checkout> [--cdp-port <port>] [--json]';
16
-
17
- if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
18
- const argv = process.argv.slice(2);
19
- if (argv.includes('--help') || argv.includes('-h')) {
20
- process.stdout.write(`${usage}\n`);
21
- process.exit(0);
22
- }
23
- const value = (flag) => {
24
- const index = argv.indexOf(flag);
25
- return index >= 0 ? argv[index + 1] : undefined;
26
- };
27
- const target = value('--target') ?? process.cwd();
28
- const rawPort = value('--cdp-port') ?? process.env.RECIPE_CDP_PORT ?? process.env.CDP_PORT;
29
- try {
30
- const log = (message) => process.stderr.write(`[${webDappPolicy().adapterId}/stop] ${message}\n`);
31
- const result = await stopWebDappBrowser(target, { cdpPort: rawPort ? Number(rawPort) : undefined, log });
32
- const collector = await stopConsoleCollector(target, { log });
33
- process.stdout.write(`${JSON.stringify({ status: 'pass', ...result, consoleCollector: collector })}\n`);
34
- } catch (error) {
35
- process.stdout.write(`${JSON.stringify({ status: 'fail', error: error.message })}\n`);
36
- process.exit(1);
37
- }
38
- }
@@ -1,51 +0,0 @@
1
- #!/usr/bin/env node
2
- // verify.mjs — `mm-harness verify --adapter <web-dapp adapter>`: read-only readiness of a
3
- // web-dapp slot (checkout, deps, dev server, forced testnet, fixture,
4
- // browser start probe, MetaMask extension copy, CDP port).
5
- //
6
- // Inputs: --target <checkout> [--cdp-port <port>] [--watcher-port <app port>]
7
- // [--signer extension|injected] [--account <name>] [--json]
8
- // Outputs: readiness JSON (or a human summary) on stdout. Exit 0 ready; 1 not ready.
9
- // Never launches the slot browser or the dev server.
10
-
11
- import path from 'node:path';
12
- import { fileURLToPath } from 'node:url';
13
-
14
- import { resolveBrowser } from './launch.mjs';
15
- import { webDappPolicy } from '../../library/actions/web-dapp/platform/runtime.mjs';
16
- import { webDappReadiness } from './lib/readiness.mjs';
17
-
18
- const usage = 'Usage: verify.mjs --target <checkout> [--cdp-port <port>] [--watcher-port <app port>] [--signer extension|injected] [--account <name>] [--json]';
19
-
20
- if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
21
- const argv = process.argv.slice(2);
22
- if (argv.includes('--help') || argv.includes('-h')) {
23
- process.stdout.write(`${usage}\n`);
24
- process.exit(0);
25
- }
26
- const value = (flag) => {
27
- const index = argv.indexOf(flag);
28
- return index >= 0 ? argv[index + 1] : undefined;
29
- };
30
- const num = (raw) => (raw && /^\d+$/u.test(raw) ? Number(raw) : undefined);
31
- const policy = webDappPolicy();
32
- const report = await webDappReadiness({
33
- policy,
34
- target: path.resolve(value('--target') ?? process.cwd()),
35
- appPort: num(value('--watcher-port') ?? process.env.TERMINAL_APP_PORT ?? process.env.WATCHER_PORT),
36
- cdpPort: num(value('--cdp-port') ?? process.env.RECIPE_CDP_PORT ?? process.env.CDP_PORT),
37
- signer: value('--signer') ?? process.env.TERMINAL_SIGNER ?? 'extension',
38
- account: value('--account') ?? process.env.TERMINAL_ACCOUNT ?? 'dev1',
39
- probeBrowser: resolveBrowser,
40
- });
41
- if (argv.includes('--json')) {
42
- process.stdout.write(`${JSON.stringify({ schemaVersion: 1, command: 'verify', adapter: policy.adapterId, ...report }, null, 2)}\n`);
43
- } else {
44
- for (const check of report.checks) {
45
- const mark = check.status === 'pass' ? '✓' : check.status === 'warn' || !check.required ? '·' : '✗';
46
- process.stdout.write(`${mark} ${check.id}: ${check.detail}\n`);
47
- }
48
- process.stdout.write(report.status === 'pass' ? `${policy.adapterId} slot ready\n` : `not ready: ${report.failed.join(', ')}\n`);
49
- }
50
- process.exit(report.status === 'pass' ? 0 : 1);
51
- }