@deeeed/metamask-harness 0.36.0 → 0.38.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 +51 -0
- package/adapters/extension/artifact-runtime-state.cjs +138 -0
- package/adapters/extension/inject.mjs +2 -0
- package/adapters/extension/launch-browser.cjs +24 -0
- package/adapters/extension/live.sh +82 -12
- package/adapters/extension/readiness.mjs +77 -36
- package/adapters/extension/snapshot-dist.sh +88 -3
- package/adapters/extension/verify.sh +6 -2
- package/adapters/manifest.json +17 -1
- package/adapters/shared/log-tui.mjs +1 -1
- package/dist/adapters/extension/artifact-integrity.js +38 -0
- package/dist/adapters/extension/extension-id.js +23 -4
- package/dist/adapters/extension/release-artifact.js +386 -0
- package/dist/adapters/extension/runtime-decision.js +164 -20
- package/dist/adapters/extension/runtime.js +169 -3
- package/dist/adapters/mobile/provision.js +38 -3
- package/dist/adapters/mobile/release-artifact-state.js +124 -0
- package/dist/adapters/mobile/release-artifact.js +295 -0
- package/dist/adapters.js +78 -6
- package/dist/app-lifecycle.js +4 -3
- package/dist/command-contract.js +16 -0
- package/dist/commands/call.js +6 -2
- package/dist/commands/launch/extension.js +74 -9
- package/dist/commands/launch/index.js +3 -0
- package/dist/commands/launch/mobile.js +2 -0
- package/dist/commands/provision.js +2 -0
- package/dist/commands/run-engine.js +109 -14
- package/dist/commands/run.js +4 -2
- package/dist/commands/runtime-launch.js +178 -10
- package/dist/heal-bounds.js +1 -1
- package/dist/live-adapter-contract.js +3 -1
- package/dist/metamask-action-validation.js +47 -1
- package/dist/mm-harness-cli.js +31 -1
- package/dist/recipe-security.js +40 -0
- package/dist/run-diagnostics.js +1 -1
- package/dist/runner.js +3 -0
- package/docs/RELEASE-QA-CAPABILITY-MAP.md +125 -0
- package/library/actions/extension/assets/finish_send.mjs +128 -0
- package/library/actions/extension/assets/import_custom_token.mjs +227 -0
- package/library/actions/extension/assets/prepare_send.mjs +53 -0
- package/library/actions/extension/assets/read_details.mjs +57 -0
- package/library/actions/extension/assets/read_visible_state.mjs +50 -0
- package/library/actions/extension/assets/review_send.mjs +23 -0
- package/library/actions/extension/assets/select_network_scope.mjs +30 -0
- package/library/actions/extension/assets/send.mjs +104 -0
- package/library/actions/extension/assets/set_custom_gas.mjs +56 -0
- package/library/actions/extension/assets/set_token_visibility.mjs +129 -0
- package/library/actions/extension/assets/verify_sorting.mjs +89 -0
- package/library/actions/extension/networks/add_chainlist.mjs +94 -0
- package/library/actions/extension/networks/add_custom.mjs +123 -0
- package/library/actions/extension/networks/custom_network.mjs +103 -0
- package/library/actions/extension/networks/read_visible_state.mjs +50 -0
- package/library/actions/extension/networks/remove_custom.mjs +68 -0
- package/library/actions/extension/perps/close_orders.mjs +1 -0
- package/library/actions/extension/perps/close_visible_position.mjs +133 -0
- package/library/actions/extension/perps/compare_provider_market.mjs +109 -0
- package/library/actions/extension/perps/edit_margin.mjs +82 -0
- package/library/actions/extension/perps/open_balance_action.mjs +58 -0
- package/library/actions/extension/perps/open_position_action.mjs +80 -0
- package/library/actions/extension/perps/perps.mjs +196 -2
- package/library/actions/extension/perps/place_order.mjs +5 -2
- package/library/actions/extension/perps/read_eligibility.mjs +34 -0
- package/library/actions/extension/perps/read_funds_confirmation.mjs +69 -0
- package/library/actions/extension/perps/read_snapshot.mjs +470 -0
- package/library/actions/extension/perps/read_visible_state.mjs +98 -0
- package/library/actions/extension/perps/select_activity_filter.mjs +44 -0
- package/library/actions/extension/perps/select_market_filter.mjs +71 -0
- package/library/actions/extension/perps/set_market_favorite.mjs +47 -0
- package/library/actions/extension/perps/update_position_tpsl.mjs +106 -0
- package/library/actions/extension/platform/cdp.mjs +109 -3
- package/library/actions/extension/settings/set_basic_functionality.mjs +58 -0
- package/library/actions/extension/swap_bridge/read_visible_state.mjs +71 -0
- package/library/actions/extension/swap_bridge/select_assets.mjs +103 -0
- package/library/actions/extension/swap_bridge/set_amount.mjs +36 -0
- package/library/actions/extension/swap_bridge/set_max_amount.mjs +58 -0
- package/library/actions/extension/swap_bridge/set_slippage.mjs +121 -0
- package/library/actions/extension/swap_bridge/submit_transaction.mjs +363 -0
- package/library/actions/extension/ui/activate_and_observe.mjs +96 -0
- package/library/actions/extension/ui/navigate.mjs +264 -8
- package/library/actions/extension/wallet/ensure_unlocked.mjs +2 -6
- package/library/actions/extension/wallet/import.mjs +13 -46
- package/library/actions/extension/wallet/lock.mjs +35 -0
- package/library/actions/extension/wallet/secret-input.mjs +98 -0
- package/library/actions/extension/wallet/select_account.mjs +71 -7
- package/library/actions/mobile/assets/read_visible_state.mjs +37 -0
- package/library/actions/mobile/perps/read_visible_state.mjs +31 -0
- package/library/actions/mobile/platform/native-session-name.mjs +5 -0
- package/library/actions/mobile/platform/observe-ui.mjs +86 -2
- package/library/actions/mobile/swap_bridge/native-session.mjs +45 -0
- package/library/actions/mobile/swap_bridge/read_visible_state.mjs +31 -0
- package/library/actions/mobile/swap_bridge/select_assets.mjs +94 -0
- package/library/actions/mobile/swap_bridge/set_amount.mjs +73 -0
- package/library/actions/mobile/ui/native-navigation.mjs +265 -0
- package/library/actions/mobile/ui/navigate.mjs +10 -1
- package/library/actions/mobile/wallet/ensure_unlocked.mjs +17 -0
- package/library/actions/mobile/wallet/import.mjs +71 -2
- package/library/actions/mobile/wallet/native-ui.mjs +512 -0
- package/library/actions/mobile/wallet/read_state.mjs +16 -0
- package/library/actions/mobile/wallet/reset.mjs +17 -4
- package/library/actions/shared/assets/visible-state.mjs +176 -0
- package/library/actions/shared/networks/visible-state.mjs +70 -0
- package/library/actions/shared/perps/visible-state.mjs +354 -0
- package/library/actions/shared/swap-bridge/transaction.mjs +54 -0
- package/library/actions/shared/swap-bridge/visible-state.mjs +97 -0
- package/library/actions/shared/ui/locators.mjs +7 -0
- package/library/manifests/extension.action-manifest.json +1366 -184
- package/library/manifests/mobile.action-manifest.json +235 -1
- package/library/recipes/assets/release-custom-gas-send.recipe.json +88 -0
- package/library/recipes/assets/release-custom-network-token-persistence.recipe.json +106 -0
- package/library/recipes/assets/release-native-max-cancel.recipe.json +70 -0
- package/library/recipes/assets/release-native-max-send.recipe.json +78 -0
- package/library/recipes/assets/release-own-account-send.recipe.json +80 -0
- package/library/recipes/assets/release-token-address-import.recipe.json +47 -0
- package/library/recipes/assets/release-token-details.recipe.json +48 -0
- package/library/recipes/assets/release-token-hide-readd.recipe.json +55 -0
- package/library/recipes/assets/release-token-list.recipe.json +75 -0
- package/library/recipes/assets/release-token-persistence.recipe.json +73 -0
- package/library/recipes/assets/release-token-search-import.recipe.json +40 -0
- package/library/recipes/assets/release-token-sorting.recipe.json +29 -0
- package/library/recipes/extension/networks/release-balance-stability.recipe.json +51 -0
- package/library/recipes/extension/perps/release-live-limit-order.recipe.json +104 -0
- package/library/recipes/extension/perps/release-order-entry.recipe.json +155 -0
- package/library/recipes/extension/perps/release-order-validation.recipe.json +220 -0
- package/library/recipes/extension/runner/action-validation.recipe.json +12 -1
- package/library/recipes/perps/release-activity-filters.recipe.json +51 -0
- package/library/recipes/perps/release-funds-flows.recipe.json +76 -0
- package/library/recipes/perps/release-live-market-order.recipe.json +122 -0
- package/library/recipes/perps/release-market-details.recipe.json +83 -0
- package/library/recipes/perps/release-market-search.recipe.json +79 -0
- package/library/recipes/perps/release-watchlist.recipe.json +99 -0
- package/library/recipes/swap-bridge/release-custom-slippage.recipe.json +90 -0
- package/library/recipes/swap-bridge/release-live-max-swap.recipe.json +99 -0
- package/library/recipes/swap-bridge/release-live-swap.recipe.json +101 -0
- package/library/recipes/swap-bridge/release-max-quote.recipe.json +72 -0
- package/library/recipes/swap-bridge/release-quote.recipe.json +72 -0
- package/library/recipes/swap-bridge/release-surface.recipe.json +43 -0
- package/library/recipes/wallet/import.recipe.json +20 -1
- package/library/recipes/wallet/reset-import.recipe.json +20 -1
- package/package.json +6 -5
- package/scripts/validate-release-report.mjs +319 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,57 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.38.0 - 2026-08-14
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Add visible Perps release actions and recipes for market details, search, filters, watchlists, order entry, position management, funds confirmations, and activity on untouched Extension builds, plus native Android market-state evidence without a development bridge.
|
|
10
|
+
- Add shared Assets release actions and recipes for token lists, details, sorting, import, visibility, persistence, send review, custom gas, and network scope on Extension, plus native Android holdings evidence.
|
|
11
|
+
- Add shared Swap and Bridge actions and recipes for exact asset selection, quote amounts, Max, slippage, and guarded submission on Extension, with native Android selection and quote evidence.
|
|
12
|
+
- Add Extension custom-network lifecycle and visible-state actions plus bounded balance-stability validation, and native Android multi-network holdings evidence.
|
|
13
|
+
- Add a canonical release-report gate that rejects missing case IDs, incomplete evidence, non-terminal verdicts, and unfinished case inventories before writing report metadata and quality output.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Recover Runway provisioning from an unresponsive iOS simulator by restarting the selected device once, waiting for a confirmed boot, and retrying the bounded installed-app probe.
|
|
18
|
+
- Consume Protocol 0.21.0, Recipe Harness 0.15.0, Agent Runtime 0.8.1, and Expo Recipe 0.8.1 as one compatible QA runtime set.
|
|
19
|
+
- Stop every Chrome process using an Extension validation profile before replacing its runtime snapshot, preventing stale pages from loading chunks from a different build.
|
|
20
|
+
- Prefer an unlocked Extension Home page over stale onboarding and Side Panel pages when multiple CDP targets remain open.
|
|
21
|
+
- Fixed opaque Android navigation completing the same native session twice after reaching its destination.
|
|
22
|
+
- Keep Android release actions on one device-and-app-scoped accessibility session so sequential recipe nodes do not conflict over stale leases.
|
|
23
|
+
- Navigate Android Swap through semantic accessibility identifiers, dismiss the keyboard before selecting exact chain-qualified assets, and scroll the token list until the requested row is visible.
|
|
24
|
+
- Unlock an opaque Android release with the wallet password instead of entering biometric authentication, and omit Metro launch data from native release lifecycle actions.
|
|
25
|
+
- Bind send and limit-order evidence to the newly submitted operation, reject selectors that native UI cannot disambiguate, and fail when Perps navigation or partial TP/SL reconciliation is not proven.
|
|
26
|
+
- Classify live Swap submission as an external mutation, reject ID-less release-report cases, and probe competing Extension tabs before selecting the runtime health target.
|
|
27
|
+
- Require explicit approval for live Perps submissions and reject release evidence that escapes its declared root or lacks regular trace leaves.
|
|
28
|
+
- Bind custom-token reuse to its chain and contract, reject vacuous asset sorting proof, and report capped Perps observations as truncated.
|
|
29
|
+
- Bind Swap confirmation to a newly created transaction, reconcile exact TP/SL values, assert disabled zero-withdraw submission, and require structured run evidence.
|
|
30
|
+
- Dismiss the first-run Perps tutorial through its visible Skip control before release recipes act on the Perps home screen.
|
|
31
|
+
|
|
32
|
+
## 0.37.0 - 2026-08-13
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- Add archive and extracted-tree integrity checks, caching, provenance, and isolated launch for downloaded Extension release artifacts without using the checkout build or product configuration.
|
|
37
|
+
- Add verified local Android APK install and launch with expected version name, version code, package ID, trusted digest, explicit device selection, terminal provenance, and native-accessibility recipes that do not require a development bridge or HUD.
|
|
38
|
+
- Add an Extension Perps snapshot action for bounded, redacted account, market, position, open-order, history, and watchlist observations when the background test hook is available.
|
|
39
|
+
- Add a release QA capability map covering the current Perps, Assets, Swap and Bridge, and Networks checklists, with a prioritized parity roadmap.
|
|
40
|
+
- Add native Android release navigation and accessibility observation for wallet home, Perps home, and parameterized Perps markets without Metro, a development bridge, a HUD, or fixed screen coordinates.
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
|
|
44
|
+
- Probe Ethereum through the Extension UI provider when production builds omit test state hooks, while preserving explicit unreachable failures.
|
|
45
|
+
- Import recovery phrases through production LavaMoat onboarding without browser clipboard permissions, and remove the temporary browser bridge after use.
|
|
46
|
+
- Keep release-artifact recipe failures on the acquired binary and fail closed instead of recovering through a checkout build.
|
|
47
|
+
- Relaunch a stopped verified Extension artifact from its integrity-checked cache without switching builds or deleting the imported wallet profile.
|
|
48
|
+
- Recover a newly opened Extension home tab from Chromium's transient blocked-page shell by navigating the selected extension target once before readiness fails.
|
|
49
|
+
- Prefer the visible side panel after official-build onboarding opens it, instead of leaving recipes attached to the completed onboarding tab.
|
|
50
|
+
- Reject unattended biometric setup explicitly, handle default Mobile notification and wallet-tour choices on bridge-backed onboarding, and keep Extension-only Perps snapshot limits out of Core validation.
|
|
51
|
+
- Confine Extension runtime snapshots and artifact identity state to non-symlinked checkout runtime directories before replacing prior state.
|
|
52
|
+
- Complete opaque Mobile onboarding without Metro or a development bridge by cancelling biometric storage for password-only automation, handling notification and wallet-tour prompts with configurable defaults, and proving the imported address through native UI.
|
|
53
|
+
- Close each physical-device accessibility session only after its wallet proof completes, so composed reset/import/state recipes hand the device to the next node cleanly.
|
|
54
|
+
- Label artifact preparation as preparation instead of reporting it as a client build.
|
|
55
|
+
|
|
5
56
|
## 0.36.0 - 2026-08-13
|
|
6
57
|
|
|
7
58
|
### Added
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
|
|
7
|
+
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
8
|
+
console.log('Usage: artifact-runtime-state.cjs <set|clear> --target <absolute-dir> --runtime-dir <relative-dir> [--source-dir <absolute-dir> --runtime-dist <absolute-dir> --provenance <absolute-file> --chrome-user-data-dir <absolute-dir>]');
|
|
9
|
+
process.exit(0);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const [action, ...argv] = process.argv.slice(2);
|
|
13
|
+
const args = parseArgs(argv);
|
|
14
|
+
const target = requiredPath(args, 'target');
|
|
15
|
+
const runtimeDir = requiredRelative(args, 'runtime-dir');
|
|
16
|
+
|
|
17
|
+
if (action === 'clear') {
|
|
18
|
+
const directory = safeRuntimeDirectory(target, runtimeDir, false);
|
|
19
|
+
if (!directory) process.exit(0);
|
|
20
|
+
const statePath = path.join(directory, 'extension-release-artifact.json');
|
|
21
|
+
const existing = fs.lstatSync(statePath, { throwIfNoEntry: false });
|
|
22
|
+
if (!existing) process.exit(0);
|
|
23
|
+
if (!existing.isFile() || existing.isSymbolicLink()) throw invalidRuntimeState(statePath);
|
|
24
|
+
fs.unlinkSync(statePath);
|
|
25
|
+
process.exit(0);
|
|
26
|
+
}
|
|
27
|
+
if (action !== 'set') throw new Error('Usage: artifact-runtime-state.cjs <set|clear> --target <dir> --runtime-dir <relative> [set flags]');
|
|
28
|
+
|
|
29
|
+
const sourceDir = requiredPath(args, 'source-dir');
|
|
30
|
+
const runtimeDist = requiredPath(args, 'runtime-dist');
|
|
31
|
+
const provenancePath = requiredPath(args, 'provenance');
|
|
32
|
+
const chromeUserDataDir = requiredPath(args, 'chrome-user-data-dir');
|
|
33
|
+
const directory = safeRuntimeDirectory(target, runtimeDir, true);
|
|
34
|
+
const runtimeRoot = path.resolve(target, runtimeDir);
|
|
35
|
+
if (!chromeUserDataDir.startsWith(`${runtimeRoot}${path.sep}`)) {
|
|
36
|
+
throw new Error('Release artifact Chrome profile must be a dedicated child of the runtime directory.');
|
|
37
|
+
}
|
|
38
|
+
const profileStat = fs.lstatSync(chromeUserDataDir, { throwIfNoEntry: false });
|
|
39
|
+
if (!profileStat?.isDirectory() || profileStat.isSymbolicLink()) {
|
|
40
|
+
throw new Error('Release artifact Chrome profile must be an existing non-symlink directory.');
|
|
41
|
+
}
|
|
42
|
+
const provenance = readRegularJson(provenancePath);
|
|
43
|
+
const relativeExtensionDir = provenance.extensionDir;
|
|
44
|
+
if (
|
|
45
|
+
typeof relativeExtensionDir !== 'string' ||
|
|
46
|
+
!relativeExtensionDir ||
|
|
47
|
+
path.isAbsolute(relativeExtensionDir) ||
|
|
48
|
+
relativeExtensionDir.split(/[\\/]+/).includes('..')
|
|
49
|
+
) {
|
|
50
|
+
throw new Error('Release artifact provenance has an invalid extensionDir.');
|
|
51
|
+
}
|
|
52
|
+
const provenanceRoot = path.dirname(provenancePath);
|
|
53
|
+
const boundSource = path.resolve(provenanceRoot, relativeExtensionDir);
|
|
54
|
+
if (boundSource !== sourceDir) throw new Error('Release artifact provenance does not bind the selected extension directory.');
|
|
55
|
+
if (!/^[a-f0-9]{64}$/.test(provenance.sha256 || '')) throw new Error('Release artifact provenance has an invalid SHA-256.');
|
|
56
|
+
if (!/^[a-f0-9]{64}$/.test(provenance.treeSha256 || '')) throw new Error('Release artifact provenance has an invalid tree SHA-256.');
|
|
57
|
+
if (!fs.existsSync(path.join(sourceDir, 'manifest.json'))) throw new Error('Release artifact source manifest is missing.');
|
|
58
|
+
if (!fs.existsSync(path.join(runtimeDist, 'manifest.json'))) throw new Error('Release artifact runtime manifest is missing.');
|
|
59
|
+
|
|
60
|
+
const state = {
|
|
61
|
+
schemaVersion: 2,
|
|
62
|
+
sourceDir,
|
|
63
|
+
runtimeDist,
|
|
64
|
+
provenancePath,
|
|
65
|
+
chromeUserDataDir,
|
|
66
|
+
sha256: provenance.sha256,
|
|
67
|
+
treeSha256: provenance.treeSha256,
|
|
68
|
+
expectedVersion: provenance.expectedVersion,
|
|
69
|
+
manifestVersion: provenance.manifestVersion,
|
|
70
|
+
recordedAt: new Date().toISOString(),
|
|
71
|
+
};
|
|
72
|
+
const statePath = path.join(directory, 'extension-release-artifact.json');
|
|
73
|
+
const existing = fs.lstatSync(statePath, { throwIfNoEntry: false });
|
|
74
|
+
if (existing && (!existing.isFile() || existing.isSymbolicLink())) throw invalidRuntimeState(statePath);
|
|
75
|
+
const temporary = `${statePath}.${process.pid}.tmp`;
|
|
76
|
+
fs.writeFileSync(temporary, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600, flag: 'wx' });
|
|
77
|
+
fs.renameSync(temporary, statePath);
|
|
78
|
+
|
|
79
|
+
function parseArgs(values) {
|
|
80
|
+
const parsed = {};
|
|
81
|
+
for (let index = 0; index < values.length; index += 2) {
|
|
82
|
+
const flag = values[index];
|
|
83
|
+
const value = values[index + 1];
|
|
84
|
+
if (!flag?.startsWith('--') || value === undefined) throw new Error(`Invalid argument: ${flag || ''}`);
|
|
85
|
+
parsed[flag.slice(2)] = value;
|
|
86
|
+
}
|
|
87
|
+
return parsed;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function requiredPath(values, key) {
|
|
91
|
+
if (!values[key] || !path.isAbsolute(values[key])) throw new Error(`--${key} must be an absolute path.`);
|
|
92
|
+
return path.resolve(values[key]);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function requiredRelative(values, key) {
|
|
96
|
+
const value = values[key];
|
|
97
|
+
const parts = value?.split(/[\\/]+/) ?? [];
|
|
98
|
+
if (!value || path.isAbsolute(value) || parts.some((part) => !part || part === '.' || part === '..')) {
|
|
99
|
+
throw new Error(`--${key} must be a safe relative path.`);
|
|
100
|
+
}
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function safeRuntimeDirectory(target, runtimeDir, create) {
|
|
105
|
+
const targetStat = fs.lstatSync(target, { throwIfNoEntry: false });
|
|
106
|
+
if (!targetStat?.isDirectory() || targetStat.isSymbolicLink()) {
|
|
107
|
+
throw invalidRuntimeState(path.join(target, runtimeDir));
|
|
108
|
+
}
|
|
109
|
+
let current = target;
|
|
110
|
+
for (const component of runtimeDir.split(/[\\/]+/)) {
|
|
111
|
+
current = path.join(current, component);
|
|
112
|
+
const stat = fs.lstatSync(current, { throwIfNoEntry: false });
|
|
113
|
+
if (!stat) {
|
|
114
|
+
if (!create) return null;
|
|
115
|
+
fs.mkdirSync(current, { mode: 0o700 });
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw invalidRuntimeState(current);
|
|
119
|
+
}
|
|
120
|
+
const realTarget = fs.realpathSync(target);
|
|
121
|
+
const realRuntime = fs.realpathSync(current);
|
|
122
|
+
if (realRuntime !== realTarget && !realRuntime.startsWith(`${realTarget}${path.sep}`)) {
|
|
123
|
+
throw invalidRuntimeState(current);
|
|
124
|
+
}
|
|
125
|
+
return current;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function invalidRuntimeState(file) {
|
|
129
|
+
return new Error(`Release artifact runtime state path is unsafe: ${file}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function readRegularJson(file) {
|
|
133
|
+
const stat = fs.lstatSync(file);
|
|
134
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size <= 0 || stat.size > 64 * 1024) {
|
|
135
|
+
throw new Error(`Release artifact provenance must be a regular JSON file: ${file}`);
|
|
136
|
+
}
|
|
137
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
138
|
+
}
|
|
@@ -104,6 +104,7 @@ copyFile(path.join(runnerDir, 'adapters/extension/sync-webpack-dist.cjs'), path.
|
|
|
104
104
|
copyFile(path.join(runnerDir, 'adapters/extension/configure-runtime-manifest.cjs'), path.join(harnessDir, 'scripts/configure-runtime-manifest.cjs'));
|
|
105
105
|
copyFile(path.join(runnerDir, 'adapters/extension/check-infura-readiness.cjs'), path.join(harnessDir, 'scripts/check-infura-readiness.cjs'));
|
|
106
106
|
copyFile(path.join(runnerDir, 'adapters/extension/stamp-runtime-title.cjs'), path.join(harnessDir, 'scripts/stamp-runtime-title.cjs'));
|
|
107
|
+
copyFile(path.join(runnerDir, 'adapters/extension/artifact-runtime-state.cjs'), path.join(harnessDir, 'scripts/artifact-runtime-state.cjs'));
|
|
107
108
|
copyFile(path.join(runnerDir, 'adapters/extension/stop-viewers.sh'), path.join(harnessDir, 'scripts/stop-viewers.sh'));
|
|
108
109
|
copyFile(path.join(runnerDir, 'adapters/extension/snapshot-dist.sh'), path.join(harnessDir, 'scripts/snapshot-dist.sh'));
|
|
109
110
|
copyFile(path.join(runnerDir, 'adapters/extension/pin-remote-flags.cjs'), path.join(harnessDir, 'scripts/pin-remote-flags.cjs'));
|
|
@@ -131,6 +132,7 @@ copyFile(path.join(runnerDir, 'adapters/extension/lib/slot-title.cjs'), path.joi
|
|
|
131
132
|
copyFile(path.join(runnerDir, 'adapters/extension/lib/chrome-args.cjs'), path.join(harnessDir, 'scripts/lib/chrome-args.cjs'));
|
|
132
133
|
copyFile(path.join(runnerDir, 'adapters/extension/lib/macos-focus.cjs'), path.join(harnessDir, 'scripts/lib/macos-focus.cjs'));
|
|
133
134
|
copyFile(path.join(runnerDir, 'adapters/extension/lib/product-config.cjs'), path.join(harnessDir, 'scripts/lib/product-config.cjs'));
|
|
135
|
+
copyFile(path.join(runnerDir, 'adapters/extension/lib/validation-process-ownership.cjs'), path.join(harnessDir, 'scripts/lib/validation-process-ownership.cjs'));
|
|
134
136
|
makeExecutableTree(path.join(harnessDir, 'scripts'));
|
|
135
137
|
fs.writeFileSync(path.join(harnessDir, 'installed-scripts.sha256'), `${dirContentHash(path.join(harnessDir, 'scripts'))}\n`);
|
|
136
138
|
|
|
@@ -24,6 +24,7 @@ const os = require('node:os');
|
|
|
24
24
|
const path = require('node:path');
|
|
25
25
|
const { execFileSync, spawn, spawnSync } = require('node:child_process');
|
|
26
26
|
const { extensionIdFromExtensionDir } = require('./lib/extension-id.cjs');
|
|
27
|
+
const { profileProcessPids } = require('./lib/validation-process-ownership.cjs');
|
|
27
28
|
const {
|
|
28
29
|
automationRuntimeArgs,
|
|
29
30
|
clearDetachedLaunchUnproven,
|
|
@@ -72,6 +73,7 @@ if (args['stop-only'] === undefined) {
|
|
|
72
73
|
// injects its own already-isolated profile via runtime-context, so this only trips
|
|
73
74
|
// on a genuinely personal/shared path — protecting the user's browser data.
|
|
74
75
|
assertIsolatedProfile(args.profile);
|
|
76
|
+
if (args['reset-profile'] !== undefined) assertResettableProfile(args.profile, runtimeDir);
|
|
75
77
|
|
|
76
78
|
fs.mkdirSync(args.profile, { recursive: true });
|
|
77
79
|
fs.mkdirSync(path.dirname(args['chrome-log']), { recursive: true });
|
|
@@ -90,6 +92,7 @@ if (foreignPids.length > 0) {
|
|
|
90
92
|
);
|
|
91
93
|
}
|
|
92
94
|
const ownedPids = new Set(listenerPids);
|
|
95
|
+
for (const pid of profileProcessPids(args.profile)) ownedPids.add(pid);
|
|
93
96
|
const previousPid = readPidFile(args['chrome-pid']);
|
|
94
97
|
if (previousPid !== null && processIsOwnedHarnessBrowser(previousPid, args.profile, args['extension-dir'])) ownedPids.add(previousPid);
|
|
95
98
|
try {
|
|
@@ -301,6 +304,27 @@ function assertIsolatedProfile(profile) {
|
|
|
301
304
|
}
|
|
302
305
|
}
|
|
303
306
|
|
|
307
|
+
function assertResettableProfile(profile, expectedRuntimeDir) {
|
|
308
|
+
const resolved = path.resolve(profile);
|
|
309
|
+
const runtime = path.resolve(expectedRuntimeDir);
|
|
310
|
+
if (!resolved.startsWith(`${runtime}${path.sep}`)) {
|
|
311
|
+
throw new Error(`Refusing to reset --profile ${resolved}: it must be a dedicated child of ${runtime}.`);
|
|
312
|
+
}
|
|
313
|
+
const runtimeStat = fs.lstatSync(runtime);
|
|
314
|
+
if (!runtimeStat.isDirectory() || runtimeStat.isSymbolicLink()) {
|
|
315
|
+
throw new Error(`Refusing to reset --profile: runtime directory is not a regular directory: ${runtime}.`);
|
|
316
|
+
}
|
|
317
|
+
let current = runtime;
|
|
318
|
+
for (const segment of path.relative(runtime, resolved).split(path.sep)) {
|
|
319
|
+
current = path.join(current, segment);
|
|
320
|
+
if (!fs.existsSync(current)) break;
|
|
321
|
+
const stat = fs.lstatSync(current);
|
|
322
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
323
|
+
throw new Error(`Refusing to reset --profile: path contains an unsafe entry: ${current}.`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
304
328
|
// Proof of ownership: a process is ours iff its command line loads the exact
|
|
305
329
|
// --user-data-dir we launch with. If the process can't be inspected we treat it
|
|
306
330
|
// as NOT ours (fail safe — never assume ownership of an opaque process).
|
|
@@ -43,6 +43,9 @@ PREPARE_CMD="${RECIPE_HARNESS_EXTENSION_LAUNCH_CMD:-}"
|
|
|
43
43
|
LAUNCH_EXISTING_DIST=false
|
|
44
44
|
START_WATCH=false
|
|
45
45
|
BUILD_LAVAMOAT=false
|
|
46
|
+
EXTERNAL_ARTIFACT=false
|
|
47
|
+
ARTIFACT_PROVENANCE=""
|
|
48
|
+
RESET_PROFILE=true
|
|
46
49
|
DIST_DIR="dist/chrome"
|
|
47
50
|
CHROME_USER_DATA_DIR="${CHROME_USER_DATA_DIR:-}"
|
|
48
51
|
REMOTE_FLAGS=""
|
|
@@ -56,6 +59,9 @@ while [ "$#" -gt 0 ]; do
|
|
|
56
59
|
--artifacts-dir) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; ARTIFACTS="$2"; shift 2 ;;
|
|
57
60
|
--prepare-cmd) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; PREPARE_CMD="$2"; shift 2 ;;
|
|
58
61
|
--launch-existing-dist) LAUNCH_EXISTING_DIST=true; shift ;;
|
|
62
|
+
--external-artifact) EXTERNAL_ARTIFACT=true; LAUNCH_EXISTING_DIST=true; shift ;;
|
|
63
|
+
--artifact-provenance) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; ARTIFACT_PROVENANCE="$2"; shift 2 ;;
|
|
64
|
+
--preserve-profile) RESET_PROFILE=false; shift ;;
|
|
59
65
|
--start-url) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; START_URL="$2"; shift 2 ;;
|
|
60
66
|
--start-watch|--start-test-watch) START_WATCH=true; LAUNCH_EXISTING_DIST=true; shift ;;
|
|
61
67
|
--build-lavamoat) BUILD_LAVAMOAT=true; LAUNCH_EXISTING_DIST=true; shift ;;
|
|
@@ -63,7 +69,7 @@ while [ "$#" -gt 0 ]; do
|
|
|
63
69
|
--dist-dir) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; DIST_DIR="$2"; shift 2 ;;
|
|
64
70
|
--chrome-user-data-dir) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; CHROME_USER_DATA_DIR="$2"; shift 2 ;;
|
|
65
71
|
--remote-flag) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; REMOTE_FLAGS="$2"; shift 2 ;;
|
|
66
|
-
-h|--help) echo "Usage: live.sh [--target <metamask-extension>] [--out <recipes-dir>] [--cdp-port <port>] [--launch-existing-dist|--start-watch|--build-lavamoat|--prepare-cmd <cmd>] [--launch-only] [--dist-dir dist/chrome] [--remote-flag KEY=VARIANT] [--artifacts-dir <dir>]"; exit 0 ;;
|
|
72
|
+
-h|--help) echo "Usage: live.sh [--target <metamask-extension>] [--out <recipes-dir>] [--cdp-port <port>] [--launch-existing-dist|--start-watch|--build-lavamoat|--external-artifact --dist-dir <absolute-dir> --artifact-provenance <absolute-file> [--preserve-profile]|--prepare-cmd <cmd>] [--launch-only] [--dist-dir dist/chrome] [--remote-flag KEY=VARIANT] [--artifacts-dir <dir>]"; exit 0 ;;
|
|
67
73
|
*) echo "Unknown arg: $1" >&2; exit 2 ;;
|
|
68
74
|
esac
|
|
69
75
|
done
|
|
@@ -72,6 +78,24 @@ if $BUILD_LAVAMOAT && $START_WATCH; then
|
|
|
72
78
|
echo "--build-lavamoat cannot be combined with --start-watch" >&2
|
|
73
79
|
exit 2
|
|
74
80
|
fi
|
|
81
|
+
if $EXTERNAL_ARTIFACT && { $BUILD_LAVAMOAT || $START_WATCH || [ -n "$REMOTE_FLAGS" ]; }; then
|
|
82
|
+
echo "--external-artifact cannot be combined with build, watch, or remote-flag options" >&2
|
|
83
|
+
exit 2
|
|
84
|
+
fi
|
|
85
|
+
if $EXTERNAL_ARTIFACT && [ ! -f "$ARTIFACT_PROVENANCE" ]; then
|
|
86
|
+
echo "--external-artifact requires --artifact-provenance <file>" >&2
|
|
87
|
+
exit 2
|
|
88
|
+
fi
|
|
89
|
+
if ! $RESET_PROFILE && ! $EXTERNAL_ARTIFACT; then
|
|
90
|
+
echo "--preserve-profile is only valid for a verified external artifact" >&2
|
|
91
|
+
exit 2
|
|
92
|
+
fi
|
|
93
|
+
if $EXTERNAL_ARTIFACT; then
|
|
94
|
+
case "$ARTIFACT_PROVENANCE" in
|
|
95
|
+
/*) ;;
|
|
96
|
+
*) echo "--artifact-provenance must be an absolute file path" >&2; exit 2 ;;
|
|
97
|
+
esac
|
|
98
|
+
fi
|
|
75
99
|
|
|
76
100
|
case "$CHROME_USER_DATA_DIR" in
|
|
77
101
|
""|/*) ;;
|
|
@@ -120,6 +144,12 @@ if [ -z "$VERIFY_SH" ]; then
|
|
|
120
144
|
fi
|
|
121
145
|
RUNTIME_DIR="$(recipe_runtime_dir)"
|
|
122
146
|
RUNTIME_DIST_DIR="${RECIPE_RUNTIME_DIST_DIR:-runtime-dist}"
|
|
147
|
+
case "$RUNTIME_DIST_DIR" in
|
|
148
|
+
""|.|..|*/*|*[!A-Za-z0-9._-]*)
|
|
149
|
+
echo "RECIPE_RUNTIME_DIST_DIR must be one safe directory name: '$RUNTIME_DIST_DIR'" >&2
|
|
150
|
+
exit 2
|
|
151
|
+
;;
|
|
152
|
+
esac
|
|
123
153
|
CHROME_PROFILE_NAME="${RECIPE_CHROME_PROFILE_NAME:-chrome-profile}"
|
|
124
154
|
# Runner bin (installed wrapper → source runner). Used by the watch prepare path
|
|
125
155
|
# to defer the cache-clear DECISION to `runtime-decision` (single source) and to
|
|
@@ -145,21 +175,36 @@ if [ -d "$LIVE_ROOT" ]; then
|
|
|
145
175
|
fi
|
|
146
176
|
|
|
147
177
|
if $LAUNCH_EXISTING_DIST && [ -z "$PREPARE_CMD" ]; then
|
|
148
|
-
|
|
178
|
+
if $EXTERNAL_ARTIFACT; then
|
|
179
|
+
case "$DIST_DIR" in
|
|
180
|
+
/*) DIST_ABS="$DIST_DIR" ;;
|
|
181
|
+
*) echo "--external-artifact requires an absolute --dist-dir" >&2; exit 2 ;;
|
|
182
|
+
esac
|
|
183
|
+
else
|
|
184
|
+
DIST_ABS="$TARGET/$DIST_DIR"
|
|
185
|
+
fi
|
|
149
186
|
# Stable beside the profile: the reused profile registers the unpacked
|
|
150
187
|
# extension by PATH; a per-run snapshot path gets pruned and the next launch
|
|
151
188
|
# loads nothing ("No EIP-6963 Provider Detected").
|
|
152
|
-
|
|
189
|
+
RUNTIME_ROOT_ABS="$TARGET/$RUNTIME_DIR"
|
|
190
|
+
RUNTIME_DIST_ABS="$RUNTIME_ROOT_ABS/$RUNTIME_DIST_DIR"
|
|
153
191
|
# Stable per-checkout profile: successive runs recognize their own Chrome
|
|
154
192
|
# (ownership guard matches on --user-data-dir), take it over cleanly, and the
|
|
155
193
|
# seeded wallet persists across relaunches. A per-run profile made every
|
|
156
194
|
# relaunch "foreign" and let live/ pruning delete profiles still in use.
|
|
157
195
|
PROFILE_ABS="${CHROME_USER_DATA_DIR:-$TARGET/$RUNTIME_DIR/$CHROME_PROFILE_NAME}"
|
|
196
|
+
case "$PROFILE_ABS" in
|
|
197
|
+
"$TARGET/$RUNTIME_DIR/"*) ;;
|
|
198
|
+
*) echo "--chrome-user-data-dir must be a dedicated child of $TARGET/$RUNTIME_DIR" >&2; exit 2 ;;
|
|
199
|
+
esac
|
|
158
200
|
FIXTURE_STATE_ABS="$ARTIFACTS/fixture-state.json"
|
|
159
201
|
FIXTURE_VALIDATION_ABS="$ARTIFACTS/logs/fixture-account-parity.json"
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
202
|
+
WALLET_FIXTURE_ABS=""
|
|
203
|
+
if ! $EXTERNAL_ARTIFACT; then
|
|
204
|
+
# Wallet fixture resolution chain + provenance live in seed-fixture.sh.
|
|
205
|
+
WALLET_FIXTURE_ABS="$(bash "$SCRIPT_DIR/seed-fixture.sh" resolve --target "$TARGET" --cdp-port "$CDP_PORT" --source-out "$ARTIFACTS/logs/fixture-source.json")"
|
|
206
|
+
fi
|
|
207
|
+
if ! $EXTERNAL_ARTIFACT && [ ! -f "$ARTIFACTS/logs/fixture-source.json" ]; then
|
|
163
208
|
# resolve failed before writing provenance (invalid RECIPE_WALLET_FIXTURE);
|
|
164
209
|
# keep the documented missing-provenance artifact.
|
|
165
210
|
node - "$ARTIFACTS/logs/fixture-source.json" "" <<'NODE'
|
|
@@ -175,6 +220,7 @@ NODE
|
|
|
175
220
|
fi
|
|
176
221
|
mkdir -p "$PROFILE_ABS"
|
|
177
222
|
quoted_dist="$(printf '%q' "$DIST_ABS")"
|
|
223
|
+
quoted_runtime_root="$(printf '%q' "$RUNTIME_ROOT_ABS")"
|
|
178
224
|
quoted_runtime_dist="$(printf '%q' "$RUNTIME_DIST_ABS")"
|
|
179
225
|
quoted_profile="$(printf '%q' "$PROFILE_ABS")"
|
|
180
226
|
quoted_seed_fixture="$(printf '%q' "$SCRIPT_DIR/seed-fixture.sh")"
|
|
@@ -185,6 +231,7 @@ NODE
|
|
|
185
231
|
quoted_check_infura="$(printf '%q' "$SCRIPT_DIR/check-infura-readiness.cjs")"
|
|
186
232
|
quoted_stamp_title="$(printf '%q' "$SCRIPT_DIR/stamp-runtime-title.cjs")"
|
|
187
233
|
quoted_chrome_launcher="$(printf '%q' "$SCRIPT_DIR/launch-browser.cjs")"
|
|
234
|
+
quoted_artifact_state="$(printf '%q' "$SCRIPT_DIR/artifact-runtime-state.cjs")"
|
|
188
235
|
quoted_fixture_state="$(printf '%q' "$FIXTURE_STATE_ABS")"
|
|
189
236
|
quoted_fixture_validation="$(printf '%q' "$FIXTURE_VALIDATION_ABS")"
|
|
190
237
|
quoted_extension_id_file="$(printf '%q' "$TARGET/$RUNTIME_DIR/extension.id")"
|
|
@@ -257,17 +304,37 @@ NODE
|
|
|
257
304
|
quoted_chrome_log="$(printf '%q' "$ARTIFACTS/logs/chrome.log")"
|
|
258
305
|
quoted_chrome_pid="$(printf '%q' "$ARTIFACTS/logs/chrome.pid")"
|
|
259
306
|
prepare_parts=()
|
|
307
|
+
if $EXTERNAL_ARTIFACT; then
|
|
308
|
+
quoted_artifact_provenance="$(printf '%q' "$ARTIFACT_PROVENANCE")"
|
|
309
|
+
else
|
|
310
|
+
node "$SCRIPT_DIR/artifact-runtime-state.cjs" clear --target "$TARGET" --runtime-dir "$RUNTIME_DIR"
|
|
311
|
+
fi
|
|
260
312
|
if $START_WATCH; then
|
|
261
313
|
prepare_parts+=("bash ${quoted_start_watch} --runtime-dir ${quoted_runtime_dir} --runner-bin ${quoted_runner}")
|
|
262
314
|
fi
|
|
263
315
|
if $BUILD_LAVAMOAT; then
|
|
264
316
|
prepare_parts+=("bash ${quoted_build_lavamoat} --target ${quoted_target} --runtime-dir ${quoted_runtime_dir}")
|
|
265
317
|
fi
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
318
|
+
if ! $EXTERNAL_ARTIFACT; then
|
|
319
|
+
prepare_parts+=("node ${quoted_check_infura} --target ${quoted_target} --runtime-dist ${quoted_dist}")
|
|
320
|
+
fi
|
|
321
|
+
if $EXTERNAL_ARTIFACT; then
|
|
322
|
+
prepare_parts+=("bash ${quoted_snapshot_dist} --dist ${quoted_dist} --target ${quoted_target} --runtime-root ${quoted_runtime_root} --runtime-dist ${quoted_runtime_dist} --exact")
|
|
323
|
+
else
|
|
324
|
+
prepare_parts+=("bash ${quoted_snapshot_dist} --dist ${quoted_dist} --target ${quoted_target} --runtime-root ${quoted_runtime_root} --runtime-dist ${quoted_runtime_dist}")
|
|
325
|
+
fi
|
|
326
|
+
if $EXTERNAL_ARTIFACT; then
|
|
327
|
+
prepare_parts+=("node ${quoted_artifact_state} set --target ${quoted_target} --runtime-dir ${quoted_runtime_dir} --source-dir ${quoted_dist} --runtime-dist ${quoted_runtime_dist} --provenance ${quoted_artifact_provenance} --chrome-user-data-dir ${quoted_profile}")
|
|
328
|
+
fi
|
|
329
|
+
if ! $EXTERNAL_ARTIFACT; then
|
|
330
|
+
prepare_parts+=("node ${quoted_configure_manifest} --target ${quoted_target} --manifest ${quoted_runtime_dist}/manifest.json")
|
|
331
|
+
fi
|
|
332
|
+
reset_profile_arg=""
|
|
333
|
+
$RESET_PROFILE && reset_profile_arg=" --reset-profile 1"
|
|
334
|
+
prepare_parts+=("node ${quoted_chrome_launcher} --stop-only 1${reset_profile_arg} --chrome-bin ${quoted_chrome} --profile ${quoted_profile} --cdp-port ${CDP_PORT} --extension-dir ${quoted_runtime_dist} --chrome-log ${quoted_chrome_log} --chrome-pid ${quoted_chrome_pid}")
|
|
335
|
+
if ! $EXTERNAL_ARTIFACT; then
|
|
336
|
+
prepare_parts+=("node ${quoted_stamp_title} --target ${quoted_target} --runtime-dist ${quoted_runtime_dist} --runtime-dir ${quoted_runtime_dir}")
|
|
337
|
+
fi
|
|
271
338
|
# Optional A/B feature-flag pinning: patch the ephemeral snapshot manifest so
|
|
272
339
|
# manifest._flags wins over the fetched ClientConfigApi value. No-op unless
|
|
273
340
|
# --remote-flag was passed; the snapshot is re-created each run, so no revert.
|
|
@@ -302,6 +369,8 @@ echo "Extension live validation command:"
|
|
|
302
369
|
display_args=(mm-harness runtime-launch --adapter extension --target "$TARGET" --cdp-port "$CDP_PORT")
|
|
303
370
|
$START_WATCH && display_args+=(--start-watch)
|
|
304
371
|
$BUILD_LAVAMOAT && display_args+=(--build-lavamoat)
|
|
372
|
+
$EXTERNAL_ARTIFACT && display_args+=(--external-artifact --dist-dir "$DIST_DIR" --artifact-provenance "$ARTIFACT_PROVENANCE")
|
|
373
|
+
$RESET_PROFILE || display_args+=(--preserve-profile)
|
|
305
374
|
[ -n "$REMOTE_FLAGS" ] && display_args+=(--remote-flag "$REMOTE_FLAGS")
|
|
306
375
|
[ -n "$CHROME_USER_DATA_DIR" ] && display_args+=(--chrome-user-data-dir "$CHROME_USER_DATA_DIR")
|
|
307
376
|
printf ' '
|
|
@@ -333,7 +402,7 @@ else
|
|
|
333
402
|
echo "Skipping Extension live verify for launch-only fixture application." >&2
|
|
334
403
|
fi
|
|
335
404
|
|
|
336
|
-
TARGET_FOR_SUMMARY="$TARGET" ARTIFACTS_FOR_SUMMARY="$ARTIFACTS" CDP_PORT_FOR_SUMMARY="$CDP_PORT" LAUNCH_STATUS="$launch_status" VERIFY_STATUS="$verify_status" RUN_VERIFY="$RUN_VERIFY" LAUNCH_EXISTING_DIST="$LAUNCH_EXISTING_DIST" START_WATCH="$START_WATCH" BUILD_LAVAMOAT="$BUILD_LAVAMOAT" node <<'NODE'
|
|
405
|
+
TARGET_FOR_SUMMARY="$TARGET" ARTIFACTS_FOR_SUMMARY="$ARTIFACTS" CDP_PORT_FOR_SUMMARY="$CDP_PORT" LAUNCH_STATUS="$launch_status" VERIFY_STATUS="$verify_status" RUN_VERIFY="$RUN_VERIFY" LAUNCH_EXISTING_DIST="$LAUNCH_EXISTING_DIST" START_WATCH="$START_WATCH" BUILD_LAVAMOAT="$BUILD_LAVAMOAT" EXTERNAL_ARTIFACT="$EXTERNAL_ARTIFACT" node <<'NODE'
|
|
337
406
|
const fs = require('fs');
|
|
338
407
|
const path = require('path');
|
|
339
408
|
const artifacts = process.env.ARTIFACTS_FOR_SUMMARY;
|
|
@@ -351,6 +420,7 @@ fs.writeFileSync(path.join(artifacts, 'summary.json'), `${JSON.stringify({
|
|
|
351
420
|
launchExistingDist: process.env.LAUNCH_EXISTING_DIST === 'true',
|
|
352
421
|
startWatch: process.env.START_WATCH === 'true',
|
|
353
422
|
buildLavaMoat: process.env.BUILD_LAVAMOAT === 'true',
|
|
423
|
+
externalArtifact: process.env.EXTERNAL_ARTIFACT === 'true',
|
|
354
424
|
launch: { exitCode: launchStatus, summaryPath: fs.existsSync(launchSummary) ? launchSummary : null },
|
|
355
425
|
verify: runVerify
|
|
356
426
|
? { status: verifyStatus === 0 ? 'pass' : 'fail', exitCode: verifyStatus, summaryPath: fs.existsSync(verifySummary) ? verifySummary : null }
|
|
@@ -64,10 +64,29 @@ function parseArgs(argv) {
|
|
|
64
64
|
return out;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
function extensionBuildRoot(target) {
|
|
68
|
+
const runtimeRoot = path.join(target, recipeRuntimeDir());
|
|
69
|
+
const statePath = path.join(runtimeRoot, 'extension-release-artifact.json');
|
|
70
|
+
if (!fs.existsSync(statePath)) return path.join(target, 'dist/chrome');
|
|
71
|
+
try {
|
|
72
|
+
const stat = fs.lstatSync(statePath);
|
|
73
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size <= 0 || stat.size > 64 * 1024) {
|
|
74
|
+
throw new Error('Loaded release artifact identity is not a regular bounded file.');
|
|
75
|
+
}
|
|
76
|
+
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
77
|
+
const expected = path.join(runtimeRoot, process.env.RECIPE_RUNTIME_DIST_DIR || 'runtime-dist');
|
|
78
|
+
if (state.schemaVersion === 2 && path.resolve(state.runtimeDist || '') === expected) return expected;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
throw new Error(`Loaded release artifact identity is invalid: ${error instanceof Error ? error.message : String(error)}`);
|
|
81
|
+
}
|
|
82
|
+
throw new Error('Loaded release artifact identity is invalid.');
|
|
83
|
+
}
|
|
84
|
+
|
|
67
85
|
function readManifest(target) {
|
|
68
|
-
const
|
|
86
|
+
const buildRoot = extensionBuildRoot(target);
|
|
87
|
+
const manifestPath = path.join(buildRoot, 'manifest.json');
|
|
69
88
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
70
|
-
return { manifestPath, manifest };
|
|
89
|
+
return { buildRoot, manifestPath, manifest };
|
|
71
90
|
}
|
|
72
91
|
|
|
73
92
|
function manifestExpectedFiles(manifest) {
|
|
@@ -190,6 +209,17 @@ function resolveWebSocket(target) {
|
|
|
190
209
|
}
|
|
191
210
|
|
|
192
211
|
async function cdpEvaluate(target, webSocketDebuggerUrl, expression, timeoutMs = 5000) {
|
|
212
|
+
const result = await cdpCall(
|
|
213
|
+
target,
|
|
214
|
+
webSocketDebuggerUrl,
|
|
215
|
+
'Runtime.evaluate',
|
|
216
|
+
{ expression, awaitPromise: true, returnByValue: true },
|
|
217
|
+
timeoutMs,
|
|
218
|
+
);
|
|
219
|
+
return result?.result?.value ?? null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function cdpCall(target, webSocketDebuggerUrl, method, params, timeoutMs = 5000) {
|
|
193
223
|
assertLocalWebSocketUrl(webSocketDebuggerUrl);
|
|
194
224
|
const WebSocketImpl = resolveWebSocket(target);
|
|
195
225
|
if (!WebSocketImpl) return { skipped: true, reason: 'WebSocket unavailable in this Node runtime' };
|
|
@@ -206,8 +236,8 @@ async function cdpEvaluate(target, webSocketDebuggerUrl, expression, timeoutMs =
|
|
|
206
236
|
const onOpen = () => {
|
|
207
237
|
ws.send(JSON.stringify({
|
|
208
238
|
id: 1,
|
|
209
|
-
method
|
|
210
|
-
params
|
|
239
|
+
method,
|
|
240
|
+
params,
|
|
211
241
|
}));
|
|
212
242
|
};
|
|
213
243
|
const onMessage = (event) => {
|
|
@@ -220,11 +250,11 @@ async function cdpEvaluate(target, webSocketDebuggerUrl, expression, timeoutMs =
|
|
|
220
250
|
reject(new Error(msg.error.message || JSON.stringify(msg.error)));
|
|
221
251
|
return;
|
|
222
252
|
}
|
|
223
|
-
resolve(msg.result
|
|
253
|
+
resolve(msg.result ?? null);
|
|
224
254
|
};
|
|
225
255
|
const onError = (err) => {
|
|
226
256
|
clearTimeout(timer);
|
|
227
|
-
reject(new Error(`CDP websocket error
|
|
257
|
+
reject(new Error(`CDP websocket error during ${method}: ${err?.message || err || 'unknown'}`));
|
|
228
258
|
};
|
|
229
259
|
if (typeof ws.on === 'function') {
|
|
230
260
|
ws.on('open', onOpen);
|
|
@@ -286,6 +316,23 @@ async function openExtensionPage(cdpPort, extensionId, pagePath) {
|
|
|
286
316
|
}
|
|
287
317
|
}
|
|
288
318
|
|
|
319
|
+
const PAGE_UI_EXPRESSION = `(() => {
|
|
320
|
+
const text = document.body?.innerText || '';
|
|
321
|
+
const cell = (sel) => document.querySelector(sel)?.innerText?.trim() || '';
|
|
322
|
+
const errorBoundaryMessage = cell('[data-testid="error-page-error-message"]');
|
|
323
|
+
const errorBoundaryName = cell('[data-testid="error-page-error-name"]');
|
|
324
|
+
const hasErrorBoundary = Boolean(errorBoundaryMessage) || Boolean(errorBoundaryName);
|
|
325
|
+
return {
|
|
326
|
+
title: document.title,
|
|
327
|
+
url: location.href,
|
|
328
|
+
textSample: text.slice(0, 500),
|
|
329
|
+
hasStartupError: /MetaMask had trouble starting|Background connection unresponsive|Unknown Infura network/i.test(text),
|
|
330
|
+
hasErrorBoundary,
|
|
331
|
+
errorBoundaryName,
|
|
332
|
+
errorBoundaryMessage: errorBoundaryMessage || (hasErrorBoundary ? text.slice(0, 300) : ''),
|
|
333
|
+
};
|
|
334
|
+
})()`;
|
|
335
|
+
|
|
289
336
|
// The page path only, with any '#fragment' or '?query' stripped. The app router
|
|
290
337
|
// rewrites home.html to home.html#/ once loaded, so an exact '/home.html' suffix
|
|
291
338
|
// match must compare against this, not the raw url.
|
|
@@ -349,29 +396,7 @@ async function inspectCdp(target, cdpPort, expectedExtensionId, expectedServiceW
|
|
|
349
396
|
// tab has none; skip inspection and report it rather than opening a duplicate.
|
|
350
397
|
if (pageTarget && typeof pageTarget.webSocketDebuggerUrl === 'string') {
|
|
351
398
|
const slotId = readSlotId(target);
|
|
352
|
-
ui = await cdpEvaluate(
|
|
353
|
-
target,
|
|
354
|
-
pageTarget.webSocketDebuggerUrl,
|
|
355
|
-
`(() => {
|
|
356
|
-
const text = document.body?.innerText || '';
|
|
357
|
-
const cell = (sel) => document.querySelector(sel)?.innerText?.trim() || '';
|
|
358
|
-
// React error-boundary screen (ui/pages/error-page); deterministic testids.
|
|
359
|
-
const errorBoundaryMessage = cell('[data-testid="error-page-error-message"]');
|
|
360
|
-
const errorBoundaryName = cell('[data-testid="error-page-error-name"]');
|
|
361
|
-
// Testid-only trigger: ui/pages/error-page reliably renders these testids.
|
|
362
|
-
// Avoid matching the visible phrase in body text (false positives).
|
|
363
|
-
const hasErrorBoundary = Boolean(errorBoundaryMessage) || Boolean(errorBoundaryName);
|
|
364
|
-
return {
|
|
365
|
-
title: document.title,
|
|
366
|
-
url: location.href,
|
|
367
|
-
textSample: text.slice(0, 500),
|
|
368
|
-
hasStartupError: /MetaMask had trouble starting|Background connection unresponsive|Unknown Infura network/i.test(text),
|
|
369
|
-
hasErrorBoundary,
|
|
370
|
-
errorBoundaryName,
|
|
371
|
-
errorBoundaryMessage: errorBoundaryMessage || (hasErrorBoundary ? text.slice(0, 300) : ''),
|
|
372
|
-
};
|
|
373
|
-
})()`,
|
|
374
|
-
);
|
|
399
|
+
ui = await cdpEvaluate(target, pageTarget.webSocketDebuggerUrl, PAGE_UI_EXPRESSION);
|
|
375
400
|
pageInspected = Boolean(ui) && !ui.skipped;
|
|
376
401
|
if (pageInspected && ui.hasStartupError) {
|
|
377
402
|
throw Object.assign(new Error('MetaMask extension page loaded startup error UI'), {
|
|
@@ -385,9 +410,25 @@ async function inspectCdp(target, cdpPort, expectedExtensionId, expectedServiceW
|
|
|
385
410
|
});
|
|
386
411
|
}
|
|
387
412
|
if (pageInspected && String(ui.url || '').startsWith('chrome-error://')) {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
413
|
+
const destination = `chrome-extension://${selectedExtensionId}/${String(extensionPagePath || 'home.html').replace(/^\/+/u, '')}`;
|
|
414
|
+
await cdpCall(
|
|
415
|
+
target,
|
|
416
|
+
pageTarget.webSocketDebuggerUrl,
|
|
417
|
+
'Page.navigate',
|
|
418
|
+
{ url: destination },
|
|
419
|
+
);
|
|
420
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
421
|
+
ui = await cdpEvaluate(target, pageTarget.webSocketDebuggerUrl, PAGE_UI_EXPRESSION);
|
|
422
|
+
if (
|
|
423
|
+
!ui ||
|
|
424
|
+
String(ui.url || '').startsWith('chrome-error://') ||
|
|
425
|
+
ui.hasStartupError ||
|
|
426
|
+
ui.hasErrorBoundary
|
|
427
|
+
) {
|
|
428
|
+
throw Object.assign(new Error('MetaMask extension page loaded Chrome error UI'), {
|
|
429
|
+
report: { cdp: { browser: version.Browser || 'unknown', selectedExtensionId, ui } },
|
|
430
|
+
});
|
|
431
|
+
}
|
|
391
432
|
}
|
|
392
433
|
if (pageInspected && slotId) {
|
|
393
434
|
const stampedTitle = await cdpEvaluate(
|
|
@@ -419,12 +460,12 @@ async function main() {
|
|
|
419
460
|
const args = parseArgs(process.argv.slice(2));
|
|
420
461
|
const target = path.resolve(args.target);
|
|
421
462
|
const checks = [];
|
|
422
|
-
const { manifestPath, manifest } = readManifest(target);
|
|
463
|
+
const { buildRoot, manifestPath, manifest } = readManifest(target);
|
|
423
464
|
const expectedFiles = manifestExpectedFiles(manifest);
|
|
424
465
|
const missingFiles = [];
|
|
425
466
|
for (const rel of expectedFiles) {
|
|
426
|
-
const exists = fs.existsSync(path.join(
|
|
427
|
-
checks.push({ name:
|
|
467
|
+
const exists = fs.existsSync(path.join(buildRoot, rel));
|
|
468
|
+
checks.push({ name: `${path.relative(target, buildRoot)}/${rel}`, status: exists ? 'pass' : 'fail' });
|
|
428
469
|
if (!exists) missingFiles.push(rel);
|
|
429
470
|
}
|
|
430
471
|
if (missingFiles.length > 0) {
|
|
@@ -434,7 +475,7 @@ async function main() {
|
|
|
434
475
|
);
|
|
435
476
|
}
|
|
436
477
|
|
|
437
|
-
const defaultPage = fs.existsSync(path.join(
|
|
478
|
+
const defaultPage = fs.existsSync(path.join(buildRoot, 'home.html'))
|
|
438
479
|
? 'home.html'
|
|
439
480
|
: manifestDefaultPage(manifest);
|
|
440
481
|
const report = {
|