@deeeed/metamask-harness 0.28.0 → 0.29.1

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 (54) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +41 -0
  3. package/adapters/extension/build-lavamoat.sh +2 -1
  4. package/adapters/extension/ensure-browser.sh +82 -9
  5. package/adapters/extension/inject.mjs +1 -0
  6. package/adapters/extension/launch-browser.cjs +83 -1
  7. package/adapters/extension/lib/chrome-args.cjs +325 -1
  8. package/adapters/extension/lib/playwright-cdp.cjs +34 -0
  9. package/adapters/extension/lib/slot-title.cjs +2 -4
  10. package/adapters/extension/lib/validation-launch-supervisor.cjs +292 -0
  11. package/adapters/extension/lib/validation-process-ownership.cjs +69 -0
  12. package/adapters/extension/reattach.sh +2 -1
  13. package/adapters/extension/sidepanel-toggle.sh +14 -96
  14. package/adapters/extension/wallet-fixture-state.cjs +8 -31
  15. package/adapters/manifest.json +16 -0
  16. package/adapters/shared/private-atomic-write.cjs +47 -0
  17. package/adapters/shared/setup-base.sh +864 -0
  18. package/dist/adapters/extension/runtime.js +367 -24
  19. package/dist/adapters/extension/validation-process-ownership.js +10 -0
  20. package/dist/cli-commands.js +1 -0
  21. package/dist/command-contract.js +12 -0
  22. package/dist/commands/launch/extension.js +130 -19
  23. package/dist/commands/setup-base.js +24 -0
  24. package/dist/mm-harness-cli.js +28 -2
  25. package/docs/RECIPES.md +26 -1
  26. package/docs/SECURITY.md +31 -0
  27. package/library/actions/extension/analytics/consent.mjs +203 -0
  28. package/library/actions/extension/analytics/set_consent.mjs +19 -143
  29. package/library/actions/extension/perps/perps.mjs +2 -16
  30. package/library/actions/extension/perps/state.mjs +20 -0
  31. package/library/actions/extension/wallet/list_accounts.mjs +3 -25
  32. package/library/actions/extension/wallet/read_state.mjs +3 -23
  33. package/library/actions/extension/wallet/select_account.mjs +6 -33
  34. package/library/actions/extension/wallet/setup.mjs +2 -20
  35. package/library/actions/extension/wallet/state.mjs +111 -0
  36. package/library/recipes/runner/action-validation.extension.recipe.json +1 -1
  37. package/library/recipes/runner/action-validation.mobile.recipe.json +1 -1
  38. package/package.json +7 -4
  39. package/scripts/site-contrast.mjs +538 -0
  40. package/site/architecture.html +474 -0
  41. package/site/assets/progress.mjs +272 -0
  42. package/site/assets/style.css +808 -0
  43. package/site/cheatsheet.html +305 -0
  44. package/site/index.html +647 -0
  45. package/site/recipes.html +396 -0
  46. package/site/reviewers.html +374 -0
  47. package/site/tutorials/index.html +180 -0
  48. package/site/tutorials/v1.html +211 -0
  49. package/site/tutorials/v2.html +207 -0
  50. package/site/tutorials/v3.html +214 -0
  51. package/site/tutorials/v4.html +195 -0
  52. package/site/tutorials/v5.html +163 -0
  53. package/site/tutorials/v6.html +165 -0
  54. package/site/tutorials/v7.html +184 -0
@@ -1,155 +1,31 @@
1
1
  import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
2
  import { consentParams } from '../../shared/analytics/consent.mjs';
3
+ import {
4
+ consentControllerExpression,
5
+ withConsentChromeBridge,
6
+ } from './consent.mjs';
3
7
 
