@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,68 @@
1
+ /**
2
+ * Error types for the Android snapshot-helper trust + install pipeline.
3
+ *
4
+ * Two failure classes are deliberately distinct because the ADB backend treats
5
+ * them differently (see `adb-backend.ts`):
6
+ *
7
+ * - {@link SnapshotHelperError} — a *generic* failure (install failed, adb
8
+ * hiccup, malformed artifact). The `auto` snapshot strategy is allowed to fall
9
+ * back to `uiautomator dump` when one of these occurs.
10
+ * - {@link UntrustedHelperError} — the installed helper's signing certificate
11
+ * does NOT match the pinned signer. This means a possibly-malicious app is
12
+ * squatting the helper's package name. The snapshot flow MUST fail closed:
13
+ * it is never swallowed and never triggers a dump fallback, because silently
14
+ * returning a dump snapshot would hide a compromised device from a wallet
15
+ * test harness.
16
+ *
17
+ * Structure adapted from agent-device (MIT, Copyright (c) Callstack); the
18
+ * `UntrustedHelperError` trust class and messaging are ours.
19
+ */
20
+ /** Machine-readable code for a {@link SnapshotHelperError}. */
21
+ export type SnapshotHelperErrorCode = 'COMMAND_FAILED' | 'INVALID_ARGS' | 'ARTIFACT_INVALID';
22
+ /**
23
+ * A generic, recoverable snapshot-helper failure. Callers in `auto` mode may
24
+ * fall back to `uiautomator dump` after catching one of these.
25
+ */
26
+ export declare class SnapshotHelperError extends Error {
27
+ readonly code: SnapshotHelperErrorCode;
28
+ /**
29
+ * @param code - Machine-readable failure category.
30
+ * @param message - Human-readable description.
31
+ * @param options - Standard error options (e.g. `cause`).
32
+ */
33
+ constructor(code: SnapshotHelperErrorCode, message: string, options?: ErrorOptions);
34
+ }
35
+ /**
36
+ * A trust failure: the installed helper is not signed by our pinned key.
37
+ *
38
+ * This is a hard stop. It must propagate out of the snapshot flow — never be
39
+ * converted to `null`, never be collected into a generic failure list, and
40
+ * never trigger a `uiautomator dump` fallback.
41
+ */
42
+ export declare class UntrustedHelperError extends Error {
43
+ /** The signer SHA-256 we required. */
44
+ readonly expectedSignerSha256: string;
45
+ /** The signer SHA-256 actually found on the device, if one was extracted. */
46
+ readonly actualSignerSha256?: string;
47
+ /**
48
+ * @param message - Human-readable description of the mismatch.
49
+ * @param details - The expected and (optionally) observed signer digests.
50
+ * @param details.expectedSignerSha256 - The pinned signer SHA-256.
51
+ * @param details.actualSignerSha256 - The observed signer SHA-256, if known.
52
+ * @param options - Standard error options (e.g. `cause`).
53
+ */
54
+ constructor(message: string, details: {
55
+ expectedSignerSha256: string;
56
+ actualSignerSha256?: string;
57
+ }, options?: ErrorOptions);
58
+ }
59
+ /**
60
+ * Type guard for the fail-closed trust error. The ADB backend uses this to
61
+ * decide whether an instrumentation failure may fall back to `dump` (generic)
62
+ * or must propagate (trust).
63
+ *
64
+ * @param error - The value to test.
65
+ * @returns True when `error` is an {@link UntrustedHelperError}.
66
+ */
67
+ export declare function isUntrustedHelperError(error: unknown): error is UntrustedHelperError;
68
+ //# sourceMappingURL=errors.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.cts","sourceRoot":"","sources":["../../../src/backends/android-instrumentation/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,+DAA+D;AAC/D,MAAM,MAAM,uBAAuB,GAC/B,gBAAgB,GAChB,cAAc,GACd,kBAAkB,CAAC;AAEvB;;;GAGG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,SAAgB,IAAI,EAAE,uBAAuB,CAAC;IAE9C;;;;OAIG;gBAED,IAAI,EAAE,uBAAuB,EAC7B,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,YAAY;CAQzB;AAED;;;;;;GAMG;AACH,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,sCAAsC;IACtC,SAAgB,oBAAoB,EAAE,MAAM,CAAC;IAE7C,6EAA6E;IAC7E,SAAgB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAE5C;;;;;;OAMG;gBAED,OAAO,EAAE,MAAM,EACf,OAAO,EAAE;QACP,oBAAoB,EAAE,MAAM,CAAC;QAC7B,kBAAkB,CAAC,EAAE,MAAM,CAAC;KAC7B,EACD,OAAO,CAAC,EAAE,YAAY;CASzB;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,OAAO,GACb,KAAK,IAAI,oBAAoB,CAE/B"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Error types for the Android snapshot-helper trust + install pipeline.
3
+ *
4
+ * Two failure classes are deliberately distinct because the ADB backend treats
5
+ * them differently (see `adb-backend.ts`):
6
+ *
7
+ * - {@link SnapshotHelperError} — a *generic* failure (install failed, adb
8
+ * hiccup, malformed artifact). The `auto` snapshot strategy is allowed to fall
9
+ * back to `uiautomator dump` when one of these occurs.
10
+ * - {@link UntrustedHelperError} — the installed helper's signing certificate
11
+ * does NOT match the pinned signer. This means a possibly-malicious app is
12
+ * squatting the helper's package name. The snapshot flow MUST fail closed:
13
+ * it is never swallowed and never triggers a dump fallback, because silently
14
+ * returning a dump snapshot would hide a compromised device from a wallet
15
+ * test harness.
16
+ *
17
+ * Structure adapted from agent-device (MIT, Copyright (c) Callstack); the
18
+ * `UntrustedHelperError` trust class and messaging are ours.
19
+ */
20
+ /** Machine-readable code for a {@link SnapshotHelperError}. */
21
+ export type SnapshotHelperErrorCode = 'COMMAND_FAILED' | 'INVALID_ARGS' | 'ARTIFACT_INVALID';
22
+ /**
23
+ * A generic, recoverable snapshot-helper failure. Callers in `auto` mode may
24
+ * fall back to `uiautomator dump` after catching one of these.
25
+ */
26
+ export declare class SnapshotHelperError extends Error {
27
+ readonly code: SnapshotHelperErrorCode;
28
+ /**
29
+ * @param code - Machine-readable failure category.
30
+ * @param message - Human-readable description.
31
+ * @param options - Standard error options (e.g. `cause`).
32
+ */
33
+ constructor(code: SnapshotHelperErrorCode, message: string, options?: ErrorOptions);
34
+ }
35
+ /**
36
+ * A trust failure: the installed helper is not signed by our pinned key.
37
+ *
38
+ * This is a hard stop. It must propagate out of the snapshot flow — never be
39
+ * converted to `null`, never be collected into a generic failure list, and
40
+ * never trigger a `uiautomator dump` fallback.
41
+ */
42
+ export declare class UntrustedHelperError extends Error {
43
+ /** The signer SHA-256 we required. */
44
+ readonly expectedSignerSha256: string;
45
+ /** The signer SHA-256 actually found on the device, if one was extracted. */
46
+ readonly actualSignerSha256?: string;
47
+ /**
48
+ * @param message - Human-readable description of the mismatch.
49
+ * @param details - The expected and (optionally) observed signer digests.
50
+ * @param details.expectedSignerSha256 - The pinned signer SHA-256.
51
+ * @param details.actualSignerSha256 - The observed signer SHA-256, if known.
52
+ * @param options - Standard error options (e.g. `cause`).
53
+ */
54
+ constructor(message: string, details: {
55
+ expectedSignerSha256: string;
56
+ actualSignerSha256?: string;
57
+ }, options?: ErrorOptions);
58
+ }
59
+ /**
60
+ * Type guard for the fail-closed trust error. The ADB backend uses this to
61
+ * decide whether an instrumentation failure may fall back to `dump` (generic)
62
+ * or must propagate (trust).
63
+ *
64
+ * @param error - The value to test.
65
+ * @returns True when `error` is an {@link UntrustedHelperError}.
66
+ */
67
+ export declare function isUntrustedHelperError(error: unknown): error is UntrustedHelperError;
68
+ //# sourceMappingURL=errors.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.mts","sourceRoot":"","sources":["../../../src/backends/android-instrumentation/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,+DAA+D;AAC/D,MAAM,MAAM,uBAAuB,GAC/B,gBAAgB,GAChB,cAAc,GACd,kBAAkB,CAAC;AAEvB;;;GAGG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,SAAgB,IAAI,EAAE,uBAAuB,CAAC;IAE9C;;;;OAIG;gBAED,IAAI,EAAE,uBAAuB,EAC7B,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,YAAY;CAQzB;AAED;;;;;;GAMG;AACH,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,sCAAsC;IACtC,SAAgB,oBAAoB,EAAE,MAAM,CAAC;IAE7C,6EAA6E;IAC7E,SAAgB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAE5C;;;;;;OAMG;gBAED,OAAO,EAAE,MAAM,EACf,OAAO,EAAE;QACP,oBAAoB,EAAE,MAAM,CAAC;QAC7B,kBAAkB,CAAC,EAAE,MAAM,CAAC;KAC7B,EACD,OAAO,CAAC,EAAE,YAAY;CASzB;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,OAAO,GACb,KAAK,IAAI,oBAAoB,CAE/B"}
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Error types for the Android snapshot-helper trust + install pipeline.
3
+ *
4
+ * Two failure classes are deliberately distinct because the ADB backend treats
5
+ * them differently (see `adb-backend.ts`):
6
+ *
7
+ * - {@link SnapshotHelperError} — a *generic* failure (install failed, adb
8
+ * hiccup, malformed artifact). The `auto` snapshot strategy is allowed to fall
9
+ * back to `uiautomator dump` when one of these occurs.
10
+ * - {@link UntrustedHelperError} — the installed helper's signing certificate
11
+ * does NOT match the pinned signer. This means a possibly-malicious app is
12
+ * squatting the helper's package name. The snapshot flow MUST fail closed:
13
+ * it is never swallowed and never triggers a dump fallback, because silently
14
+ * returning a dump snapshot would hide a compromised device from a wallet
15
+ * test harness.
16
+ *
17
+ * Structure adapted from agent-device (MIT, Copyright (c) Callstack); the
18
+ * `UntrustedHelperError` trust class and messaging are ours.
19
+ */
20
+ /**
21
+ * A generic, recoverable snapshot-helper failure. Callers in `auto` mode may
22
+ * fall back to `uiautomator dump` after catching one of these.
23
+ */
24
+ export class SnapshotHelperError extends Error {
25
+ /**
26
+ * @param code - Machine-readable failure category.
27
+ * @param message - Human-readable description.
28
+ * @param options - Standard error options (e.g. `cause`).
29
+ */
30
+ constructor(code, message, options) {
31
+ super(message, options);
32
+ this.name = 'SnapshotHelperError';
33
+ this.code = code;
34
+ // Restore prototype chain for `instanceof` across the dual ESM/CJS build.
35
+ Object.setPrototypeOf(this, SnapshotHelperError.prototype);
36
+ }
37
+ }
38
+ /**
39
+ * A trust failure: the installed helper is not signed by our pinned key.
40
+ *
41
+ * This is a hard stop. It must propagate out of the snapshot flow — never be
42
+ * converted to `null`, never be collected into a generic failure list, and
43
+ * never trigger a `uiautomator dump` fallback.
44
+ */
45
+ export class UntrustedHelperError extends Error {
46
+ /**
47
+ * @param message - Human-readable description of the mismatch.
48
+ * @param details - The expected and (optionally) observed signer digests.
49
+ * @param details.expectedSignerSha256 - The pinned signer SHA-256.
50
+ * @param details.actualSignerSha256 - The observed signer SHA-256, if known.
51
+ * @param options - Standard error options (e.g. `cause`).
52
+ */
53
+ constructor(message, details, options) {
54
+ super(message, options);
55
+ this.name = 'UntrustedHelperError';
56
+ this.expectedSignerSha256 = details.expectedSignerSha256;
57
+ this.actualSignerSha256 = details.actualSignerSha256;
58
+ // Restore prototype chain for `instanceof` across the dual ESM/CJS build.
59
+ Object.setPrototypeOf(this, UntrustedHelperError.prototype);
60
+ }
61
+ }
62
+ /**
63
+ * Type guard for the fail-closed trust error. The ADB backend uses this to
64
+ * decide whether an instrumentation failure may fall back to `dump` (generic)
65
+ * or must propagate (trust).
66
+ *
67
+ * @param error - The value to test.
68
+ * @returns True when `error` is an {@link UntrustedHelperError}.
69
+ */
70
+ export function isUntrustedHelperError(error) {
71
+ return error instanceof UntrustedHelperError;
72
+ }
73
+ //# sourceMappingURL=errors.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.mjs","sourceRoot":"","sources":["../../../src/backends/android-instrumentation/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAQH;;;GAGG;AACH,MAAM,OAAO,mBAAoB,SAAQ,KAAK;IAG5C;;;;OAIG;IACH,YACE,IAA6B,EAC7B,OAAe,EACf,OAAsB;QAEtB,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACxB,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC;QAClC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,0EAA0E;QAC1E,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,mBAAmB,CAAC,SAAS,CAAC,CAAC;IAC7D,CAAC;CACF;AAED;;;;;;GAMG;AACH,MAAM,OAAO,oBAAqB,SAAQ,KAAK;IAO7C;;;;;;OAMG;IACH,YACE,OAAe,EACf,OAGC,EACD,OAAsB;QAEtB,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACxB,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC;QACnC,IAAI,CAAC,oBAAoB,GAAG,OAAO,CAAC,oBAAoB,CAAC;QACzD,IAAI,CAAC,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC;QACrD,0EAA0E;QAC1E,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,oBAAoB,CAAC,SAAS,CAAC,CAAC;IAC9D,CAAC;CACF;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,sBAAsB,CACpC,KAAc;IAEd,OAAO,KAAK,YAAY,oBAAoB,CAAC;AAC/C,CAAC","sourcesContent":["/**\n * Error types for the Android snapshot-helper trust + install pipeline.\n *\n * Two failure classes are deliberately distinct because the ADB backend treats\n * them differently (see `adb-backend.ts`):\n *\n * - {@link SnapshotHelperError} — a *generic* failure (install failed, adb\n * hiccup, malformed artifact). The `auto` snapshot strategy is allowed to fall\n * back to `uiautomator dump` when one of these occurs.\n * - {@link UntrustedHelperError} — the installed helper's signing certificate\n * does NOT match the pinned signer. This means a possibly-malicious app is\n * squatting the helper's package name. The snapshot flow MUST fail closed:\n * it is never swallowed and never triggers a dump fallback, because silently\n * returning a dump snapshot would hide a compromised device from a wallet\n * test harness.\n *\n * Structure adapted from agent-device (MIT, Copyright (c) Callstack); the\n * `UntrustedHelperError` trust class and messaging are ours.\n */\n\n/** Machine-readable code for a {@link SnapshotHelperError}. */\nexport type SnapshotHelperErrorCode =\n | 'COMMAND_FAILED'\n | 'INVALID_ARGS'\n | 'ARTIFACT_INVALID';\n\n/**\n * A generic, recoverable snapshot-helper failure. Callers in `auto` mode may\n * fall back to `uiautomator dump` after catching one of these.\n */\nexport class SnapshotHelperError extends Error {\n public readonly code: SnapshotHelperErrorCode;\n\n /**\n * @param code - Machine-readable failure category.\n * @param message - Human-readable description.\n * @param options - Standard error options (e.g. `cause`).\n */\n public constructor(\n code: SnapshotHelperErrorCode,\n message: string,\n options?: ErrorOptions,\n ) {\n super(message, options);\n this.name = 'SnapshotHelperError';\n this.code = code;\n // Restore prototype chain for `instanceof` across the dual ESM/CJS build.\n Object.setPrototypeOf(this, SnapshotHelperError.prototype);\n }\n}\n\n/**\n * A trust failure: the installed helper is not signed by our pinned key.\n *\n * This is a hard stop. It must propagate out of the snapshot flow — never be\n * converted to `null`, never be collected into a generic failure list, and\n * never trigger a `uiautomator dump` fallback.\n */\nexport class UntrustedHelperError extends Error {\n /** The signer SHA-256 we required. */\n public readonly expectedSignerSha256: string;\n\n /** The signer SHA-256 actually found on the device, if one was extracted. */\n public readonly actualSignerSha256?: string;\n\n /**\n * @param message - Human-readable description of the mismatch.\n * @param details - The expected and (optionally) observed signer digests.\n * @param details.expectedSignerSha256 - The pinned signer SHA-256.\n * @param details.actualSignerSha256 - The observed signer SHA-256, if known.\n * @param options - Standard error options (e.g. `cause`).\n */\n public constructor(\n message: string,\n details: {\n expectedSignerSha256: string;\n actualSignerSha256?: string;\n },\n options?: ErrorOptions,\n ) {\n super(message, options);\n this.name = 'UntrustedHelperError';\n this.expectedSignerSha256 = details.expectedSignerSha256;\n this.actualSignerSha256 = details.actualSignerSha256;\n // Restore prototype chain for `instanceof` across the dual ESM/CJS build.\n Object.setPrototypeOf(this, UntrustedHelperError.prototype);\n }\n}\n\n/**\n * Type guard for the fail-closed trust error. The ADB backend uses this to\n * decide whether an instrumentation failure may fall back to `dump` (generic)\n * or must propagate (trust).\n *\n * @param error - The value to test.\n * @returns True when `error` is an {@link UntrustedHelperError}.\n */\nexport function isUntrustedHelperError(\n error: unknown,\n): error is UntrustedHelperError {\n return error instanceof UntrustedHelperError;\n}\n"]}
@@ -0,0 +1,235 @@
1
+ "use strict";
2
+ /**
3
+ * Installs and TRUSTS the bundled Android snapshot-helper instrumentation APK.
4
+ *
5
+ * The APK is built during `prepack` into `dist/android/` and shipped with the
6
+ * npm package (`files: ["dist"]`). Before the ADB backend runs `am instrument`
7
+ * it must guarantee that the package on the device is OUR helper — not a
8
+ * malicious app squatting the package name. Package name and versionCode are
9
+ * attacker-controlled metadata; the only trust anchor is the signing
10
+ * certificate. So `ensureHelperTrusted` installs/upgrades by versionCode
11
+ * (freshness only) and then UNCONDITIONALLY verifies the installed APK's v2/v3
12
+ * signer against the pinned SHA-256 from the build manifest. A mismatch throws
13
+ * {@link UntrustedHelperError} and the caller must fail closed (no dump
14
+ * fallback) — see `adb-backend.ts`.
15
+ *
16
+ * `__dirname` is used for path resolution because it is portable across the
17
+ * dual ESM/CJS build: ts-bridge rewrites it to an `import.meta.url`-based shim
18
+ * in the `.mjs` output and leaves the native binding in the `.cjs` output.
19
+ *
20
+ * Version/policy/signer-verify flow adapted from agent-device (MIT, Copyright
21
+ * (c) Callstack) onto our source-built APK and pure-JS signer verifier.
22
+ */
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.INSTRUMENTATION_NOT_FOUND_SIGNATURE = void 0;
25
+ exports.resolveHelperApkPath = resolveHelperApkPath;
26
+ exports.isHelperInstalled = isHelperInstalled;
27
+ exports.installHelper = installHelper;
28
+ exports.verifyInstalledSigner = verifyInstalledSigner;
29
+ exports.ensureHelperInstalled = ensureHelperInstalled;
30
+ exports.assertInstalledHelperTrusted = assertInstalledHelperTrusted;
31
+ exports.ensureHelperTrusted = ensureHelperTrusted;
32
+ const node_fs_1 = require("node:fs");
33
+ const node_path_1 = require("node:path");
34
+ const adb_js_1 = require("./adb.cjs");
35
+ const artifact_js_1 = require("./artifact.cjs");
36
+ const errors_js_1 = require("./errors.cjs");
37
+ const signer_js_1 = require("./signer.cjs");
38
+ const snapshot_js_1 = require("./snapshot.cjs");
39
+ const exec_js_1 = require("../../utils/exec.cjs");
40
+ const output_path_js_1 = require("../../utils/output-path.cjs");
41
+ const INSTALL_TIMEOUT_MS = 60000;
42
+ const QUERY_TIMEOUT_MS = 10000;
43
+ const PULL_TIMEOUT_MS = 30000;
44
+ /**
45
+ * `am instrument` prints this when the target instrumentation is not installed.
46
+ * The ADB backend keys reactive re-installs off this signature.
47
+ */
48
+ exports.INSTRUMENTATION_NOT_FOUND_SIGNATURE = 'INSTRUMENTATION_FAILED';
49
+ /**
50
+ * Resolve the path to the bundled helper APK (validated against its manifest).
51
+ *
52
+ * @returns The absolute path to the APK.
53
+ * @throws When the bundled artifact is missing, malformed, or its digest does
54
+ * not match the manifest.
55
+ */
56
+ async function resolveHelperApkPath() {
57
+ const artifact = await (0, artifact_js_1.loadHelperArtifact)();
58
+ return artifact.apkPath;
59
+ }
60
+ /**
61
+ * Check whether the helper package is present on the device.
62
+ *
63
+ * @param serial - The target device serial.
64
+ * @returns True when the helper package is installed (any signer).
65
+ */
66
+ async function isHelperInstalled(serial) {
67
+ const result = await (0, exec_js_1.exec)('adb', ['-s', serial, 'shell', 'pm', 'list', 'packages', snapshot_js_1.HELPER_PACKAGE], { timeoutMs: QUERY_TIMEOUT_MS });
68
+ return result.stdout.includes(`package:${snapshot_js_1.HELPER_PACKAGE}`);
69
+ }
70
+ /**
71
+ * Read the installed helper's versionCode via `dumpsys package`.
72
+ *
73
+ * versionCode is attacker-controlled metadata: it is used ONLY to decide
74
+ * whether to (re)install a fresher build, never as a trust signal.
75
+ *
76
+ * @param serial - The target device serial.
77
+ * @returns The installed versionCode, or null when not installed / unreadable.
78
+ */
79
+ async function readInstalledVersionCode(serial) {
80
+ const result = await (0, exec_js_1.exec)('adb', ['-s', serial, 'shell', 'dumpsys', 'package', snapshot_js_1.HELPER_PACKAGE], { timeoutMs: QUERY_TIMEOUT_MS });
81
+ const match = /versionCode=(\d+)/u.exec(result.stdout);
82
+ return match ? Number(match[1]) : null;
83
+ }
84
+ /**
85
+ * Android install-result tokens that specifically indicate the already-
86
+ * installed same-package app was signed by a DIFFERENT certificate, so the
87
+ * reinstall was refused on trust grounds (not a generic install failure). These
88
+ * constant names are stable across API 23-35 and are printed by PackageManager
89
+ * in the `Failure [<TOKEN>: ...]` line (on stdout; we scan both streams).
90
+ *
91
+ * Deliberately excludes generic failures like INSTALL_FAILED_TEST_ONLY,
92
+ * INSTALL_FAILED_VERSION_DOWNGRADE, INSTALL_FAILED_DUPLICATE_PERMISSION, and
93
+ * the parse-time no/inconsistent-certificate codes: none of those mean a
94
+ * squatter is signed by a different key on a normal reinstall.
95
+ */
96
+ const SIGNER_CONFLICT_TOKENS = [
97
+ 'INSTALL_FAILED_UPDATE_INCOMPATIBLE',
98
+ 'INSTALL_FAILED_SHARED_USER_INCOMPATIBLE',
99
+ ];
100
+ /**
101
+ * Install the bundled helper APK onto the device.
102
+ *
103
+ * `-r` reinstalls over an existing copy signed by the SAME key and `-t` allows
104
+ * the `testOnly` APK. It CANNOT overwrite a package signed by a different key:
105
+ * Android refuses with `INSTALL_FAILED_UPDATE_INCOMPATIBLE` (or
106
+ * `INSTALL_FAILED_SHARED_USER_INCOMPATIBLE`). That is a squatter signed by a
107
+ * foreign certificate, so it is a TRUST failure, not a generic one — we throw
108
+ * {@link UntrustedHelperError} to make the caller fail closed, consistent with
109
+ * the equal/higher-versionCode squatter path (which fails closed via the signer
110
+ * verification). Without this, a lower-versionCode squatter would surface as a
111
+ * generic Error and `auto` mode would silently fall back to `uiautomator dump`.
112
+ *
113
+ * @param serial - The target device serial.
114
+ * @param artifact - The bundled artifact carrying the pinned signer SHA-256,
115
+ * used both to locate the APK and to describe a trust failure.
116
+ * @throws {@link UntrustedHelperError} when the install is refused because the
117
+ * installed package is signed by a different certificate.
118
+ * @throws When the install command fails for a generic reason.
119
+ */
120
+ async function installHelper(serial, artifact) {
121
+ const resolved = artifact ?? (await (0, artifact_js_1.loadHelperArtifact)());
122
+ const { apkPath } = resolved;
123
+ const result = await (0, exec_js_1.exec)('adb', ['-s', serial, 'install', '-r', '-t', apkPath], { timeoutMs: INSTALL_TIMEOUT_MS });
124
+ if (result.exitCode !== 0 || !result.stdout.includes('Success')) {
125
+ const output = `${result.stdout.trim()} ${result.stderr.trim()}`.trim();
126
+ if (SIGNER_CONFLICT_TOKENS.some((token) => output.includes(token))) {
127
+ throw new errors_js_1.UntrustedHelperError(`Snapshot helper install was refused because the installed package is ` +
128
+ `signed by a different certificate (a squatter). ${output}`, { expectedSignerSha256: resolved.signerSha256 });
129
+ }
130
+ throw new Error(`Failed to install snapshot helper APK (${apkPath}).\n${output}`);
131
+ }
132
+ }
133
+ /**
134
+ * Pull every installed APK path for the helper package (base + splits) into a
135
+ * private temp dir and verify each is signed by the pinned certificate.
136
+ *
137
+ * @param serial - The target device serial.
138
+ * @param artifact - The bundled artifact carrying the pinned signer SHA-256.
139
+ * @throws {@link UntrustedHelperError} when the installed signer does not match
140
+ * the pin.
141
+ */
142
+ async function verifyInstalledSigner(serial, artifact) {
143
+ const run = (0, adb_js_1.createAndroidAdbExecutor)(serial);
144
+ const pathsResult = await run(['shell', 'pm', 'path', snapshot_js_1.HELPER_PACKAGE], {
145
+ timeoutMs: QUERY_TIMEOUT_MS,
146
+ });
147
+ const remotePaths = pathsResult.stdout
148
+ .split('\n')
149
+ .map((line) => line.trim())
150
+ .filter((line) => line.startsWith('package:'))
151
+ .map((line) => line.slice('package:'.length));
152
+ if (remotePaths.length === 0) {
153
+ throw new errors_js_1.UntrustedHelperError('Snapshot helper is installed but its APK path could not be resolved', { expectedSignerSha256: artifact.signerSha256 });
154
+ }
155
+ const tempDir = (0, output_path_js_1.createPrivateTempDir)('helper-verify');
156
+ try {
157
+ let index = 0;
158
+ for (const remotePath of remotePaths) {
159
+ const localPath = (0, node_path_1.join)(tempDir, `apk-${index}.apk`);
160
+ index += 1;
161
+ const pull = await run(['pull', remotePath, localPath], {
162
+ timeoutMs: PULL_TIMEOUT_MS,
163
+ });
164
+ if (pull.exitCode !== 0) {
165
+ throw new errors_js_1.UntrustedHelperError(`Could not pull installed helper APK for verification (${remotePath})`, { expectedSignerSha256: artifact.signerSha256 });
166
+ }
167
+ await (0, signer_js_1.assertApkSignerSha256)(localPath, artifact.signerSha256);
168
+ }
169
+ }
170
+ finally {
171
+ (0, node_fs_1.rmSync)(tempDir, { recursive: true, force: true });
172
+ }
173
+ }
174
+ /**
175
+ * Install/upgrade the bundled helper by versionCode (freshness only).
176
+ *
177
+ * versionCode is attacker-controlled metadata, so this is NOT a trust decision:
178
+ * it only decides whether a fresher build should be pushed. It performs NO
179
+ * signer verification. Callers MUST separately call
180
+ * {@link assertInstalledHelperTrusted} before every use of the helper.
181
+ *
182
+ * This is safe to gate behind a per-process "installed" cache because a stale
183
+ * cache at worst skips a redundant (idempotent) install; it never skips a trust
184
+ * check.
185
+ *
186
+ * @param serial - The target device serial.
187
+ * @throws When install fails for a generic reason.
188
+ */
189
+ async function ensureHelperInstalled(serial) {
190
+ const artifact = await (0, artifact_js_1.loadHelperArtifact)();
191
+ const installedVersion = await readInstalledVersionCode(serial);
192
+ if (installedVersion === null || installedVersion < artifact.versionCode) {
193
+ await installHelper(serial, artifact);
194
+ }
195
+ }
196
+ /**
197
+ * Verify that the helper currently installed on the device is signed by the
198
+ * pinned certificate. This is the trust invariant and MUST run before EVERY
199
+ * `am instrument`, never cached: package name and versionCode are
200
+ * attacker-controlled, so an actor who can install packages could replace a
201
+ * previously-trusted helper with a malicious same-package instrumentation
202
+ * between snapshots. Re-verifying the installed signer on every use (pm path +
203
+ * pull + hash — cheap relative to the instrumentation run) is what keeps the
204
+ * signature pin a per-use invariant instead of a one-time check.
205
+ *
206
+ * @param serial - The target device serial.
207
+ * @throws {@link UntrustedHelperError} when the installed helper is signed by
208
+ * an unexpected certificate (a squatter). The caller MUST NOT fall back to
209
+ * `uiautomator dump` in that case.
210
+ */
211
+ async function assertInstalledHelperTrusted(serial) {
212
+ const artifact = await (0, artifact_js_1.loadHelperArtifact)();
213
+ await verifyInstalledSigner(serial, artifact);
214
+ }
215
+ /**
216
+ * Ensure a TRUSTED helper is installed: install/upgrade by versionCode
217
+ * (freshness), then unconditionally verify the installed signer against the
218
+ * pinned certificate. Fails closed on a signer mismatch.
219
+ *
220
+ * Convenience wrapper over {@link ensureHelperInstalled} +
221
+ * {@link assertInstalledHelperTrusted}. Callers that cache the install step
222
+ * MUST still call {@link assertInstalledHelperTrusted} on every use rather than
223
+ * caching this whole call.
224
+ *
225
+ * @param serial - The target device serial.
226
+ * @throws {@link UntrustedHelperError} when the installed helper is signed by
227
+ * an unexpected certificate (a squatter). The caller MUST NOT fall back to
228
+ * `uiautomator dump` in that case.
229
+ * @throws When install fails for a generic reason.
230
+ */
231
+ async function ensureHelperTrusted(serial) {
232
+ await ensureHelperInstalled(serial);
233
+ await assertInstalledHelperTrusted(serial);
234
+ }
235
+ //# sourceMappingURL=installer.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"installer.cjs","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"]}
@@ -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.cjs";
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.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"installer.d.cts","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"}