@deeeed/metamask-harness 0.83.0 → 0.84.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/CHANGELOG.md +28 -0
- package/README.md +8 -1
- package/adapters/manifest.json +14 -110
- package/adapters/shared/session-lock.mjs +43 -0
- package/adapters/web-dapp/metamask-signer.mjs +212 -0
- package/dist/adapters/extension/run.js +4 -2
- package/dist/adapters/extension/runtime.js +71 -18
- package/dist/adapters/mobile/devices.js +5 -1
- package/dist/adapters/mobile/run.js +1 -1
- package/dist/adapters/mobile/surface.js +1 -2
- package/dist/adapters/plugin-surface.js +14 -42
- package/dist/adapters/session-lock.js +23 -0
- package/dist/adapters/web-dapp/console-capture.js +2 -20
- package/dist/adapters/web-dapp/run.js +25 -24
- package/dist/adapters/web-dapp/surface.js +32 -108
- package/dist/command-contract.js +1342 -643
- package/dist/commands/doctor.js +1 -1
- package/dist/commands/launch/device.js +1 -1
- package/dist/doctor.js +5 -2
- package/dist/mm-harness-cli.js +228 -1210
- package/docs/CONTRIBUTING.md +6 -4
- package/docs/cli-contract.md +1 -1
- package/docs/feature-guard.md +1 -1
- package/library/actions/shared/console-collector.mjs +1 -1
- package/library/actions/web-dapp/app/launch.mjs +1 -1
- package/library/actions/web-dapp/platform/adapter-web-lib.mjs +18 -0
- package/library/actions/web-dapp/platform/hud.mjs +1 -1
- package/library/actions/web-dapp/platform/page.mjs +2 -3
- package/library/actions/web-dapp/platform/runtime.mjs +29 -178
- package/package.json +7 -7
- package/scripts/completions.sh +1 -1
- package/adapters/web-dapp/cleanup.mjs +0 -69
- package/adapters/web-dapp/inject.mjs +0 -75
- package/adapters/web-dapp/launch.mjs +0 -631
- package/adapters/web-dapp/lib/blocked-hosts.mjs +0 -24
- package/adapters/web-dapp/lib/cdp-client.mjs +0 -84
- package/adapters/web-dapp/lib/display.mjs +0 -115
- package/adapters/web-dapp/lib/hud.mjs +0 -148
- package/adapters/web-dapp/lib/policy-fence.mjs +0 -119
- package/adapters/web-dapp/lib/policy.mjs +0 -84
- package/adapters/web-dapp/lib/processes.mjs +0 -187
- package/adapters/web-dapp/lib/readiness.mjs +0 -272
- package/adapters/web-dapp/stop.mjs +0 -38
- package/adapters/web-dapp/verify.mjs +0 -51
- package/adapters/web-dapp/wallet-host.mjs +0 -520
- package/dist/adapters/web-dapp/doctor-checks.js +0 -33
- package/dist/adapters/web-dapp/launch.js +0 -87
- package/dist/adapters/web-dapp/overlay.js +0 -21
- package/dist/adapters/web-dapp/readiness.js +0 -10
- package/dist/adapters/web-dapp/stop.js +0 -20
- package/dist/cli.js +0 -202
|
@@ -5,6 +5,7 @@ import { createRequire } from "node:module";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { importRecipeHarnessRuntimeBrowserExtension, importRecipeHarnessRuntimeCdp, resolveFarmslotPoolDir, runnerDir } from "../../paths.js";
|
|
7
7
|
import { poolFilesForHost } from "../resolve-slot-ports.js";
|
|
8
|
+
import { macosSessionLocked, SESSION_LOCKED_MESSAGE, SESSION_LOCKED_USER_ACTION } from "../../../adapters/shared/session-lock.mjs";
|
|
8
9
|
import { profileProcessPids } from "./validation-process-ownership.js";
|
|
9
10
|
const require2 = createRequire(import.meta.url);
|
|
10
11
|
const { RUNTIME_NONCE_PREFIX, detachedLaunchUnprovenPath, hasDetachedLaunchUnproven, shellQuote } = require2(
|
|
@@ -62,7 +63,7 @@ async function assertHealthyExtensionRuntime(options) {
|
|
|
62
63
|
seededLaunchDir: options.seededLaunchDir
|
|
63
64
|
});
|
|
64
65
|
if (lastReport.status === "PASS") return lastReport;
|
|
65
|
-
if (lastReport.errorCode === "UI_COMPOSITOR_SUSPENDED" || lastReport.errorCode === "EXTENSION_BACKGROUND_UNRESPONSIVE") {
|
|
66
|
+
if (lastReport.errorCode === "SESSION_LOCKED" || lastReport.errorCode === "UI_COMPOSITOR_SUSPENDED" || lastReport.errorCode === "EXTENSION_BACKGROUND_UNRESPONSIVE") {
|
|
66
67
|
throw new Error(formatHealthFailure(lastReport, options.projectRoot));
|
|
67
68
|
}
|
|
68
69
|
if (lastReport.errorCode === "FIXTURE_SEED_NOT_APPLIED") {
|
|
@@ -149,18 +150,17 @@ async function checkExtensionRuntimeHealth(projectRoot, cdpPort, options = {}) {
|
|
|
149
150
|
Math.min(1e3, cdpCallTimeoutMs)
|
|
150
151
|
);
|
|
151
152
|
if (compositor.status === "suspended") {
|
|
152
|
-
const
|
|
153
|
+
const suspended = `UI_COMPOSITOR_SUSPENDED: the Extension page is reachable over CDP but its render compositor is not advancing (${compositor.reason ?? "unknown reason"}).`;
|
|
154
|
+
const locked = macosSessionLocked() === true;
|
|
153
155
|
return {
|
|
154
156
|
status: "FAIL",
|
|
155
|
-
errorCode: "UI_COMPOSITOR_SUSPENDED",
|
|
156
|
-
userAction,
|
|
157
|
+
errorCode: locked ? "SESSION_LOCKED" : "UI_COMPOSITOR_SUSPENDED",
|
|
158
|
+
userAction: locked ? SESSION_LOCKED_USER_ACTION : compositorRecoveryAction(projectRoot),
|
|
157
159
|
cdpPort,
|
|
158
160
|
targetUrl: target.url,
|
|
159
161
|
extensionId: safeExtensionId(target),
|
|
160
162
|
extensionPageTargets: extensionTargets.length,
|
|
161
|
-
findings: [
|
|
162
|
-
`UI_COMPOSITOR_SUSPENDED: the Extension page is reachable over CDP but its render compositor is not advancing (${compositor.reason ?? "unknown reason"}).`
|
|
163
|
-
],
|
|
163
|
+
findings: locked ? [`SESSION_LOCKED: ${SESSION_LOCKED_MESSAGE}`, suspended] : [suspended],
|
|
164
164
|
details: {
|
|
165
165
|
projectRoot,
|
|
166
166
|
targetUrls: targets.map((entry) => entry.url).filter(Boolean),
|
|
@@ -669,7 +669,7 @@ async function launchLockedValidationBrowser(options) {
|
|
|
669
669
|
await stopOwnedCdpListener(options.cdpPort, [
|
|
670
670
|
validationRuntimePath,
|
|
671
671
|
path.join(options.projectRoot, recipeRuntimeDir())
|
|
672
|
-
]);
|
|
672
|
+
], path.join(options.projectRoot, recipeRuntimeDir()));
|
|
673
673
|
assertNoLiveValidationProfileOwners(validationProfileRoot);
|
|
674
674
|
pruneValidationRuns(validationProfileRoot, 0);
|
|
675
675
|
pruneValidationRuns(validationArtifactsRoot, 4);
|
|
@@ -933,16 +933,32 @@ function runProcess(command, args, options) {
|
|
|
933
933
|
});
|
|
934
934
|
});
|
|
935
935
|
}
|
|
936
|
-
async function stopOwnedCdpListener(cdpPort, runtimePaths) {
|
|
936
|
+
async function stopOwnedCdpListener(cdpPort, runtimePaths, checkoutRuntimeRoot) {
|
|
937
937
|
if (process.platform !== "darwin" && process.platform !== "linux") return;
|
|
938
938
|
let listenerPids = await inspectCdpListenerPids(cdpPort);
|
|
939
939
|
if (listenerPids.length === 0) return;
|
|
940
940
|
const identity = runtimePaths.map((runtimePath) => readValidationRuntimeIdentity(runtimePath, cdpPort)).find((candidate) => candidate?.pid === listenerPids[0]);
|
|
941
|
-
|
|
942
|
-
|
|
941
|
+
let pid;
|
|
942
|
+
if (identity && listenerPids.length === 1 && listenerPids[0] === identity.pid && await browserEchoesRuntimeNonce(cdpPort, identity.nonce)) {
|
|
943
|
+
pid = identity.pid;
|
|
944
|
+
}
|
|
945
|
+
if (pid === void 0) {
|
|
946
|
+
const listeners = await listenerCommands(listenerPids);
|
|
947
|
+
const listener = listenerPids.length === 1 ? listeners.get(listenerPids[0]) : void 0;
|
|
948
|
+
if (listener?.chrome && !listener.ambiguous && pathInsideReal(checkoutRuntimeRoot, listener.profile) && (await listenerCommands(listenerPids)).get(listenerPids[0])?.command === listener.command) {
|
|
949
|
+
pid = listenerPids[0];
|
|
950
|
+
} else {
|
|
951
|
+
const seen = listenerPids.map((candidate) => {
|
|
952
|
+
const profile = listeners.get(candidate)?.profile;
|
|
953
|
+
return profile ? `pid ${candidate} --user-data-dir=${profile}` : `pid ${candidate}`;
|
|
954
|
+
}).join("; ");
|
|
955
|
+
throw new Error(
|
|
956
|
+
`Extension validation will not stop an unowned CDP listener on port ${cdpPort} (${seen}). Next: if that is this slot's browser, stop it (kill ${listenerPids.join(" ")}); otherwise free port ${cdpPort}. Then rerun.`
|
|
957
|
+
);
|
|
958
|
+
}
|
|
943
959
|
}
|
|
944
960
|
try {
|
|
945
|
-
process.kill(
|
|
961
|
+
process.kill(pid, "SIGTERM");
|
|
946
962
|
} catch (error) {
|
|
947
963
|
if (error.code !== "ESRCH") throw error;
|
|
948
964
|
}
|
|
@@ -950,10 +966,10 @@ async function stopOwnedCdpListener(cdpPort, runtimePaths) {
|
|
|
950
966
|
do {
|
|
951
967
|
await sleep(50);
|
|
952
968
|
listenerPids = await inspectCdpListenerPids(cdpPort);
|
|
953
|
-
} while ((listenerPids.length > 0 || processExists(
|
|
954
|
-
if (listenerPids.length > 0 || processExists(
|
|
969
|
+
} while ((listenerPids.length > 0 || processExists(pid)) && Date.now() < termDeadline);
|
|
970
|
+
if (listenerPids.length > 0 || processExists(pid)) {
|
|
955
971
|
try {
|
|
956
|
-
process.kill(
|
|
972
|
+
process.kill(pid, "SIGKILL");
|
|
957
973
|
} catch (error) {
|
|
958
974
|
if (error.code !== "ESRCH") throw error;
|
|
959
975
|
}
|
|
@@ -961,12 +977,48 @@ async function stopOwnedCdpListener(cdpPort, runtimePaths) {
|
|
|
961
977
|
do {
|
|
962
978
|
await sleep(50);
|
|
963
979
|
listenerPids = await inspectCdpListenerPids(cdpPort);
|
|
964
|
-
} while ((listenerPids.length > 0 || processExists(
|
|
980
|
+
} while ((listenerPids.length > 0 || processExists(pid)) && Date.now() < killDeadline);
|
|
965
981
|
}
|
|
966
|
-
if (listenerPids.length > 0 || processExists(
|
|
982
|
+
if (listenerPids.length > 0 || processExists(pid)) {
|
|
967
983
|
throw new Error(`Extension validation could not stop its CDP listener on port ${cdpPort}.`);
|
|
968
984
|
}
|
|
969
985
|
}
|
|
986
|
+
async function listenerCommands(pids) {
|
|
987
|
+
const listeners = /* @__PURE__ */ new Map();
|
|
988
|
+
const result = await runProcess("ps", ["-ww", "-o", "pid=,command=", "-p", pids.join(",")], {
|
|
989
|
+
cwd: runnerDir,
|
|
990
|
+
env: process.env,
|
|
991
|
+
timeoutMs: 5e3
|
|
992
|
+
});
|
|
993
|
+
if (result.timedOut) return listeners;
|
|
994
|
+
for (const line of result.stdout.split("\n")) {
|
|
995
|
+
const match = line.match(/^\s*(\d+)\s+(.*)$/u);
|
|
996
|
+
if (!match) continue;
|
|
997
|
+
const command = match[2];
|
|
998
|
+
const flags = [...command.matchAll(/(?:^|\s)--user-data-dir=/gu)];
|
|
999
|
+
const last = flags.at(-1);
|
|
1000
|
+
if (!last) continue;
|
|
1001
|
+
const value = command.slice(last.index + last[0].length);
|
|
1002
|
+
const ends = [...value.matchAll(/\s--/gu)].map((end) => end.index);
|
|
1003
|
+
const profile = value.slice(0, ends[0] ?? value.length);
|
|
1004
|
+
const longerReadings = [...ends.slice(1), value.length].map((end) => value.slice(0, end));
|
|
1005
|
+
listeners.set(Number(match[1]), {
|
|
1006
|
+
command,
|
|
1007
|
+
profile,
|
|
1008
|
+
chrome: /chrom/iu.test(path.basename(command.split(/\s-/u)[0])),
|
|
1009
|
+
ambiguous: flags.length > 1 || !path.isAbsolute(profile) || ends.length > 0 && longerReadings.some((reading) => fs.existsSync(reading))
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
return listeners;
|
|
1013
|
+
}
|
|
1014
|
+
function pathInsideReal(root, candidate) {
|
|
1015
|
+
try {
|
|
1016
|
+
const relative = path.relative(fs.realpathSync(root), fs.realpathSync(candidate));
|
|
1017
|
+
return Boolean(relative) && relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
1018
|
+
} catch {
|
|
1019
|
+
return false;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
970
1022
|
async function inspectCdpListenerPids(cdpPort) {
|
|
971
1023
|
const result = await runProcess("lsof", ["-ti", `tcp:${cdpPort}`, "-sTCP:LISTEN"], {
|
|
972
1024
|
cwd: runnerDir,
|
|
@@ -1040,5 +1092,6 @@ export {
|
|
|
1040
1092
|
extensionBackgroundProbeTimeoutMs,
|
|
1041
1093
|
formatHealthFailure,
|
|
1042
1094
|
prepareExtensionRuntime,
|
|
1043
|
-
seededLaunchDir
|
|
1095
|
+
seededLaunchDir,
|
|
1096
|
+
stopOwnedCdpListener
|
|
1044
1097
|
};
|
|
@@ -9,7 +9,11 @@ import {
|
|
|
9
9
|
} from "../../commands/mobile-device-view.js";
|
|
10
10
|
const mobileDevices = {
|
|
11
11
|
view: mobileDeviceView,
|
|
12
|
-
live
|
|
12
|
+
// adapter-sdk's live view names the app's feature-flag overrides `featureFlags`.
|
|
13
|
+
async live(target, view) {
|
|
14
|
+
const live = await mobileDeviceLiveView(target, view);
|
|
15
|
+
return { ...live, featureFlags: live.featureFlagOverrides };
|
|
16
|
+
},
|
|
13
17
|
renderList: renderMobileDeviceList,
|
|
14
18
|
renderAdditional: renderAdditionalReachableDevices,
|
|
15
19
|
renderLive: renderMobileLiveBlock,
|
|
@@ -82,7 +82,7 @@ async function ensureMobileProofRuntime(target, _heal, state, json, deps = {}) {
|
|
|
82
82
|
}
|
|
83
83
|
if (initial.status === "current") return null;
|
|
84
84
|
const targetArg = JSON.stringify(path.resolve(target));
|
|
85
|
-
const userAction = `mm-harness call app.lifecycle
|
|
85
|
+
const userAction = `mm-harness call app.lifecycle command=restart --adapter mobile --target ${targetArg}`;
|
|
86
86
|
const message = `Mobile loaded source is not current (${initial.status}).`;
|
|
87
87
|
emitProofFailure(json, {
|
|
88
88
|
recoverable: true,
|
|
@@ -53,9 +53,8 @@ const mobileSurface = defineAdapter({
|
|
|
53
53
|
devServer: {
|
|
54
54
|
label: "metro",
|
|
55
55
|
describe: () => "Metro dev server",
|
|
56
|
-
// Metro's leaves read the port as METRO_PORT
|
|
56
|
+
// Metro's leaves read the port as METRO_PORT.
|
|
57
57
|
portEnv: ["METRO_PORT"],
|
|
58
|
-
portFlags: ["metroPort"],
|
|
59
58
|
stop(target) {
|
|
60
59
|
const leaf = path.join(runnerDir, "adapters", "mobile", "stop-metro.sh");
|
|
61
60
|
const args = ["--target", target];
|
|
@@ -1,16 +1,9 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
adapterSelectionFailureOut,
|
|
6
|
-
ensureAdapterLoaded,
|
|
7
|
-
selectedAdapterId
|
|
8
|
-
} from "@farmslot/recipe-cli/harness";
|
|
3
|
+
import { fencePolicy, POLICY_DIGEST_ENV, POLICY_ENV, PolicyFenceError } from "@farmslot/adapter-web/web-dapp";
|
|
4
|
+
import { adapterPlugin, AdapterPluginError } from "@farmslot/recipe-cli/harness";
|
|
9
5
|
import { libraryAdapterFiles } from "@farmslot/recipe-runner";
|
|
10
|
-
import { POLICY_ENV } from "../../adapters/web-dapp/lib/policy.mjs";
|
|
11
|
-
import { POLICY_DIGEST_ENV, PolicyFenceError, fencePolicy } from "../../adapters/web-dapp/lib/policy-fence.mjs";
|
|
12
6
|
import { configPath, declaredLibraryPathEnv, readConfig } from "../review/knowledge.js";
|
|
13
|
-
import { adapterRegistry } from "./registry.js";
|
|
14
7
|
import { bindWebDappPolicy } from "./web-dapp/surface.js";
|
|
15
8
|
function configuredLibrarySources() {
|
|
16
9
|
let libraries;
|
|
@@ -29,38 +22,6 @@ function operatorLibraryEnv() {
|
|
|
29
22
|
else env.RECIPE_LIBRARY_PATH = declared.value;
|
|
30
23
|
return env;
|
|
31
24
|
}
|
|
32
|
-
async function loadSelectedAdapter(command, args) {
|
|
33
|
-
const passthrough = args.indexOf("--");
|
|
34
|
-
const own = passthrough < 0 ? args : args.slice(0, passthrough);
|
|
35
|
-
const libraries = [];
|
|
36
|
-
own.forEach((arg, index) => {
|
|
37
|
-
if (arg === "--library" && own[index + 1] !== void 0) libraries.push(own[index + 1]);
|
|
38
|
-
else if (arg.startsWith("--library=")) libraries.push(arg.slice("--library=".length));
|
|
39
|
-
});
|
|
40
|
-
const selected = selectedAdapterId(args);
|
|
41
|
-
const builtin = selected !== void 0 && adapterRegistry.has(selected);
|
|
42
|
-
try {
|
|
43
|
-
await ensureAdapterLoaded(selected, {
|
|
44
|
-
libraries,
|
|
45
|
-
configured: configuredLibrarySources(),
|
|
46
|
-
env: operatorLibraryEnv(),
|
|
47
|
-
adopt: adoptPluginAdapter
|
|
48
|
-
});
|
|
49
|
-
await fenceBoundPolicy();
|
|
50
|
-
return void 0;
|
|
51
|
-
} catch (error) {
|
|
52
|
-
const refusal = error;
|
|
53
|
-
if (typeof refusal.code !== "string" || typeof refusal.userAction !== "string") throw error;
|
|
54
|
-
if (builtin && refusal.code !== "ADAPTER_ID_CONFLICT") {
|
|
55
|
-
return void 0;
|
|
56
|
-
}
|
|
57
|
-
return adapterSelectionFailureOut(own.includes("--json"), command, {
|
|
58
|
-
code: refusal.code,
|
|
59
|
-
message: String(refusal.message),
|
|
60
|
-
userAction: refusal.userAction
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
25
|
const ADAPTER_PLUGIN_ENV = "METAMASK_RECIPE_ADAPTER_PLUGIN";
|
|
65
26
|
let boundPolicy;
|
|
66
27
|
async function fenceBoundPolicy() {
|
|
@@ -80,6 +41,16 @@ async function fenceBoundPolicy() {
|
|
|
80
41
|
throw error;
|
|
81
42
|
}
|
|
82
43
|
}
|
|
44
|
+
async function afterAdapterLoad() {
|
|
45
|
+
try {
|
|
46
|
+
await fenceBoundPolicy();
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (error instanceof PolicyFenceError) {
|
|
49
|
+
throw new AdapterPluginError("ADAPTER_PLUGIN_INVALID", error.message, error.userAction);
|
|
50
|
+
}
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
83
54
|
function adoptPluginAdapter(adapter) {
|
|
84
55
|
const surface = bindWebDappPolicy(adapter);
|
|
85
56
|
const policy = adapter;
|
|
@@ -120,7 +91,8 @@ function adoptPluginAdapter(adapter) {
|
|
|
120
91
|
export {
|
|
121
92
|
ADAPTER_PLUGIN_ENV,
|
|
122
93
|
adoptPluginAdapter,
|
|
94
|
+
afterAdapterLoad,
|
|
123
95
|
configuredLibrarySources,
|
|
124
|
-
|
|
96
|
+
fenceBoundPolicy,
|
|
125
97
|
operatorLibraryEnv
|
|
126
98
|
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Writable } from "node:stream";
|
|
2
|
+
import { EXIT, optionFlag } from "@farmslot/recipe-cli/harness";
|
|
3
|
+
import {
|
|
4
|
+
macosSessionLocked,
|
|
5
|
+
SESSION_LOCKED_MESSAGE,
|
|
6
|
+
SESSION_LOCKED_USER_ACTION
|
|
7
|
+
} from "../../adapters/shared/session-lock.mjs";
|
|
8
|
+
import { emitProofFailure } from "./proof-failure.js";
|
|
9
|
+
function refuseLockedSession(json, options = {}) {
|
|
10
|
+
if (macosSessionLocked() !== true) return null;
|
|
11
|
+
const error = { code: "SESSION_LOCKED", message: SESSION_LOCKED_MESSAGE, userAction: SESSION_LOCKED_USER_ACTION };
|
|
12
|
+
if (optionFlag(options, "jsonStream")) emitRunStreamError(error);
|
|
13
|
+
emitProofFailure(json, { recoverable: false, error });
|
|
14
|
+
return EXIT.infra;
|
|
15
|
+
}
|
|
16
|
+
function emitRunStreamError(error) {
|
|
17
|
+
const line = JSON.stringify({ schemaVersion: 1, command: "run", event: "error", error, ts: (/* @__PURE__ */ new Date()).toISOString() });
|
|
18
|
+
Writable.prototype.write.call(process.stdout, `${line}
|
|
19
|
+
`);
|
|
20
|
+
}
|
|
21
|
+
export {
|
|
22
|
+
refuseLockedSession
|
|
23
|
+
};
|
|
@@ -1,23 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
-
import
|
|
3
|
-
import { recipeRuntimePath } from "@farmslot/recipe-cli/harness";
|
|
2
|
+
import { WEB_DAPP_CONSOLE_FILES, webDappAdapterId, webDappRuntimeFile } from "@farmslot/adapter-web/web-dapp";
|
|
4
3
|
import { verifyCapture } from "../../../library/actions/shared/console-collector.mjs";
|
|
5
4
|
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
5
|
function webDappCaptureFiles(target) {
|
|
22
6
|
return {
|
|
23
7
|
extensionLog: webDappRuntimeFile(target, WEB_DAPP_CONSOLE_FILES.extension),
|
|
@@ -54,10 +38,8 @@ async function verifyWebDappConsoleControl(capture) {
|
|
|
54
38
|
return verifyCapture({ cdpPort: capture.cdpPort, log: capture.pageLog, target: "page", appOrigin: browser.appOrigin });
|
|
55
39
|
}
|
|
56
40
|
export {
|
|
57
|
-
WEB_DAPP_CONSOLE_FILES,
|
|
58
41
|
ensureWebDappConsoleCapture,
|
|
59
42
|
verifyWebDappConsoleControl,
|
|
60
43
|
webDappCaptureFiles,
|
|
61
|
-
webDappCdpPort
|
|
62
|
-
webDappRuntimeFile
|
|
44
|
+
webDappCdpPort
|
|
63
45
|
};
|
|
@@ -1,44 +1,43 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
1
2
|
import path from "node:path";
|
|
3
|
+
import { ownsProcess, webDappRuntimeFile } from "@farmslot/adapter-web/web-dapp";
|
|
2
4
|
import { isRecord } from "@farmslot/recipe-cli/harness";
|
|
3
5
|
import { verifyOwnedCollectorControl } from "../console-capture.js";
|
|
6
|
+
import { refuseLockedSession } from "../session-lock.js";
|
|
4
7
|
import { devicePlatformFromEnv } from "../performance/device-platform.js";
|
|
5
8
|
import {
|
|
6
9
|
ensureWebDappConsoleCapture,
|
|
7
10
|
webDappCaptureFiles,
|
|
8
11
|
webDappCdpPort,
|
|
9
|
-
webDappRuntimeFile,
|
|
10
12
|
verifyWebDappConsoleControl
|
|
11
13
|
} from "./console-capture.js";
|
|
14
|
+
async function webDappRunHeadless(target) {
|
|
15
|
+
const stateFile = webDappRuntimeFile(target, "browser.json");
|
|
16
|
+
try {
|
|
17
|
+
const state = JSON.parse(fs.readFileSync(stateFile, "utf8"));
|
|
18
|
+
if (isRecord(state) && isRecord(state.browser) && typeof state.browser.headless === "boolean") {
|
|
19
|
+
if (ownsProcess("browser", state.browserPid, path.dirname(stateFile))) return state.browser.headless;
|
|
20
|
+
}
|
|
21
|
+
} catch {
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const { resolveHeadless } = await import("../../../library/actions/web-dapp/platform/adapter-web-lib.mjs");
|
|
25
|
+
return resolveHeadless();
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const webDappRun = {
|
|
31
|
+
runtimeCheck: ({ target, options, json }) => async () => await webDappRunHeadless(target) === false ? refuseLockedSession(json, options) : null
|
|
32
|
+
};
|
|
12
33
|
const webDappDiagnostics = {
|
|
13
34
|
console: {
|
|
14
35
|
start: (projectRoot) => ensureWebDappConsoleCapture(projectRoot),
|
|
15
36
|
files: (projectRoot) => webDappCaptureFiles(projectRoot),
|
|
16
37
|
cdpPort: () => webDappCdpPort(),
|
|
17
38
|
verifyControl: (capture) => verifyOwnedCollectorControl(capture, () => verifyWebDappConsoleControl(capture))
|
|
18
|
-
},
|
|
19
|
-
requestLog: {
|
|
20
|
-
path: (projectRoot) => webDappRuntimeFile(projectRoot, "wallet-requests.jsonl"),
|
|
21
|
-
findings: (lines) => walletRequestFindings(lines)
|
|
22
39
|
}
|
|
23
40
|
};
|
|
24
|
-
function walletRequestFindings(lines) {
|
|
25
|
-
const findings = [];
|
|
26
|
-
for (const line of lines) {
|
|
27
|
-
let entry;
|
|
28
|
-
try {
|
|
29
|
-
entry = JSON.parse(line);
|
|
30
|
-
} catch {
|
|
31
|
-
continue;
|
|
32
|
-
}
|
|
33
|
-
if (!isRecord(entry)) continue;
|
|
34
|
-
let text = null;
|
|
35
|
-
if (entry.kind === "blocked-mainnet" && entry.probe !== true) text = `blocked-mainnet: ${String(entry.transport ?? "request")} to ${String(entry.url ?? "?")}`;
|
|
36
|
-
else if (entry.kind === "refused-mainnet") text = `refused-mainnet: ${String(entry.method ?? "?")} (${String(entry.reason ?? "?")})`;
|
|
37
|
-
else if (entry.kind === "unattributed") text = `unattributed wallet ${entry.binding === "request" ? "request" : "log entry"}${typeof entry.method === "string" ? ` (${entry.method})` : ""}`;
|
|
38
|
-
if (text) findings.push({ level: "error", source: "wallet", text });
|
|
39
|
-
}
|
|
40
|
-
return findings;
|
|
41
|
-
}
|
|
42
41
|
const webDappObservation = {
|
|
43
42
|
network: {
|
|
44
43
|
backend: async (target, env, artifactsDir) => (await import("../extension/network-observer.js")).createLoadedExtensionNetworkBackend(
|
|
@@ -59,5 +58,7 @@ const webDappObservation = {
|
|
|
59
58
|
};
|
|
60
59
|
export {
|
|
61
60
|
webDappDiagnostics,
|
|
62
|
-
webDappObservation
|
|
61
|
+
webDappObservation,
|
|
62
|
+
webDappRun,
|
|
63
|
+
webDappRunHeadless
|
|
63
64
|
};
|
|
@@ -1,82 +1,38 @@
|
|
|
1
|
-
import { shellQuote } from "@farmslot/recipe-cli/harness";
|
|
2
1
|
import path from "node:path";
|
|
3
2
|
import { defineAdapter } from "@farmslot/adapter-sdk";
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
import {
|
|
4
|
+
bindWebDappPolicy as bindPolicy,
|
|
5
|
+
createWebDappAdapter,
|
|
6
|
+
POLICY_ENV,
|
|
7
|
+
SIGNER_MODULE_ENV,
|
|
8
|
+
webDappAdapterId,
|
|
9
|
+
webDappPolicy
|
|
10
|
+
} from "@farmslot/adapter-web/web-dapp";
|
|
11
|
+
import { runnerDir } from "../../paths.js";
|
|
8
12
|
import { adapterRegistry } from "../registry.js";
|
|
9
13
|
import { webDappActions } from "./actions.js";
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
import { webDappDiagnostics, webDappObservation } from "./run.js";
|
|
13
|
-
function portFromEnv(...names) {
|
|
14
|
-
for (const name of names) {
|
|
15
|
-
const raw = process.env[name];
|
|
16
|
-
if (raw && /^\d+$/u.test(raw)) return Number(raw);
|
|
17
|
-
}
|
|
18
|
-
return void 0;
|
|
19
|
-
}
|
|
20
|
-
function webDappAppPort() {
|
|
21
|
-
return portFromEnv("TERMINAL_APP_PORT", "RECIPE_WATCHER_PORT", "WATCHER_PORT");
|
|
22
|
-
}
|
|
23
|
-
function webDappCdpPort() {
|
|
24
|
-
return portFromEnv("RECIPE_CDP_PORT", "CDP_PORT");
|
|
25
|
-
}
|
|
26
|
-
function webDappSignerFor(target) {
|
|
27
|
-
try {
|
|
28
|
-
const state = JSON.parse(fs.readFileSync(webDappRuntimeFile(target, "browser.json"), "utf8"));
|
|
29
|
-
if (typeof state.signer === "string") return state.signer;
|
|
30
|
-
} catch {
|
|
31
|
-
}
|
|
32
|
-
return process.env.TERMINAL_SIGNER || "extension";
|
|
33
|
-
}
|
|
14
|
+
import { webDappDiagnostics, webDappObservation, webDappRun } from "./run.js";
|
|
15
|
+
const METAMASK_SIGNER_MODULE = path.join(runnerDir, "adapters", "web-dapp", "metamask-signer.mjs");
|
|
34
16
|
function bindWebDappPolicy(adapter) {
|
|
17
|
+
bindPolicy(adapter);
|
|
35
18
|
if (adapter.extends === "web-dapp" && typeof adapter.policy?.module === "string") {
|
|
36
|
-
process.env[
|
|
19
|
+
process.env[SIGNER_MODULE_ENV] = METAMASK_SIGNER_MODULE;
|
|
37
20
|
}
|
|
38
21
|
return adapter;
|
|
39
22
|
}
|
|
40
|
-
const
|
|
23
|
+
const generic = createWebDappAdapter({
|
|
41
24
|
id: "web-dapp",
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
deps: "missing",
|
|
54
|
-
devServer: { label: "next", status: "down" }
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
const policy = webDappPolicy();
|
|
58
|
-
const { webDappReadiness: webDappReadiness2 } = await import("../../../adapters/web-dapp/lib/readiness.mjs");
|
|
59
|
-
const report = await webDappReadiness2({
|
|
60
|
-
policy,
|
|
61
|
-
target: path.resolve(target),
|
|
62
|
-
appPort: webDappAppPort(),
|
|
63
|
-
cdpPort: webDappCdpPort(),
|
|
64
|
-
signer: process.env.TERMINAL_SIGNER || "extension"
|
|
65
|
-
});
|
|
66
|
-
const blocking = report.checks.filter((check) => check.required && check.status === "fail");
|
|
67
|
-
const devServer = report.checks.find((check) => check.id === "app-dev-server");
|
|
68
|
-
return {
|
|
69
|
-
decision: blocking.length === 0 ? "ready" : "blocked",
|
|
70
|
-
reasonCode: blocking[0]?.id,
|
|
71
|
-
reasons: (blocking.length ? blocking : report.checks).map((check) => `${check.id}: ${check.detail}`),
|
|
72
|
-
nextAction: blocking.length === 0 ? void 0 : blocking[0].id === "app-dev-server" || blocking[0].id === "testnet-forced" ? "start the slot dev server with testnet forced (Farmslot preflight), then retry" : `mm-harness doctor --adapter ${policy.adapterId} --target ${shellQuote(path.resolve(target))} --json`,
|
|
73
|
-
deps: report.checks.find((check) => check.id === "dependencies")?.status === "pass" ? "current" : "missing",
|
|
74
|
-
devServer: {
|
|
75
|
-
label: "next",
|
|
76
|
-
status: devServer?.status === "pass" ? "up" : "down"
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
},
|
|
25
|
+
signerModule: METAMASK_SIGNER_MODULE,
|
|
26
|
+
cli: "mm-harness",
|
|
27
|
+
hooks: {
|
|
28
|
+
actions: webDappActions,
|
|
29
|
+
run: webDappRun,
|
|
30
|
+
diagnostics: webDappDiagnostics,
|
|
31
|
+
observation: webDappObservation
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
const webDappSurface = defineAdapter({
|
|
35
|
+
...generic,
|
|
80
36
|
runwayProvision: {
|
|
81
37
|
async run(target, options) {
|
|
82
38
|
return {
|
|
@@ -94,41 +50,12 @@ const webDappSurface = defineAdapter({
|
|
|
94
50
|
};
|
|
95
51
|
}
|
|
96
52
|
},
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
return { kind: "stopped", status: result.status, summary: result.summary, output: result.output };
|
|
103
|
-
}
|
|
104
|
-
},
|
|
105
|
-
logSources(target) {
|
|
106
|
-
return [
|
|
107
|
-
{ label: "app-console", path: webDappRuntimeFile(target, WEB_DAPP_CONSOLE_FILES.app) },
|
|
108
|
-
...webDappSignerFor(target) === "extension" ? [{ label: "extension-console", path: webDappRuntimeFile(target, WEB_DAPP_CONSOLE_FILES.extension) }] : [],
|
|
109
|
-
{ label: "wallet-requests", path: webDappRuntimeFile(target, "wallet-requests.jsonl") },
|
|
110
|
-
{ label: "wallet-host", path: webDappRuntimeFile(target, "wallet-host.log") },
|
|
111
|
-
{ label: "browser", path: webDappRuntimeFile(target, "browser.log") },
|
|
112
|
-
{ label: "next", path: path.join(target, "temp/farmslot/next-dev.log") }
|
|
113
|
-
];
|
|
114
|
-
},
|
|
115
|
-
// The app page's console and uncaught exceptions, as the Extension's
|
|
116
|
-
// diagnostics read the extension console: errors make a run REVIEW.
|
|
117
|
-
appLogSource(target) {
|
|
118
|
-
return { label: "app-console", path: webDappRuntimeFile(target, WEB_DAPP_CONSOLE_FILES.app) };
|
|
119
|
-
},
|
|
120
|
-
flags: { launch: ["freshProfile", "headless", "headful"] },
|
|
121
|
-
hints: {
|
|
122
|
-
// Bare web-dapp has no venue: every hint names the adapter that extends it.
|
|
123
|
-
launch: "pass --adapter <the adapter that extends web-dapp>, e.g. --adapter terminal, with its recipe library on RECIPE_LIBRARY_PATH",
|
|
124
|
-
relaunch: "pass --adapter <the adapter that extends web-dapp>, e.g. --adapter terminal, with --fresh-profile",
|
|
125
|
-
runtimeProbeRecovery: () => "pass --adapter <the adapter that extends web-dapp>, e.g. --adapter terminal, with its recipe library on RECIPE_LIBRARY_PATH"
|
|
53
|
+
// adapter-web's slot readiness (with MetaMask's own checks) and capture providers.
|
|
54
|
+
readiness: {
|
|
55
|
+
liveChecks: generic.readiness.liveChecks,
|
|
56
|
+
captureProviders: generic.readiness.captureProviders,
|
|
57
|
+
compatibilityMode: (target) => process.env[POLICY_ENV] && webDappPolicy().checkout.matches(target) ? "slot browser over CDP (website; no product bridge)" : "unsupported/no bridge"
|
|
126
58
|
},
|
|
127
|
-
launch: async (context) => (await import("./launch.js")).launchWebDapp(context),
|
|
128
|
-
actions: webDappActions,
|
|
129
|
-
harness: webDappHarness,
|
|
130
|
-
runtimeContext: webDappRuntimeContext,
|
|
131
|
-
readiness: webDappReadiness,
|
|
132
59
|
fixtures: { operations: ["init", "sync", "set"] },
|
|
133
60
|
debug: {
|
|
134
61
|
args: ({ devMenu }) => [
|
|
@@ -136,14 +63,11 @@ const webDappSurface = defineAdapter({
|
|
|
136
63
|
"--action",
|
|
137
64
|
devMenu ? "dev-menu" : "debug"
|
|
138
65
|
]
|
|
139
|
-
}
|
|
140
|
-
diagnostics: webDappDiagnostics,
|
|
141
|
-
observation: webDappObservation
|
|
66
|
+
}
|
|
142
67
|
});
|
|
143
68
|
adapterRegistry.register(webDappSurface);
|
|
144
69
|
export {
|
|
70
|
+
METAMASK_SIGNER_MODULE,
|
|
145
71
|
bindWebDappPolicy,
|
|
146
|
-
webDappAppPort,
|
|
147
|
-
webDappCdpPort,
|
|
148
72
|
webDappSurface
|
|
149
73
|
};
|