@deeeed/metamask-harness 0.35.0 → 0.37.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 (73) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +9 -1
  3. package/adapters/extension/artifact-runtime-state.cjs +128 -0
  4. package/adapters/extension/check-infura-readiness.cjs +102 -0
  5. package/adapters/extension/inject.mjs +2 -0
  6. package/adapters/extension/launch-browser.cjs +22 -0
  7. package/adapters/extension/live.sh +103 -18
  8. package/adapters/extension/readiness.mjs +77 -36
  9. package/adapters/extension/snapshot-dist.sh +88 -3
  10. package/adapters/extension/start-watch.sh +15 -0
  11. package/adapters/extension/verify.sh +6 -2
  12. package/adapters/extension/wallet-fixture-state.cjs +3 -1
  13. package/adapters/manifest.json +25 -1
  14. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +73 -42
  15. package/adapters/mobile/reset-app-data.sh +154 -0
  16. package/adapters/shared/log-tui.mjs +1 -1
  17. package/dist/adapters/extension/artifact-integrity.js +38 -0
  18. package/dist/adapters/extension/extension-id.js +23 -4
  19. package/dist/adapters/extension/product-config.js +29 -1
  20. package/dist/adapters/extension/release-artifact.js +386 -0
  21. package/dist/adapters/extension/runtime-decision.js +161 -20
  22. package/dist/adapters/extension/runtime.js +127 -0
  23. package/dist/adapters/mobile/release-artifact-state.js +124 -0
  24. package/dist/adapters/mobile/release-artifact.js +295 -0
  25. package/dist/adapters.js +29 -4
  26. package/dist/cli-commands.js +1 -1
  27. package/dist/cli.js +2 -2
  28. package/dist/command-contract.js +17 -1
  29. package/dist/commands/call.js +2 -1
  30. package/dist/commands/device-target.js +5 -0
  31. package/dist/commands/fixtures.js +106 -31
  32. package/dist/commands/launch/extension.js +92 -7
  33. package/dist/commands/launch/index.js +13 -0
  34. package/dist/commands/launch/mobile.js +2 -0
  35. package/dist/commands/provision.js +2 -0
  36. package/dist/commands/run-engine.js +89 -5
  37. package/dist/commands/run.js +3 -1
  38. package/dist/commands/runtime-launch.js +178 -10
  39. package/dist/heal-bounds.js +1 -1
  40. package/dist/live-adapter-contract.js +3 -1
  41. package/dist/metamask-action-validation.js +47 -1
  42. package/dist/mm-harness-cli.js +37 -4
  43. package/dist/recipe-security.js +3 -0
  44. package/dist/run-diagnostics.js +1 -1
  45. package/docs/RECIPES.md +29 -0
  46. package/docs/RELEASE-QA-CAPABILITY-MAP.md +150 -0
  47. package/library/actions/extension/perps/perps.mjs +2 -0
  48. package/library/actions/extension/perps/read_snapshot.mjs +470 -0
  49. package/library/actions/extension/platform/cdp.mjs +6 -3
  50. package/library/actions/extension/wallet/import.mjs +201 -0
  51. package/library/actions/extension/wallet/reset.mjs +98 -0
  52. package/library/actions/extension/wallet/secret-input.mjs +98 -0
  53. package/library/actions/extension/wallet/state.mjs +1 -0
  54. package/library/actions/mobile/analytics/consent-settings.mjs +112 -0
  55. package/library/actions/mobile/analytics/set_consent.mjs +4 -112
  56. package/library/actions/mobile/platform/bridge.mjs +8 -0
  57. package/library/actions/mobile/platform/observe-ui.mjs +84 -2
  58. package/library/actions/mobile/ui/native-navigation.mjs +225 -0
  59. package/library/actions/mobile/ui/navigate.mjs +7 -0
  60. package/library/actions/mobile/wallet/import.mjs +328 -0
  61. package/library/actions/mobile/wallet/native-ui.mjs +493 -0
  62. package/library/actions/mobile/wallet/read_state.mjs +16 -0
  63. package/library/actions/mobile/wallet/reset-helper.mjs +99 -0
  64. package/library/actions/mobile/wallet/reset.mjs +20 -0
  65. package/library/actions/shared/ui/locators.mjs +7 -0
  66. package/library/actions/shared/wallet/import-source.mjs +101 -0
  67. package/library/manifests/extension.action-manifest.json +228 -0
  68. package/library/manifests/mobile.action-manifest.json +124 -0
  69. package/library/recipes/extension/runner/action-validation.recipe.json +12 -1
  70. package/library/recipes/wallet/import.recipe.json +102 -0
  71. package/library/recipes/wallet/reset-import.recipe.json +107 -0
  72. package/package.json +1 -1
  73. package/scripts/completions.sh +2 -2
@@ -147,6 +147,12 @@ async function checkExtensionRuntimeHealth(projectRoot, cdpPort, options = {}) {
147
147
  };
148
148
  }
149
149
  const runtime = await evaluateHealth(session, cdpCallTimeoutMs);
