@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
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env bash
2
+ # Reset one installed MetaMask Mobile app without rebuilding it.
3
+ set -euo pipefail
4
+
5
+ PLATFORM="${PLATFORM:-ios}"
6
+ TARGET="$PWD"
7
+ SIMULATOR="${SIM_UDID:-${IOS_SIMULATOR:-booted}}"
8
+ ADB_SERIAL_ARG="${ADB_SERIAL:-${ANDROID_SERIAL:-}}"
9
+ ADB_BIN="${MM_HARNESS_ADB_PATH:-}"
10
+ MM_RESET_TEMP_ROOT=""
11
+ MM_RESET_STAGING_DIR=""
12
+
13
+ while [ "$#" -gt 0 ]; do
14
+ case "$1" in
15
+ --platform) PLATFORM="$2"; shift 2 ;;
16
+ --target) TARGET="$2"; shift 2 ;;
17
+ --simulator) SIMULATOR="$2"; shift 2 ;;
18
+ --adb-serial) ADB_SERIAL_ARG="$2"; shift 2 ;;
19
+ -h|--help)
20
+ printf 'Usage: reset-app-data.sh --platform ios|android [--target <dir>] [--simulator <udid|name>] [--adb-serial <serial>]\n'
21
+ exit 0
22
+ ;;
23
+ *) printf 'reset-app-data: unknown arg: %s\n' "$1" >&2; exit 2 ;;
24
+ esac
25
+ done
26
+
27
+ TARGET="$(cd "$TARGET" && pwd -P)"
28
+
29
+ reset_ios() {
30
+ local bundle_id="" app_source="" candidate="" staged_app=""
31
+ local temp_root="${TMPDIR:-/tmp}" staging_dir=""
32
+ local -a candidates installed_ids installed_paths
33
+
34
+ temp_root="${temp_root%/}"
35
+ [ -n "$temp_root" ] && [ "$temp_root" != "/" ] && [ -d "$temp_root" ] && [ ! -L "$temp_root" ] || {
36
+ printf 'reset-app-data: unsafe temporary directory: %s\n' "$temp_root" >&2
37
+ return 1
38
+ }
39
+ if [ -n "${IOS_BUNDLE_ID:-}" ]; then
40
+ candidates=("$IOS_BUNDLE_ID")
41
+ else
42
+ candidates=("io.metamask.MetaMask" "io.metamask" "io.metamask.MetaMask-Flask" "io.metamask.qa")
43
+ fi
44
+ for candidate in "${candidates[@]}"; do
45
+ if app_source="$(xcrun simctl get_app_container "$SIMULATOR" "$candidate" app 2>/dev/null)"; then
46
+ installed_ids+=("$candidate")
47
+ installed_paths+=("$app_source")
48
+ fi
49
+ done
50
+ if [ "${#installed_ids[@]}" -gt 1 ]; then
51
+ printf 'reset-app-data: multiple MetaMask iOS variants are installed on simulator %s: %s.\n' "$SIMULATOR" "${installed_ids[*]}" >&2
52
+ printf 'Next: set IOS_BUNDLE_ID to the exact variant and retry.\n' >&2
53
+ return 1
54
+ fi
55
+ bundle_id="${installed_ids[0]:-}"
56
+ app_source="${installed_paths[0]:-}"
57
+ [ -n "$bundle_id" ] && [ -n "$app_source" ] || {
58
+ printf 'reset-app-data: no installed MetaMask app was found on iOS simulator %s.\n' "$SIMULATOR" >&2
59
+ printf "Next: mm-harness launch --adapter mobile --platform ios --target '%s'\n" "$TARGET" >&2
60
+ return 1
61
+ }
62
+ case "$app_source" in
63
+ */Library/Developer/CoreSimulator/Devices/*/data/Containers/Bundle/Application/*/*.app) ;;
64
+ *) printf 'reset-app-data: refusing unexpected iOS app path: %s\n' "$app_source" >&2; return 1 ;;
65
+ esac
66
+ [ -d "$app_source" ] && [ ! -L "$app_source" ] || {
67
+ printf 'reset-app-data: iOS app is not a real directory: %s\n' "$app_source" >&2
68
+ return 1
69
+ }
70
+
71
+ staging_dir="$(mktemp -d "$temp_root/mm-harness-ios-reset.XXXXXX")"
72
+ case "$staging_dir" in
73
+ "$temp_root"/mm-harness-ios-reset.*) ;;
74
+ *) printf 'reset-app-data: refusing unexpected staging path: %s\n' "$staging_dir" >&2; return 1 ;;
75
+ esac
76
+ [ -d "$staging_dir" ] && [ ! -L "$staging_dir" ] || return 1
77
+ staged_app="$staging_dir/$(basename "$app_source")"
78
+ MM_RESET_TEMP_ROOT="$temp_root"
79
+ MM_RESET_STAGING_DIR="$staging_dir"
80
+ cleanup_staging() {
81
+ case "$MM_RESET_STAGING_DIR" in
82
+ "$MM_RESET_TEMP_ROOT"/mm-harness-ios-reset.*)
83
+ [ -d "$MM_RESET_STAGING_DIR" ] && [ ! -L "$MM_RESET_STAGING_DIR" ] && rm -r -- "$MM_RESET_STAGING_DIR"
84
+ ;;
85
+ *) printf 'reset-app-data: staging cleanup refused: %s\n' "$MM_RESET_STAGING_DIR" >&2 ;;
86
+ esac
87
+ }
88
+ trap cleanup_staging EXIT HUP INT TERM
89
+
90
+ ditto "$app_source" "$staged_app"
91
+ [ -d "$staged_app" ] && [ ! -L "$staged_app" ] || {
92
+ printf 'reset-app-data: failed to preserve the installed iOS app.\n' >&2
93
+ return 1
94
+ }
95
+ printf 'Resetting iOS app data: simulator=%s bundle=%s\n' "$SIMULATOR" "$bundle_id" >&2
96
+ xcrun simctl terminate "$SIMULATOR" "$bundle_id" >/dev/null 2>&1 || true
97
+ xcrun simctl uninstall "$SIMULATOR" "$bundle_id"
98
+ xcrun simctl install "$SIMULATOR" "$staged_app"
99
+ xcrun simctl get_app_container "$SIMULATOR" "$bundle_id" app >/dev/null
100
+ printf 'Mobile app data reset; preserved installed iOS build %s.\n' "$bundle_id" >&2
101
+ }
102
+
103
+ reset_android() {
104
+ local package_id="" candidate="" clear_output=""
105
+ local -a candidates adb_target installed_ids
106
+ if [ -z "$ADB_BIN" ]; then
107
+ ADB_BIN="$(command -v adb 2>/dev/null || true)"
108
+ fi
109
+ [ -n "$ADB_BIN" ] && [ "${ADB_BIN#/}" != "$ADB_BIN" ] && [ -x "$ADB_BIN" ] || {
110
+ printf 'reset-app-data: Android SDK Platform-Tools (adb) is required.\n' >&2
111
+ printf 'Next: brew install android-platform-tools\n' >&2
112
+ return 1
113
+ }
114
+ [ -n "$ADB_SERIAL_ARG" ] || {
115
+ printf 'reset-app-data: an Android serial is required.\n' >&2
116
+ printf 'Next: mm-harness devices --adapter mobile --json\n' >&2
117
+ return 1
118
+ }
119
+ adb_target=(-s "$ADB_SERIAL_ARG")
120
+ if [ -n "${ANDROID_PACKAGE_ID:-}" ]; then
121
+ candidates=("$ANDROID_PACKAGE_ID")
122
+ else
123
+ candidates=("io.metamask" "io.metamask.flask" "io.metamask.qa")
124
+ fi
125
+ for candidate in "${candidates[@]}"; do
126
+ if "$ADB_BIN" "${adb_target[@]}" shell pm path "$candidate" 2>/dev/null | grep -q '^package:'; then
127
+ installed_ids+=("$candidate")
128
+ fi
129
+ done
130
+ if [ "${#installed_ids[@]}" -gt 1 ]; then
131
+ printf 'reset-app-data: multiple MetaMask Android variants are installed on device %s: %s.\n' "$ADB_SERIAL_ARG" "${installed_ids[*]}" >&2
132
+ printf 'Next: set ANDROID_PACKAGE_ID to the exact variant and retry.\n' >&2
133
+ return 1
134
+ fi
135
+ package_id="${installed_ids[0]:-}"
136
+ [ -n "$package_id" ] || {
137
+ printf 'reset-app-data: no installed MetaMask app was found on Android device %s.\n' "$ADB_SERIAL_ARG" >&2
138
+ printf "Next: mm-harness launch --adapter mobile --platform android --target '%s' --device '%s'\n" "$TARGET" "$ADB_SERIAL_ARG" >&2
139
+ return 1
140
+ }
141
+ printf 'Resetting Android app data: device=%s package=%s\n' "$ADB_SERIAL_ARG" "$package_id" >&2
142
+ clear_output="$("$ADB_BIN" "${adb_target[@]}" shell pm clear "$package_id")"
143
+ [ "$clear_output" = "Success" ] || {
144
+ printf 'reset-app-data: Android package clear failed: %s\n' "$clear_output" >&2
145
+ return 1
146
+ }
147
+ printf 'Mobile app data reset; preserved installed Android build %s.\n' "$package_id" >&2
148
+ }
149
+
150
+ case "$PLATFORM" in
151
+ ios) reset_ios ;;
152
+ android) reset_android ;;
153
+ *) printf 'reset-app-data: --platform must be ios or android (got: %s)\n' "$PLATFORM" >&2; exit 2 ;;
154
+ esac
@@ -236,7 +236,7 @@ async function watchLog(options) {
236
236
  renderCompact({ label, logPath, events, quiet: mode === 'quiet' });
237
237
  if (mode !== 'quiet') {
238
238
  const elapsed = Math.round((Date.now() - started) / 1000);
239
- process.stderr.write(`${color('label', 'mm-harness', { stream: process.stderr })} ${color('dim', '|', { stream: process.stderr })} ${color('ok', `build complete (${elapsed}s)`, { stream: process.stderr })}\n`);
239
+ process.stderr.write(`${color('label', 'mm-harness', { stream: process.stderr })} ${color('dim', '|', { stream: process.stderr })} ${color('ok', `${label} complete (${elapsed}s)`, { stream: process.stderr })}\n`);
240
240
  }
241
241
  return 0;
242
242
  }
