@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.
- package/CHANGELOG.md +18 -1
- package/README.md +110 -21
- package/dist/android/NOTICE.md +65 -0
- package/dist/android/device-mcp-android-snapshot-helper-0.4.0.apk +0 -0
- package/dist/android/device-mcp-android-snapshot-helper-0.4.0.apk.idsig +0 -0
- package/dist/android/device-mcp-android-snapshot-helper-0.4.0.manifest.json +10 -0
- package/dist/backends/adb-backend.cjs +326 -17
- package/dist/backends/adb-backend.cjs.map +1 -1
- package/dist/backends/adb-backend.d.cts +1 -0
- package/dist/backends/adb-backend.d.cts.map +1 -1
- package/dist/backends/adb-backend.d.mts +1 -0
- package/dist/backends/adb-backend.d.mts.map +1 -1
- package/dist/backends/adb-backend.mjs +327 -18
- package/dist/backends/adb-backend.mjs.map +1 -1
- package/dist/backends/android-instrumentation/adb.cjs +28 -0
- package/dist/backends/android-instrumentation/adb.cjs.map +1 -0
- package/dist/backends/android-instrumentation/adb.d.cts +24 -0
- package/dist/backends/android-instrumentation/adb.d.cts.map +1 -0
- package/dist/backends/android-instrumentation/adb.d.mts +24 -0
- package/dist/backends/android-instrumentation/adb.d.mts.map +1 -0
- package/dist/backends/android-instrumentation/adb.mjs +25 -0
- package/dist/backends/android-instrumentation/adb.mjs.map +1 -0
- package/dist/backends/android-instrumentation/artifact.cjs +139 -0
- package/dist/backends/android-instrumentation/artifact.cjs.map +1 -0
- package/dist/backends/android-instrumentation/artifact.d.cts +28 -0
- package/dist/backends/android-instrumentation/artifact.d.cts.map +1 -0
- package/dist/backends/android-instrumentation/artifact.d.mts +28 -0
- package/dist/backends/android-instrumentation/artifact.d.mts.map +1 -0
- package/dist/backends/android-instrumentation/artifact.mjs +154 -0
- package/dist/backends/android-instrumentation/artifact.mjs.map +1 -0
- package/dist/backends/android-instrumentation/errors.cjs +79 -0
- package/dist/backends/android-instrumentation/errors.cjs.map +1 -0
- package/dist/backends/android-instrumentation/errors.d.cts +68 -0
- package/dist/backends/android-instrumentation/errors.d.cts.map +1 -0
- package/dist/backends/android-instrumentation/errors.d.mts +68 -0
- package/dist/backends/android-instrumentation/errors.d.mts.map +1 -0
- package/dist/backends/android-instrumentation/errors.mjs +73 -0
- package/dist/backends/android-instrumentation/errors.mjs.map +1 -0
- package/dist/backends/android-instrumentation/installer.cjs +235 -0
- package/dist/backends/android-instrumentation/installer.cjs.map +1 -0
- package/dist/backends/android-instrumentation/installer.d.cts +123 -0
- package/dist/backends/android-instrumentation/installer.d.cts.map +1 -0
- package/dist/backends/android-instrumentation/installer.d.mts +123 -0
- package/dist/backends/android-instrumentation/installer.d.mts.map +1 -0
- package/dist/backends/android-instrumentation/installer.mjs +225 -0
- package/dist/backends/android-instrumentation/installer.mjs.map +1 -0
- package/dist/backends/android-instrumentation/signer.cjs +435 -0
- package/dist/backends/android-instrumentation/signer.cjs.map +1 -0
- package/dist/backends/android-instrumentation/signer.d.cts +40 -0
- package/dist/backends/android-instrumentation/signer.d.cts.map +1 -0
- package/dist/backends/android-instrumentation/signer.d.mts +40 -0
- package/dist/backends/android-instrumentation/signer.d.mts.map +1 -0
- package/dist/backends/android-instrumentation/signer.mjs +431 -0
- package/dist/backends/android-instrumentation/signer.mjs.map +1 -0
- package/dist/backends/android-instrumentation/snapshot.cjs +159 -0
- package/dist/backends/android-instrumentation/snapshot.cjs.map +1 -0
- package/dist/backends/android-instrumentation/snapshot.d.cts +67 -0
- package/dist/backends/android-instrumentation/snapshot.d.cts.map +1 -0
- package/dist/backends/android-instrumentation/snapshot.d.mts +67 -0
- package/dist/backends/android-instrumentation/snapshot.d.mts.map +1 -0
- package/dist/backends/android-instrumentation/snapshot.mjs +154 -0
- package/dist/backends/android-instrumentation/snapshot.mjs.map +1 -0
- package/dist/backends/appium-backend.cjs +19 -1
- package/dist/backends/appium-backend.cjs.map +1 -1
- package/dist/backends/appium-backend.d.cts +1 -0
- package/dist/backends/appium-backend.d.cts.map +1 -1
- package/dist/backends/appium-backend.d.mts +1 -0
- package/dist/backends/appium-backend.d.mts.map +1 -1
- package/dist/backends/appium-backend.mjs +19 -1
- package/dist/backends/appium-backend.mjs.map +1 -1
- package/dist/backends/idb-backend.cjs +128 -31
- package/dist/backends/idb-backend.cjs.map +1 -1
- package/dist/backends/idb-backend.d.cts +1 -0
- package/dist/backends/idb-backend.d.cts.map +1 -1
- package/dist/backends/idb-backend.d.mts +1 -0
- package/dist/backends/idb-backend.d.mts.map +1 -1
- package/dist/backends/idb-backend.mjs +129 -32
- package/dist/backends/idb-backend.mjs.map +1 -1
- package/dist/backends/index.cjs +3 -0
- package/dist/backends/index.cjs.map +1 -1
- package/dist/backends/index.d.cts.map +1 -1
- package/dist/backends/index.d.mts.map +1 -1
- package/dist/backends/index.mjs +3 -0
- package/dist/backends/index.mjs.map +1 -1
- package/dist/backends/types.cjs.map +1 -1
- package/dist/backends/types.d.cts +6 -0
- package/dist/backends/types.d.cts.map +1 -1
- package/dist/backends/types.d.mts +6 -0
- package/dist/backends/types.d.mts.map +1 -1
- package/dist/backends/types.mjs.map +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/tools/swipe.cjs +3 -1
- package/dist/tools/swipe.cjs.map +1 -1
- package/dist/tools/swipe.d.cts.map +1 -1
- package/dist/tools/swipe.d.mts.map +1 -1
- package/dist/tools/swipe.mjs +3 -1
- package/dist/tools/swipe.mjs.map +1 -1
- package/dist/utils/output-path.cjs +13 -0
- package/dist/utils/output-path.cjs.map +1 -1
- package/dist/utils/output-path.d.cts +10 -0
- package/dist/utils/output-path.d.cts.map +1 -1
- package/dist/utils/output-path.d.mts +10 -0
- package/dist/utils/output-path.d.mts.map +1 -1
- package/dist/utils/output-path.mjs +12 -0
- package/dist/utils/output-path.mjs.map +1 -1
- package/dist/utils/platform.cjs +65 -7
- package/dist/utils/platform.cjs.map +1 -1
- package/dist/utils/platform.d.cts +33 -0
- package/dist/utils/platform.d.cts.map +1 -1
- package/dist/utils/platform.d.mts +33 -0
- package/dist/utils/platform.d.mts.map +1 -1
- package/dist/utils/platform.mjs +63 -7
- package/dist/utils/platform.mjs.map +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adb.cjs","sourceRoot":"","sources":["../../../src/backends/android-instrumentation/adb.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;AAwBH,4DAMC;AA5BD,kDAA2C;AAa3C,MAAM,kBAAkB,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AAE5C;;;;;;GAMG;AACH,SAAgB,wBAAwB,CAAC,MAAc;IACrD,OAAO,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,CAC7B,IAAA,cAAI,EAAC,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC,EAAE;QACnC,SAAS,EAAE,OAAO,EAAE,SAAS;QAC7B,SAAS,EAAE,OAAO,EAAE,SAAS,IAAI,kBAAkB;KACpD,CAAC,CAAC;AACP,CAAC","sourcesContent":["/**\n * Device-scoped ADB executor used by the snapshot-helper trust + install\n * modules. Wrapping `exec('adb', ['-s', serial, ...])` behind a small function\n * keeps the serial in one place and lets those modules be unit-tested with an\n * injected fake executor instead of spawning real `adb`.\n *\n * The executor abstraction is adapted from agent-device (MIT, Copyright (c)\n * Callstack); our `ExecResult` already matches its result shape.\n */\n\nimport { exec } from '../../utils/exec.js';\nimport type { ExecResult } from '../../utils/exec.js';\n\nexport type AndroidAdbExecOptions = {\n timeoutMs?: number;\n maxBuffer?: number;\n};\n\nexport type AndroidAdbExecutor = (\n args: string[],\n options?: AndroidAdbExecOptions,\n) => Promise<ExecResult>;\n\nconst DEFAULT_MAX_BUFFER = 16 * 1024 * 1024;\n\n/**\n * Create an ADB executor bound to a single device serial. Every call is\n * prefixed with `-s <serial>` so callers pass only the sub-command.\n *\n * @param serial - The target device serial.\n * @returns An executor that runs `adb -s <serial> <args...>`.\n */\nexport function createAndroidAdbExecutor(serial: string): AndroidAdbExecutor {\n return async (args, options) =>\n exec('adb', ['-s', serial, ...args], {\n timeoutMs: options?.timeoutMs,\n maxBuffer: options?.maxBuffer ?? DEFAULT_MAX_BUFFER,\n });\n}\n"]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device-scoped ADB executor used by the snapshot-helper trust + install
|
|
3
|
+
* modules. Wrapping `exec('adb', ['-s', serial, ...])` behind a small function
|
|
4
|
+
* keeps the serial in one place and lets those modules be unit-tested with an
|
|
5
|
+
* injected fake executor instead of spawning real `adb`.
|
|
6
|
+
*
|
|
7
|
+
* The executor abstraction is adapted from agent-device (MIT, Copyright (c)
|
|
8
|
+
* Callstack); our `ExecResult` already matches its result shape.
|
|
9
|
+
*/
|
|
10
|
+
import type { ExecResult } from "../../utils/exec.cjs";
|
|
11
|
+
export type AndroidAdbExecOptions = {
|
|
12
|
+
timeoutMs?: number;
|
|
13
|
+
maxBuffer?: number;
|
|
14
|
+
};
|
|
15
|
+
export type AndroidAdbExecutor = (args: string[], options?: AndroidAdbExecOptions) => Promise<ExecResult>;
|
|
16
|
+
/**
|
|
17
|
+
* Create an ADB executor bound to a single device serial. Every call is
|
|
18
|
+
* prefixed with `-s <serial>` so callers pass only the sub-command.
|
|
19
|
+
*
|
|
20
|
+
* @param serial - The target device serial.
|
|
21
|
+
* @returns An executor that runs `adb -s <serial> <args...>`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function createAndroidAdbExecutor(serial: string): AndroidAdbExecutor;
|
|
24
|
+
//# sourceMappingURL=adb.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adb.d.cts","sourceRoot":"","sources":["../../../src/backends/android-instrumentation/adb.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,6BAA4B;AAEtD,MAAM,MAAM,qBAAqB,GAAG;IAClC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,CAC/B,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,CAAC,EAAE,qBAAqB,KAC5B,OAAO,CAAC,UAAU,CAAC,CAAC;AAIzB;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,MAAM,GAAG,kBAAkB,CAM3E"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device-scoped ADB executor used by the snapshot-helper trust + install
|
|
3
|
+
* modules. Wrapping `exec('adb', ['-s', serial, ...])` behind a small function
|
|
4
|
+
* keeps the serial in one place and lets those modules be unit-tested with an
|
|
5
|
+
* injected fake executor instead of spawning real `adb`.
|
|
6
|
+
*
|
|
7
|
+
* The executor abstraction is adapted from agent-device (MIT, Copyright (c)
|
|
8
|
+
* Callstack); our `ExecResult` already matches its result shape.
|
|
9
|
+
*/
|
|
10
|
+
import type { ExecResult } from "../../utils/exec.mjs";
|
|
11
|
+
export type AndroidAdbExecOptions = {
|
|
12
|
+
timeoutMs?: number;
|
|
13
|
+
maxBuffer?: number;
|
|
14
|
+
};
|
|
15
|
+
export type AndroidAdbExecutor = (args: string[], options?: AndroidAdbExecOptions) => Promise<ExecResult>;
|
|
16
|
+
/**
|
|
17
|
+
* Create an ADB executor bound to a single device serial. Every call is
|
|
18
|
+
* prefixed with `-s <serial>` so callers pass only the sub-command.
|
|
19
|
+
*
|
|
20
|
+
* @param serial - The target device serial.
|
|
21
|
+
* @returns An executor that runs `adb -s <serial> <args...>`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function createAndroidAdbExecutor(serial: string): AndroidAdbExecutor;
|
|
24
|
+
//# sourceMappingURL=adb.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adb.d.mts","sourceRoot":"","sources":["../../../src/backends/android-instrumentation/adb.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,6BAA4B;AAEtD,MAAM,MAAM,qBAAqB,GAAG;IAClC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,CAC/B,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,CAAC,EAAE,qBAAqB,KAC5B,OAAO,CAAC,UAAU,CAAC,CAAC;AAIzB;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,MAAM,GAAG,kBAAkB,CAM3E"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device-scoped ADB executor used by the snapshot-helper trust + install
|
|
3
|
+
* modules. Wrapping `exec('adb', ['-s', serial, ...])` behind a small function
|
|
4
|
+
* keeps the serial in one place and lets those modules be unit-tested with an
|
|
5
|
+
* injected fake executor instead of spawning real `adb`.
|
|
6
|
+
*
|
|
7
|
+
* The executor abstraction is adapted from agent-device (MIT, Copyright (c)
|
|
8
|
+
* Callstack); our `ExecResult` already matches its result shape.
|
|
9
|
+
*/
|
|
10
|
+
import { exec } from "../../utils/exec.mjs";
|
|
11
|
+
const DEFAULT_MAX_BUFFER = 16 * 1024 * 1024;
|
|
12
|
+
/**
|
|
13
|
+
* Create an ADB executor bound to a single device serial. Every call is
|
|
14
|
+
* prefixed with `-s <serial>` so callers pass only the sub-command.
|
|
15
|
+
*
|
|
16
|
+
* @param serial - The target device serial.
|
|
17
|
+
* @returns An executor that runs `adb -s <serial> <args...>`.
|
|
18
|
+
*/
|
|
19
|
+
export function createAndroidAdbExecutor(serial) {
|
|
20
|
+
return async (args, options) => exec('adb', ['-s', serial, ...args], {
|
|
21
|
+
timeoutMs: options?.timeoutMs,
|
|
22
|
+
maxBuffer: options?.maxBuffer ?? DEFAULT_MAX_BUFFER,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=adb.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adb.mjs","sourceRoot":"","sources":["../../../src/backends/android-instrumentation/adb.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,IAAI,EAAE,6BAA4B;AAa3C,MAAM,kBAAkB,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AAE5C;;;;;;GAMG;AACH,MAAM,UAAU,wBAAwB,CAAC,MAAc;IACrD,OAAO,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,CAC7B,IAAI,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC,EAAE;QACnC,SAAS,EAAE,OAAO,EAAE,SAAS;QAC7B,SAAS,EAAE,OAAO,EAAE,SAAS,IAAI,kBAAkB;KACpD,CAAC,CAAC;AACP,CAAC","sourcesContent":["/**\n * Device-scoped ADB executor used by the snapshot-helper trust + install\n * modules. Wrapping `exec('adb', ['-s', serial, ...])` behind a small function\n * keeps the serial in one place and lets those modules be unit-tested with an\n * injected fake executor instead of spawning real `adb`.\n *\n * The executor abstraction is adapted from agent-device (MIT, Copyright (c)\n * Callstack); our `ExecResult` already matches its result shape.\n */\n\nimport { exec } from '../../utils/exec.js';\nimport type { ExecResult } from '../../utils/exec.js';\n\nexport type AndroidAdbExecOptions = {\n timeoutMs?: number;\n maxBuffer?: number;\n};\n\nexport type AndroidAdbExecutor = (\n args: string[],\n options?: AndroidAdbExecOptions,\n) => Promise<ExecResult>;\n\nconst DEFAULT_MAX_BUFFER = 16 * 1024 * 1024;\n\n/**\n * Create an ADB executor bound to a single device serial. Every call is\n * prefixed with `-s <serial>` so callers pass only the sub-command.\n *\n * @param serial - The target device serial.\n * @returns An executor that runs `adb -s <serial> <args...>`.\n */\nexport function createAndroidAdbExecutor(serial: string): AndroidAdbExecutor {\n return async (args, options) =>\n exec('adb', ['-s', serial, ...args], {\n timeoutMs: options?.timeoutMs,\n maxBuffer: options?.maxBuffer ?? DEFAULT_MAX_BUFFER,\n });\n}\n"]}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Derived from agent-device (MIT, Copyright (c) Callstack); adapted for our
|
|
3
|
+
// source-built helper APK and package constants.
|
|
4
|
+
/**
|
|
5
|
+
* Reads and validates the provenance manifest that `build-android-helper.sh`
|
|
6
|
+
* emits next to the bundled helper APK. The manifest is the single source of
|
|
7
|
+
* truth for the package name, instrumentation runner, versionCode, the APK
|
|
8
|
+
* file digest (a tamper check on `dist/`), and the pinned signer certificate
|
|
9
|
+
* SHA-256 that the installed helper must match before we run `am instrument`.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.loadHelperArtifact = loadHelperArtifact;
|
|
13
|
+
const node_crypto_1 = require("node:crypto");
|
|
14
|
+
const promises_1 = require("node:fs/promises");
|
|
15
|
+
const node_path_1 = require("node:path");
|
|
16
|
+
const errors_js_1 = require("./errors.cjs");
|
|
17
|
+
const snapshot_js_1 = require("./snapshot.cjs");
|
|
18
|
+
// The compiled module lives at dist/backends/android-instrumentation/, so the
|
|
19
|
+
// bundled helper APK directory (dist/android) is two levels up.
|
|
20
|
+
const HELPER_APK_DIR = (0, node_path_1.join)(__dirname, '..', '..', 'android');
|
|
21
|
+
const HELPER_APK_PREFIX = 'device-mcp-android-snapshot-helper-';
|
|
22
|
+
const MANIFEST_SUFFIX = '.manifest.json';
|
|
23
|
+
/**
|
|
24
|
+
* Raise an {@link SnapshotHelperError} for a missing or malformed artifact.
|
|
25
|
+
*
|
|
26
|
+
* @param message - What went wrong.
|
|
27
|
+
* @throws Always.
|
|
28
|
+
*/
|
|
29
|
+
function invalid(message) {
|
|
30
|
+
throw new errors_js_1.SnapshotHelperError('ARTIFACT_INVALID', message);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Assert a manifest field is a non-empty string.
|
|
34
|
+
*
|
|
35
|
+
* @param value - The parsed field value.
|
|
36
|
+
* @param field - The field name, for error messages.
|
|
37
|
+
* @returns The value, narrowed to string.
|
|
38
|
+
*/
|
|
39
|
+
function requireString(value, field) {
|
|
40
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
41
|
+
invalid(`Helper manifest field "${field}" is missing or not a string`);
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Assert a manifest field is a finite non-negative integer.
|
|
47
|
+
*
|
|
48
|
+
* @param value - The parsed field value.
|
|
49
|
+
* @param field - The field name, for error messages.
|
|
50
|
+
* @returns The value, narrowed to number.
|
|
51
|
+
*/
|
|
52
|
+
function requireInt(value, field) {
|
|
53
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {
|
|
54
|
+
invalid(`Helper manifest field "${field}" is missing or not an integer`);
|
|
55
|
+
}
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Locate the helper manifest in the bundled `dist/android` directory.
|
|
60
|
+
*
|
|
61
|
+
* @returns The absolute path to the newest matching manifest.
|
|
62
|
+
* @throws {@link SnapshotHelperError} when the directory or manifest is absent.
|
|
63
|
+
*/
|
|
64
|
+
async function resolveManifestPath() {
|
|
65
|
+
let entries;
|
|
66
|
+
try {
|
|
67
|
+
entries = await (0, promises_1.readdir)(HELPER_APK_DIR);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return invalid(`Snapshot helper directory is missing (${HELPER_APK_DIR}). ` +
|
|
71
|
+
'The package may have been built without the Android helper.');
|
|
72
|
+
}
|
|
73
|
+
const manifest = entries
|
|
74
|
+
.filter((name) => name.startsWith(HELPER_APK_PREFIX) && name.endsWith(MANIFEST_SUFFIX))
|
|
75
|
+
.sort()
|
|
76
|
+
.at(-1);
|
|
77
|
+
if (!manifest) {
|
|
78
|
+
return invalid(`No snapshot helper manifest found in ${HELPER_APK_DIR}. ` +
|
|
79
|
+
'The package may have been built without the Android helper.');
|
|
80
|
+
}
|
|
81
|
+
return (0, node_path_1.join)(HELPER_APK_DIR, manifest);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Compute the SHA-256 of a file as lowercase hex.
|
|
85
|
+
*
|
|
86
|
+
* @param path - The file to digest.
|
|
87
|
+
* @returns The lowercase-hex SHA-256.
|
|
88
|
+
*/
|
|
89
|
+
async function sha256File(path) {
|
|
90
|
+
const bytes = await (0, promises_1.readFile)(path);
|
|
91
|
+
return (0, node_crypto_1.createHash)('sha256').update(bytes).digest('hex');
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Load and validate the bundled helper artifact: parse the manifest, confirm
|
|
95
|
+
* it targets our package, resolve the APK path, and verify the APK's SHA-256
|
|
96
|
+
* matches the manifest (a tamper check on the shipped `dist/`).
|
|
97
|
+
*
|
|
98
|
+
* @returns The validated artifact descriptor.
|
|
99
|
+
* @throws {@link SnapshotHelperError} when anything is missing, malformed, or
|
|
100
|
+
* the APK digest does not match the manifest.
|
|
101
|
+
*/
|
|
102
|
+
async function loadHelperArtifact() {
|
|
103
|
+
const manifestPath = await resolveManifestPath();
|
|
104
|
+
let parsed;
|
|
105
|
+
try {
|
|
106
|
+
parsed = JSON.parse(await (0, promises_1.readFile)(manifestPath, 'utf8'));
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
return invalid(`Helper manifest is not valid JSON (${manifestPath}): ${String(error)}`);
|
|
110
|
+
}
|
|
111
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
112
|
+
return invalid(`Helper manifest is not an object (${manifestPath})`);
|
|
113
|
+
}
|
|
114
|
+
const manifest = parsed;
|
|
115
|
+
const packageName = requireString(manifest.packageName, 'packageName');
|
|
116
|
+
if (packageName !== snapshot_js_1.HELPER_PACKAGE) {
|
|
117
|
+
invalid(`Helper manifest packageName "${packageName}" does not match the ` +
|
|
118
|
+
`expected "${snapshot_js_1.HELPER_PACKAGE}"`);
|
|
119
|
+
}
|
|
120
|
+
const assetName = requireString(manifest.assetName, 'assetName');
|
|
121
|
+
const sha256 = requireString(manifest.sha256, 'sha256');
|
|
122
|
+
const apkPath = (0, node_path_1.join)(HELPER_APK_DIR, assetName);
|
|
123
|
+
const actualSha256 = await sha256File(apkPath).catch(() => invalid(`Helper APK is missing or unreadable (${apkPath})`));
|
|
124
|
+
if (actualSha256 !== sha256.toLowerCase()) {
|
|
125
|
+
invalid(`Helper APK digest does not match its manifest (${apkPath}). ` +
|
|
126
|
+
'The bundled artifact may be corrupt or tampered with.');
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
version: requireString(manifest.version, 'version'),
|
|
130
|
+
versionCode: requireInt(manifest.versionCode, 'versionCode'),
|
|
131
|
+
packageName,
|
|
132
|
+
instrumentationRunner: requireString(manifest.instrumentationRunner, 'instrumentationRunner'),
|
|
133
|
+
apkPath,
|
|
134
|
+
sha256: sha256.toLowerCase(),
|
|
135
|
+
signerSha256: requireString(manifest.signerSha256, 'signerSha256').toLowerCase(),
|
|
136
|
+
minSdk: requireInt(manifest.minSdk, 'minSdk'),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=artifact.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"artifact.cjs","sourceRoot":"","sources":["../../../src/backends/android-instrumentation/artifact.ts"],"names":[],"mappings":"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","sourcesContent":["// Derived from agent-device (MIT, Copyright (c) Callstack); adapted for our\n// source-built helper APK and package constants.\n/**\n * Reads and validates the provenance manifest that `build-android-helper.sh`\n * emits next to the bundled helper APK. The manifest is the single source of\n * truth for the package name, instrumentation runner, versionCode, the APK\n * file digest (a tamper check on `dist/`), and the pinned signer certificate\n * SHA-256 that the installed helper must match before we run `am instrument`.\n */\n\nimport { createHash } from 'node:crypto';\nimport { readdir, readFile } from 'node:fs/promises';\nimport { join } from 'node:path';\n\nimport { SnapshotHelperError } from './errors.js';\nimport { HELPER_PACKAGE } from './snapshot.js';\n\n// The compiled module lives at dist/backends/android-instrumentation/, so the\n// bundled helper APK directory (dist/android) is two levels up.\nconst HELPER_APK_DIR = join(__dirname, '..', '..', 'android');\n\nconst HELPER_APK_PREFIX = 'device-mcp-android-snapshot-helper-';\n\nconst MANIFEST_SUFFIX = '.manifest.json';\n\nexport type HelperArtifact = {\n version: string;\n versionCode: number;\n packageName: string;\n instrumentationRunner: string;\n apkPath: string;\n sha256: string;\n signerSha256: string;\n minSdk: number;\n};\n\n/**\n * Raise an {@link SnapshotHelperError} for a missing or malformed artifact.\n *\n * @param message - What went wrong.\n * @throws Always.\n */\nfunction invalid(message: string): never {\n throw new SnapshotHelperError('ARTIFACT_INVALID', message);\n}\n\n/**\n * Assert a manifest field is a non-empty string.\n *\n * @param value - The parsed field value.\n * @param field - The field name, for error messages.\n * @returns The value, narrowed to string.\n */\nfunction requireString(value: unknown, field: string): string {\n if (typeof value !== 'string' || value.length === 0) {\n invalid(`Helper manifest field \"${field}\" is missing or not a string`);\n }\n return value;\n}\n\n/**\n * Assert a manifest field is a finite non-negative integer.\n *\n * @param value - The parsed field value.\n * @param field - The field name, for error messages.\n * @returns The value, narrowed to number.\n */\nfunction requireInt(value: unknown, field: string): number {\n if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {\n invalid(`Helper manifest field \"${field}\" is missing or not an integer`);\n }\n return value;\n}\n\n/**\n * Locate the helper manifest in the bundled `dist/android` directory.\n *\n * @returns The absolute path to the newest matching manifest.\n * @throws {@link SnapshotHelperError} when the directory or manifest is absent.\n */\nasync function resolveManifestPath(): Promise<string> {\n let entries: string[];\n try {\n entries = await readdir(HELPER_APK_DIR);\n } catch {\n return invalid(\n `Snapshot helper directory is missing (${HELPER_APK_DIR}). ` +\n 'The package may have been built without the Android helper.',\n );\n }\n const manifest = entries\n .filter(\n (name) =>\n name.startsWith(HELPER_APK_PREFIX) && name.endsWith(MANIFEST_SUFFIX),\n )\n .sort()\n .at(-1);\n if (!manifest) {\n return invalid(\n `No snapshot helper manifest found in ${HELPER_APK_DIR}. ` +\n 'The package may have been built without the Android helper.',\n );\n }\n return join(HELPER_APK_DIR, manifest);\n}\n\n/**\n * Compute the SHA-256 of a file as lowercase hex.\n *\n * @param path - The file to digest.\n * @returns The lowercase-hex SHA-256.\n */\nasync function sha256File(path: string): Promise<string> {\n const bytes = await readFile(path);\n return createHash('sha256').update(bytes).digest('hex');\n}\n\n/**\n * Load and validate the bundled helper artifact: parse the manifest, confirm\n * it targets our package, resolve the APK path, and verify the APK's SHA-256\n * matches the manifest (a tamper check on the shipped `dist/`).\n *\n * @returns The validated artifact descriptor.\n * @throws {@link SnapshotHelperError} when anything is missing, malformed, or\n * the APK digest does not match the manifest.\n */\nexport async function loadHelperArtifact(): Promise<HelperArtifact> {\n const manifestPath = await resolveManifestPath();\n let parsed: unknown;\n try {\n parsed = JSON.parse(await readFile(manifestPath, 'utf8'));\n } catch (error) {\n return invalid(\n `Helper manifest is not valid JSON (${manifestPath}): ${String(error)}`,\n );\n }\n if (typeof parsed !== 'object' || parsed === null) {\n return invalid(`Helper manifest is not an object (${manifestPath})`);\n }\n const manifest = parsed as Record<string, unknown>;\n\n const packageName = requireString(manifest.packageName, 'packageName');\n if (packageName !== HELPER_PACKAGE) {\n invalid(\n `Helper manifest packageName \"${packageName}\" does not match the ` +\n `expected \"${HELPER_PACKAGE}\"`,\n );\n }\n\n const assetName = requireString(manifest.assetName, 'assetName');\n const sha256 = requireString(manifest.sha256, 'sha256');\n const apkPath = join(HELPER_APK_DIR, assetName);\n\n const actualSha256 = await sha256File(apkPath).catch(() =>\n invalid(`Helper APK is missing or unreadable (${apkPath})`),\n );\n if (actualSha256 !== sha256.toLowerCase()) {\n invalid(\n `Helper APK digest does not match its manifest (${apkPath}). ` +\n 'The bundled artifact may be corrupt or tampered with.',\n );\n }\n\n return {\n version: requireString(manifest.version, 'version'),\n versionCode: requireInt(manifest.versionCode, 'versionCode'),\n packageName,\n instrumentationRunner: requireString(\n manifest.instrumentationRunner,\n 'instrumentationRunner',\n ),\n apkPath,\n sha256: sha256.toLowerCase(),\n signerSha256: requireString(\n manifest.signerSha256,\n 'signerSha256',\n ).toLowerCase(),\n minSdk: requireInt(manifest.minSdk, 'minSdk'),\n };\n}\n"]}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads and validates the provenance manifest that `build-android-helper.sh`
|
|
3
|
+
* emits next to the bundled helper APK. The manifest is the single source of
|
|
4
|
+
* truth for the package name, instrumentation runner, versionCode, the APK
|
|
5
|
+
* file digest (a tamper check on `dist/`), and the pinned signer certificate
|
|
6
|
+
* SHA-256 that the installed helper must match before we run `am instrument`.
|
|
7
|
+
*/
|
|
8
|
+
export type HelperArtifact = {
|
|
9
|
+
version: string;
|
|
10
|
+
versionCode: number;
|
|
11
|
+
packageName: string;
|
|
12
|
+
instrumentationRunner: string;
|
|
13
|
+
apkPath: string;
|
|
14
|
+
sha256: string;
|
|
15
|
+
signerSha256: string;
|
|
16
|
+
minSdk: number;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Load and validate the bundled helper artifact: parse the manifest, confirm
|
|
20
|
+
* it targets our package, resolve the APK path, and verify the APK's SHA-256
|
|
21
|
+
* matches the manifest (a tamper check on the shipped `dist/`).
|
|
22
|
+
*
|
|
23
|
+
* @returns The validated artifact descriptor.
|
|
24
|
+
* @throws {@link SnapshotHelperError} when anything is missing, malformed, or
|
|
25
|
+
* the APK digest does not match the manifest.
|
|
26
|
+
*/
|
|
27
|
+
export declare function loadHelperArtifact(): Promise<HelperArtifact>;
|
|
28
|
+
//# sourceMappingURL=artifact.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"artifact.d.cts","sourceRoot":"","sources":["../../../src/backends/android-instrumentation/artifact.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AAiBH,MAAM,MAAM,cAAc,GAAG;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAmFF;;;;;;;;GAQG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,cAAc,CAAC,CAqDlE"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads and validates the provenance manifest that `build-android-helper.sh`
|
|
3
|
+
* emits next to the bundled helper APK. The manifest is the single source of
|
|
4
|
+
* truth for the package name, instrumentation runner, versionCode, the APK
|
|
5
|
+
* file digest (a tamper check on `dist/`), and the pinned signer certificate
|
|
6
|
+
* SHA-256 that the installed helper must match before we run `am instrument`.
|
|
7
|
+
*/
|
|
8
|
+
export type HelperArtifact = {
|
|
9
|
+
version: string;
|
|
10
|
+
versionCode: number;
|
|
11
|
+
packageName: string;
|
|
12
|
+
instrumentationRunner: string;
|
|
13
|
+
apkPath: string;
|
|
14
|
+
sha256: string;
|
|
15
|
+
signerSha256: string;
|
|
16
|
+
minSdk: number;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Load and validate the bundled helper artifact: parse the manifest, confirm
|
|
20
|
+
* it targets our package, resolve the APK path, and verify the APK's SHA-256
|
|
21
|
+
* matches the manifest (a tamper check on the shipped `dist/`).
|
|
22
|
+
*
|
|
23
|
+
* @returns The validated artifact descriptor.
|
|
24
|
+
* @throws {@link SnapshotHelperError} when anything is missing, malformed, or
|
|
25
|
+
* the APK digest does not match the manifest.
|
|
26
|
+
*/
|
|
27
|
+
export declare function loadHelperArtifact(): Promise<HelperArtifact>;
|
|
28
|
+
//# sourceMappingURL=artifact.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"artifact.d.mts","sourceRoot":"","sources":["../../../src/backends/android-instrumentation/artifact.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AAiBH,MAAM,MAAM,cAAc,GAAG;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAmFF;;;;;;;;GAQG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,cAAc,CAAC,CAqDlE"}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
function $__filename(fileUrl) {
|
|
2
|
+
const url = new URL(fileUrl);
|
|
3
|
+
return url.pathname.replace(/^\/([a-zA-Z]:)/u, "$1");
|
|
4
|
+
}
|
|
5
|
+
function $getDirname(path) {
|
|
6
|
+
const sanitisedPath = path.toString().replace(/\\/gu, "/").replace(/\/$/u, "");
|
|
7
|
+
const index = sanitisedPath.lastIndexOf("/");
|
|
8
|
+
if (index === -1) {
|
|
9
|
+
return path;
|
|
10
|
+
}
|
|
11
|
+
if (index === 0) {
|
|
12
|
+
return "/";
|
|
13
|
+
}
|
|
14
|
+
return sanitisedPath.slice(0, index);
|
|
15
|
+
}
|
|
16
|
+
function $__dirname(url) {
|
|
17
|
+
return $getDirname($__filename(url));
|
|
18
|
+
}
|
|
19
|
+
// Derived from agent-device (MIT, Copyright (c) Callstack); adapted for our
|
|
20
|
+
// source-built helper APK and package constants.
|
|
21
|
+
/**
|
|
22
|
+
* Reads and validates the provenance manifest that `build-android-helper.sh`
|
|
23
|
+
* emits next to the bundled helper APK. The manifest is the single source of
|
|
24
|
+
* truth for the package name, instrumentation runner, versionCode, the APK
|
|
25
|
+
* file digest (a tamper check on `dist/`), and the pinned signer certificate
|
|
26
|
+
* SHA-256 that the installed helper must match before we run `am instrument`.
|
|
27
|
+
*/
|
|
28
|
+
import { createHash } from "node:crypto";
|
|
29
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
30
|
+
import { join } from "node:path";
|
|
31
|
+
import { SnapshotHelperError } from "./errors.mjs";
|
|
32
|
+
import { HELPER_PACKAGE } from "./snapshot.mjs";
|
|
33
|
+
// The compiled module lives at dist/backends/android-instrumentation/, so the
|
|
34
|
+
// bundled helper APK directory (dist/android) is two levels up.
|
|
35
|
+
const HELPER_APK_DIR = join($__dirname(import.meta.url), '..', '..', 'android');
|
|
36
|
+
const HELPER_APK_PREFIX = 'device-mcp-android-snapshot-helper-';
|
|
37
|
+
const MANIFEST_SUFFIX = '.manifest.json';
|
|
38
|
+
/**
|
|
39
|
+
* Raise an {@link SnapshotHelperError} for a missing or malformed artifact.
|
|
40
|
+
*
|
|
41
|
+
* @param message - What went wrong.
|
|
42
|
+
* @throws Always.
|
|
43
|
+
*/
|
|
44
|
+
function invalid(message) {
|
|
45
|
+
throw new SnapshotHelperError('ARTIFACT_INVALID', message);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Assert a manifest field is a non-empty string.
|
|
49
|
+
*
|
|
50
|
+
* @param value - The parsed field value.
|
|
51
|
+
* @param field - The field name, for error messages.
|
|
52
|
+
* @returns The value, narrowed to string.
|
|
53
|
+
*/
|
|
54
|
+
function requireString(value, field) {
|
|
55
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
56
|
+
invalid(`Helper manifest field "${field}" is missing or not a string`);
|
|
57
|
+
}
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Assert a manifest field is a finite non-negative integer.
|
|
62
|
+
*
|
|
63
|
+
* @param value - The parsed field value.
|
|
64
|
+
* @param field - The field name, for error messages.
|
|
65
|
+
* @returns The value, narrowed to number.
|
|
66
|
+
*/
|
|
67
|
+
function requireInt(value, field) {
|
|
68
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {
|
|
69
|
+
invalid(`Helper manifest field "${field}" is missing or not an integer`);
|
|
70
|
+
}
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Locate the helper manifest in the bundled `dist/android` directory.
|
|
75
|
+
*
|
|
76
|
+
* @returns The absolute path to the newest matching manifest.
|
|
77
|
+
* @throws {@link SnapshotHelperError} when the directory or manifest is absent.
|
|
78
|
+
*/
|
|
79
|
+
async function resolveManifestPath() {
|
|
80
|
+
let entries;
|
|
81
|
+
try {
|
|
82
|
+
entries = await readdir(HELPER_APK_DIR);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return invalid(`Snapshot helper directory is missing (${HELPER_APK_DIR}). ` +
|
|
86
|
+
'The package may have been built without the Android helper.');
|
|
87
|
+
}
|
|
88
|
+
const manifest = entries
|
|
89
|
+
.filter((name) => name.startsWith(HELPER_APK_PREFIX) && name.endsWith(MANIFEST_SUFFIX))
|
|
90
|
+
.sort()
|
|
91
|
+
.at(-1);
|
|
92
|
+
if (!manifest) {
|
|
93
|
+
return invalid(`No snapshot helper manifest found in ${HELPER_APK_DIR}. ` +
|
|
94
|
+
'The package may have been built without the Android helper.');
|
|
95
|
+
}
|
|
96
|
+
return join(HELPER_APK_DIR, manifest);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Compute the SHA-256 of a file as lowercase hex.
|
|
100
|
+
*
|
|
101
|
+
* @param path - The file to digest.
|
|
102
|
+
* @returns The lowercase-hex SHA-256.
|
|
103
|
+
*/
|
|
104
|
+
async function sha256File(path) {
|
|
105
|
+
const bytes = await readFile(path);
|
|
106
|
+
return createHash('sha256').update(bytes).digest('hex');
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Load and validate the bundled helper artifact: parse the manifest, confirm
|
|
110
|
+
* it targets our package, resolve the APK path, and verify the APK's SHA-256
|
|
111
|
+
* matches the manifest (a tamper check on the shipped `dist/`).
|
|
112
|
+
*
|
|
113
|
+
* @returns The validated artifact descriptor.
|
|
114
|
+
* @throws {@link SnapshotHelperError} when anything is missing, malformed, or
|
|
115
|
+
* the APK digest does not match the manifest.
|
|
116
|
+
*/
|
|
117
|
+
export async function loadHelperArtifact() {
|
|
118
|
+
const manifestPath = await resolveManifestPath();
|
|
119
|
+
let parsed;
|
|
120
|
+
try {
|
|
121
|
+
parsed = JSON.parse(await readFile(manifestPath, 'utf8'));
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
return invalid(`Helper manifest is not valid JSON (${manifestPath}): ${String(error)}`);
|
|
125
|
+
}
|
|
126
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
127
|
+
return invalid(`Helper manifest is not an object (${manifestPath})`);
|
|
128
|
+
}
|
|
129
|
+
const manifest = parsed;
|
|
130
|
+
const packageName = requireString(manifest.packageName, 'packageName');
|
|
131
|
+
if (packageName !== HELPER_PACKAGE) {
|
|
132
|
+
invalid(`Helper manifest packageName "${packageName}" does not match the ` +
|
|
133
|
+
`expected "${HELPER_PACKAGE}"`);
|
|
134
|
+
}
|
|
135
|
+
const assetName = requireString(manifest.assetName, 'assetName');
|
|
136
|
+
const sha256 = requireString(manifest.sha256, 'sha256');
|
|
137
|
+
const apkPath = join(HELPER_APK_DIR, assetName);
|
|
138
|
+
const actualSha256 = await sha256File(apkPath).catch(() => invalid(`Helper APK is missing or unreadable (${apkPath})`));
|
|
139
|
+
if (actualSha256 !== sha256.toLowerCase()) {
|
|
140
|
+
invalid(`Helper APK digest does not match its manifest (${apkPath}). ` +
|
|
141
|
+
'The bundled artifact may be corrupt or tampered with.');
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
version: requireString(manifest.version, 'version'),
|
|
145
|
+
versionCode: requireInt(manifest.versionCode, 'versionCode'),
|
|
146
|
+
packageName,
|
|
147
|
+
instrumentationRunner: requireString(manifest.instrumentationRunner, 'instrumentationRunner'),
|
|
148
|
+
apkPath,
|
|
149
|
+
sha256: sha256.toLowerCase(),
|
|
150
|
+
signerSha256: requireString(manifest.signerSha256, 'signerSha256').toLowerCase(),
|
|
151
|
+
minSdk: requireInt(manifest.minSdk, 'minSdk'),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=artifact.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"artifact.mjs","sourceRoot":"","sources":["../../../src/backends/android-instrumentation/artifact.ts"],"names":[],"mappings":"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","sourcesContent":["// Derived from agent-device (MIT, Copyright (c) Callstack); adapted for our\n// source-built helper APK and package constants.\n/**\n * Reads and validates the provenance manifest that `build-android-helper.sh`\n * emits next to the bundled helper APK. The manifest is the single source of\n * truth for the package name, instrumentation runner, versionCode, the APK\n * file digest (a tamper check on `dist/`), and the pinned signer certificate\n * SHA-256 that the installed helper must match before we run `am instrument`.\n */\n\nimport { createHash } from 'node:crypto';\nimport { readdir, readFile } from 'node:fs/promises';\nimport { join } from 'node:path';\n\nimport { SnapshotHelperError } from './errors.js';\nimport { HELPER_PACKAGE } from './snapshot.js';\n\n// The compiled module lives at dist/backends/android-instrumentation/, so the\n// bundled helper APK directory (dist/android) is two levels up.\nconst HELPER_APK_DIR = join(__dirname, '..', '..', 'android');\n\nconst HELPER_APK_PREFIX = 'device-mcp-android-snapshot-helper-';\n\nconst MANIFEST_SUFFIX = '.manifest.json';\n\nexport type HelperArtifact = {\n version: string;\n versionCode: number;\n packageName: string;\n instrumentationRunner: string;\n apkPath: string;\n sha256: string;\n signerSha256: string;\n minSdk: number;\n};\n\n/**\n * Raise an {@link SnapshotHelperError} for a missing or malformed artifact.\n *\n * @param message - What went wrong.\n * @throws Always.\n */\nfunction invalid(message: string): never {\n throw new SnapshotHelperError('ARTIFACT_INVALID', message);\n}\n\n/**\n * Assert a manifest field is a non-empty string.\n *\n * @param value - The parsed field value.\n * @param field - The field name, for error messages.\n * @returns The value, narrowed to string.\n */\nfunction requireString(value: unknown, field: string): string {\n if (typeof value !== 'string' || value.length === 0) {\n invalid(`Helper manifest field \"${field}\" is missing or not a string`);\n }\n return value;\n}\n\n/**\n * Assert a manifest field is a finite non-negative integer.\n *\n * @param value - The parsed field value.\n * @param field - The field name, for error messages.\n * @returns The value, narrowed to number.\n */\nfunction requireInt(value: unknown, field: string): number {\n if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {\n invalid(`Helper manifest field \"${field}\" is missing or not an integer`);\n }\n return value;\n}\n\n/**\n * Locate the helper manifest in the bundled `dist/android` directory.\n *\n * @returns The absolute path to the newest matching manifest.\n * @throws {@link SnapshotHelperError} when the directory or manifest is absent.\n */\nasync function resolveManifestPath(): Promise<string> {\n let entries: string[];\n try {\n entries = await readdir(HELPER_APK_DIR);\n } catch {\n return invalid(\n `Snapshot helper directory is missing (${HELPER_APK_DIR}). ` +\n 'The package may have been built without the Android helper.',\n );\n }\n const manifest = entries\n .filter(\n (name) =>\n name.startsWith(HELPER_APK_PREFIX) && name.endsWith(MANIFEST_SUFFIX),\n )\n .sort()\n .at(-1);\n if (!manifest) {\n return invalid(\n `No snapshot helper manifest found in ${HELPER_APK_DIR}. ` +\n 'The package may have been built without the Android helper.',\n );\n }\n return join(HELPER_APK_DIR, manifest);\n}\n\n/**\n * Compute the SHA-256 of a file as lowercase hex.\n *\n * @param path - The file to digest.\n * @returns The lowercase-hex SHA-256.\n */\nasync function sha256File(path: string): Promise<string> {\n const bytes = await readFile(path);\n return createHash('sha256').update(bytes).digest('hex');\n}\n\n/**\n * Load and validate the bundled helper artifact: parse the manifest, confirm\n * it targets our package, resolve the APK path, and verify the APK's SHA-256\n * matches the manifest (a tamper check on the shipped `dist/`).\n *\n * @returns The validated artifact descriptor.\n * @throws {@link SnapshotHelperError} when anything is missing, malformed, or\n * the APK digest does not match the manifest.\n */\nexport async function loadHelperArtifact(): Promise<HelperArtifact> {\n const manifestPath = await resolveManifestPath();\n let parsed: unknown;\n try {\n parsed = JSON.parse(await readFile(manifestPath, 'utf8'));\n } catch (error) {\n return invalid(\n `Helper manifest is not valid JSON (${manifestPath}): ${String(error)}`,\n );\n }\n if (typeof parsed !== 'object' || parsed === null) {\n return invalid(`Helper manifest is not an object (${manifestPath})`);\n }\n const manifest = parsed as Record<string, unknown>;\n\n const packageName = requireString(manifest.packageName, 'packageName');\n if (packageName !== HELPER_PACKAGE) {\n invalid(\n `Helper manifest packageName \"${packageName}\" does not match the ` +\n `expected \"${HELPER_PACKAGE}\"`,\n );\n }\n\n const assetName = requireString(manifest.assetName, 'assetName');\n const sha256 = requireString(manifest.sha256, 'sha256');\n const apkPath = join(HELPER_APK_DIR, assetName);\n\n const actualSha256 = await sha256File(apkPath).catch(() =>\n invalid(`Helper APK is missing or unreadable (${apkPath})`),\n );\n if (actualSha256 !== sha256.toLowerCase()) {\n invalid(\n `Helper APK digest does not match its manifest (${apkPath}). ` +\n 'The bundled artifact may be corrupt or tampered with.',\n );\n }\n\n return {\n version: requireString(manifest.version, 'version'),\n versionCode: requireInt(manifest.versionCode, 'versionCode'),\n packageName,\n instrumentationRunner: requireString(\n manifest.instrumentationRunner,\n 'instrumentationRunner',\n ),\n apkPath,\n sha256: sha256.toLowerCase(),\n signerSha256: requireString(\n manifest.signerSha256,\n 'signerSha256',\n ).toLowerCase(),\n minSdk: requireInt(manifest.minSdk, 'minSdk'),\n };\n}\n"]}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Error types for the Android snapshot-helper trust + install pipeline.
|
|
4
|
+
*
|
|
5
|
+
* Two failure classes are deliberately distinct because the ADB backend treats
|
|
6
|
+
* them differently (see `adb-backend.ts`):
|
|
7
|
+
*
|
|
8
|
+
* - {@link SnapshotHelperError} — a *generic* failure (install failed, adb
|
|
9
|
+
* hiccup, malformed artifact). The `auto` snapshot strategy is allowed to fall
|
|
10
|
+
* back to `uiautomator dump` when one of these occurs.
|
|
11
|
+
* - {@link UntrustedHelperError} — the installed helper's signing certificate
|
|
12
|
+
* does NOT match the pinned signer. This means a possibly-malicious app is
|
|
13
|
+
* squatting the helper's package name. The snapshot flow MUST fail closed:
|
|
14
|
+
* it is never swallowed and never triggers a dump fallback, because silently
|
|
15
|
+
* returning a dump snapshot would hide a compromised device from a wallet
|
|
16
|
+
* test harness.
|
|
17
|
+
*
|
|
18
|
+
* Structure adapted from agent-device (MIT, Copyright (c) Callstack); the
|
|
19
|
+
* `UntrustedHelperError` trust class and messaging are ours.
|
|
20
|
+
*/
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.UntrustedHelperError = exports.SnapshotHelperError = void 0;
|
|
23
|
+
exports.isUntrustedHelperError = isUntrustedHelperError;
|
|
24
|
+
/**
|
|
25
|
+
* A generic, recoverable snapshot-helper failure. Callers in `auto` mode may
|
|
26
|
+
* fall back to `uiautomator dump` after catching one of these.
|
|
27
|
+
*/
|
|
28
|
+
class SnapshotHelperError extends Error {
|
|
29
|
+
/**
|
|
30
|
+
* @param code - Machine-readable failure category.
|
|
31
|
+
* @param message - Human-readable description.
|
|
32
|
+
* @param options - Standard error options (e.g. `cause`).
|
|
33
|
+
*/
|
|
34
|
+
constructor(code, message, options) {
|
|
35
|
+
super(message, options);
|
|
36
|
+
this.name = 'SnapshotHelperError';
|
|
37
|
+
this.code = code;
|
|
38
|
+
// Restore prototype chain for `instanceof` across the dual ESM/CJS build.
|
|
39
|
+
Object.setPrototypeOf(this, SnapshotHelperError.prototype);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
exports.SnapshotHelperError = SnapshotHelperError;
|
|
43
|
+
/**
|
|
44
|
+
* A trust failure: the installed helper is not signed by our pinned key.
|
|
45
|
+
*
|
|
46
|
+
* This is a hard stop. It must propagate out of the snapshot flow — never be
|
|
47
|
+
* converted to `null`, never be collected into a generic failure list, and
|
|
48
|
+
* never trigger a `uiautomator dump` fallback.
|
|
49
|
+
*/
|
|
50
|
+
class UntrustedHelperError extends Error {
|
|
51
|
+
/**
|
|
52
|
+
* @param message - Human-readable description of the mismatch.
|
|
53
|
+
* @param details - The expected and (optionally) observed signer digests.
|
|
54
|
+
* @param details.expectedSignerSha256 - The pinned signer SHA-256.
|
|
55
|
+
* @param details.actualSignerSha256 - The observed signer SHA-256, if known.
|
|
56
|
+
* @param options - Standard error options (e.g. `cause`).
|
|
57
|
+
*/
|
|
58
|
+
constructor(message, details, options) {
|
|
59
|
+
super(message, options);
|
|
60
|
+
this.name = 'UntrustedHelperError';
|
|
61
|
+
this.expectedSignerSha256 = details.expectedSignerSha256;
|
|
62
|
+
this.actualSignerSha256 = details.actualSignerSha256;
|
|
63
|
+
// Restore prototype chain for `instanceof` across the dual ESM/CJS build.
|
|
64
|
+
Object.setPrototypeOf(this, UntrustedHelperError.prototype);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
exports.UntrustedHelperError = UntrustedHelperError;
|
|
68
|
+
/**
|
|
69
|
+
* Type guard for the fail-closed trust error. The ADB backend uses this to
|
|
70
|
+
* decide whether an instrumentation failure may fall back to `dump` (generic)
|
|
71
|
+
* or must propagate (trust).
|
|
72
|
+
*
|
|
73
|
+
* @param error - The value to test.
|
|
74
|
+
* @returns True when `error` is an {@link UntrustedHelperError}.
|
|
75
|
+
*/
|
|
76
|
+
function isUntrustedHelperError(error) {
|
|
77
|
+
return error instanceof UntrustedHelperError;
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=errors.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.cjs","sourceRoot":"","sources":["../../../src/backends/android-instrumentation/errors.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;GAkBG;;;AA+EH,wDAIC;AA3ED;;;GAGG;AACH,MAAa,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;AAnBD,kDAmBC;AAED;;;;;;GAMG;AACH,MAAa,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;AA7BD,oDA6BC;AAED;;;;;;;GAOG;AACH,SAAgB,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"]}
|