150
+ if (runtime.hasSubmitRequest !== true) {
151
+ const providerProbe = await probeUiEthereumProvider(session, cdpCallTimeoutMs);
152
+ runtime.evmRpcProbeOk = providerProbe.ok;
153
+ runtime.evmRpcProbeError = providerProbe.error;
154
+ runtime.evmRpcProbeSource = providerProbe.source;
155
+ }
150
156
  if (runtime.href && !String(runtime.href).startsWith("chrome-extension://")) {
151
157
  findings.push(`Extension page href is not an extension URL: ${runtime.href}`);
152
158
  }
@@ -316,6 +322,127 @@ async function evaluateHealth(session, timeoutMs) {
316
322
  }
317
323
  return result.result?.value ?? {};
318
324
  }
325
+ async function probeUiEthereumProvider(session, timeoutMs) {
326
+ const bridgeKey = "mm-harness.runtime.evm-provider-probe";
327
+ const preload = await boundedCdpCall(
328
+ session,
329
+ "Page.addScriptToEvaluateOnNewDocument",
330
+ {
331
+ source: `(() => {
332
+ const bridgeKey = Symbol.for(${JSON.stringify(bridgeKey)});
333
+ let provider;
334
+ Object.defineProperty(globalThis, 'ethereumProvider', {
335
+ configurable: true,
336
+ get() { return provider; },
337
+ set(value) { provider = value; },
338
+ });
339
+ Object.defineProperty(globalThis, bridgeKey, {
340
+ configurable: true,
341
+ value: {
342
+ async probe() {
343
+ if (!provider || typeof provider.request !== 'function') {
344
+ return { ok: false, error: 'Ethereum provider is unavailable' };
345
+ }
346
+ try {
347
+ const code = await provider.request({
348
+ method: 'eth_getCode',
349
+ params: ['0x0000000000000000000000000000000000000000', 'latest'],
350
+ });
351
+ const ok = typeof code === 'string' && /^0x[0-9a-f]*$/iu.test(code);
352
+ return {
353
+ ok,
354
+ error: ok ? null : 'Ethereum provider returned invalid contract code',
355
+ };
356
+ } catch (error) {
357
+ return { ok: false, error: String(error?.message || error) };
358
+ }
359
+ },
360
+ cleanup() {
361
+ const descriptor = Object.getOwnPropertyDescriptor(globalThis, 'ethereumProvider');
362
+ if (descriptor?.configurable === true) {
363
+ Object.defineProperty(globalThis, 'ethereumProvider', {
364
+ configurable: true,
365
+ writable: true,
366
+ value: provider,
367
+ });
368
+ }
369
+ return delete globalThis[bridgeKey];
370
+ },
371
+ },
372
+ });
373
+ })();`
374
+ },
375
+ timeoutMs
376
+ );
377
+ if (typeof preload?.identifier !== "string" || preload.identifier.length === 0) {
378
+ return {
379
+ ok: false,
380
+ error: "Ethereum provider preload was not installed",
381
+ source: "ui-ethereum-provider"
382
+ };
383
+ }
384
+ let outcome = {
385
+ ok: false,
386
+ error: "Ethereum provider is unavailable",
387
+ source: "ui-ethereum-provider"
388
+ };
389
+ const cleanupErrors = [];
390
+ try {
391
+ await boundedCdpCall(session, "Page.reload", { ignoreCache: false }, timeoutMs);
392
+ const deadline = Date.now() + timeoutMs;
393
+ while (Date.now() <= deadline) {
394
+ try {
395
+ const result = await boundedCdpCall(session, "Runtime.evaluate", {
396
+ expression: `globalThis[Symbol.for(${JSON.stringify(bridgeKey)})]?.probe()`,
397
+ awaitPromise: true,
398
+ returnByValue: true
399
+ }, Math.max(1, deadline - Date.now()));
400
+ const value = result.result?.value;
401
+ if (value?.ok === true) {
402
+ outcome = { ok: true, error: null, source: "ui-ethereum-provider" };
403
+ break;
404
+ }
405
+ if (typeof value?.error === "string") outcome.error = value.error;
406
+ } catch (error) {
407
+ outcome.error = messageOf(error);
408
+ }
409
+ if (outcome.ok) break;
410
+ await sleep(100);
411
+ }
412
+ } catch (error) {
413
+ outcome = { ok: false, error: messageOf(error), source: "ui-ethereum-provider" };
414
+ } finally {
415
+ try {
416
+ const cleanup = await boundedCdpCall(session, "Runtime.evaluate", {
417
+ expression: `(() => {
418
+ const key = Symbol.for(${JSON.stringify(bridgeKey)});
419
+ const bridge = globalThis[key];
420
+ if (!bridge) return true;
421
+ return bridge.cleanup() === true && !(key in globalThis);
422
+ })()`,
423
+ returnByValue: true
424
+ }, timeoutMs);
425
+ if (cleanup.result?.value !== true) cleanupErrors.push("page bridge removal was not confirmed");
426
+ } catch (error) {
427
+ cleanupErrors.push(messageOf(error));
428
+ }
429
+ try {
430
+ await boundedCdpCall(
431
+ session,
432
+ "Page.removeScriptToEvaluateOnNewDocument",
433
+ { identifier: preload.identifier },
434
+ timeoutMs
435
+ );
436
+ } catch (error) {
437
+ cleanupErrors.push(messageOf(error));
438
+ }
439
+ }
440
+ return cleanupErrors.length > 0 ? {
441
+ ok: false,
442
+ error: `Ethereum provider probe cleanup failed: ${cleanupErrors.join("; ")}`,
443
+ source: "ui-ethereum-provider"
444
+ } : outcome;
445
+ }
319
446
  function extensionBackgroundProbeTimeoutMs(cdpCallTimeoutMs) {
320
447
  return Math.max(1, Math.floor(cdpCallTimeoutMs * 0.8));
321
448
  }
