@deeeed/metamask-harness 0.29.3 → 0.31.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 +12 -0
- package/dist/recipe-security.js +1 -0
- package/library/actions/extension/deeplink/open.mjs +26 -0
- package/library/actions/mobile/deeplink/open.mjs +16 -0
- package/library/actions/mobile/platform/bridge.mjs +80 -0
- package/library/actions/shared/deeplink/url.mjs +37 -0
- package/library/manifests/extension.action-manifest.json +33 -0
- package/library/manifests/mobile.action-manifest.json +33 -0
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.31.0 - 2026-08-02
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Add one cross-platform `metamask.deeplink.open` capability that exercises real Chrome or device URL ingress for external deeplink proofs.
|
|
10
|
+
|
|
11
|
+
## 0.30.0 - 2026-08-02
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- Upgrade the Farmslot protocol, recipe runtime, and agent runtime to their continuous-gesture-compatible releases and converge published installs on protocol 0.16.0.
|
|
16
|
+
|
|
5
17
|
## 0.29.3 - 2026-08-01
|
|
6
18
|
|
|
7
19
|
### Fixed
|
package/dist/recipe-security.js
CHANGED
|
@@ -20,6 +20,7 @@ const APP_MUTATION_CUSTOM_ACTIONS = /* @__PURE__ */ new Set([
|
|
|
20
20
|
"metamask.wallet.setup",
|
|
21
21
|
"metamask.wallet.ensure_unlocked",
|
|
22
22
|
"metamask.wallet.select_account",
|
|
23
|
+
"metamask.deeplink.open",
|
|
23
24
|
"metamask.perps.close_positions",
|
|
24
25
|
"metamask.perps.close_orders",
|
|
25
26
|
"metamask.perps.place_order",
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { pathToFileURL } from 'node:url';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
externalDeeplinkTimeout,
|
|
5
|
+
externalDeeplinkUrl,
|
|
6
|
+
} from '../../shared/deeplink/url.mjs';
|
|
7
|
+
import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
8
|
+
|
|
9
|
+
export async function openExtensionDeeplink(input) {
|
|
10
|
+
return withExtensionPage(input, (page) => navigateExtensionDeeplink(page, input));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function navigateExtensionDeeplink(page, input) {
|
|
14
|
+
const url = externalDeeplinkUrl(input.node);
|
|
15
|
+
const timeoutMs = externalDeeplinkTimeout(input.node);
|
|
16
|
+
return {
|
|
17
|
+
action: input.action,
|
|
18
|
+
url,
|
|
19
|
+
navigation: await page.navigate(url, timeoutMs),
|
|
20
|
+
proofPath: 'external-deeplink',
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
25
|
+
runAdapter(openExtensionDeeplink);
|
|
26
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import {
|
|
2
|
+
externalDeeplinkTimeout,
|
|
3
|
+
externalDeeplinkUrl,
|
|
4
|
+
} from '../../shared/deeplink/url.mjs';
|
|
5
|
+
import { openExternalDeeplink, runAdapter } from '../platform/bridge.mjs';
|
|
6
|
+
|
|
7
|
+
runAdapter(async (input) => {
|
|
8
|
+
const url = externalDeeplinkUrl(input.node);
|
|
9
|
+
const timeoutMs = externalDeeplinkTimeout(input.node);
|
|
10
|
+
return {
|
|
11
|
+
action: input.action,
|
|
12
|
+
url,
|
|
13
|
+
...(await openExternalDeeplink(input, url, timeoutMs)),
|
|
14
|
+
proofPath: 'external-deeplink',
|
|
15
|
+
};
|
|
16
|
+
});
|
|
@@ -160,6 +160,86 @@ function resolveMobileTarget(input) {
|
|
|
160
160
|
return { watcherPort, iosSimulator, androidDevice, androidTargetDeviceName, adbSerial };
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
export async function openExternalDeeplink(input, url, timeout = 15000) {
|
|
164
|
+
const target = resolveMobileTarget(input);
|
|
165
|
+
const requestedPlatform = String(
|
|
166
|
+
input.context?.env?.PLATFORM ?? process.env.PLATFORM ?? '',
|
|
167
|
+
).toLowerCase();
|
|
168
|
+
const serial = String(
|
|
169
|
+
input.context?.env?.ADB_SERIAL ??
|
|
170
|
+
input.context?.env?.ANDROID_SERIAL ??
|
|
171
|
+
process.env.ADB_SERIAL ??
|
|
172
|
+
process.env.ANDROID_SERIAL ??
|
|
173
|
+
'',
|
|
174
|
+
).trim();
|
|
175
|
+
const android =
|
|
176
|
+
requestedPlatform === 'android' || (requestedPlatform !== 'ios' && Boolean(serial));
|
|
177
|
+
if (android) {
|
|
178
|
+
if (!serial) {
|
|
179
|
+
throw new Error(
|
|
180
|
+
'Android deeplink ingress requires an adb serial.\n' +
|
|
181
|
+
' Next: mm-harness devices --adapter mobile --json',
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
const adb = resolveMobileToolPath('adb', { required: true });
|
|
185
|
+
const { stdout } = await runDeeplinkCommand(
|
|
186
|
+
adb,
|
|
187
|
+
[
|
|
188
|
+
'-s',
|
|
189
|
+
serial,
|
|
190
|
+
'shell',
|
|
191
|
+
'am',
|
|
192
|
+
'start',
|
|
193
|
+
'-W',
|
|
194
|
+
'-a',
|
|
195
|
+
'android.intent.action.VIEW',
|
|
196
|
+
'-d',
|
|
197
|
+
quoteRemoteShellArgument(url),
|
|
198
|
+
],
|
|
199
|
+
timeout,
|
|
200
|
+
`Android device ${serial}`,
|
|
201
|
+
);
|
|
202
|
+
if (!/^Status:\s*ok\s*$/imu.test(stdout)) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
`Android device ${serial} did not report a successful deeplink launch.\n` +
|
|
205
|
+
' Next: mm-harness doctor --adapter mobile --json',
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
return { platform: 'android', selectedDevice: serial, method: 'adb-am-start-view' };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const simulator = String(target.iosSimulator ?? '').trim();
|
|
212
|
+
if (!simulator) {
|
|
213
|
+
throw new Error(
|
|
214
|
+
'iOS deeplink ingress requires an iOS simulator.\n' +
|
|
215
|
+
' Next: mm-harness devices --adapter mobile --json',
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
await runDeeplinkCommand(
|
|
219
|
+
'xcrun',
|
|
220
|
+
['simctl', 'openurl', simulator, url],
|
|
221
|
+
timeout,
|
|
222
|
+
`iOS simulator ${simulator}`,
|
|
223
|
+
);
|
|
224
|
+
return { platform: 'ios', selectedDevice: simulator, method: 'simctl-openurl' };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function quoteRemoteShellArgument(value) {
|
|
228
|
+
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function runDeeplinkCommand(command, args, timeout, target) {
|
|
232
|
+
try {
|
|
233
|
+
return await execFileAsync(command, args, { timeout, encoding: 'utf8' });
|
|
234
|
+
} catch (error) {
|
|
235
|
+
const detail = String(error?.stderr ?? error?.message ?? error).trim();
|
|
236
|
+
throw new Error(
|
|
237
|
+
`Could not open the deeplink on ${target}${detail ? `: ${detail}` : '.'}\n` +
|
|
238
|
+
' Next: mm-harness doctor --adapter mobile --json',
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
163
243
|
function androidDeviceNameMatches(deviceName, targetName) {
|
|
164
244
|
if (!targetName) return false;
|
|
165
245
|
return deviceName === targetName || deviceName.startsWith(`${targetName} -`);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export function externalDeeplinkUrl(node) {
|
|
2
|
+
const raw = node?.url;
|
|
3
|
+
if (typeof raw !== 'string' || raw.trim().length === 0) {
|
|
4
|
+
throw new Error(
|
|
5
|
+
'metamask.deeplink.open requires url.\n' +
|
|
6
|
+
' Next: mm-harness call metamask.deeplink.open url=https://link.metamask.io/home',
|
|
7
|
+
);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let url;
|
|
11
|
+
try {
|
|
12
|
+
url = new URL(raw);
|
|
13
|
+
} catch {
|
|
14
|
+
throw new Error(
|
|
15
|
+
'metamask.deeplink.open requires an absolute HTTPS url.\n' +
|
|
16
|
+
' Next: mm-harness call metamask.deeplink.open url=https://link.metamask.io/home',
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
if (url.protocol !== 'https:') {
|
|
20
|
+
throw new Error(
|
|
21
|
+
'metamask.deeplink.open accepts only HTTPS urls.\n' +
|
|
22
|
+
' Next: mm-harness call metamask.deeplink.open url=https://link.metamask.io/home',
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
return url.toString();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function externalDeeplinkTimeout(node) {
|
|
29
|
+
const timeout = Number(node?.timeout_ms ?? 15000);
|
|
30
|
+
if (!Number.isFinite(timeout) || timeout <= 0 || timeout > 120000) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
'metamask.deeplink.open timeout_ms must be between 1 and 120000.\n' +
|
|
33
|
+
' Next: mm-harness call metamask.deeplink.open url=https://link.metamask.io/home timeout_ms=15000',
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return timeout;
|
|
37
|
+
}
|
|
@@ -2779,6 +2779,37 @@
|
|
|
2779
2779
|
"external-mutation"
|
|
2780
2780
|
]
|
|
2781
2781
|
},
|
|
2782
|
+
"metamask.deeplink.open": {
|
|
2783
|
+
"description": "Open an HTTPS deeplink through real browser ingress so the Extension owns parsing, verification, and destination navigation.",
|
|
2784
|
+
"execution_capabilities": [
|
|
2785
|
+
"app-mutation"
|
|
2786
|
+
],
|
|
2787
|
+
"examples": [
|
|
2788
|
+
{
|
|
2789
|
+
"action": "metamask.deeplink.open",
|
|
2790
|
+
"url": "https://link.metamask.io/home",
|
|
2791
|
+
"intent": "Open the deeplink through external browser navigation",
|
|
2792
|
+
"next": "done"
|
|
2793
|
+
}
|
|
2794
|
+
],
|
|
2795
|
+
"schema": {
|
|
2796
|
+
"type": "object",
|
|
2797
|
+
"properties": {
|
|
2798
|
+
"url": {
|
|
2799
|
+
"type": "string",
|
|
2800
|
+
"description": "Absolute HTTPS deeplink URL."
|
|
2801
|
+
},
|
|
2802
|
+
"timeout_ms": {
|
|
2803
|
+
"type": "number",
|
|
2804
|
+
"description": "Maximum time for the external navigation. Default 15000."
|
|
2805
|
+
}
|
|
2806
|
+
},
|
|
2807
|
+
"required": [
|
|
2808
|
+
"url"
|
|
2809
|
+
],
|
|
2810
|
+
"additionalProperties": false
|
|
2811
|
+
}
|
|
2812
|
+
},
|
|
2782
2813
|
"metamask.analytics.start_capture": {
|
|
2783
2814
|
"description": "extension Start capturing MetaMetrics events at the Segment transport and return a cursor to bracket later reads.",
|
|
2784
2815
|
"execution_capabilities": [
|
|
@@ -2945,6 +2976,7 @@
|
|
|
2945
2976
|
{
|
|
2946
2977
|
"ref": "ui.screen",
|
|
2947
2978
|
"default_for": [
|
|
2979
|
+
"metamask.deeplink.open",
|
|
2948
2980
|
"ui.navigate",
|
|
2949
2981
|
"ui.press",
|
|
2950
2982
|
"ui.key_press",
|
|
@@ -2957,6 +2989,7 @@
|
|
|
2957
2989
|
{
|
|
2958
2990
|
"ref": "ui.visible",
|
|
2959
2991
|
"default_for": [
|
|
2992
|
+
"metamask.deeplink.open",
|
|
2960
2993
|
"ui.navigate",
|
|
2961
2994
|
"ui.press",
|
|
2962
2995
|
"ui.key_press",
|
|
@@ -2901,6 +2901,37 @@
|
|
|
2901
2901
|
"external-mutation"
|
|
2902
2902
|
]
|
|
2903
2903
|
},
|
|
2904
|
+
"metamask.deeplink.open": {
|
|
2905
|
+
"description": "Open an HTTPS deeplink through the selected Android device or iOS simulator so Mobile owns parsing and destination navigation.",
|
|
2906
|
+
"execution_capabilities": [
|
|
2907
|
+
"app-mutation"
|
|
2908
|
+
],
|
|
2909
|
+
"examples": [
|
|
2910
|
+
{
|
|
2911
|
+
"action": "metamask.deeplink.open",
|
|
2912
|
+
"url": "https://link.metamask.io/home",
|
|
2913
|
+
"intent": "Open the deeplink through external device navigation",
|
|
2914
|
+
"next": "done"
|
|
2915
|
+
}
|
|
2916
|
+
],
|
|
2917
|
+
"schema": {
|
|
2918
|
+
"type": "object",
|
|
2919
|
+
"properties": {
|
|
2920
|
+
"url": {
|
|
2921
|
+
"type": "string",
|
|
2922
|
+
"description": "Absolute HTTPS deeplink URL."
|
|
2923
|
+
},
|
|
2924
|
+
"timeout_ms": {
|
|
2925
|
+
"type": "number",
|
|
2926
|
+
"description": "Maximum time for the external navigation. Default 15000."
|
|
2927
|
+
}
|
|
2928
|
+
},
|
|
2929
|
+
"required": [
|
|
2930
|
+
"url"
|
|
2931
|
+
],
|
|
2932
|
+
"additionalProperties": false
|
|
2933
|
+
}
|
|
2934
|
+
},
|
|
2904
2935
|
"metamask.analytics.start_capture": {
|
|
2905
2936
|
"description": "mobile Start capturing MetaMetrics events at the Segment transport and return a cursor to bracket later reads.",
|
|
2906
2937
|
"execution_capabilities": [
|
|
@@ -3067,6 +3098,7 @@
|
|
|
3067
3098
|
{
|
|
3068
3099
|
"ref": "ui.screen",
|
|
3069
3100
|
"default_for": [
|
|
3101
|
+
"metamask.deeplink.open",
|
|
3070
3102
|
"ui.navigate",
|
|
3071
3103
|
"ui.press",
|
|
3072
3104
|
"ui.screenshot",
|
|
@@ -3078,6 +3110,7 @@
|
|
|
3078
3110
|
{
|
|
3079
3111
|
"ref": "ui.visible",
|
|
3080
3112
|
"default_for": [
|
|
3113
|
+
"metamask.deeplink.open",
|
|
3081
3114
|
"ui.navigate",
|
|
3082
3115
|
"ui.press",
|
|
3083
3116
|
"ui.screenshot",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deeeed/metamask-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"mm-harness": "bin/mm-harness"
|
|
@@ -20,10 +20,10 @@
|
|
|
20
20
|
"check:syntax": "find . -name '*.mjs' -print0 | xargs -0 -n1 node --check"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@farmslot/agent-runtime": "
|
|
23
|
+
"@farmslot/agent-runtime": "0.5.1",
|
|
24
24
|
"@farmslot/handoff": "^0.3.1",
|
|
25
|
-
"@farmslot/protocol": "
|
|
26
|
-
"@farmslot/recipe-harness": "
|
|
25
|
+
"@farmslot/protocol": "0.16.0",
|
|
26
|
+
"@farmslot/recipe-harness": "0.12.1",
|
|
27
27
|
"commander": "^12.0.0",
|
|
28
28
|
"es-module-lexer": "2.3.1",
|
|
29
29
|
"esbuild": "0.28.1",
|