@metamask/device-mcp 0.3.2 → 0.4.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 (119) hide show
  1. package/CHANGELOG.md +18 -1
  2. package/README.md +110 -21
  3. package/dist/android/NOTICE.md +65 -0
  4. package/dist/android/device-mcp-android-snapshot-helper-0.4.0.apk +0 -0
  5. package/dist/android/device-mcp-android-snapshot-helper-0.4.0.apk.idsig +0 -0
  6. package/dist/android/device-mcp-android-snapshot-helper-0.4.0.manifest.json +10 -0
  7. package/dist/backends/adb-backend.cjs +326 -17
  8. package/dist/backends/adb-backend.cjs.map +1 -1
  9. package/dist/backends/adb-backend.d.cts +1 -0
  10. package/dist/backends/adb-backend.d.cts.map +1 -1
  11. package/dist/backends/adb-backend.d.mts +1 -0
  12. package/dist/backends/adb-backend.d.mts.map +1 -1
  13. package/dist/backends/adb-backend.mjs +327 -18
  14. package/dist/backends/adb-backend.mjs.map +1 -1
  15. package/dist/backends/android-instrumentation/adb.cjs +28 -0
  16. package/dist/backends/android-instrumentation/adb.cjs.map +1 -0
  17. package/dist/backends/android-instrumentation/adb.d.cts +24 -0
  18. package/dist/backends/android-instrumentation/adb.d.cts.map +1 -0
  19. package/dist/backends/android-instrumentation/adb.d.mts +24 -0
  20. package/dist/backends/android-instrumentation/adb.d.mts.map +1 -0
  21. package/dist/backends/android-instrumentation/adb.mjs +25 -0
  22. package/dist/backends/android-instrumentation/adb.mjs.map +1 -0
  23. package/dist/backends/android-instrumentation/artifact.cjs +139 -0
  24. package/dist/backends/android-instrumentation/artifact.cjs.map +1 -0
  25. package/dist/backends/android-instrumentation/artifact.d.cts +28 -0
  26. package/dist/backends/android-instrumentation/artifact.d.cts.map +1 -0
  27. package/dist/backends/android-instrumentation/artifact.d.mts +28 -0
  28. package/dist/backends/android-instrumentation/artifact.d.mts.map +1 -0
  29. package/dist/backends/android-instrumentation/artifact.mjs +154 -0
  30. package/dist/backends/android-instrumentation/artifact.mjs.map +1 -0
  31. package/dist/backends/android-instrumentation/errors.cjs +79 -0
  32. package/dist/backends/android-instrumentation/errors.cjs.map +1 -0
  33. package/dist/backends/android-instrumentation/errors.d.cts +68 -0
  34. package/dist/backends/android-instrumentation/errors.d.cts.map +1 -0
  35. package/dist/backends/android-instrumentation/errors.d.mts +68 -0
  36. package/dist/backends/android-instrumentation/errors.d.mts.map +1 -0
  37. package/dist/backends/android-instrumentation/errors.mjs +73 -0
  38. package/dist/backends/android-instrumentation/errors.mjs.map +1 -0
  39. package/dist/backends/android-instrumentation/installer.cjs +235 -0
  40. package/dist/backends/android-instrumentation/installer.cjs.map +1 -0
  41. package/dist/backends/android-instrumentation/installer.d.cts +123 -0
  42. package/dist/backends/android-instrumentation/installer.d.cts.map +1 -0
  43. package/dist/backends/android-instrumentation/installer.d.mts +123 -0
  44. package/dist/backends/android-instrumentation/installer.d.mts.map +1 -0
  45. package/dist/backends/android-instrumentation/installer.mjs +225 -0
  46. package/dist/backends/android-instrumentation/installer.mjs.map +1 -0
  47. package/dist/backends/android-instrumentation/signer.cjs +435 -0
  48. package/dist/backends/android-instrumentation/signer.cjs.map +1 -0
  49. package/dist/backends/android-instrumentation/signer.d.cts +40 -0
  50. package/dist/backends/android-instrumentation/signer.d.cts.map +1 -0
  51. package/dist/backends/android-instrumentation/signer.d.mts +40 -0
  52. package/dist/backends/android-instrumentation/signer.d.mts.map +1 -0
  53. package/dist/backends/android-instrumentation/signer.mjs +431 -0
  54. package/dist/backends/android-instrumentation/signer.mjs.map +1 -0
  55. package/dist/backends/android-instrumentation/snapshot.cjs +159 -0
  56. package/dist/backends/android-instrumentation/snapshot.cjs.map +1 -0
  57. package/dist/backends/android-instrumentation/snapshot.d.cts +67 -0
  58. package/dist/backends/android-instrumentation/snapshot.d.cts.map +1 -0
  59. package/dist/backends/android-instrumentation/snapshot.d.mts +67 -0
  60. package/dist/backends/android-instrumentation/snapshot.d.mts.map +1 -0
  61. package/dist/backends/android-instrumentation/snapshot.mjs +154 -0
  62. package/dist/backends/android-instrumentation/snapshot.mjs.map +1 -0
  63. package/dist/backends/appium-backend.cjs +19 -1
  64. package/dist/backends/appium-backend.cjs.map +1 -1
  65. package/dist/backends/appium-backend.d.cts +1 -0
  66. package/dist/backends/appium-backend.d.cts.map +1 -1
  67. package/dist/backends/appium-backend.d.mts +1 -0
  68. package/dist/backends/appium-backend.d.mts.map +1 -1
  69. package/dist/backends/appium-backend.mjs +19 -1
  70. package/dist/backends/appium-backend.mjs.map +1 -1
  71. package/dist/backends/idb-backend.cjs +128 -31
  72. package/dist/backends/idb-backend.cjs.map +1 -1
  73. package/dist/backends/idb-backend.d.cts +1 -0
  74. package/dist/backends/idb-backend.d.cts.map +1 -1
  75. package/dist/backends/idb-backend.d.mts +1 -0
  76. package/dist/backends/idb-backend.d.mts.map +1 -1
  77. package/dist/backends/idb-backend.mjs +129 -32
  78. package/dist/backends/idb-backend.mjs.map +1 -1
  79. package/dist/backends/index.cjs +3 -0
  80. package/dist/backends/index.cjs.map +1 -1
  81. package/dist/backends/index.d.cts.map +1 -1
  82. package/dist/backends/index.d.mts.map +1 -1
  83. package/dist/backends/index.mjs +3 -0
  84. package/dist/backends/index.mjs.map +1 -1
  85. package/dist/backends/types.cjs.map +1 -1
  86. package/dist/backends/types.d.cts +6 -0
  87. package/dist/backends/types.d.cts.map +1 -1
  88. package/dist/backends/types.d.mts +6 -0
  89. package/dist/backends/types.d.mts.map +1 -1
  90. package/dist/backends/types.mjs.map +1 -1
  91. package/dist/index.cjs.map +1 -1
  92. package/dist/index.d.cts +1 -1
  93. package/dist/index.d.cts.map +1 -1
  94. package/dist/index.d.mts +1 -1
  95. package/dist/index.d.mts.map +1 -1
  96. package/dist/index.mjs.map +1 -1
  97. package/dist/tools/swipe.cjs +3 -1
  98. package/dist/tools/swipe.cjs.map +1 -1
  99. package/dist/tools/swipe.d.cts.map +1 -1
  100. package/dist/tools/swipe.d.mts.map +1 -1
  101. package/dist/tools/swipe.mjs +3 -1
  102. package/dist/tools/swipe.mjs.map +1 -1
  103. package/dist/utils/output-path.cjs +13 -0
  104. package/dist/utils/output-path.cjs.map +1 -1
  105. package/dist/utils/output-path.d.cts +10 -0
  106. package/dist/utils/output-path.d.cts.map +1 -1
  107. package/dist/utils/output-path.d.mts +10 -0
  108. package/dist/utils/output-path.d.mts.map +1 -1
  109. package/dist/utils/output-path.mjs +12 -0
  110. package/dist/utils/output-path.mjs.map +1 -1
  111. package/dist/utils/platform.cjs +65 -7
  112. package/dist/utils/platform.cjs.map +1 -1
  113. package/dist/utils/platform.d.cts +33 -0
  114. package/dist/utils/platform.d.cts.map +1 -1
  115. package/dist/utils/platform.d.mts +33 -0
  116. package/dist/utils/platform.d.mts.map +1 -1
  117. package/dist/utils/platform.mjs +63 -7
  118. package/dist/utils/platform.mjs.map +1 -1
  119. package/package.json +2 -1
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Installs and TRUSTS the bundled Android snapshot-helper instrumentation APK.
3
+ *
4
+ * The APK is built during `prepack` into `dist/android/` and shipped with the
5
+ * npm package (`files: ["dist"]`). Before the ADB backend runs `am instrument`
6
+ * it must guarantee that the package on the device is OUR helper — not a
7
+ * malicious app squatting the package name. Package name and versionCode are
8
+ * attacker-controlled metadata; the only trust anchor is the signing
9
+ * certificate. So `ensureHelperTrusted` installs/upgrades by versionCode
10
+ * (freshness only) and then UNCONDITIONALLY verifies the installed APK's v2/v3
11
+ * signer against the pinned SHA-256 from the build manifest. A mismatch throws
12
+ * {@link UntrustedHelperError} and the caller must fail closed (no dump
13
+ * fallback) — see `adb-backend.ts`.
14
+ *
15
+ * `__dirname` is used for path resolution because it is portable across the
16
+ * dual ESM/CJS build: ts-bridge rewrites it to an `import.meta.url`-based shim
17
+ * in the `.mjs` output and leaves the native binding in the `.cjs` output.
18
+ *
19
+ * Version/policy/signer-verify flow adapted from agent-device (MIT, Copyright
20
+ * (c) Callstack) onto our source-built APK and pure-JS signer verifier.
21
+ */
22
+ import type { HelperArtifact } from "./artifact.mjs";
23
+ /**
24
+ * `am instrument` prints this when the target instrumentation is not installed.
25
+ * The ADB backend keys reactive re-installs off this signature.
26
+ */
27
+ export declare const INSTRUMENTATION_NOT_FOUND_SIGNATURE = "INSTRUMENTATION_FAILED";
28
+ /**
29
+ * Resolve the path to the bundled helper APK (validated against its manifest).
30
+ *
31
+ * @returns The absolute path to the APK.
32
+ * @throws When the bundled artifact is missing, malformed, or its digest does
33
+ * not match the manifest.
34
+ */
35
+ export declare function resolveHelperApkPath(): Promise<string>;
36
+ /**
37
+ * Check whether the helper package is present on the device.
38
+ *
39
+ * @param serial - The target device serial.
40
+ * @returns True when the helper package is installed (any signer).
41
+ */
42
+ export declare function isHelperInstalled(serial: string): Promise<boolean>;
43
+ /**
44
+ * Install the bundled helper APK onto the device.
45
+ *
46
+ * `-r` reinstalls over an existing copy signed by the SAME key and `-t` allows
47
+ * the `testOnly` APK. It CANNOT overwrite a package signed by a different key:
48
+ * Android refuses with `INSTALL_FAILED_UPDATE_INCOMPATIBLE` (or
49
+ * `INSTALL_FAILED_SHARED_USER_INCOMPATIBLE`). That is a squatter signed by a
50
+ * foreign certificate, so it is a TRUST failure, not a generic one — we throw
51
+ * {@link UntrustedHelperError} to make the caller fail closed, consistent with
52
+ * the equal/higher-versionCode squatter path (which fails closed via the signer
53
+ * verification). Without this, a lower-versionCode squatter would surface as a
54
+ * generic Error and `auto` mode would silently fall back to `uiautomator dump`.
55
+ *
56
+ * @param serial - The target device serial.
57
+ * @param artifact - The bundled artifact carrying the pinned signer SHA-256,
58
+ * used both to locate the APK and to describe a trust failure.
59
+ * @throws {@link UntrustedHelperError} when the install is refused because the
60
+ * installed package is signed by a different certificate.
61
+ * @throws When the install command fails for a generic reason.
62
+ */
63
+ export declare function installHelper(serial: string, artifact?: HelperArtifact): Promise<void>;
64
+ /**
65
+ * Pull every installed APK path for the helper package (base + splits) into a
66
+ * private temp dir and verify each is signed by the pinned certificate.
67
+ *
68
+ * @param serial - The target device serial.
69
+ * @param artifact - The bundled artifact carrying the pinned signer SHA-256.
70
+ * @throws {@link UntrustedHelperError} when the installed signer does not match
71
+ * the pin.
72
+ */
73
+ export declare function verifyInstalledSigner(serial: string, artifact: HelperArtifact): Promise<void>;
74
+ /**
75
+ * Install/upgrade the bundled helper by versionCode (freshness only).
76
+ *
77
+ * versionCode is attacker-controlled metadata, so this is NOT a trust decision:
78
+ * it only decides whether a fresher build should be pushed. It performs NO
79
+ * signer verification. Callers MUST separately call
80
+ * {@link assertInstalledHelperTrusted} before every use of the helper.
81
+ *
82
+ * This is safe to gate behind a per-process "installed" cache because a stale
83
+ * cache at worst skips a redundant (idempotent) install; it never skips a trust
84
+ * check.
85
+ *
86
+ * @param serial - The target device serial.
87
+ * @throws When install fails for a generic reason.
88
+ */
89
+ export declare function ensureHelperInstalled(serial: string): Promise<void>;
90
+ /**
91
+ * Verify that the helper currently installed on the device is signed by the
92
+ * pinned certificate. This is the trust invariant and MUST run before EVERY
93
+ * `am instrument`, never cached: package name and versionCode are
94
+ * attacker-controlled, so an actor who can install packages could replace a
95
+ * previously-trusted helper with a malicious same-package instrumentation
96
+ * between snapshots. Re-verifying the installed signer on every use (pm path +
97
+ * pull + hash — cheap relative to the instrumentation run) is what keeps the
98
+ * signature pin a per-use invariant instead of a one-time check.
99
+ *
100
+ * @param serial - The target device serial.
101
+ * @throws {@link UntrustedHelperError} when the installed helper is signed by
102
+ * an unexpected certificate (a squatter). The caller MUST NOT fall back to
103
+ * `uiautomator dump` in that case.
104
+ */
105
+ export declare function assertInstalledHelperTrusted(serial: string): Promise<void>;
106
+ /**
107
+ * Ensure a TRUSTED helper is installed: install/upgrade by versionCode
108
+ * (freshness), then unconditionally verify the installed signer against the
109
+ * pinned certificate. Fails closed on a signer mismatch.
110
+ *
111
+ * Convenience wrapper over {@link ensureHelperInstalled} +
112
+ * {@link assertInstalledHelperTrusted}. Callers that cache the install step
113
+ * MUST still call {@link assertInstalledHelperTrusted} on every use rather than
114
+ * caching this whole call.
115
+ *
116
+ * @param serial - The target device serial.
117
+ * @throws {@link UntrustedHelperError} when the installed helper is signed by
118
+ * an unexpected certificate (a squatter). The caller MUST NOT fall back to
119
+ * `uiautomator dump` in that case.
120
+ * @throws When install fails for a generic reason.
121
+ */
122
+ export declare function ensureHelperTrusted(serial: string): Promise<void>;
123
+ //# sourceMappingURL=installer.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"installer.d.mts","sourceRoot":"","sources":["../../../src/backends/android-instrumentation/installer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAOH,OAAO,KAAK,EAAE,cAAc,EAAE,uBAAsB;AAapD;;;GAGG;AACH,eAAO,MAAM,mCAAmC,2BAA2B,CAAC;AAE5E;;;;;;GAMG;AACH,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,MAAM,CAAC,CAG5D;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAOxE;AAwCD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,cAAc,GACxB,OAAO,CAAC,IAAI,CAAC,CAqBf;AAED;;;;;;;;GAQG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,cAAc,GACvB,OAAO,CAAC,IAAI,CAAC,CAqCf;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAOzE;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,4BAA4B,CAChD,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAGf;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAGvE"}
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Installs and TRUSTS the bundled Android snapshot-helper instrumentation APK.
3
+ *
4
+ * The APK is built during `prepack` into `dist/android/` and shipped with the
5
+ * npm package (`files: ["dist"]`). Before the ADB backend runs `am instrument`
6
+ * it must guarantee that the package on the device is OUR helper — not a
7
+ * malicious app squatting the package name. Package name and versionCode are
8
+ * attacker-controlled metadata; the only trust anchor is the signing
9
+ * certificate. So `ensureHelperTrusted` installs/upgrades by versionCode
10
+ * (freshness only) and then UNCONDITIONALLY verifies the installed APK's v2/v3
11
+ * signer against the pinned SHA-256 from the build manifest. A mismatch throws
12
+ * {@link UntrustedHelperError} and the caller must fail closed (no dump
13
+ * fallback) — see `adb-backend.ts`.
14
+ *
15
+ * `__dirname` is used for path resolution because it is portable across the
16
+ * dual ESM/CJS build: ts-bridge rewrites it to an `import.meta.url`-based shim
17
+ * in the `.mjs` output and leaves the native binding in the `.cjs` output.
18
+ *
19
+ * Version/policy/signer-verify flow adapted from agent-device (MIT, Copyright
20
+ * (c) Callstack) onto our source-built APK and pure-JS signer verifier.
21
+ */
22
+ import { rmSync } from "node:fs";
23
+ import { join } from "node:path";
24
+ import { createAndroidAdbExecutor } from "./adb.mjs";
25
+ import { loadHelperArtifact } from "./artifact.mjs";
26
+ import { UntrustedHelperError } from "./errors.mjs";
27
+ import { assertApkSignerSha256 } from "./signer.mjs";
28
+ import { HELPER_PACKAGE } from "./snapshot.mjs";
29
+ import { exec } from "../../utils/exec.mjs";
30
+ import { createPrivateTempDir } from "../../utils/output-path.mjs";
31
+ const INSTALL_TIMEOUT_MS = 60000;
32
+ const QUERY_TIMEOUT_MS = 10000;
33
+ const PULL_TIMEOUT_MS = 30000;
34
+ /**
35
+ * `am instrument` prints this when the target instrumentation is not installed.
36
+ * The ADB backend keys reactive re-installs off this signature.
37
+ */
38
+ export const INSTRUMENTATION_NOT_FOUND_SIGNATURE = 'INSTRUMENTATION_FAILED';
39
+ /**
40
+ * Resolve the path to the bundled helper APK (validated against its manifest).
41
+ *
42
+ * @returns The absolute path to the APK.
43
+ * @throws When the bundled artifact is missing, malformed, or its digest does
44
+ * not match the manifest.
45
+ */
46
+ export async function resolveHelperApkPath() {
47
+ const artifact = await loadHelperArtifact();
48
+ return artifact.apkPath;
49
+ }
50
+ /**
51
+ * Check whether the helper package is present on the device.
52
+ *
53
+ * @param serial - The target device serial.
54
+ * @returns True when the helper package is installed (any signer).
55
+ */
56
+ export async function isHelperInstalled(serial) {
57
+ const result = await exec('adb', ['-s', serial, 'shell', 'pm', 'list', 'packages', HELPER_PACKAGE], { timeoutMs: QUERY_TIMEOUT_MS });
58
+ return result.stdout.includes(`package:${HELPER_PACKAGE}`);
59
+ }
60
+ /**
61
+ * Read the installed helper's versionCode via `dumpsys package`.
62
+ *
63
+ * versionCode is attacker-controlled metadata: it is used ONLY to decide
64
+ * whether to (re)install a fresher build, never as a trust signal.
65
+ *
66
+ * @param serial - The target device serial.
67
+ * @returns The installed versionCode, or null when not installed / unreadable.
68
+ */
69
+ async function readInstalledVersionCode(serial) {
70
+ const result = await exec('adb', ['-s', serial, 'shell', 'dumpsys', 'package', HELPER_PACKAGE], { timeoutMs: QUERY_TIMEOUT_MS });
71
+ const match = /versionCode=(\d+)/u.exec(result.stdout);
72
+ return match ? Number(match[1]) : null;
73
+ }
74
+ /**
75
+ * Android install-result tokens that specifically indicate the already-
76
+ * installed same-package app was signed by a DIFFERENT certificate, so the
77
+ * reinstall was refused on trust grounds (not a generic install failure). These
78
+ * constant names are stable across API 23-35 and are printed by PackageManager
79
+ * in the `Failure [<TOKEN>: ...]` line (on stdout; we scan both streams).
80
+ *
81
+ * Deliberately excludes generic failures like INSTALL_FAILED_TEST_ONLY,
82
+ * INSTALL_FAILED_VERSION_DOWNGRADE, INSTALL_FAILED_DUPLICATE_PERMISSION, and
83
+ * the parse-time no/inconsistent-certificate codes: none of those mean a
84
+ * squatter is signed by a different key on a normal reinstall.
85
+ */
86
+ const SIGNER_CONFLICT_TOKENS = [
87
+ 'INSTALL_FAILED_UPDATE_INCOMPATIBLE',
88
+ 'INSTALL_FAILED_SHARED_USER_INCOMPATIBLE',
89
+ ];
90
+ /**
91
+ * Install the bundled helper APK onto the device.
92
+ *
93
+ * `-r` reinstalls over an existing copy signed by the SAME key and `-t` allows
94
+ * the `testOnly` APK. It CANNOT overwrite a package signed by a different key:
95
+ * Android refuses with `INSTALL_FAILED_UPDATE_INCOMPATIBLE` (or
96
+ * `INSTALL_FAILED_SHARED_USER_INCOMPATIBLE`). That is a squatter signed by a
97
+ * foreign certificate, so it is a TRUST failure, not a generic one — we throw
98
+ * {@link UntrustedHelperError} to make the caller fail closed, consistent with
99
+ * the equal/higher-versionCode squatter path (which fails closed via the signer
100
+ * verification). Without this, a lower-versionCode squatter would surface as a
101
+ * generic Error and `auto` mode would silently fall back to `uiautomator dump`.
102
+ *
103
+ * @param serial - The target device serial.
104
+ * @param artifact - The bundled artifact carrying the pinned signer SHA-256,
105
+ * used both to locate the APK and to describe a trust failure.
106
+ * @throws {@link UntrustedHelperError} when the install is refused because the
107
+ * installed package is signed by a different certificate.
108
+ * @throws When the install command fails for a generic reason.
109
+ */
110
+ export async function installHelper(serial, artifact) {
111
+ const resolved = artifact ?? (await loadHelperArtifact());
112
+ const { apkPath } = resolved;
113
+ const result = await exec('adb', ['-s', serial, 'install', '-r', '-t', apkPath], { timeoutMs: INSTALL_TIMEOUT_MS });
114
+ if (result.exitCode !== 0 || !result.stdout.includes('Success')) {
115
+ const output = `${result.stdout.trim()} ${result.stderr.trim()}`.trim();
116
+ if (SIGNER_CONFLICT_TOKENS.some((token) => output.includes(token))) {
117
+ throw new UntrustedHelperError(`Snapshot helper install was refused because the installed package is ` +
118
+ `signed by a different certificate (a squatter). ${output}`, { expectedSignerSha256: resolved.signerSha256 });
119
+ }
120
+ throw new Error(`Failed to install snapshot helper APK (${apkPath}).\n${output}`);
121
+ }
122
+ }
123
+ /**
124
+ * Pull every installed APK path for the helper package (base + splits) into a
125
+ * private temp dir and verify each is signed by the pinned certificate.
126
+ *
127
+ * @param serial - The target device serial.
128
+ * @param artifact - The bundled artifact carrying the pinned signer SHA-256.
129
+ * @throws {@link UntrustedHelperError} when the installed signer does not match
130
+ * the pin.
131
+ */
132
+ export async function verifyInstalledSigner(serial, artifact) {
133
+ const run = createAndroidAdbExecutor(serial);
134
+ const pathsResult = await run(['shell', 'pm', 'path', HELPER_PACKAGE], {
135
+ timeoutMs: QUERY_TIMEOUT_MS,
136
+ });
137
+ const remotePaths = pathsResult.stdout
138
+ .split('\n')
139
+ .map((line) => line.trim())
140
+ .filter((line) => line.startsWith('package:'))
141
+ .map((line) => line.slice('package:'.length));
142
+ if (remotePaths.length === 0) {
143
+ throw new UntrustedHelperError('Snapshot helper is installed but its APK path could not be resolved', { expectedSignerSha256: artifact.signerSha256 });
144
+ }
145
+ const tempDir = createPrivateTempDir('helper-verify');
146
+ try {
147
+ let index = 0;
148
+ for (const remotePath of remotePaths) {
149
+ const localPath = join(tempDir, `apk-${index}.apk`);
150
+ index += 1;
151
+ const pull = await run(['pull', remotePath, localPath], {
152
+ timeoutMs: PULL_TIMEOUT_MS,
153
+ });
154
+ if (pull.exitCode !== 0) {
155
+ throw new UntrustedHelperError(`Could not pull installed helper APK for verification (${remotePath})`, { expectedSignerSha256: artifact.signerSha256 });
156
+ }
157
+ await assertApkSignerSha256(localPath, artifact.signerSha256);
158
+ }
159
+ }
160
+ finally {
161
+ rmSync(tempDir, { recursive: true, force: true });
162
+ }
163
+ }
164
+ /**
165
+ * Install/upgrade the bundled helper by versionCode (freshness only).
166
+ *
167
+ * versionCode is attacker-controlled metadata, so this is NOT a trust decision:
168
+ * it only decides whether a fresher build should be pushed. It performs NO
169
+ * signer verification. Callers MUST separately call
170
+ * {@link assertInstalledHelperTrusted} before every use of the helper.
171
+ *
172
+ * This is safe to gate behind a per-process "installed" cache because a stale
173
+ * cache at worst skips a redundant (idempotent) install; it never skips a trust
174
+ * check.
175
+ *
176
+ * @param serial - The target device serial.
177
+ * @throws When install fails for a generic reason.
178
+ */
179
+ export async function ensureHelperInstalled(serial) {
180
+ const artifact = await loadHelperArtifact();
181
+ const installedVersion = await readInstalledVersionCode(serial);
182
+ if (installedVersion === null || installedVersion < artifact.versionCode) {
183
+ await installHelper(serial, artifact);
184
+ }
185
+ }
186
+ /**
187
+ * Verify that the helper currently installed on the device is signed by the
188
+ * pinned certificate. This is the trust invariant and MUST run before EVERY
189
+ * `am instrument`, never cached: package name and versionCode are
190
+ * attacker-controlled, so an actor who can install packages could replace a
191
+ * previously-trusted helper with a malicious same-package instrumentation
192
+ * between snapshots. Re-verifying the installed signer on every use (pm path +
193
+ * pull + hash — cheap relative to the instrumentation run) is what keeps the
194
+ * signature pin a per-use invariant instead of a one-time check.
195
+ *
196
+ * @param serial - The target device serial.
197
+ * @throws {@link UntrustedHelperError} when the installed helper is signed by
198
+ * an unexpected certificate (a squatter). The caller MUST NOT fall back to
199
+ * `uiautomator dump` in that case.
200
+ */
201
+ export async function assertInstalledHelperTrusted(serial) {
202
+ const artifact = await loadHelperArtifact();
203
+ await verifyInstalledSigner(serial, artifact);
204
+ }
205
+ /**
206
+ * Ensure a TRUSTED helper is installed: install/upgrade by versionCode
207
+ * (freshness), then unconditionally verify the installed signer against the
208
+ * pinned certificate. Fails closed on a signer mismatch.
209
+ *
210
+ * Convenience wrapper over {@link ensureHelperInstalled} +
211
+ * {@link assertInstalledHelperTrusted}. Callers that cache the install step
212
+ * MUST still call {@link assertInstalledHelperTrusted} on every use rather than
213
+ * caching this whole call.
214
+ *
215
+ * @param serial - The target device serial.
216
+ * @throws {@link UntrustedHelperError} when the installed helper is signed by
217
+ * an unexpected certificate (a squatter). The caller MUST NOT fall back to
218
+ * `uiautomator dump` in that case.
219
+ * @throws When install fails for a generic reason.
220
+ */
221
+ export async function ensureHelperTrusted(serial) {
222
+ await ensureHelperInstalled(serial);
223
+ await assertInstalledHelperTrusted(serial);
224
+ }
225
+ //# sourceMappingURL=installer.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"installer.mjs","sourceRoot":"","sources":["../../../src/backends/android-instrumentation/installer.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Installs and TRUSTS the bundled Android snapshot-helper instrumentation APK.\n *\n * The APK is built during `prepack` into `dist/android/` and shipped with the\n * npm package (`files: [\"dist\"]`). Before the ADB backend runs `am instrument`\n * it must guarantee that the package on the device is OUR helper — not a\n * malicious app squatting the package name. Package name and versionCode are\n * attacker-controlled metadata; the only trust anchor is the signing\n * certificate. So `ensureHelperTrusted` installs/upgrades by versionCode\n * (freshness only) and then UNCONDITIONALLY verifies the installed APK's v2/v3\n * signer against the pinned SHA-256 from the build manifest. A mismatch throws\n * {@link UntrustedHelperError} and the caller must fail closed (no dump\n * fallback) — see `adb-backend.ts`.\n *\n * `__dirname` is used for path resolution because it is portable across the\n * dual ESM/CJS build: ts-bridge rewrites it to an `import.meta.url`-based shim\n * in the `.mjs` output and leaves the native binding in the `.cjs` output.\n *\n * Version/policy/signer-verify flow adapted from agent-device (MIT, Copyright\n * (c) Callstack) onto our source-built APK and pure-JS signer verifier.\n */\n\nimport { rmSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport { createAndroidAdbExecutor } from './adb.js';\nimport { loadHelperArtifact } from './artifact.js';\nimport type { HelperArtifact } from './artifact.js';\nimport { UntrustedHelperError } from './errors.js';\nimport { assertApkSignerSha256 } from './signer.js';\nimport { HELPER_PACKAGE } from './snapshot.js';\nimport { exec } from '../../utils/exec.js';\nimport { createPrivateTempDir } from '../../utils/output-path.js';\n\nconst INSTALL_TIMEOUT_MS = 60_000;\n\nconst QUERY_TIMEOUT_MS = 10_000;\n\nconst PULL_TIMEOUT_MS = 30_000;\n\n/**\n * `am instrument` prints this when the target instrumentation is not installed.\n * The ADB backend keys reactive re-installs off this signature.\n */\nexport const INSTRUMENTATION_NOT_FOUND_SIGNATURE = 'INSTRUMENTATION_FAILED';\n\n/**\n * Resolve the path to the bundled helper APK (validated against its manifest).\n *\n * @returns The absolute path to the APK.\n * @throws When the bundled artifact is missing, malformed, or its digest does\n * not match the manifest.\n */\nexport async function resolveHelperApkPath(): Promise<string> {\n const artifact = await loadHelperArtifact();\n return artifact.apkPath;\n}\n\n/**\n * Check whether the helper package is present on the device.\n *\n * @param serial - The target device serial.\n * @returns True when the helper package is installed (any signer).\n */\nexport async function isHelperInstalled(serial: string): Promise<boolean> {\n const result = await exec(\n 'adb',\n ['-s', serial, 'shell', 'pm', 'list', 'packages', HELPER_PACKAGE],\n { timeoutMs: QUERY_TIMEOUT_MS },\n );\n return result.stdout.includes(`package:${HELPER_PACKAGE}`);\n}\n\n/**\n * Read the installed helper's versionCode via `dumpsys package`.\n *\n * versionCode is attacker-controlled metadata: it is used ONLY to decide\n * whether to (re)install a fresher build, never as a trust signal.\n *\n * @param serial - The target device serial.\n * @returns The installed versionCode, or null when not installed / unreadable.\n */\nasync function readInstalledVersionCode(\n serial: string,\n): Promise<number | null> {\n const result = await exec(\n 'adb',\n ['-s', serial, 'shell', 'dumpsys', 'package', HELPER_PACKAGE],\n { timeoutMs: QUERY_TIMEOUT_MS },\n );\n const match = /versionCode=(\\d+)/u.exec(result.stdout);\n return match ? Number(match[1]) : null;\n}\n\n/**\n * Android install-result tokens that specifically indicate the already-\n * installed same-package app was signed by a DIFFERENT certificate, so the\n * reinstall was refused on trust grounds (not a generic install failure). These\n * constant names are stable across API 23-35 and are printed by PackageManager\n * in the `Failure [<TOKEN>: ...]` line (on stdout; we scan both streams).\n *\n * Deliberately excludes generic failures like INSTALL_FAILED_TEST_ONLY,\n * INSTALL_FAILED_VERSION_DOWNGRADE, INSTALL_FAILED_DUPLICATE_PERMISSION, and\n * the parse-time no/inconsistent-certificate codes: none of those mean a\n * squatter is signed by a different key on a normal reinstall.\n */\nconst SIGNER_CONFLICT_TOKENS = [\n 'INSTALL_FAILED_UPDATE_INCOMPATIBLE',\n 'INSTALL_FAILED_SHARED_USER_INCOMPATIBLE',\n];\n\n/**\n * Install the bundled helper APK onto the device.\n *\n * `-r` reinstalls over an existing copy signed by the SAME key and `-t` allows\n * the `testOnly` APK. It CANNOT overwrite a package signed by a different key:\n * Android refuses with `INSTALL_FAILED_UPDATE_INCOMPATIBLE` (or\n * `INSTALL_FAILED_SHARED_USER_INCOMPATIBLE`). That is a squatter signed by a\n * foreign certificate, so it is a TRUST failure, not a generic one — we throw\n * {@link UntrustedHelperError} to make the caller fail closed, consistent with\n * the equal/higher-versionCode squatter path (which fails closed via the signer\n * verification). Without this, a lower-versionCode squatter would surface as a\n * generic Error and `auto` mode would silently fall back to `uiautomator dump`.\n *\n * @param serial - The target device serial.\n * @param artifact - The bundled artifact carrying the pinned signer SHA-256,\n * used both to locate the APK and to describe a trust failure.\n * @throws {@link UntrustedHelperError} when the install is refused because the\n * installed package is signed by a different certificate.\n * @throws When the install command fails for a generic reason.\n */\nexport async function installHelper(\n serial: string,\n artifact?: HelperArtifact,\n): Promise<void> {\n const resolved = artifact ?? (await loadHelperArtifact());\n const { apkPath } = resolved;\n const result = await exec(\n 'adb',\n ['-s', serial, 'install', '-r', '-t', apkPath],\n { timeoutMs: INSTALL_TIMEOUT_MS },\n );\n if (result.exitCode !== 0 || !result.stdout.includes('Success')) {\n const output = `${result.stdout.trim()} ${result.stderr.trim()}`.trim();\n if (SIGNER_CONFLICT_TOKENS.some((token) => output.includes(token))) {\n throw new UntrustedHelperError(\n `Snapshot helper install was refused because the installed package is ` +\n `signed by a different certificate (a squatter). ${output}`,\n { expectedSignerSha256: resolved.signerSha256 },\n );\n }\n throw new Error(\n `Failed to install snapshot helper APK (${apkPath}).\\n${output}`,\n );\n }\n}\n\n/**\n * Pull every installed APK path for the helper package (base + splits) into a\n * private temp dir and verify each is signed by the pinned certificate.\n *\n * @param serial - The target device serial.\n * @param artifact - The bundled artifact carrying the pinned signer SHA-256.\n * @throws {@link UntrustedHelperError} when the installed signer does not match\n * the pin.\n */\nexport async function verifyInstalledSigner(\n serial: string,\n artifact: HelperArtifact,\n): Promise<void> {\n const run = createAndroidAdbExecutor(serial);\n const pathsResult = await run(['shell', 'pm', 'path', HELPER_PACKAGE], {\n timeoutMs: QUERY_TIMEOUT_MS,\n });\n const remotePaths = pathsResult.stdout\n .split('\\n')\n .map((line) => line.trim())\n .filter((line) => line.startsWith('package:'))\n .map((line) => line.slice('package:'.length));\n if (remotePaths.length === 0) {\n throw new UntrustedHelperError(\n 'Snapshot helper is installed but its APK path could not be resolved',\n { expectedSignerSha256: artifact.signerSha256 },\n );\n }\n\n const tempDir = createPrivateTempDir('helper-verify');\n try {\n let index = 0;\n for (const remotePath of remotePaths) {\n const localPath = join(tempDir, `apk-${index}.apk`);\n index += 1;\n const pull = await run(['pull', remotePath, localPath], {\n timeoutMs: PULL_TIMEOUT_MS,\n });\n if (pull.exitCode !== 0) {\n throw new UntrustedHelperError(\n `Could not pull installed helper APK for verification (${remotePath})`,\n { expectedSignerSha256: artifact.signerSha256 },\n );\n }\n await assertApkSignerSha256(localPath, artifact.signerSha256);\n }\n } finally {\n rmSync(tempDir, { recursive: true, force: true });\n }\n}\n\n/**\n * Install/upgrade the bundled helper by versionCode (freshness only).\n *\n * versionCode is attacker-controlled metadata, so this is NOT a trust decision:\n * it only decides whether a fresher build should be pushed. It performs NO\n * signer verification. Callers MUST separately call\n * {@link assertInstalledHelperTrusted} before every use of the helper.\n *\n * This is safe to gate behind a per-process \"installed\" cache because a stale\n * cache at worst skips a redundant (idempotent) install; it never skips a trust\n * check.\n *\n * @param serial - The target device serial.\n * @throws When install fails for a generic reason.\n */\nexport async function ensureHelperInstalled(serial: string): Promise<void> {\n const artifact = await loadHelperArtifact();\n const installedVersion = await readInstalledVersionCode(serial);\n\n if (installedVersion === null || installedVersion < artifact.versionCode) {\n await installHelper(serial, artifact);\n }\n}\n\n/**\n * Verify that the helper currently installed on the device is signed by the\n * pinned certificate. This is the trust invariant and MUST run before EVERY\n * `am instrument`, never cached: package name and versionCode are\n * attacker-controlled, so an actor who can install packages could replace a\n * previously-trusted helper with a malicious same-package instrumentation\n * between snapshots. Re-verifying the installed signer on every use (pm path +\n * pull + hash — cheap relative to the instrumentation run) is what keeps the\n * signature pin a per-use invariant instead of a one-time check.\n *\n * @param serial - The target device serial.\n * @throws {@link UntrustedHelperError} when the installed helper is signed by\n * an unexpected certificate (a squatter). The caller MUST NOT fall back to\n * `uiautomator dump` in that case.\n */\nexport async function assertInstalledHelperTrusted(\n serial: string,\n): Promise<void> {\n const artifact = await loadHelperArtifact();\n await verifyInstalledSigner(serial, artifact);\n}\n\n/**\n * Ensure a TRUSTED helper is installed: install/upgrade by versionCode\n * (freshness), then unconditionally verify the installed signer against the\n * pinned certificate. Fails closed on a signer mismatch.\n *\n * Convenience wrapper over {@link ensureHelperInstalled} +\n * {@link assertInstalledHelperTrusted}. Callers that cache the install step\n * MUST still call {@link assertInstalledHelperTrusted} on every use rather than\n * caching this whole call.\n *\n * @param serial - The target device serial.\n * @throws {@link UntrustedHelperError} when the installed helper is signed by\n * an unexpected certificate (a squatter). The caller MUST NOT fall back to\n * `uiautomator dump` in that case.\n * @throws When install fails for a generic reason.\n */\nexport async function ensureHelperTrusted(serial: string): Promise<void> {\n await ensureHelperInstalled(serial);\n await assertInstalledHelperTrusted(serial);\n}\n"]}