4
8
  runAdapter((input) => withExtensionPage(input, async (page) => {
5
9
  const { participate, marketing, timeoutMs } = consentParams(input.node);
6
10
  const bridgeSymbol = 'metamask-harness-consent-bridge';
7
11
 
8
- async function readConsentState() {
9
- return page.evaluate(`(async () => {
10
- const hooks = globalThis.stateHooks || {};
11
- const storeState = hooks.store?.getState?.();
12
- const cleanState = typeof hooks.getCleanAppState === 'function'
13
- ? await hooks.getCleanAppState()
14
- : undefined;
15
- if (!storeState && !cleanState) {
16
- throw new Error('Extension consent state is unavailable.');
17
- }
18
- const metamask = storeState?.metamask ?? cleanState?.metamask ?? {};
19
- return {
20
- optedIn: Boolean(metamask.optedIn),
21
- dataCollectionForMarketing: Boolean(metamask.dataCollectionForMarketing),
22
- analyticsId: metamask.analyticsId ? 'set' : null
23
- };
24
- })()`, { awaitPromise: true });
25
- }
26
-
27
- async function waitForConsentState(expected) {
28
- const deadline = Date.now() + timeoutMs;
29
- let state;
30
- do {
31
- state = await readConsentState();
32
- if (Object.entries(expected).every(([key, value]) => state[key] === value)) {
33
- return state;
34
- }
35
- await new Promise((resolve) => setTimeout(resolve, 100));
36
- } while (Date.now() < deadline);
37
- throw new Error(`Consent state did not settle to ${JSON.stringify(expected)}; got ${JSON.stringify(state)}.`);
38
- }
39
-
40
12
  const hasDebugBridge = await page.evaluate(
41
13
  `typeof globalThis.stateHooks?.submitRequestToBackground === 'function'`,
42
14
  );
43
- let preloadIdentifier;
44
- if (!hasDebugBridge) {
45
- const preload = await page.session.call('Page.addScriptToEvaluateOnNewDocument', {
46
- source: `Object.defineProperty(
47
- globalThis,
48
- Symbol.for(${JSON.stringify(bridgeSymbol)}),
49
- { value: globalThis.chrome, configurable: true }
50
- );`,
51
- });
52
- preloadIdentifier = preload?.identifier;
53
- try {
54
- let notifyLoaded;
55
- const loaded = new Promise((resolve) => {
56
- notifyLoaded = resolve;
57
- });
58
- const unsubscribe = page.session.on('Page.loadEventFired', () => notifyLoaded?.());
59
- let reloadTimeout;
60
- try {
61
- await page.session.call('Page.reload', { ignoreCache: false });
62
- await Promise.race([
63
- loaded,
64
- new Promise((_, reject) => {
65
- reloadTimeout = setTimeout(
66
- () => reject(new Error(`Extension consent reload timed out after ${timeoutMs}ms.`)),
67
- timeoutMs,
68
- );
69
- }),
70
- ]);
71
- } finally {
72
- clearTimeout(reloadTimeout);
73
- unsubscribe();
74
- }
75
- await page.waitForExpression(
76
- `typeof globalThis[Symbol.for(${JSON.stringify(bridgeSymbol)})]?.runtime?.connect === 'function'`,
77
- { timeoutMs },
78
- );
79
- } finally {
80
- if (preloadIdentifier) {
81
- await page.session.call('Page.removeScriptToEvaluateOnNewDocument', {
82
- identifier: preloadIdentifier,
83
- });
84
- }
85
- }
86
- }
87
-
88
- const controllerResult = await page.evaluate(`(async () => {
89
- const submit = globalThis.stateHooks?.submitRequestToBackground;
90
- const bridgeKey = Symbol.for(${JSON.stringify(bridgeSymbol)});
91
- const capturedChrome = globalThis[bridgeKey];
92
- let port;
93
- let nextId = Date.now();
94
-
95
- const rawSubmit = (method, params) => new Promise((resolve, reject) => {
96
- if (!port) {
97
- const connectionName = location.pathname.includes('sidepanel')
98
- ? 'sidepanel'
99
- : location.pathname.includes('popup')
100
- ? 'popup'
101
- : 'fullscreen';
102
- port = capturedChrome.runtime.connect({ name: connectionName });
103
- }
104
- const id = nextId++;
105
- const timer = setTimeout(
106
- () => reject(new Error(method + ' timed out after ${timeoutMs}ms')),
107
- ${timeoutMs},
108
- );
109
- const listener = (message) => {
110
- const data = message?.name === 'controller' ? message.data : null;
111
- if (data?.id !== id) return;
112
- clearTimeout(timer);
113
- port.onMessage.removeListener(listener);
114
- if (data.error) reject(new Error(JSON.stringify(data.error)));
115
- else resolve(data.result);
116
- };
117
- port.onMessage.addListener(listener);
118
- port.postMessage({
119
- name: 'controller',
120
- data: { jsonrpc: '2.0', id, method, params }
121
- });
122
- });
123
-
124
- const callController = typeof submit === 'function' ? submit : rawSubmit;
125
- if (typeof callController !== 'function') {
126
- throw new Error('Extension consent setup could not reach the background controller.');
127
- }
128
-
129
- const setMarketing = async (value) => {
130
- await callController('setDataCollectionForMarketing', [value]);
131
- };
132
-
133
- try {
134
- if (!${JSON.stringify(participate)}) await setMarketing(false);
135
- const analyticsId = await callController(
136
- 'setParticipateInMetaMetrics',
137
- [${JSON.stringify(participate)}],
138
- );
139
- if (${JSON.stringify(participate)}) {
140
- await setMarketing(${JSON.stringify(marketing)});
141
- }
142
- return { analyticsId: analyticsId ? 'set' : null };
143
- } finally {
144
- port?.disconnect();
145
- if (capturedChrome) delete globalThis[bridgeKey];
146
- }
147
- })()`, { awaitPromise: true });
148
-
149
- const state = await waitForConsentState({
150
- optedIn: participate,
151
- dataCollectionForMarketing: marketing,
152
- });
15
+ const runController = () => page.evaluate(consentControllerExpression({
16
+ bridgeSymbol,
17
+ participate,
18
+ marketing,
19
+ timeoutMs,
20
+ }), { awaitPromise: true });
21
+ const controllerResult = hasDebugBridge
22
+ ? await runController()
23
+ : await withConsentChromeBridge(
24
+ page,
25
+ { bridgeSymbol, timeoutMs },
26
+ runController,
27
+ );
28
+ const state = controllerResult.state;
153
29
 
154
30
  if (state.optedIn !== participate) {
155
31
  throw new Error(`Expected optedIn=${participate}, got ${state.optedIn}.`);
@@ -166,6 +42,6 @@ runAdapter((input) => withExtensionPage(input, async (page) => {
166
42
  ...state,
167
43
  analyticsId: controllerResult.analyticsId,
168
44
  },
169
- proofPath: 'extension-background-controller',
45
+ proofPath: `extension-${controllerResult.stateSource}`,
170
46
  };
171
47
  }));
@@ -1,5 +1,6 @@
1
1
  import { pathToFileURL } from 'node:url';
2
2
  import { dataTestId, marketSymbol, normalizeMarketSymbol, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
3
+ import { readPerpsRuntimeStateExpression } from './state.mjs';
3
4
 
4
5
  function sleep(ms) {
5
6
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -704,22 +705,7 @@ async function applyStateNavigation(input, config) {
704
705
 
705
706
 
706
707
  async function readPerpsRuntimeState(page) {
707
- return page.evaluate(`(async () => {
708
- const hooks = globalThis.stateHooks || {};
709
- const request = hooks.submitRequestToBackground;
710
- const reduxState = hooks.store?.getState?.() || {};
711
- const cleanState = (await hooks.getCleanAppState?.()) || hooks.getState?.() || {};
712
- const metamask = reduxState.metamask || cleanState.metamask || {};
713
- const nestedPerps = metamask.PerpsController || cleanState.PerpsController || cleanState.perps || {};
714
- return {
715
- available: typeof request === 'function',
716
- activeProvider: metamask.activeProvider || nestedPerps.activeProvider || 'hyperliquid',
717
- networkKnown: typeof nestedPerps.isTestnet === 'boolean',
718
- isTestnet: Boolean(nestedPerps.isTestnet),
719
- initializationState: metamask.initializationState || nestedPerps.initializationState || null,
720
- isEligible: Boolean(metamask.isEligible ?? nestedPerps.isEligible),
721
- };
722
- })()`);
708
+ return page.evaluate(readPerpsRuntimeStateExpression());
723
709
  }
724
710
 
725
711
  async function ensureProvider(input, config) {
@@ -0,0 +1,20 @@
1
+ export function readPerpsRuntimeStateExpression() {
2
+ return `(async () => {
3
+ const hooks = globalThis.stateHooks || {};
4
+ const request = hooks.submitRequestToBackground;
5
+ const reduxState = hooks.store?.getState?.() || {};
6
+ const cleanState = (await hooks.getCleanAppState?.()) || hooks.getState?.() || {};
7
+ const reduxMetamask = reduxState.metamask || {};
8
+ const cleanMetamask = cleanState.metamask || {};
9
+ const metamask = { ...cleanMetamask, ...reduxMetamask };
10
+ const nestedPerps = reduxMetamask.PerpsController || cleanMetamask.PerpsController || cleanState.PerpsController || cleanState.perps || {};
11
+ return {
12
+ available: typeof request === 'function',
13
+ activeProvider: metamask.activeProvider || nestedPerps.activeProvider || 'hyperliquid',
14
+ networkKnown: typeof metamask.isTestnet === 'boolean' || typeof nestedPerps.isTestnet === 'boolean',
15
+ isTestnet: Boolean(metamask.isTestnet ?? nestedPerps.isTestnet),
16
+ initializationState: metamask.initializationState || nestedPerps.initializationState || null,
17
+ isEligible: Boolean(metamask.isEligible ?? nestedPerps.isEligible),
18
+ };
19
+ })()`;
20
+ }
@@ -1,37 +1,15 @@
1
1
  import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
+ import { listWalletAccountsExpression } from './state.mjs';
2
3
 
3
4
  runAdapter((input) => withExtensionPage(input, async (page) => {
4
5
  const scope = input.node?.scope == null ? 'named' : String(input.node.scope);
5
6
  if (scope !== 'selected' && scope !== 'named' && scope !== 'evm' && scope !== 'all') {
6
7
  throw new Error('metamask.wallet.list_accounts scope must be selected, named, evm, or all.');
7
8
  }
8
- const result = await page.evaluate(`(() => {
9
- const store = globalThis.stateHooks?.store;
10
- if (!store || typeof store.getState !== 'function') {
11
- throw new Error('stateHooks.store.getState is unavailable.');
12
- }
13
- const internal = store.getState()?.metamask?.internalAccounts || {};
14
- const selectedId = internal.selectedAccount || null;
15
- const scope = ${JSON.stringify(scope)};
16
- const accounts = Object.entries(internal.accounts || {})
17
- .map(([id, account]) => ({
18
- id,
19
- address: account?.address || null,
20
- name: account?.metadata?.name || null,
21
- type: account?.type || null,
22
- selected: id === selectedId
23
- }))
24
- .filter((account) => {
25
- if (scope === 'selected') return account.selected;
26
- if (scope === 'all') return true;
27
- if (scope === 'evm') return /^0x[0-9a-f]{40}$/iu.test(account.address || '');
28
- return account.selected || Boolean(String(account.name || '').trim());
29
- });
30
- return { accounts };
31
- })()`);
9
+ const result = await page.evaluate(listWalletAccountsExpression(scope), { awaitPromise: true });
32
10
  return {
33
11
  action: input.action,
34
- source: 'extension-stateHooks-store',
12
+ source: 'extension-stateHooks-clean-state',
35
13
  scope,
36
14
  ...result,
37
15
  count: result.accounts.length,
@@ -1,27 +1,7 @@
1
1
  import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
+ import { readWalletStateExpression } from './state.mjs';
2
3
 
3
4
  runAdapter((input) => withExtensionPage(input, async (page) => {
4
- const state = await page.evaluate(`(() => {
5
- const hooks = globalThis.stateHooks;
6
- const store = hooks && hooks.store;
7
- const root = store && typeof store.getState === 'function' ? store.getState() : {};
8
- const metamask = root.metamask || {};
9
- const internal = metamask.internalAccounts || {};
10
- const accounts = internal.accounts || {};
11
- const selectedId = internal.selectedAccount || null;
12
- const selected = selectedId && accounts[selectedId] ? accounts[selectedId] : null;
13
- const metadata = selected && selected.metadata ? selected.metadata : {};
14
- return {
15
- href: String(globalThis.location && globalThis.location.href || ''),
16
- selectedAccount: selected ? {
17
- id: selectedId,
18
- address: selected.address || null,
19
- name: metadata.name || null,
20
- type: selected.type || null
21
- } : null,
22
- completedOnboarding: Boolean(metamask.completedOnboarding),
23
- selectedNetworkClientId: metamask.selectedNetworkClientId || null
24
- };
25
- })()`);
26
- return { action: input.action, source: 'extension-stateHooks-store', state, redacted: true, proofPath: 'extension-wallet-state' };
5
+ const state = await page.evaluate(readWalletStateExpression(), { awaitPromise: true });
6
+ return { action: input.action, source: 'extension-stateHooks-clean-state', state, redacted: true, proofPath: 'extension-wallet-state' };
27
7
  }));
@@ -1,4 +1,5 @@
1
1
  import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
+ import { selectWalletAccountExpression } from './state.mjs';
2
3
 
3
4
  function normalize(value) {
4
5
  return String(value ?? '').toLowerCase();
@@ -11,38 +12,10 @@ runAdapter((input) => withExtensionPage(input, async (page) => {
11
12
  if (!requestedAddress && !requestedId && !requestedName) {
12
13
  throw new Error('metamask.wallet.select_account extension live adapter requires node.address, node.id, or node.name.');
13
14
  }
14
- const selected = await page.evaluate(`(async () => {
15
- const hooks = globalThis.stateHooks;
16
- const store = hooks && hooks.store;
17
- const request = hooks && hooks.submitRequestToBackground;
18
- if (!store || typeof store.getState !== 'function') throw new Error('stateHooks.store.getState is unavailable.');
19
- if (typeof request !== 'function') throw new Error('stateHooks.submitRequestToBackground is unavailable.');
20
- const state = store.getState() || {};
21
- const internal = state.metamask?.internalAccounts || {};
22
- const accounts = internal.accounts || {};
23
- const requestedAddress = ${JSON.stringify(requestedAddress)};
24
- const requestedId = ${JSON.stringify(requestedId)};
25
- const requestedName = ${JSON.stringify(requestedName)};
26
- const entries = Object.entries(accounts);
27
- const match = entries.find(([id, account]) => {
28
- const metadata = account?.metadata || {};
29
- return (requestedId && id === requestedId) ||
30
- (requestedAddress && String(account?.address || '').toLowerCase() === requestedAddress) ||
31
- (requestedName && metadata.name === requestedName);
32
- });
33
- if (!match) throw new Error('Requested account was not found in internalAccounts.');
34
- const [id, account] = match;
35
- await request('setSelectedInternalAccount', [id]);
36
- const after = store.getState()?.metamask?.internalAccounts || {};
37
- const selectedId = after.selectedAccount;
38
- const selected = after.accounts?.[selectedId];
39
- if (selectedId !== id) throw new Error('Expected selected account ' + id + ', got ' + selectedId);
40
- return {
41
- id: selectedId,
42
- address: selected?.address || null,
43
- name: selected?.metadata?.name || null,
44
- type: selected?.type || null
45
- };
46
- })()`, { awaitPromise: true });
15
+ const selected = await page.evaluate(selectWalletAccountExpression({
16
+ address: requestedAddress,
17
+ id: requestedId,
18
+ name: requestedName,
19
+ }), { awaitPromise: true });
47
20
  return { action: input.action, selected, proofPath: 'extension-account-selection' };
48
21
  }));
@@ -1,26 +1,8 @@
1
1
  import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
+ import { readWalletStateExpression } from './state.mjs';
2
3
 
3
4
  runAdapter((input) => withExtensionPage(input, async (page) => {
4
- const state = await page.evaluate(`(() => {
5
- const passwordInput = Boolean(document.querySelector('input[type="password"]'));
6
- const hooks = globalThis.stateHooks;
7
- const store = hooks && hooks.store;
8
- const root = store && typeof store.getState === 'function' ? store.getState() : {};
9
- const metamask = root.metamask || {};
10
- const internal = metamask.internalAccounts || {};
11
- const accounts = internal.accounts || {};
12
- const selectedId = internal.selectedAccount || null;
13
- const selected = selectedId && accounts[selectedId] ? accounts[selectedId] : null;
14
- return {
15
- href: String(globalThis.location && globalThis.location.href || ''),
16
- passwordInput,
17
- completedOnboarding: Boolean(metamask.completedOnboarding),
18
- selectedAccount: selected ? {
19
- id: selectedId,
20
- address: selected.address || null,
21
- } : null,
22
- };
23
- })()`);
5
+ const state = await page.evaluate(readWalletStateExpression(), { awaitPromise: true });
24
6
  const seededProfilePresent = Boolean(state?.passwordInput || (state?.completedOnboarding && state?.selectedAccount?.address));
25
7
  if (!seededProfilePresent) {
26
8
  throw new Error(`Extension fixture profile is not ready; expected a locked password screen or completed onboarding with a selected account, got ${JSON.stringify(state)}`);
@@ -0,0 +1,111 @@
1
+ const walletStatePreamble = `
2
+ const readWalletState = async () => {
3
+ const hooks = globalThis.stateHooks || {};
4
+ let root = null;
5
+ if (typeof hooks.getCleanAppState === 'function') {
6
+ root = await hooks.getCleanAppState();
7
+ }
8
+ if ((!root || typeof root !== 'object') && typeof hooks.store?.getState === 'function') {
9
+ root = hooks.store.getState();
10
+ }
11
+ if (!root || typeof root !== 'object') {
12
+ throw new Error('stateHooks.getCleanAppState and stateHooks.store.getState are unavailable.');
13
+ }
14
+ const metamask = root.metamask || root;
15
+ return {
16
+ hooks,
17
+ metamask,
18
+ internal: metamask.internalAccounts || {},
19
+ };
20
+ };
21
+ `;
22
+
23
+ export function readWalletStateExpression() {
24
+ return `(async () => {
25
+ ${walletStatePreamble}
26
+ const { metamask, internal } = await readWalletState();
27
+ const accounts = internal.accounts || {};
28
+ const selectedId = internal.selectedAccount || null;
29
+ const selected = selectedId && accounts[selectedId] ? accounts[selectedId] : null;
30
+ const metadata = selected?.metadata || {};
31
+ return {
32
+ href: String(globalThis.location?.href || ''),
33
+ passwordInput: Boolean(globalThis.document?.querySelector?.('input[type="password"]')),
34
+ selectedAccount: selected ? {
35
+ id: selectedId,
36
+ address: selected.address || null,
37
+ name: metadata.name || null,
38
+ type: selected.type || null,
39
+ } : null,
40
+ completedOnboarding: Boolean(metamask.completedOnboarding),
41
+ selectedNetworkClientId: metamask.selectedNetworkClientId || null,
42
+ };
43
+ })()`;
44
+ }
45
+
46
+ export function listWalletAccountsExpression(scope) {
47
+ return `(async () => {
48
+ ${walletStatePreamble}
49
+ const { internal } = await readWalletState();
50
+ const selectedId = internal.selectedAccount || null;
51
+ const scope = ${JSON.stringify(scope)};
52
+ const accounts = Object.entries(internal.accounts || {})
53
+ .map(([id, account]) => ({
54
+ id,
55
+ address: account?.address || null,
56
+ name: account?.metadata?.name || null,
57
+ type: account?.type || null,
58
+ selected: id === selectedId,
59
+ }))
60
+ .filter((account) => {
61
+ if (scope === 'selected') return account.selected;
62
+ if (scope === 'all') return true;
63
+ if (scope === 'evm') return /^0x[0-9a-f]{40}$/iu.test(account.address || '');
64
+ return account.selected || Boolean(String(account.name || '').trim());
65
+ });
66
+ return { accounts };
67
+ })()`;
68
+ }
69
+
70
+ export function selectWalletAccountExpression(criteria, timeoutMs = 5000) {
71
+ return `(async () => {
72
+ ${walletStatePreamble}
73
+ const before = await readWalletState();
74
+ const accounts = before.internal.accounts || {};
75
+ const requestedAddress = ${JSON.stringify(criteria.address)};
76
+ const requestedId = ${JSON.stringify(criteria.id)};
77
+ const requestedName = ${JSON.stringify(criteria.name)};
78
+ const match = Object.entries(accounts).find(([id, account]) => {
79
+ const metadata = account?.metadata || {};
80
+ return (requestedId && id === requestedId) ||
81
+ (requestedAddress && String(account?.address || '').toLowerCase() === requestedAddress) ||
82
+ (requestedName && metadata.name === requestedName);
83
+ });
84
+ if (!match) throw new Error('Requested account was not found in internalAccounts.');
85
+ if (typeof before.hooks.submitRequestToBackground !== 'function') {
86
+ throw new Error('stateHooks.submitRequestToBackground is unavailable.');
87
+ }
88
+ const [id] = match;
89
+ await before.hooks.submitRequestToBackground('setSelectedInternalAccount', [id]);
90
+ const deadline = Date.now() + ${Number(timeoutMs)};
91
+ let observedId = before.internal.selectedAccount || null;
92
+ while (Date.now() <= deadline) {
93
+ const after = await readWalletState();
94
+ observedId = after.internal.selectedAccount || null;
95
+ if (observedId === id) {
96
+ const selected = after.internal.accounts?.[observedId];
97
+ return {
98
+ id: observedId,
99
+ address: selected?.address || null,
100
+ name: selected?.metadata?.name || null,
101
+ type: selected?.type || null,
102
+ };
103
+ }
104
+ await new Promise((resolve) => setTimeout(resolve, 100));
105
+ }
106
+ throw new Error(
107
+ 'Expected selected account ' + id + ', got ' + String(observedId) +
108
+ ' after ${Number(timeoutMs)}ms.',
109
+ );
110
+ })()`;
111
+ }
@@ -7,7 +7,7 @@
7
7
  "nodes": {
8
8
  "prepare-files": {
9
9
  "action": "command",
10
- "cmd": "mkdir -p temp/recipe-runner.action-validation && printf 'E2E_ACTION_VALIDATION extension\\n' > temp/recipe-runner.action-validation/probe.txt && printf '{\"platform\":\"extension\",\"ok\":true}\n' > temp/recipe-runner.action-validation/probe.json && printf 'extension log E2E_ACTION_VALIDATION\\n' > temp/recipe-runner.action-validation/validation.log && echo E2E_ACTION_VALIDATION extension",
10
+ "cmd": "mkdir -p temp/recipe-runner.action-validation && printf 'E2E_ACTION_VALIDATION extension\\n' > temp/recipe-runner.action-validation/probe.txt && printf '{\"platform\":\"extension\",\"ok\":true}\n' > temp/recipe-runner.action-validation/probe.json && printf 'extension log E2E_ACTION_VALIDATION\\n' >> temp/recipe-runner.action-validation/validation.log && echo E2E_ACTION_VALIDATION extension",
11
11
  "next": "assert-command-exit",
12
12
  "intent": "Prepare runner.action-validation probe files"
13
13
  },
@@ -7,7 +7,7 @@
7
7
  "nodes": {
8
8
  "prepare-files": {
9
9
  "action": "command",
10
- "cmd": "mkdir -p temp/recipe-runner.action-validation && printf 'E2E_ACTION_VALIDATION mobile\\n' > temp/recipe-runner.action-validation/probe.txt && printf '{\"platform\":\"mobile\",\"ok\":true}\n' > temp/recipe-runner.action-validation/probe.json && printf 'mobile log E2E_ACTION_VALIDATION\\n' > temp/recipe-runner.action-validation/validation.log && echo E2E_ACTION_VALIDATION mobile",
10
+ "cmd": "mkdir -p temp/recipe-runner.action-validation && printf 'E2E_ACTION_VALIDATION mobile\\n' > temp/recipe-runner.action-validation/probe.txt && printf '{\"platform\":\"mobile\",\"ok\":true}\n' > temp/recipe-runner.action-validation/probe.json && printf 'mobile log E2E_ACTION_VALIDATION\\n' >> temp/recipe-runner.action-validation/validation.log && echo E2E_ACTION_VALIDATION mobile",
11
11
  "next": "assert-command-exit",
12
12
  "intent": "Prepare runner.action-validation probe files"
13
13
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeeed/metamask-harness",
3
- "version": "0.28.0",
3
+ "version": "0.29.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mm-harness": "bin/mm-harness"
@@ -13,16 +13,17 @@
13
13
  "test:unit": "vitest run --config scripts/vitest.config.mjs",
14
14
  "test:coverage": "vitest run --coverage --config scripts/vitest.config.mjs",
15
15
  "qa:human": "node scripts/validate-human-outcomes.mjs",
16
+ "site:contrast": "node scripts/site-contrast.mjs",
16
17
  "self-test": "bin/mm-harness self-test",
17
18
  "manifest:mobile": "bin/mm-harness actions --raw --adapter mobile --json",
18
19
  "manifest:extension": "bin/mm-harness actions --raw --adapter extension --json",
19
20
  "check:syntax": "find . -name '*.mjs' -print0 | xargs -0 -n1 node --check"
20
21
  },
21
22
  "dependencies": {
22
- "@farmslot/agent-runtime": "^0.4.0",
23
+ "@farmslot/agent-runtime": "^0.5.0",
23
24
  "@farmslot/handoff": "^0.3.1",
24
- "@farmslot/protocol": "^0.14.0",
25
- "@farmslot/recipe-harness": "^0.10.4",
25
+ "@farmslot/protocol": "^0.15.0",
26
+ "@farmslot/recipe-harness": "^0.11.0",
26
27
  "commander": "^12.0.0",
27
28
  "es-module-lexer": "2.3.1",
28
29
  "esbuild": "0.28.1",
@@ -61,8 +62,10 @@
61
62
  "dist",
62
63
  "adapters",
63
64
  "library",
65
+ "site",
64
66
  "scripts/completions.sh",
65
67
  "scripts/install-completions.sh",
68
+ "scripts/site-contrast.mjs",
66
69
  "scripts/validate-human-outcomes.mjs",
67
70
  "!scripts/README.md",
68
71
  "docs",