@@ -0,0 +1,38 @@
1
+ import { createHash } from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ function extensionTreeSha256(root) {
5
+ const resolvedRoot = path.resolve(root);
6
+ const rootStat = fs.lstatSync(resolvedRoot);
7
+ if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {
8
+ throw new Error(`Extension artifact root must be a regular directory: ${resolvedRoot}`);
9
+ }
10
+ const digest = createHash("sha256");
11
+ walk(resolvedRoot, "");
12
+ return digest.digest("hex");
13
+ function walk(directory, relativeDirectory) {
14
+ const entries = fs.readdirSync(directory, { withFileTypes: true }).sort((left, right) => Buffer.compare(Buffer.from(left.name), Buffer.from(right.name)));
15
+ for (const entry of entries) {
16
+ const absolute = path.join(directory, entry.name);
17
+ const relative = relativeDirectory ? `${relativeDirectory}/${entry.name}` : entry.name;
18
+ const stat = fs.lstatSync(absolute);
19
+ if (stat.isSymbolicLink()) {
20
+ throw new Error(`Extension artifact tree contains a symbolic link: ${relative}`);
21
+ }
22
+ if (stat.isDirectory()) {
23
+ digest.update(`directory\0${relative}\0`);
24
+ walk(absolute, relative);
25
+ continue;
26
+ }
27
+ if (!stat.isFile()) {
28
+ throw new Error(`Extension artifact tree contains an unsupported entry: ${relative}`);
29
+ }
30
+ digest.update(`file\0${relative}\0${stat.size}\0`);
31
+ digest.update(fs.readFileSync(absolute));
32
+ digest.update("\0");
33
+ }
34
+ }
35
+ }
36
+ export {
37
+ extensionTreeSha256
38
+ };
@@ -1,6 +1,8 @@
1
1
  import crypto from "node:crypto";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