@@ -0,0 +1,124 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { recipeRuntimePath } from "../../paths.js";
5
+ import {
6
+ assertAndroidReleaseArtifactRuntime
7
+ } from "./release-artifact.js";
8
+ function mobileReleaseArtifactStatePath(target) {
9
+ return recipeRuntimePath(target, "mobile-release-artifact.json");
10
+ }
11
+ function writeMobileReleaseArtifactState(target, report) {
12
+ const state = {
13
+ schemaVersion: 1,
14
+ adapter: "mobile",
15
+ platform: "android",
16
+ packageId: report.packageId,
17
+ versionName: report.versionName,
18
+ versionCode: report.versionCode,
19
+ sha256: report.sha256,
20
+ deviceSerial: report.deviceSerial,
21
+ installedAt: (/* @__PURE__ */ new Date()).toISOString()
22
+ };
23
+ const file = mobileReleaseArtifactStatePath(target);
24
+ const directory = ensureSafeRuntimeDirectory(target, file);
25
+ const existing = fs.lstatSync(file, { throwIfNoEntry: false });
26
+ if (existing && (!existing.isFile() || existing.isSymbolicLink())) throw invalidState(file);
27
+ const temporary = path.join(
28
+ directory,
29
+ `.mobile-release-artifact.${process.pid}.${randomBytes(8).toString("hex")}.tmp`
30
+ );
31
+ let descriptor;
32
+ try {
33
+ descriptor = fs.openSync(temporary, "wx", 384);
34
+ fs.writeFileSync(descriptor, `${JSON.stringify(state, null, 2)}
35
+ `, "utf8");
36
+ fs.fsyncSync(descriptor);
37
+ fs.closeSync(descriptor);
38
+ descriptor = void 0;
39
+ fs.renameSync(temporary, file);
40
+ } finally {
41
+ if (descriptor !== void 0) fs.closeSync(descriptor);
42
+ const temporaryStat = fs.lstatSync(temporary, { throwIfNoEntry: false });
43
+ if (temporaryStat) fs.unlinkSync(temporary);
44
+ }
45
+ return state;
46
+ }
47
+ function requireCurrentMobileReleaseArtifactState(target, verify = assertAndroidReleaseArtifactRuntime) {
48
+ const state = readMobileReleaseArtifactState(target);
49
+ if (state) verify(state);
50
+ return state;
51
+ }
52
+ function clearMobileReleaseArtifactState(target) {
53
+ const file = mobileReleaseArtifactStatePath(target);
54
+ const existing = fs.lstatSync(file, { throwIfNoEntry: false });
55
+ if (!existing) return;
56
+ ensureSafeRuntimeDirectory(target, file);
57
+ if (!existing.isFile() || existing.isSymbolicLink()) throw invalidState(file);
58
+ fs.unlinkSync(file);
59
+ }
60
+ function readMobileReleaseArtifactState(target) {
61
+ const file = mobileReleaseArtifactStatePath(target);
62
+ let stat;
63
+ try {
64
+ stat = fs.lstatSync(file);
65
+ } catch (error) {
66
+ if (isMissing(error)) return null;
67
+ throw invalidState(file);
68
+ }
69
+ try {
70
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size <= 0 || stat.size > 16 * 1024) {
71
+ throw invalidState(file);
72
+ }
73
+ const value = JSON.parse(fs.readFileSync(file, "utf8"));
74
+ if (value.schemaVersion !== 1 || value.adapter !== "mobile" || value.platform !== "android" || !text(value.packageId) || !text(value.versionName) || !text(value.versionCode) || !/^[a-f0-9]{64}$/u.test(value.sha256 ?? "") || !text(value.deviceSerial) || !text(value.installedAt)) throw invalidState(file);
75
+ return value;
76
+ } catch (error) {
77
+ if (error instanceof Error && error.message.startsWith("The Mobile release artifact state is invalid")) {
78
+ throw error;
79
+ }
80
+ throw invalidState(file);
81
+ }
82
+ }
83
+ function text(value) {
84
+ return typeof value === "string" && value.trim().length > 0;
85
+ }
86
+ function isMissing(error) {
87
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
88
+ }
89
+ function invalidState(file) {
90
+ return new Error(`The Mobile release artifact state is invalid: ${file}. Next: rerun runtime-launch with the same artifact source.`);
91
+ }
92
+ function ensureSafeRuntimeDirectory(target, file) {
93
+ const root = path.resolve(target);
94
+ const directory = path.dirname(path.resolve(file));
95
+ const relative = path.relative(root, directory);
96
+ if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
97
+ throw invalidState(file);
98
+ }
99
+ let current = root;
100
+ const rootStat = fs.lstatSync(root, { throwIfNoEntry: false });
101
+ if (!rootStat?.isDirectory() || rootStat.isSymbolicLink()) throw invalidState(file);
102
+ for (const segment of relative.split(path.sep)) {
103
+ current = path.join(current, segment);
104
+ const stat = fs.lstatSync(current, { throwIfNoEntry: false });
105
+ if (stat) {
106
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw invalidState(file);
107
+ } else {
108
+ fs.mkdirSync(current, { mode: 448 });
109
+ }
110
+ }
111
+ const realRoot = fs.realpathSync(root);
112
+ const realDirectory = fs.realpathSync(directory);
113
+ if (realDirectory !== realRoot && !realDirectory.startsWith(`${realRoot}${path.sep}`)) {
114
+ throw invalidState(file);
115
+ }
116
+ return directory;
117
+ }
118
+ export {
119
+ clearMobileReleaseArtifactState,
120
+ mobileReleaseArtifactStatePath,
121
+ readMobileReleaseArtifactState,
122
+ requireCurrentMobileReleaseArtifactState,
123
+ writeMobileReleaseArtifactState
124
+ };
@@ -0,0 +1,295 @@
1
+ import { createHash } from "node:crypto";
2
+ import fs from "node:fs";
3
+ import { promises as fsp } from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { spawnSync } from "node:child_process";
7
+ async function installAndroidReleaseArtifact(options, deps = defaultDeps) {
8
+ validateInputs(options);
9
+ const sourcePath = path.resolve(options.file);
10
+ const stagingDirectory = await fsp.mkdtemp(path.join(os.tmpdir(), "mm-harness-android-artifact-"));
11
+ try {
12
+ const stagedPath = path.join(stagingDirectory, "release.apk");
13
+ const { sha256, bytes } = await stageRegularFile(sourcePath, stagedPath);
14
+ if (sha256 !== options.expectedSha256) {
15
+ throw new Error(`Android artifact SHA-256 ${sha256} does not match expected ${options.expectedSha256}.`);
16
+ }
17
+ const { packageId, versionName, versionCode } = readAndroidPackageIdentity(deps, stagedPath);
18
+ if (packageId !== options.packageId) {
19
+ throw new Error(`Android artifact package ${packageId || "missing"} does not match expected ${options.packageId}.`);
20
+ }
21
+ if (versionName !== options.expectedVersion) {
22
+ throw new Error(`Android artifact version ${versionName || "missing"} does not match expected ${options.expectedVersion}.`);
23
+ }
24
+ if (versionCode !== options.expectedBuildId) {
25
+ throw new Error(`Android artifact build ${versionCode || "missing"} does not match expected ${options.expectedBuildId}.`);
26
+ }
27
+ const adb = resolveAdb(deps);
28
+ const state = runChecked(deps, adb, ["-s", options.deviceSerial, "get-state"]).trim();
29
+ if (state !== "device") {
30
+ throw new Error(`Android device ${options.deviceSerial} is not ready.`);
31
+ }
32
+ runChecked(deps, adb, ["-s", options.deviceSerial, "install", "-r", "-d", stagedPath]);
33
+ assertInstalledAndroidReleaseArtifact({
34
+ packageId,
35
+ versionName,
36
+ versionCode,
37
+ sha256,
38
+ deviceSerial: options.deviceSerial
39
+ }, deps, adb);
40
+ runChecked(deps, adb, [
41
+ "-s",
42
+ options.deviceSerial,
43
+ "shell",
44
+ "monkey",
45
+ "-p",
46
+ packageId,
47
+ "-c",
48
+ "android.intent.category.LAUNCHER",
49
+ "1"
50
+ ]);
51
+ let pid = "";
52
+ for (let attempt = 0; attempt < 20; attempt += 1) {
53
+ const result = deps.run(adb, ["-s", options.deviceSerial, "shell", "pidof", packageId]);
54
+ if (result.status === 0 && result.stdout.trim()) {
55
+ pid = result.stdout.trim();
56
+ break;
57
+ }
58
+ await deps.sleep(250);
59
+ }
60
+ if (!pid) throw new Error(`Android package ${packageId} did not start on ${options.deviceSerial}.`);
61
+ return {
62
+ schemaVersion: 1,
63
+ status: "pass",
64
+ adapter: "mobile",
65
+ platform: "android",
66
+ source: { kind: "local", path: sourcePath },
67
+ sha256,
68
+ bytes,
69
+ packageId,
70
+ versionName,
71
+ versionCode,
72
+ deviceSerial: options.deviceSerial,
73
+ installed: true,
74
+ launched: true
75
+ };
76
+ } finally {
77
+ await fsp.rm(stagingDirectory, { recursive: true, force: true });
78
+ }
79
+ }
80
+ function assertAndroidReleaseArtifactRuntime(identity, deps = defaultDeps) {
81
+ validateInputs({
82
+ file: "runtime.apk",
83
+ expectedVersion: identity.versionName,
84
+ expectedBuildId: identity.versionCode,
85
+ expectedSha256: identity.sha256,
86
+ packageId: identity.packageId,
87
+ deviceSerial: identity.deviceSerial
88
+ });
89
+ const adb = resolveAdb(deps);
90
+ assertInstalledAndroidReleaseArtifact(identity, deps, adb);
91
+ }
92
+ async function relaunchAndroidReleaseArtifactRuntime(identity, deps = defaultDeps) {
93
+ assertAndroidReleaseArtifactRuntime(identity, deps);
94
+ const adb = resolveAdb(deps);
95
+ runChecked(deps, adb, [
96
+ "-s",
97
+ identity.deviceSerial,
98
+ "shell",
99
+ "monkey",
100
+ "-p",
101
+ identity.packageId,
102
+ "-c",
103
+ "android.intent.category.LAUNCHER",
104
+ "1"
105
+ ]);
106
+ for (let attempt = 0; attempt < 20; attempt += 1) {
107
+ const result = deps.run(adb, ["-s", identity.deviceSerial, "shell", "pidof", identity.packageId]);
108
+ if (result.status === 0 && result.stdout.trim()) return;
109
+ await deps.sleep(250);
110
+ }
111
+ throw new Error(`Android package ${identity.packageId} did not start on ${identity.deviceSerial}.`);
112
+ }
113
+ function validateInputs(options) {
114
+ if (!/^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9._-]+)?$/u.test(options.expectedVersion)) {
115
+ throw new Error("--artifact-version must be an Android version name such as X.Y.Z.");
116
+ }
117
+ if (!/^\d+$/u.test(options.expectedBuildId)) {
118
+ throw new Error("--artifact-build-id must be the numeric Android version code.");
119
+ }
120
+ if (!/^[a-f0-9]{64}$/u.test(options.expectedSha256)) {
121
+ throw new Error("--artifact-sha256 must be 64 lowercase hexadecimal characters.");
122
+ }
123
+ if (!/^[A-Za-z][A-Za-z0-9_]*(?:\.[A-Za-z][A-Za-z0-9_]*)+$/u.test(options.packageId)) {
124
+ throw new Error("--artifact-package-id must be an Android application ID.");
125
+ }
126
+ if (!/^[A-Za-z0-9._:-]+$/u.test(options.deviceSerial)) {
127
+ throw new Error("Android artifact launch requires an explicit device serial.");
128
+ }
129
+ }
130
+ function assertInstalledAndroidReleaseArtifact(identity, deps, adb) {
131
+ const state = runChecked(deps, adb, ["-s", identity.deviceSerial, "get-state"]).trim();
132
+ if (state !== "device") throw new Error(`Android device ${identity.deviceSerial} is not ready.`);
133
+ const paths = runChecked(deps, adb, [
134
+ "-s",
135
+ identity.deviceSerial,
136
+ "shell",
137
+ "pm",
138
+ "path",
139
+ identity.packageId
140
+ ]).split(/\r?\n/u).filter(Boolean);
141
+ if (paths.length !== 1 || !paths[0].startsWith("package:/") || !paths[0].endsWith("/base.apk")) {
142
+ throw new Error(`Android package ${identity.packageId} is not a single installed APK on ${identity.deviceSerial}.`);
143
+ }
144
+ const installedPath = paths[0].slice("package:".length);
145
+ if (!/^\/data\/app\/[A-Za-z0-9._~+=/-]+\/base\.apk$/u.test(installedPath)) {
146
+ throw new Error(`Android package ${identity.packageId} returned an unsafe installed path.`);
147
+ }
148
+ const installedSha256 = runChecked(deps, adb, [
149
+ "-s",
150
+ identity.deviceSerial,
151
+ "shell",
152
+ "sha256sum",
153
+ installedPath
154
+ ]).trim().split(/\s+/u)[0];
155
+ if (installedSha256 !== identity.sha256) {
156
+ throw new Error(`Android package ${identity.packageId} no longer matches the trusted release artifact SHA-256.`);
157
+ }
158
+ const installed = runChecked(deps, adb, [
159
+ "-s",
160
+ identity.deviceSerial,
161
+ "shell",
162
+ "dumpsys",
163
+ "package",
164
+ identity.packageId
165
+ ]);
166
+ if (!new RegExp(`^\\s*versionName=${escapeRegExp(identity.versionName)}\\s*$`, "mu").test(installed) || !new RegExp(`^\\s*versionCode=${escapeRegExp(identity.versionCode)}(?:\\s|$)`, "mu").test(installed)) {
167
+ throw new Error(`Android package ${identity.packageId} installed with an unexpected version on ${identity.deviceSerial}.`);
168
+ }
169
+ }
170
+ function escapeRegExp(value) {
171
+ return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
172
+ }
173
+ async function stageRegularFile(file, destination) {
174
+ const descriptor = await fsp.open(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
175
+ const output = await fsp.open(destination, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL, 384);
176
+ try {
177
+ const stat = await descriptor.stat();
178
+ if (!stat.isFile() || stat.size <= 0 || stat.size > 2 * 1024 * 1024 * 1024) {
179
+ throw new Error(`Android artifact must be a non-empty regular file smaller than 2 GiB: ${file}`);
180
+ }
181
+ const digest = createHash("sha256");
182
+ const stream = descriptor.createReadStream({ autoClose: false });
183
+ for await (const chunk of stream) {
184
+ digest.update(chunk);
185
+ await output.writeFile(chunk);
186
+ }
187
+ await output.sync();
188
+ return { sha256: digest.digest("hex"), bytes: stat.size };
189
+ } finally {
190
+ await output.close();
191
+ await descriptor.close();
192
+ }
193
+ }
194
+ function resolveAdb(deps) {
195
+ const configured = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT;
196
+ const candidates = [
197
+ configured ? path.join(configured, "platform-tools", "adb") : "",
198
+ path.join(os.homedir(), "Library", "Android", "sdk", "platform-tools", "adb"),
199
+ "adb"
200
+ ].filter(Boolean);
201
+ return firstRunnable(deps, candidates, ["version"], "adb");
202
+ }
203
+ function readAndroidPackageIdentity(deps, file) {
204
+ const analyzer = resolveApkAnalyzer(deps);
205
+ if (analyzer) {
206
+ const packageId2 = deps.run(analyzer, ["manifest", "application-id", file]);
207
+ const versionName2 = deps.run(analyzer, ["manifest", "version-name", file]);
208
+ const versionCode2 = deps.run(analyzer, ["manifest", "version-code", file]);
209
+ if (packageId2.status === 0 && packageId2.stdout.trim() && versionName2.status === 0 && versionName2.stdout.trim() && versionCode2.status === 0 && versionCode2.stdout.trim()) {
210
+ return {
211
+ packageId: packageId2.stdout.trim(),
212
+ versionName: versionName2.stdout.trim(),
213
+ versionCode: versionCode2.stdout.trim()
214
+ };
215
+ }
216
+ }
217
+ const aapt2 = resolveAapt2(deps);
218
+ const badging = runChecked(deps, aapt2, ["dump", "badging", file]);
219
+ const line = badging.split(/\r?\n/u).find((entry) => entry.startsWith("package: ")) ?? "";
220
+ const packageId = /\bname='([^']+)'/u.exec(line)?.[1] ?? "";
221
+ const versionCode = /\bversionCode='([^']+)'/u.exec(line)?.[1] ?? "";
222
+ const versionName = /\bversionName='([^']+)'/u.exec(line)?.[1] ?? "";
223
+ if (!packageId || !versionCode || !versionName) {
224
+ throw new Error("aapt2 could not read the Android artifact package identity.");
225
+ }
226
+ return { packageId, versionName, versionCode };
227
+ }
228
+ function resolveApkAnalyzer(deps) {
229
+ const configured = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT;
230
+ const candidates = [
231
+ configured ? path.join(configured, "cmdline-tools", "latest", "bin", "apkanalyzer") : "",
232
+ path.join(os.homedir(), "Library", "Android", "sdk", "cmdline-tools", "latest", "bin", "apkanalyzer")
233
+ ].filter(Boolean);
234
+ for (const candidate of candidates) {
235
+ try {
236
+ fs.accessSync(candidate, fs.constants.X_OK);
237
+ if (fs.statSync(candidate).isFile()) return candidate;
238
+ } catch {
239
+ continue;
240
+ }
241
+ }
242
+ const probe = deps.run("apkanalyzer", []);
243
+ if (`${probe.stdout}
244
+ ${probe.stderr}`.includes("manifest application-id")) return "apkanalyzer";
245
+ return null;
246
+ }
247
+ function resolveAapt2(deps) {
248
+ const configured = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT;
249
+ const buildToolsRoot = configured ? path.join(configured, "build-tools") : path.join(os.homedir(), "Library", "Android", "sdk", "build-tools");
250
+ let candidates = [];
251
+ try {
252
+ candidates = fs.readdirSync(buildToolsRoot).sort((left, right) => right.localeCompare(left, void 0, { numeric: true })).map((version) => path.join(buildToolsRoot, version, "aapt2"));
253
+ } catch {
254
+ candidates = [];
255
+ }
256
+ candidates.push("aapt2");
257
+ return firstRunnable(deps, candidates, ["version"], "apkanalyzer or aapt2");
258
+ }
259
+ function firstRunnable(deps, candidates, args, label) {
260
+ for (const candidate of candidates) {
261
+ if (deps.run(candidate, args).status === 0) return candidate;
262
+ }
263
+ throw new Error(`${label} is required to validate and install an Android release artifact.`);
264
+ }
265
+ function runChecked(deps, command, args) {
266
+ const result = deps.run(command, args);
267
+ if (result.status !== 0) {
268
+ const detail = result.stderr.trim() || result.stdout.trim() || `exit ${result.status}`;
269
+ throw new Error(`${path.basename(command)} failed: ${detail}`);
270
+ }
271
+ return result.stdout;
272
+ }
273
+ const defaultDeps = {
274
+ run(command, args) {
275
+ const result = spawnSync(command, args, {
276
+ encoding: "utf8",
277
+ stdio: ["ignore", "pipe", "pipe"],
278
+ timeout: 12e4,
279
+ maxBuffer: 4 * 1024 * 1024
280
+ });
281
+ return {
282
+ status: result.status ?? 1,
283
+ stdout: result.stdout || "",
284
+ stderr: result.stderr || result.error?.message || ""
285
+ };
286
+ },
287
+ sleep(ms) {
288
+ return new Promise((resolve) => setTimeout(resolve, ms));
289
+ }
290
+ };
291
+ export {
292
+ assertAndroidReleaseArtifactRuntime,
293
+ installAndroidReleaseArtifact,
294
+ relaunchAndroidReleaseArtifactRuntime
295
+ };
package/dist/adapters.js CHANGED
@@ -9,12 +9,18 @@ import { withExtensionPage } from "../library/actions/extension/platform/cdp.mjs
9
9
  import { bridgeCommand, evalAsync, evalSync, MOBILE_BRIDGE_ERROR_CODES, selectBridgeStatusEntry, simulatorScreenshot } from "../library/actions/mobile/platform/bridge.mjs";
