@deeeed/metamask-harness 0.81.0 → 0.82.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 +23 -0
- package/adapters/manifest.json +51 -51
- package/adapters/{terminal → web-dapp}/cleanup.mjs +9 -9
- package/adapters/{terminal → web-dapp}/inject.mjs +14 -14
- package/adapters/{terminal → web-dapp}/launch.mjs +23 -24
- package/adapters/web-dapp/lib/blocked-hosts.mjs +24 -0
- package/adapters/{terminal → web-dapp}/lib/cdp-client.mjs +1 -1
- package/adapters/{terminal → web-dapp}/lib/display.mjs +1 -1
- package/adapters/{terminal → web-dapp}/lib/hud.mjs +1 -1
- package/adapters/web-dapp/lib/policy.mjs +79 -0
- package/adapters/{terminal → web-dapp}/lib/processes.mjs +8 -8
- package/adapters/{terminal → web-dapp}/lib/readiness.mjs +15 -25
- package/adapters/{terminal → web-dapp}/stop.mjs +5 -4
- package/adapters/{terminal → web-dapp}/verify.mjs +11 -15
- package/adapters/{terminal → web-dapp}/wallet-host.mjs +10 -11
- package/dist/adapters/extension/run.js +5 -28
- package/dist/adapters/plugin-surface.js +98 -0
- package/dist/adapters/surface.js +1 -1
- package/dist/adapters/{terminal → web-dapp}/actions.js +7 -7
- package/dist/adapters/web-dapp/console-capture.js +63 -0
- package/dist/adapters/web-dapp/doctor-checks.js +33 -0
- package/dist/adapters/{terminal → web-dapp}/launch.js +21 -22
- package/dist/adapters/web-dapp/overlay.js +21 -0
- package/dist/adapters/web-dapp/readiness.js +10 -0
- package/dist/adapters/{terminal → web-dapp}/run.js +15 -15
- package/dist/adapters/{terminal → web-dapp}/stop.js +5 -4
- package/dist/adapters/web-dapp/surface.js +149 -0
- package/dist/cli.js +3 -0
- package/dist/command-contract.js +29 -7
- package/dist/commands/check.js +5 -4
- package/dist/commands/doctor.js +19 -2
- package/dist/commands/farmslot-ready.js +1 -0
- package/dist/commands/help.js +1 -1
- package/dist/commands/launch/device.js +5 -5
- package/dist/commands/self-test.js +9 -5
- package/dist/engine.js +6 -16
- package/dist/manifest.js +48 -24
- package/dist/mm-harness-cli.js +17 -0
- package/dist/performance-observation.js +7 -13
- package/dist/runner.js +26 -7
- package/docs/cli-contract.md +5 -3
- package/library/actions/shared/console-collector.mjs +1 -1
- package/library/actions/{terminal → web-dapp}/app/assert_no_console_errors.mjs +4 -4
- package/library/actions/{terminal → web-dapp}/app/launch.mjs +7 -7
- package/library/actions/{terminal → web-dapp}/platform/hud.mjs +5 -5
- package/library/actions/{terminal → web-dapp}/platform/page.mjs +11 -11
- package/library/actions/{terminal → web-dapp}/platform/runtime.mjs +16 -12
- package/library/actions/web-dapp/ui/navigate.mjs +24 -0
- package/library/actions/web-dapp/wallet/_signature-log.mjs +32 -0
- package/library/actions/{terminal → web-dapp}/wallet/assert_signatures.mjs +4 -4
- package/library/actions/{terminal → web-dapp}/wallet/list_accounts.mjs +2 -2
- package/library/actions/{terminal → web-dapp}/wallet/read_signatures.mjs +6 -6
- package/library/manifests/{terminal.action-manifest.json → web-dapp.action-manifest.json} +18 -702
- package/package.json +5 -5
- package/adapters/terminal/lib/mainnet-guard.mjs +0 -186
- package/adapters/terminal/policy.mjs +0 -35
- package/dist/adapters/terminal/console-capture.js +0 -59
- package/dist/adapters/terminal/doctor-checks.js +0 -24
- package/dist/adapters/terminal/overlay.js +0 -25
- package/dist/adapters/terminal/readiness.js +0 -11
- package/dist/adapters/terminal/surface.js +0 -135
- package/library/actions/terminal/perps/_venue.mjs +0 -242
- package/library/actions/terminal/perps/assert_orders.mjs +0 -54
- package/library/actions/terminal/perps/assert_positions.mjs +0 -40
- package/library/actions/terminal/perps/read_orders.mjs +0 -26
- package/library/actions/terminal/perps/read_positions.mjs +0 -25
- package/library/actions/terminal/perps/read_snapshot.mjs +0 -40
- package/library/actions/terminal/perps/revoke_agent.mjs +0 -58
- package/library/actions/terminal/perps/teardown_state.mjs +0 -104
- package/library/actions/terminal/ui/navigate.mjs +0 -31
- package/library/actions/terminal/wallet/_signature-log.mjs +0 -56
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Stop the
|
|
1
|
+
// Stop the web-dapp slot browser and wallet host by their pid files and prove
|
|
2
2
|
// the slot CDP port is released. A pid is signalled only when its command line
|
|
3
3
|
// carries this slot's owner marker for that process kind. A pid file naming a
|
|
4
4
|
// dead process is removed; one naming a live process without the marker is
|
|
@@ -10,7 +10,7 @@ import { existsSync, readFileSync, rmSync } from 'node:fs';
|
|
|
10
10
|
import net from 'node:net';
|
|
11
11
|
import path from 'node:path';
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import { webDappPolicy, webDappRuntimeDir } from '../../../library/actions/web-dapp/platform/runtime.mjs';
|
|
14
14
|
|
|
15
15
|
export function pidAlive(pid) {
|
|
16
16
|
if (!Number.isInteger(Number(pid)) || Number(pid) <= 0) return false;
|
|
@@ -58,7 +58,7 @@ export const OWNER_FLAG = '--mm-harness-owner';
|
|
|
58
58
|
export const OWNER_KINDS = Object.freeze(['browser', 'wallet-host', 'console-collector']);
|
|
59
59
|
|
|
60
60
|
export function ownerMarker(kind, runtimeDir) {
|
|
61
|
-
if (!OWNER_KINDS.includes(kind)) throw new Error(`unknown
|
|
61
|
+
if (!OWNER_KINDS.includes(kind)) throw new Error(`unknown web-dapp process kind ${kind}`);
|
|
62
62
|
const id = createHash('sha256').update(path.resolve(runtimeDir)).digest('hex').slice(0, 32);
|
|
63
63
|
return `${OWNER_FLAG}=${kind}-${id}`;
|
|
64
64
|
}
|
|
@@ -104,10 +104,10 @@ export async function terminate(pid, graceMs = 8000) {
|
|
|
104
104
|
return true;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
// The console collector a run started (src/adapters/
|
|
107
|
+
// The console collector a run started (src/adapters/web-dapp/console-capture.ts)
|
|
108
108
|
// outlives browser relaunches inside the run; stop and cleanup end it.
|
|
109
109
|
export async function stopConsoleCollector(target, { log = () => {}, runtimeDir } = {}) {
|
|
110
|
-
const runtime = runtimeDir ??
|
|
110
|
+
const runtime = runtimeDir ?? webDappRuntimeDir(target);
|
|
111
111
|
const pidFile = path.join(runtime, 'console-tail.pid');
|
|
112
112
|
const pid = readPid(pidFile);
|
|
113
113
|
if (!pid) return { stopped: null };
|
|
@@ -149,8 +149,8 @@ export function cdpListenerPids(port) {
|
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
export async function
|
|
153
|
-
const runtime = runtimeDir ??
|
|
152
|
+
export async function stopWebDappBrowser(target, { cdpPort, log = () => {}, runtimeDir } = {}) {
|
|
153
|
+
const runtime = runtimeDir ?? webDappRuntimeDir(target);
|
|
154
154
|
const stopped = [];
|
|
155
155
|
const stale = [];
|
|
156
156
|
const foreign = [];
|
|
@@ -181,7 +181,7 @@ export async function stopTerminalBrowser(target, { cdpPort, log = () => {}, run
|
|
|
181
181
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
182
182
|
free = await portFree(cdpPort);
|
|
183
183
|
}
|
|
184
|
-
if (!free) throw new Error(`CDP port ${cdpPort} is still in use after stopping the
|
|
184
|
+
if (!free) throw new Error(`CDP port ${cdpPort} is still in use after stopping the ${webDappPolicy().adapterId} browser; another process owns it.`);
|
|
185
185
|
}
|
|
186
186
|
return { stopped, stale, foreign, cdpPortFree: free };
|
|
187
187
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Read-only readiness of a
|
|
1
|
+
// Read-only readiness of a web-dapp slot for recipe runs: checkout shape,
|
|
2
2
|
// dependencies, the slot dev server, forced testnet, wallet fixture, browser,
|
|
3
3
|
// MetaMask extension copy and the per-slot CDP port. Never launches anything
|
|
4
4
|
// except the optional short-lived browser start probe.
|
|
@@ -7,23 +7,13 @@ import { execFileSync } from 'node:child_process';
|
|
|
7
7
|
import { existsSync, readFileSync } from 'node:fs';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
|
|
10
|
-
import { canonicalPath, fixtureEntry, loadWalletFixture, readBrowserState,
|
|
10
|
+
import { canonicalPath, fixtureEntry, loadWalletFixture, readBrowserState, viemAccountFromEntry, webDappPolicy, webDappRuntimeDir } from '../../../library/actions/web-dapp/platform/runtime.mjs';
|
|
11
11
|
import { ownsProcess, pidAlive, portFree } from './processes.mjs';
|
|
12
12
|
|
|
13
13
|
function check(id, ok, detail, { required = true, status } = {}) {
|
|
14
14
|
return { id, status: status ?? (ok ? 'pass' : 'fail'), required, detail };
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
export function isTerminalCheckout(target) {
|
|
18
|
-
try {
|
|
19
|
-
const pkg = JSON.parse(readFileSync(path.join(target, 'package.json'), 'utf8'));
|
|
20
|
-
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
21
|
-
return Boolean(deps.next) && existsSync(path.join(target, 'src/features/perpetuals'));
|
|
22
|
-
} catch {
|
|
23
|
-
return false;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
17
|
async function httpStatus(url, timeoutMs = 20000) {
|
|
28
18
|
try {
|
|
29
19
|
const response = await fetch(url, { signal: AbortSignal.timeout(timeoutMs), redirect: 'manual' });
|
|
@@ -39,9 +29,9 @@ async function httpStatus(url, timeoutMs = 20000) {
|
|
|
39
29
|
// NEXT_PUBLIC_HYPERLIQUID_FORCE_TESTNET). Process inspection cannot prove
|
|
40
30
|
// what the browser talks to (macOS reports a process's initial environment,
|
|
41
31
|
// and lsof cannot see other users' sockets), so this only warns; the browser
|
|
42
|
-
// is held to testnet by the network and wallet layers (
|
|
43
|
-
// wallet host). Each listener on the app port must descend
|
|
44
|
-
// process and run from the checkout (its cwd); every process on the way from
|
|
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
|
|
45
35
|
// a listener up to the pid-file process is read exactly (argv and environment
|
|
46
36
|
// entries from sysctl KERN_PROCARGS2) and must carry the testnet variable. An
|
|
47
37
|
// unreadable process makes the result unknown.
|
|
@@ -183,14 +173,14 @@ export function devServerTestnetDiagnostic(target, { appPort, variable, probe =
|
|
|
183
173
|
export const MAINNET_CONFIRMATION = 'REAL_FUNDS';
|
|
184
174
|
|
|
185
175
|
// The launch only checks the requested network: mainnet needs the real-funds
|
|
186
|
-
// confirmation. Testnet is enforced inside the browser (
|
|
176
|
+
// confirmation. Testnet is enforced inside the browser (the venue policy's blocked hosts).
|
|
187
177
|
export function assertLaunchNetwork(_target, { network, mainnetConfirmation } = {}) {
|
|
188
178
|
const requested = network == null || network === '' ? 'testnet' : String(network);
|
|
189
179
|
if (requested !== 'testnet' && requested !== 'mainnet') {
|
|
190
180
|
throw new Error(`network must be testnet or mainnet, got ${JSON.stringify(network)}.`);
|
|
191
181
|
}
|
|
192
182
|
if (requested === 'mainnet' && mainnetConfirmation !== MAINNET_CONFIRMATION) {
|
|
193
|
-
throw new Error(`network=mainnet needs mainnet_confirmation=${MAINNET_CONFIRMATION}; refusing to launch the
|
|
183
|
+
throw new Error(`network=mainnet needs mainnet_confirmation=${MAINNET_CONFIRMATION}; refusing to launch the ${webDappPolicy().adapterId} browser.`);
|
|
194
184
|
}
|
|
195
185
|
return requested;
|
|
196
186
|
}
|
|
@@ -200,10 +190,10 @@ function listing(names) {
|
|
|
200
190
|
return names.length > 1 ? `${names.slice(0, -1).join(', ')} and ${names.at(-1)}` : names.join('');
|
|
201
191
|
}
|
|
202
192
|
|
|
203
|
-
// `policy`: the venue policy (
|
|
204
|
-
//
|
|
205
|
-
export async function
|
|
206
|
-
policy,
|
|
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(),
|
|
207
197
|
target,
|
|
208
198
|
appPort,
|
|
209
199
|
cdpPort,
|
|
@@ -213,8 +203,8 @@ export async function terminalReadiness({
|
|
|
213
203
|
probeBrowser = undefined,
|
|
214
204
|
}) {
|
|
215
205
|
const checks = [];
|
|
216
|
-
const shape =
|
|
217
|
-
checks.push(check('checkout', shape, shape ?
|
|
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`));
|
|
218
208
|
const deps = policy.dependencies.filter((name) => !existsSync(path.join(target, 'node_modules', name, 'package.json')));
|
|
219
209
|
checks.push(check('dependencies', deps.length === 0, deps.length === 0 ? `${listing(policy.dependencies)} installed` : `missing: ${deps.join(', ')} (run npm ci on Node 22)`));
|
|
220
210
|
|
|
@@ -224,7 +214,7 @@ export async function terminalReadiness({
|
|
|
224
214
|
} else {
|
|
225
215
|
checks.push(check('app-dev-server', false, 'no dev-server port; pass --watcher-port <port> or set TERMINAL_APP_PORT'));
|
|
226
216
|
}
|
|
227
|
-
// Advisory: the browser enforces testnet itself (
|
|
217
|
+
// Advisory: the browser enforces testnet itself (the venue policy's blocked hosts).
|
|
228
218
|
const testnet = devServerTestnetDiagnostic(target, { appPort, variable: policy.testnetVariable });
|
|
229
219
|
checks.push(check('testnet-forced', testnet.known && testnet.forced, testnet.detail, {
|
|
230
220
|
required: false,
|
|
@@ -264,7 +254,7 @@ export async function terminalReadiness({
|
|
|
264
254
|
: 'TERMINAL_EXTENSION_CHECKOUT unset or not installed (needed to import the wallet into the extension)', { required: extensionRequired }));
|
|
265
255
|
|
|
266
256
|
const state = await readBrowserState(target);
|
|
267
|
-
const runtime =
|
|
257
|
+
const runtime = webDappRuntimeDir(target);
|
|
268
258
|
const live = Boolean(state && ownsProcess('browser', state.browserPid, runtime) && ownsProcess('wallet-host', state.hostPid, runtime));
|
|
269
259
|
if (cdpPort) {
|
|
270
260
|
const free = await portFree(cdpPort);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// stop.mjs — stop the
|
|
2
|
+
// stop.mjs — stop the web-dapp slot browser, wallet host and console collector, verify the CDP port
|
|
3
3
|
// is free. Idempotent: nothing running is success.
|
|
4
4
|
//
|
|
5
5
|
// Inputs: --target <checkout> [--cdp-port <port>] [--json]
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { fileURLToPath } from 'node:url';
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import { webDappPolicy } from '../../library/actions/web-dapp/platform/runtime.mjs';
|
|
13
|
+
import { stopConsoleCollector, stopWebDappBrowser } from './lib/processes.mjs';
|
|
13
14
|
|
|
14
15
|
const usage = 'Usage: stop.mjs --target <checkout> [--cdp-port <port>] [--json]';
|
|
15
16
|
|
|
@@ -26,8 +27,8 @@ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.me
|
|
|
26
27
|
const target = value('--target') ?? process.cwd();
|
|
27
28
|
const rawPort = value('--cdp-port') ?? process.env.RECIPE_CDP_PORT ?? process.env.CDP_PORT;
|
|
28
29
|
try {
|
|
29
|
-
const log = (message) => process.stderr.write(`[
|
|
30
|
-
const result = await
|
|
30
|
+
const log = (message) => process.stderr.write(`[${webDappPolicy().adapterId}/stop] ${message}\n`);
|
|
31
|
+
const result = await stopWebDappBrowser(target, { cdpPort: rawPort ? Number(rawPort) : undefined, log });
|
|
31
32
|
const collector = await stopConsoleCollector(target, { log });
|
|
32
33
|
process.stdout.write(`${JSON.stringify({ status: 'pass', ...result, consoleCollector: collector })}\n`);
|
|
33
34
|
} catch (error) {
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// verify.mjs — `mm-harness verify --adapter
|
|
3
|
-
//
|
|
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
4
|
// browser start probe, MetaMask extension copy, CDP port).
|
|
5
5
|
//
|
|
6
|
-
// Inputs: --
|
|
6
|
+
// Inputs: --target <checkout> [--cdp-port <port>] [--watcher-port <app port>]
|
|
7
7
|
// [--signer extension|injected] [--account <name>] [--json]
|
|
8
8
|
// Outputs: readiness JSON (or a human summary) on stdout. Exit 0 ready; 1 not ready.
|
|
9
9
|
// Never launches the slot browser or the dev server.
|
|
10
10
|
|
|
11
11
|
import path from 'node:path';
|
|
12
|
-
import { fileURLToPath
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
13
|
|
|
14
14
|
import { resolveBrowser } from './launch.mjs';
|
|
15
|
-
import {
|
|
15
|
+
import { webDappPolicy } from '../../library/actions/web-dapp/platform/runtime.mjs';
|
|
16
|
+
import { webDappReadiness } from './lib/readiness.mjs';
|
|
16
17
|
|
|
17
|
-
const usage = 'Usage: verify.mjs --
|
|
18
|
+
const usage = 'Usage: verify.mjs --target <checkout> [--cdp-port <port>] [--watcher-port <app port>] [--signer extension|injected] [--account <name>] [--json]';
|
|
18
19
|
|
|
19
20
|
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
20
21
|
const argv = process.argv.slice(2);
|
|
@@ -27,13 +28,8 @@ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.me
|
|
|
27
28
|
return index >= 0 ? argv[index + 1] : undefined;
|
|
28
29
|
};
|
|
29
30
|
const num = (raw) => (raw && /^\d+$/u.test(raw) ? Number(raw) : undefined);
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
process.stderr.write(`Missing --policy-module\n${usage}\n`);
|
|
33
|
-
process.exit(2);
|
|
34
|
-
}
|
|
35
|
-
const { policy } = await import(pathToFileURL(path.resolve(policyModule)).href);
|
|
36
|
-
const report = await terminalReadiness({
|
|
31
|
+
const policy = webDappPolicy();
|
|
32
|
+
const report = await webDappReadiness({
|
|
37
33
|
policy,
|
|
38
34
|
target: path.resolve(value('--target') ?? process.cwd()),
|
|
39
35
|
appPort: num(value('--watcher-port') ?? process.env.TERMINAL_APP_PORT ?? process.env.WATCHER_PORT),
|
|
@@ -43,13 +39,13 @@ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.me
|
|
|
43
39
|
probeBrowser: resolveBrowser,
|
|
44
40
|
});
|
|
45
41
|
if (argv.includes('--json')) {
|
|
46
|
-
process.stdout.write(`${JSON.stringify({ schemaVersion: 1, command: 'verify', adapter:
|
|
42
|
+
process.stdout.write(`${JSON.stringify({ schemaVersion: 1, command: 'verify', adapter: policy.adapterId, ...report }, null, 2)}\n`);
|
|
47
43
|
} else {
|
|
48
44
|
for (const check of report.checks) {
|
|
49
45
|
const mark = check.status === 'pass' ? '✓' : check.status === 'warn' || !check.required ? '·' : '✗';
|
|
50
46
|
process.stdout.write(`${mark} ${check.id}: ${check.detail}\n`);
|
|
51
47
|
}
|
|
52
|
-
process.stdout.write(report.status === 'pass' ?
|
|
48
|
+
process.stdout.write(report.status === 'pass' ? `${policy.adapterId} slot ready\n` : `not ready: ${report.failed.join(', ')}\n`);
|
|
53
49
|
}
|
|
54
50
|
process.exit(report.status === 'pass' ? 0 : 1);
|
|
55
51
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// wallet-host.mjs — long-running observer (and, in injected mode, signer) for the
|
|
3
|
-
//
|
|
3
|
+
// web-dapp slot browser.
|
|
4
4
|
//
|
|
5
5
|
// Purpose:
|
|
6
6
|
// Attaches to the slot browser over CDP and, for every app document, installs
|
|
@@ -27,20 +27,20 @@
|
|
|
27
27
|
import { appendFileSync, chmodSync, existsSync, unwatchFile, watchFile, writeFileSync } from 'node:fs';
|
|
28
28
|
import { createRequire } from 'node:module';
|
|
29
29
|
import path from 'node:path';
|
|
30
|
-
import { pathToFileURL } from 'node:url';
|
|
31
30
|
|
|
32
31
|
import { createStrictWallet, createWalletRequestBinding, pageReadyExpression, pageScriptSource } from '@farmslot/adapter-web/dapp';
|
|
33
32
|
import { isAppUrl as isExactAppUrl, shortUrl } from '@farmslot/adapter-web/origin';
|
|
34
33
|
|
|
35
34
|
import { CdpClient, browserWebSocketUrl } from './lib/cdp-client.mjs';
|
|
36
35
|
import { hudFile, hudRenderExpression, readHudState } from './lib/hud.mjs';
|
|
37
|
-
import {
|
|
36
|
+
import { fixtureAccount, webDappPolicy } from '../../library/actions/web-dapp/platform/runtime.mjs';
|
|
37
|
+
import { hostOf, isBlockedUrl } from './lib/blocked-hosts.mjs';
|
|
38
38
|
import { ownedBrowserPids } from './lib/processes.mjs';
|
|
39
39
|
|
|
40
40
|
const { captureMacFrontmost, macFocusDisabled } = createRequire(import.meta.url)('@farmslot/adapter-web/macos-focus');
|
|
41
41
|
|
|
42
42
|
function usage() {
|
|
43
|
-
return 'Usage: wallet-host.mjs --cdp-port <port> --runtime-dir <dir> --signer <extension|injected> --app-origin <url>
|
|
43
|
+
return 'Usage: wallet-host.mjs --cdp-port <port> --runtime-dir <dir> --signer <extension|injected> --app-origin <url> [--network testnet|mainnet] [--mainnet-hosts <h1,h2>] [--testnet-hosts <h1,h2>] [--extension-id <id>] [--target <checkout> --account <name> --start-chain <id>] [--observe-focus 1] [--window x,y,w,h] [--start-path <path>] [--mm-harness-owner=<marker>]';
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
function parseArgs(argv) {
|
|
@@ -59,16 +59,16 @@ function parseArgs(argv) {
|
|
|
59
59
|
args[flag.slice(2)] = argv[index + 1];
|
|
60
60
|
index += 1;
|
|
61
61
|
}
|
|
62
|
-
for (const key of ['cdp-port', 'runtime-dir', 'signer', 'app-origin'
|
|
62
|
+
for (const key of ['cdp-port', 'runtime-dir', 'signer', 'app-origin']) {
|
|
63
63
|
if (!args[key]) throw new Error(`Missing --${key}\n${usage()}`);
|
|
64
64
|
}
|
|
65
65
|
return args;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
const args = parseArgs(process.argv.slice(2));
|
|
69
|
-
// The venue policy (the Terminal's
|
|
70
|
-
// the probe
|
|
71
|
-
const
|
|
69
|
+
// The venue policy (e.g. the Terminal plugin's policy.mjs), from RECIPE_WEB_DAPP_POLICY:
|
|
70
|
+
// link hosts, the probe, the typed-data refusal, the start page and chain.
|
|
71
|
+
const policy = webDappPolicy();
|
|
72
72
|
const LINK_HOSTS = new Set(policy.linkHosts);
|
|
73
73
|
const runtimeDir = path.resolve(args['runtime-dir']);
|
|
74
74
|
const logFile = path.join(runtimeDir, 'wallet-requests.jsonl');
|
|
@@ -111,7 +111,6 @@ function say(message) {
|
|
|
111
111
|
|
|
112
112
|
let wallet = null;
|
|
113
113
|
if (signer === 'injected') {
|
|
114
|
-
const { fixtureAccount } = await import('../../library/actions/terminal/platform/runtime.mjs');
|
|
115
114
|
if (!args.target || !args.account) throw new Error('injected mode requires --target and --account');
|
|
116
115
|
const account = await fixtureAccount(path.resolve(args.target), args.account);
|
|
117
116
|
wallet = createStrictWallet({ account, chainId: Number(args['start-chain'] ?? policy.startChain) });
|
|
@@ -334,7 +333,7 @@ client.on('Page.frameNavigated', ({ frame }, sessionId) => {
|
|
|
334
333
|
}
|
|
335
334
|
});
|
|
336
335
|
client.on('Fetch.requestPaused', ({ requestId, request, resourceType }, sessionId) => {
|
|
337
|
-
const mainnet =
|
|
336
|
+
const mainnet = isBlockedUrl(request.url, mainnetHosts);
|
|
338
337
|
if (mainnet) {
|
|
339
338
|
appendEntry({ kind: 'blocked-mainnet', t: Date.now(), transport: 'http', url: shortUrl(request.url), resourceType, probe: request.url.includes(PROBE_MARK) });
|
|
340
339
|
say(`blocked a mainnet request to ${shortUrl(request.url)}`);
|
|
@@ -361,7 +360,7 @@ client.on('Network.webSocketCreated', ({ requestId, url }, sessionId) => {
|
|
|
361
360
|
if (url.includes(PROBE_MARK)) probeRequests.set(requestId, 'websocket');
|
|
362
361
|
recordVenueTraffic(sessionId, url);
|
|
363
362
|
// The host resolver rule fails the connection; the attempt is still evidence.
|
|
364
|
-
if (
|
|
363
|
+
if (isBlockedUrl(url, mainnetHosts)) {
|
|
365
364
|
appendEntry({ kind: 'blocked-mainnet', t: Date.now(), transport: 'websocket', url: shortUrl(url), probe: url.includes(PROBE_MARK) });
|
|
366
365
|
say(`blocked a mainnet WebSocket to ${shortUrl(url)}`);
|
|
367
366
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { EXIT, optionFlag
|
|
2
|
+
import { EXIT, optionFlag } from "@farmslot/recipe-cli/harness";
|
|
3
3
|
import { verifyOwnedCollectorControl } from "../console-capture.js";
|
|
4
4
|
import { resolveExtensionBrowserPid } from "./browser-pid.js";
|
|
5
5
|
import {
|
|
@@ -38,28 +38,9 @@ const extensionRun = {
|
|
|
38
38
|
pageMode: "home-or-sidepanel"
|
|
39
39
|
});
|
|
40
40
|
},
|
|
41
|
-
// The proof runtime is checked on the run's own ports
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return async () => {
|
|
45
|
-
const previousCdpPort = process.env.CDP_PORT;
|
|
46
|
-
const previousRecipeCdpPort = process.env.RECIPE_CDP_PORT;
|
|
47
|
-
const previousWatcherPort = process.env.WATCHER_PORT;
|
|
48
|
-
const explicitCdpPort = optionString(options, "cdpPort");
|
|
49
|
-
const explicitWatcherPort = optionString(options, "watcherPort") ?? optionString(options, "metroPort");
|
|
50
|
-
if (explicitCdpPort) {
|
|
51
|
-
process.env.CDP_PORT = explicitCdpPort;
|
|
52
|
-
process.env.RECIPE_CDP_PORT = explicitCdpPort;
|
|
53
|
-
}
|
|
54
|
-
if (explicitWatcherPort) process.env.WATCHER_PORT = explicitWatcherPort;
|
|
55
|
-
try {
|
|
56
|
-
return await ensureExtensionProofRuntime(target, heal, state, json, { onRecovery });
|
|
57
|
-
} finally {
|
|
58
|
-
restoreEnv("CDP_PORT", previousCdpPort);
|
|
59
|
-
restoreEnv("RECIPE_CDP_PORT", previousRecipeCdpPort);
|
|
60
|
-
restoreEnv("WATCHER_PORT", previousWatcherPort);
|
|
61
|
-
}
|
|
62
|
-
};
|
|
41
|
+
// The proof runtime is checked on the run's own ports (the engine's scope).
|
|
42
|
+
runtimeCheck({ target, heal, state, json, onRecovery }) {
|
|
43
|
+
return () => ensureExtensionProofRuntime(target, heal, state, json, { onRecovery });
|
|
63
44
|
},
|
|
64
45
|
launchedBrowser: (target, artifactsDir, cdpPort) => launchedBrowser(target, artifactsDir, cdpPort),
|
|
65
46
|
browserProvenance: (browser) => ({
|
|
@@ -207,16 +188,12 @@ const extensionObservation = {
|
|
|
207
188
|
start: async (context) => {
|
|
208
189
|
const observation = await import("../../performance-observation.js");
|
|
209
190
|
return observation.startRunPerformanceObservation({
|
|
210
|
-
backend: (target, env
|
|
191
|
+
backend: (target, env) => observation.createExtensionPerformanceObserver(target, env),
|
|
211
192
|
platform: () => "extension"
|
|
212
193
|
}, context);
|
|
213
194
|
}
|
|
214
195
|
}
|
|
215
196
|
};
|
|
216
|
-
function restoreEnv(key, value) {
|
|
217
|
-
if (value === void 0) delete process.env[key];
|
|
218
|
-
else process.env[key] = value;
|
|
219
|
-
}
|
|
220
197
|
export {
|
|
221
198
|
ensureExtensionProofRuntime,
|
|
222
199
|
extensionDiagnostics,
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import {
|
|
3
|
+
adapterPlugin,
|
|
4
|
+
adapterSelectionFailureOut,
|
|
5
|
+
ensureAdapterLoaded,
|
|
6
|
+
selectedAdapterId
|
|
7
|
+
} from "@farmslot/recipe-cli/harness";
|
|
8
|
+
import { configPath, declaredLibraryPathEnv, readConfig } from "../review/knowledge.js";
|
|
9
|
+
import { adapterRegistry } from "./registry.js";
|
|
10
|
+
import { bindWebDappPolicy } from "./web-dapp/surface.js";
|
|
11
|
+
function configuredLibrarySources() {
|
|
12
|
+
let libraries;
|
|
13
|
+
try {
|
|
14
|
+
libraries = readConfig(configPath()).libraries;
|
|
15
|
+
} catch {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
return Object.entries(libraries ?? {}).map(([name, root]) => ({ name, root: path.resolve(root) }));
|
|
19
|
+
}
|
|
20
|
+
function operatorLibraryEnv() {
|
|
21
|
+
const declared = declaredLibraryPathEnv();
|
|
22
|
+
if (!declared) return process.env;
|
|
23
|
+
const env = { ...process.env };
|
|
24
|
+
if (declared.value === void 0) delete env.RECIPE_LIBRARY_PATH;
|
|
25
|
+
else env.RECIPE_LIBRARY_PATH = declared.value;
|
|
26
|
+
return env;
|
|
27
|
+
}
|
|
28
|
+
async function loadSelectedAdapter(command, args) {
|
|
29
|
+
const passthrough = args.indexOf("--");
|
|
30
|
+
const own = passthrough < 0 ? args : args.slice(0, passthrough);
|
|
31
|
+
const libraries = [];
|
|
32
|
+
own.forEach((arg, index) => {
|
|
33
|
+
if (arg === "--library" && own[index + 1] !== void 0) libraries.push(own[index + 1]);
|
|
34
|
+
else if (arg.startsWith("--library=")) libraries.push(arg.slice("--library=".length));
|
|
35
|
+
});
|
|
36
|
+
const selected = selectedAdapterId(args);
|
|
37
|
+
try {
|
|
38
|
+
await ensureAdapterLoaded(selected, {
|
|
39
|
+
libraries,
|
|
40
|
+
configured: configuredLibrarySources(),
|
|
41
|
+
env: operatorLibraryEnv(),
|
|
42
|
+
adopt: adoptPluginAdapter
|
|
43
|
+
});
|
|
44
|
+
return void 0;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
const refusal = error;
|
|
47
|
+
if (typeof refusal.code !== "string" || typeof refusal.userAction !== "string") throw error;
|
|
48
|
+
if (selected !== void 0 && adapterRegistry.has(selected) && refusal.code !== "ADAPTER_ID_CONFLICT") {
|
|
49
|
+
return void 0;
|
|
50
|
+
}
|
|
51
|
+
return adapterSelectionFailureOut(own.includes("--json"), command, {
|
|
52
|
+
code: refusal.code,
|
|
53
|
+
message: String(refusal.message),
|
|
54
|
+
userAction: refusal.userAction
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const ADAPTER_PLUGIN_ENV = "METAMASK_RECIPE_ADAPTER_PLUGIN";
|
|
59
|
+
function adoptPluginAdapter(adapter) {
|
|
60
|
+
const surface = bindWebDappPolicy(adapter);
|
|
61
|
+
return {
|
|
62
|
+
...surface,
|
|
63
|
+
run: {
|
|
64
|
+
...surface.run,
|
|
65
|
+
envKeys: [...surface.run?.envKeys ?? [], ADAPTER_PLUGIN_ENV],
|
|
66
|
+
activateEnv(projectRoot, options) {
|
|
67
|
+
surface.run?.activateEnv?.(projectRoot, options);
|
|
68
|
+
process.env[ADAPTER_PLUGIN_ENV] = `${adapter.id} ${adapterPlugin(adapter.id)?.digest ?? "unknown"}`;
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
runwayProvision: surface.runwayProvision ?? {
|
|
72
|
+
async run(target) {
|
|
73
|
+
return {
|
|
74
|
+
schemaVersion: 1,
|
|
75
|
+
command: "provision",
|
|
76
|
+
adapter: adapter.id,
|
|
77
|
+
target: path.resolve(target),
|
|
78
|
+
status: "fail",
|
|
79
|
+
exitCode: 2,
|
|
80
|
+
error: {
|
|
81
|
+
code: "UNSUPPORTED_ADAPTER",
|
|
82
|
+
message: `${adapter.id} has no mobile simulator to provision.`,
|
|
83
|
+
userAction: `mm-harness run <recipe> --adapter ${adapter.id}`
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
readiness: surface.readiness ?? { compatibilityMode: () => "unsupported/no bridge" },
|
|
89
|
+
fixtures: surface.fixtures ?? { operations: [] }
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export {
|
|
93
|
+
ADAPTER_PLUGIN_ENV,
|
|
94
|
+
adoptPluginAdapter,
|
|
95
|
+
configuredLibrarySources,
|
|
96
|
+
loadSelectedAdapter,
|
|
97
|
+
operatorLibraryEnv
|
|
98
|
+
};
|
package/dist/adapters/surface.js
CHANGED
|
@@ -3,7 +3,7 @@ import { adapterRegistry } from "./registry.js";
|
|
|
3
3
|
import "./mobile/surface.js";
|
|
4
4
|
import "./extension/surface.js";
|
|
5
5
|
import "./core/surface.js";
|
|
6
|
-
import "./
|
|
6
|
+
import "./web-dapp/surface.js";
|
|
7
7
|
function getAdapterSurface(adapter) {
|
|
8
8
|
return adapterRegistry.get(adapter);
|
|
9
9
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { manifestPath } from "../../paths.js";
|
|
2
2
|
import { perpsOrderInputFindings } from "../perps-order-inputs.js";
|
|
3
|
-
const
|
|
4
|
-
manifestPath: () => manifestPath("
|
|
3
|
+
const webDappActions = {
|
|
4
|
+
manifestPath: () => manifestPath("web-dapp"),
|
|
5
5
|
semantic: [
|
|
6
6
|
"app.network_capture",
|
|
7
7
|
"app.network_assert",
|
|
@@ -12,8 +12,8 @@ const terminalActions = {
|
|
|
12
12
|
ui: (harness, { hudPolicy }) => ({
|
|
13
13
|
base: harness.createCdpWebUiTransport({
|
|
14
14
|
async withPage(input, callback) {
|
|
15
|
-
const {
|
|
16
|
-
return
|
|
15
|
+
const { withAppPage } = await import("../../../library/actions/web-dapp/platform/page.mjs");
|
|
16
|
+
return withAppPage(
|
|
17
17
|
{
|
|
18
18
|
action: input.action,
|
|
19
19
|
node: input.node,
|
|
@@ -29,8 +29,8 @@ const terminalActions = {
|
|
|
29
29
|
}),
|
|
30
30
|
async execute(action, node, context, standard) {
|
|
31
31
|
if (action === "app.hud") {
|
|
32
|
-
const {
|
|
33
|
-
return
|
|
32
|
+
const { showAppHud } = await import("../../../library/actions/web-dapp/platform/hud.mjs");
|
|
33
|
+
return showAppHud(
|
|
34
34
|
{ action, node, context: { nodeId: context.nodeId, projectRoot: context.projectRoot } },
|
|
35
35
|
{ hudPolicy }
|
|
36
36
|
);
|
|
@@ -42,5 +42,5 @@ const terminalActions = {
|
|
|
42
42
|
inputFindings: (nodeId, node) => perpsOrderInputFindings(nodeId, node, { ensureChecksOnly: false })
|
|
43
43
|
};
|
|
44
44
|
export {
|
|
45
|
-
|
|
45
|
+
webDappActions
|
|
46
46
|
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { recipeRuntimePath } from "@farmslot/recipe-cli/harness";
|
|
4
|
+
import { verifyCapture } from "../../../library/actions/shared/console-collector.mjs";
|
|
5
|
+
import { ensureConsoleCollector } from "../console-capture.js";
|
|
6
|
+
import { webDappAdapterId } from "../../../adapters/web-dapp/lib/policy.mjs";
|
|
7
|
+
const WEB_DAPP_CONSOLE_FILES = Object.freeze({
|
|
8
|
+
app: "app-console.log",
|
|
9
|
+
extension: "extension-console.log",
|
|
10
|
+
collectorLog: "console-tail.log",
|
|
11
|
+
pidFile: "console-tail.pid"
|
|
12
|
+
});
|
|
13
|
+
function webDappRuntimeFile(target, name) {
|
|
14
|
+
let root = path.resolve(target);
|
|
15
|
+
try {
|
|
16
|
+
root = fs.realpathSync(root);
|
|
17
|
+
} catch {
|
|
18
|
+
}
|
|
19
|
+
return recipeRuntimePath(root, webDappAdapterId(), name);
|
|
20
|
+
}
|
|
21
|
+
function webDappCaptureFiles(target) {
|
|
22
|
+
return {
|
|
23
|
+
extensionLog: webDappRuntimeFile(target, WEB_DAPP_CONSOLE_FILES.extension),
|
|
24
|
+
pageLog: webDappRuntimeFile(target, WEB_DAPP_CONSOLE_FILES.app),
|
|
25
|
+
collectorLog: webDappRuntimeFile(target, WEB_DAPP_CONSOLE_FILES.collectorLog),
|
|
26
|
+
pidFile: webDappRuntimeFile(target, WEB_DAPP_CONSOLE_FILES.pidFile)
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function webDappCdpPort() {
|
|
30
|
+
return process.env.RECIPE_CDP_PORT ?? process.env.CDP_PORT;
|
|
31
|
+
}
|
|
32
|
+
async function ensureWebDappConsoleCapture(target) {
|
|
33
|
+
const cdpPort = webDappCdpPort();
|
|
34
|
+
if (!cdpPort) {
|
|
35
|
+
const id = webDappAdapterId();
|
|
36
|
+
throw new Error(`${id.charAt(0).toUpperCase()}${id.slice(1)} console capture requires the slot CDP port.`);
|
|
37
|
+
}
|
|
38
|
+
await ensureConsoleCollector({
|
|
39
|
+
cdpPort,
|
|
40
|
+
...webDappCaptureFiles(target),
|
|
41
|
+
waitForAttachment: false,
|
|
42
|
+
cwd: target
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
async function verifyWebDappConsoleControl(capture) {
|
|
46
|
+
let browser = null;
|
|
47
|
+
try {
|
|
48
|
+
const value = JSON.parse(fs.readFileSync(webDappRuntimeFile(capture.projectRoot, "browser.json"), "utf8"));
|
|
49
|
+
browser = typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
50
|
+
} catch {
|
|
51
|
+
browser = null;
|
|
52
|
+
}
|
|
53
|
+
if (typeof browser?.appOrigin !== "string") return { ok: false, detail: "no slot browser state (browser.json) to verify the page console capture against" };
|
|
54
|
+
return verifyCapture({ cdpPort: capture.cdpPort, log: capture.pageLog, target: "page", appOrigin: browser.appOrigin });
|
|
55
|
+
}
|
|
56
|
+
export {
|
|
57
|
+
WEB_DAPP_CONSOLE_FILES,
|
|
58
|
+
ensureWebDappConsoleCapture,
|
|
59
|
+
verifyWebDappConsoleControl,
|
|
60
|
+
webDappCaptureFiles,
|
|
61
|
+
webDappCdpPort,
|
|
62
|
+
webDappRuntimeFile
|
|
63
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { POLICY_ENV, webDappPolicy } from "../../../adapters/web-dapp/lib/policy.mjs";
|
|
2
|
+
import { webDappAppPort, webDappCdpPort } from "./surface.js";
|
|
3
|
+
async function webDappDoctorChecks(target) {
|
|
4
|
+
if (!process.env[POLICY_ENV]) {
|
|
5
|
+
return [{
|
|
6
|
+
id: "web-dapp-venue-policy",
|
|
7
|
+
status: "fail",
|
|
8
|
+
required: true,
|
|
9
|
+
message: "web-dapp needs a venue policy: select an adapter that extends it (e.g. --adapter terminal with its recipe library on RECIPE_LIBRARY_PATH)."
|
|
10
|
+
}];
|
|
11
|
+
}
|
|
12
|
+
const policy = webDappPolicy();
|
|
13
|
+
const { webDappReadiness } = await import("../../../adapters/web-dapp/lib/readiness.mjs");
|
|
14
|
+
const { resolveBrowser } = await import("../../../adapters/web-dapp/launch.mjs");
|
|
15
|
+
const report = await webDappReadiness({
|
|
16
|
+
policy,
|
|
17
|
+
target,
|
|
18
|
+
appPort: webDappAppPort(),
|
|
19
|
+
cdpPort: webDappCdpPort(),
|
|
20
|
+
signer: process.env.TERMINAL_SIGNER || "extension",
|
|
21
|
+
account: process.env.TERMINAL_ACCOUNT || "dev1",
|
|
22
|
+
probeBrowser: resolveBrowser
|
|
23
|
+
});
|
|
24
|
+
return report.checks.map((check) => ({
|
|
25
|
+
id: `${policy.adapterId}-${check.id}`,
|
|
26
|
+
status: check.status === "pass" ? "pass" : "fail",
|
|
27
|
+
required: check.required,
|
|
28
|
+
message: check.detail
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
31
|
+
export {
|
|
32
|
+
webDappDoctorChecks
|
|
33
|
+
};
|