+ import { recipeRuntimeDir } from "../../paths.js";
5
+ import { releaseArtifactState, runtimeDistCheck } from "./runtime-decision.js";
4
6
  const DIST_MANIFEST = "dist/chrome/manifest.json";
5
7
  function extensionIdFromKey(keyBase64) {
6
8
  const der = Buffer.from(keyBase64, "base64");
@@ -12,8 +14,8 @@ function extensionIdFromKey(keyBase64) {
12
14
  }
13
15
  return id;
14
16
  }
15
- function idFromDistManifest(target) {
16
- const manifestPath = path.join(target, DIST_MANIFEST);
17
+ function idFromManifest(manifestPath) {
18
+ if (!manifestPath) return null;
17
19
  if (!fs.existsSync(manifestPath)) return null;
18
20
  try {
19
21
  const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
@@ -39,7 +41,24 @@ async function cdpExtensionIds(cdpPort) {
39
41
  }
40
42
  async function resolveExtensionId(target, options = {}) {
41
43
  const resolved = path.resolve(target);
42
- const fromKey = idFromDistManifest(resolved);
44
+ const runtimeSnapshot = runtimeDistCheck(resolved, releaseArtifactState(resolved));
45
+ const releaseArtifact = runtimeSnapshot.source === "release-artifact";
46
+ if (releaseArtifact && runtimeSnapshot.status !== "fresh") {
47
+ return {
48
+ adapter: "extension",
49
+ target: resolved,
50
+ extensionId: null,
51
+ source: "none",
52
+ verified: false
53
+ };
54
+ }
55
+ const manifestPath = releaseArtifact ? runtimeSnapshot.status === "fresh" ? path.join(
56
+ resolved,
57
+ recipeRuntimeDir(),
58
+ process.env.RECIPE_RUNTIME_DIST_DIR || "runtime-dist",
59
+ "manifest.json"
60
+ ) : null : path.join(resolved, DIST_MANIFEST);
61
+ const fromKey = idFromManifest(manifestPath);
43
62
  let cdpIds = null;
44
63
  if (options.cdpPort) cdpIds = await cdpExtensionIds(options.cdpPort);
45
64
  if (fromKey) {
@@ -47,7 +66,7 @@ async function resolveExtensionId(target, options = {}) {
47
66
  adapter: "extension",
48
67
  target: resolved,
49
68
  extensionId: fromKey,
50
- source: "manifest-key",
69
+ source: releaseArtifact ? "release-manifest-key" : "manifest-key",
51
70
  verified: cdpIds ? cdpIds.includes(fromKey) : null
52
71
  };
53
72
  }
@@ -1,6 +1,31 @@
1
+ import { createHash } from "node:crypto";
2
+ import fs from "node:fs";
1
3
  import path from "node:path";
2
4
  import productConfig from "../../../adapters/extension/lib/product-config.cjs";
3
5
  import { shellQuote } from "../../commands/parse-args.js";
6
+ const EXTENSION_PRODUCT_CONFIG_FINGERPRINT_FILENAME = "extension-product-config.sha256";
7
+ function extensionProductConfigFingerprint(target, environment = process.env) {
8
+ const resolution = productConfig.resolveExtensionInfuraProjectId(target, environment);
9
+ if (resolution.kind !== "configured") return null;
10
+ return createHash("sha256").update(resolution.value).digest("hex");
11
+ }
12
+ function extensionCompiledScriptsMatchProductConfig(target, dist, environment = process.env) {
13
+ const resolution = productConfig.resolveExtensionInfuraProjectId(target, environment);
14
+ if (resolution.kind !== "configured") return true;
15
+ const pending = [dist];
16
+ while (pending.length > 0) {
17
+ const current = pending.pop();
18
+ if (!current || !fs.existsSync(current)) continue;
19
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
20
+ const file = path.join(current, entry.name);
21
+ if (entry.isDirectory()) pending.push(file);
22
+ else if (entry.isFile() && entry.name.endsWith(".js") && fs.readFileSync(file, "utf8").includes(resolution.value)) {
23
+ return true;
24
+ }
25
+ }
26
+ }
27
+ return false;
28
+ }
4
29
  function extensionProductConfigBlock(target) {
5
30
  const resolution = productConfig.resolveExtensionInfuraProjectId(
6
31
  target,
@@ -55,5 +80,8 @@ function invalidConfigBlock(target, name, reason) {
55
80
  };
56
81
  }
57
82
  export {
58
- extensionProductConfigBlock
83
+ EXTENSION_PRODUCT_CONFIG_FINGERPRINT_FILENAME,
84
+ extensionCompiledScriptsMatchProductConfig,
85
+ extensionProductConfigBlock,
86
+ extensionProductConfigFingerprint
59
87
  };