10
10
  import { observeNativeUi } from "../library/actions/mobile/platform/observe-ui.mjs";
11
11
  import { resolveMobileToolPath } from "../library/actions/mobile/platform/tool-paths.mjs";
12
+ import { resolveWalletImportCredentials, validateWalletImportOptions } from "../library/actions/shared/wallet/import-source.mjs";
12
13
  const execFileAsync = promisify(execFile);
13
14
  const NATIVE_PROVIDER_UI_ACTIONS = /* @__PURE__ */ new Set([
15
+ "ui.press",
16
+ "ui.set_input",
17
+ "ui.scroll",
14
18
  "ui.swipe",
15
19
  "ui.pan",
16
20
  "ui.drag",
17
21
  "ui.long_press",
22
+ "ui.wait_for",
23
+ "ui.screenshot",
18
24
  "ui.capture_surface"
19
25
  ]);
20
26
  function sleep(ms) {
@@ -29,6 +35,7 @@ function simpleAdapter(action, executor) {
29
35
  };
30
36
  }
31
37
  const LIVE_ONLY_PERPS_ACTIONS = /* @__PURE__ */ new Set([
38
+ "metamask.perps.read_snapshot",
32
39
  "metamask.perps.read_positions",
33
40
  "metamask.perps.ensure_positions",
34
41
  "metamask.perps.assert_positions",
@@ -46,11 +53,12 @@ const LIVE_ONLY_PERPS_ACTIONS = /* @__PURE__ */ new Set([
46
53
  "metamask.perps.capture_performance"
47
54
  ]);
48
55
  const CORE_ONLY_PERPS_ACTIONS = /* @__PURE__ */ new Set([
49
- "metamask.perps.read_account",
50
- "metamask.perps.read_snapshot"
56
+ "metamask.perps.read_account"
51
57
  ]);
52
58
  const LIVE_ONLY_WALLET_ACTIONS = /* @__PURE__ */ new Set([
53
59
  "metamask.wallet.setup",
60
+ "metamask.wallet.import",
61
+ "metamask.wallet.reset",
54
62
  "metamask.wallet.ensure_unlocked",
55
63
  "metamask.wallet.select_account",
56
64
  "metamask.wallet.list_accounts",
@@ -110,6 +118,18 @@ async function semanticResult(platform, action, node, context, forceLive = false
110
118
  }
111
119
  const output = { platform, action, redacted: true };
112
120
  if (action === "metamask.wallet.fixture_status") return { output: fixtureSummary(context.projectRoot) };
121
+ if (action === "metamask.wallet.validate_import") {
122
+ validateWalletImportOptions(platform, node);
123
+ const credentials = await resolveWalletImportCredentials({ node, context });
124
+ return {
125
+ output: {
126
+ ...output,
127
+ credentialSource: credentials.source,
128
+ credentialSourceName: credentials.sourceName,
129
+ expectedAddress: credentials.expectedAddress
130
+ }
131
+ };
132
+ }
113
133
  return {
114
134
  output: {
115
135
  ...output,
@@ -183,7 +203,10 @@ function probeHttpJson(url, timeoutMs = 1e3) {
183
203
  function createMetaMaskSemanticAdapters(platform, declaredCustomActions = [], preparedLiveAdapters) {
184
204
  const walletActions = [
185
205
  "metamask.wallet.fixture_status",
206
+ "metamask.wallet.validate_import",
186
207
  "metamask.wallet.setup",
208
+ "metamask.wallet.import",
209
+ "metamask.wallet.reset",
187
210
  "metamask.wallet.ensure_unlocked",
188
211
  "metamask.wallet.select_account",
189
212
  "metamask.wallet.list_accounts",
@@ -203,13 +226,14 @@ function createMetaMaskSemanticAdapters(platform, declaredCustomActions = [], pr
203
226
  "metamask.perps.start_state",
204
227
  "metamask.perps.teardown_state",
205
228
  "metamask.perps.capture_performance",
229
+ ...platform === "extension" || platform === "core" ? ["metamask.perps.read_snapshot"] : [],
206
230
  ...platform === "mobile" ? [
207
231
  "metamask.perps.clear_performance_caches",
208
232
  "metamask.perps.measure_homepage_visible",
209
233
  "metamask.perps.prepare_local_snapshot_endpoint"
210
234
  ] : [],
211
235
  // read_account is core-only: only the headless core adapter implements it.
212
- ...platform === "core" ? ["metamask.perps.read_account", "metamask.perps.read_snapshot"] : []
236
+ ...platform === "core" ? ["metamask.perps.read_account"] : []
213
237
  ];
214
238
  const bundled = new Set(bundledActions);
215
239
  const actions = [.../* @__PURE__ */ new Set([...bundledActions, ...declaredCustomActions])];
@@ -644,6 +668,7 @@ async function handleMobileHud(payload, context) {
644
668
  }
645
669
  }
646
670
  async function hideMobileHudOnTeardown(projectRoot, env = {}) {
671
+ if (env.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME === "1" || process.env.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME === "1") return;
647
672
  if (process.env.METAMASK_RECIPE_AUTO_HUD === "0" || process.env.METAMASK_RECIPE_AUTO_HUD === "false") return;
648
673
  const input = {
649
674
  node: { bridge_timeout_ms: 8e3, cdp_timeout_ms: 5e3 },
@@ -785,7 +810,7 @@ function createMetaMaskUiTransport(platform, harness, preparedLiveAdapters) {
785
810
  if (live) return live.output;
786
811
  throw new Error(`ui.navigate requires library/actions/${platform}/ui/navigate.mjs.`);
787
812
  }
788
- if (platform === "mobile" && NATIVE_PROVIDER_UI_ACTIONS.has(action)) {
813
+ if (platform === "mobile" && NATIVE_PROVIDER_UI_ACTIONS.has(action) && (context.env.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME === "1" || process.env.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME === "1" || ["ui.swipe", "ui.pan", "ui.drag", "ui.long_press", "ui.capture_surface"].includes(action))) {
789
814
  return executeNativeProviderAction(action, node, context, harness.createNativeUiTransport);
790
815
  }
791
816
  return base.execute(action, action === "ui.wait_for" ? normalizeUiWaitNode(node) : node, context);
@@ -15,7 +15,7 @@ const SPEC = {
15
15
  { name: "sync", desc: "Refresh harness + canonicalize wallet fixture", flags: ["--json"] },
16
16
  { name: "logs", aliases: ["tail"], desc: "Compact build events or full log", flags: ["--full", "-f", "--window", "--events", "--source", "--json"] },
17
17
  { name: "debug", aliases: ["devtools", "inspect"], desc: "Open DevTools UI", flags: ["--json", "--no-open"] },
18
- { name: "fixtures", desc: "Manage the canonical wallet fixture (sync/set/generate)", args: ["sync", "set", "generate"], flags: ["--fixture", "--out", "--adapter", "--target", "--device", "--json"] },
18
+ { name: "fixtures", desc: "Manage the canonical wallet fixture (sync/set/reset/generate)", args: ["sync", "set", "reset", "generate"], flags: ["--fixture", "--out", "--adapter", "--target", "--device", "--json"] },
19
19
  { name: "actions", desc: "List runnable recipe actions", flags: ["--json", "--matrix", "--categories", "--category", "--action", "--library"] },
20
20
  { name: "doctor", desc: "Check harness/orchestration health", flags: ["--json", "--target", "--adapter", "--runtime-dir", "--expect-live", "--print-ready", "--cdp-port", "--device"] },
21
21
  { name: "run", desc: "Execute a proof recipe (path or library name, e.g. run perps.smoke)", args: ["recipe.json|name"], flags: ["--list", "--device"] },
package/dist/cli.js CHANGED
@@ -47,8 +47,8 @@ DAILY LOOP \u2014 what a teammate runs many times a day:
47
47
  mm-harness logs
48
48
  debug Open the debug console (extension DevTools / mobile RN).
49
49
  mm-harness debug
50
- fixtures Sync files + set the wallet + generate fixture-state + finalize labels over CDP.
51
- mm-harness fixtures sync # or: set | generate --fixture <f> --out <o> | finalize \u2026
50
+ fixtures Sync files, set/reset the wallet, generate fixture-state, or finalize labels over CDP.
51
+ mm-harness fixtures sync # or: set | reset | generate --fixture <f> --out <o> | finalize \u2026
52
52
 
53
53
  PROVE \u2014 run recipes and inspect capabilities:
54
54
  run Run a recipe and write evidence (summary/trace/artifacts).