@deeeed/metamask-harness 0.28.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +19 -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/library/actions/extension/analytics/consent.mjs +203 -0
  26. package/library/actions/extension/analytics/set_consent.mjs +19 -143
  27. package/library/actions/extension/perps/perps.mjs +2 -16
  28. package/library/actions/extension/perps/state.mjs +20 -0
  29. package/library/actions/extension/wallet/list_accounts.mjs +3 -25
  30. package/library/actions/extension/wallet/read_state.mjs +3 -23
  31. package/library/actions/extension/wallet/select_account.mjs +6 -33
  32. package/library/actions/extension/wallet/setup.mjs +2 -20
  33. package/library/actions/extension/wallet/state.mjs +111 -0
  34. package/library/recipes/runner/action-validation.extension.recipe.json +1 -1
  35. package/library/recipes/runner/action-validation.mobile.recipe.json +1 -1
  36. package/package.json +7 -4
  37. package/scripts/site-contrast.mjs +538 -0
  38. package/site/architecture.html +415 -0
  39. package/site/assets/progress.mjs +272 -0
  40. package/site/assets/style.css +808 -0
  41. package/site/cheatsheet.html +305 -0
  42. package/site/index.html +643 -0
  43. package/site/recipes.html +396 -0
  44. package/site/reviewers.html +374 -0
  45. package/site/tutorials/index.html +180 -0
  46. package/site/tutorials/v1.html +211 -0
  47. package/site/tutorials/v2.html +207 -0
  48. package/site/tutorials/v3.html +214 -0
  49. package/site/tutorials/v4.html +195 -0
  50. package/site/tutorials/v5.html +163 -0
  51. package/site/tutorials/v6.html +165 -0
  52. package/site/tutorials/v7.html +184 -0
@@ -1,4 +1,11 @@
1
1
  'use strict';
2
+ const crypto = require('node:crypto');
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ const RUNTIME_IDENTITY_FILENAME = 'extension-runtime-identity.json';
7
+ const RUNTIME_NONCE_PREFIX = '--mm-harness-runtime-nonce=';
8
+ const DETACHED_LAUNCH_UNPROVEN_FILENAME = '.mm-harness-detached-launch-unproven';
2
9
  // chrome-args.cjs — single source of truth for the remote-debugging launch flags
3
10
  // shared by BOTH extension launchers: the fresh spawn (launch-browser.cjs) and the
4
11
  // reopen path (ensure-browser.sh → reopen-browser.sh). Keeping the debug-port trio
@@ -38,4 +45,321 @@ function automationRuntimeArgs() {
38
45
  ];
39
46
  }
40
47
 
41
- module.exports = { automationRuntimeArgs, isolatedProfileArgs, remoteDebuggingArgs };
48
+ function createRuntimeIdentityNonce() {
49
+ return crypto.randomBytes(32).toString('hex');
50
+ }
51
+
52
+ function runtimeIdentityArgs(nonce) {
53
+ if (!/^[a-f0-9]{64}$/u.test(nonce)) {
54
+ throw new Error('runtimeIdentityArgs: nonce must be 32 bytes encoded as lowercase hex');
55
+ }
56
+ return ['--enable-automation', `${RUNTIME_NONCE_PREFIX}${nonce}`];
57
+ }
58
+
59
+ function runtimeIdentityPath(runtimeDir) {
60
+ return path.join(path.resolve(runtimeDir), RUNTIME_IDENTITY_FILENAME);
61
+ }
62
+
63
+ function detachedLaunchUnprovenPath(profile) {
64
+ return path.join(path.resolve(profile), DETACHED_LAUNCH_UNPROVEN_FILENAME);
65
+ }
66
+
67
+ function validationPortQuarantineRoot() {
68
+ if (typeof process.getuid !== 'function') {
69
+ throw new Error('Extension validation port quarantine requires a POSIX user identity.');
70
+ }
71
+ const uid = process.getuid();
72
+ const root = path.join(fs.realpathSync('/tmp'), `mm-harness-extension-validation-${uid}`);
73
+ try {
74
+ fs.mkdirSync(root, { mode: 0o700 });
75
+ } catch (error) {
76
+ if (error.code !== 'EEXIST') throw error;
77
+ }
78
+ const stat = fs.lstatSync(root);
79
+ if (
80
+ stat.isSymbolicLink() ||
81
+ !stat.isDirectory() ||
82
+ stat.uid !== uid ||
83
+ (stat.mode & 0o777) !== 0o700
84
+ ) {
85
+ throw new Error(`Extension validation quarantine root is not an owner-only directory: ${root}`);
86
+ }
87
+ return root;
88
+ }
89
+
90
+ function validationPortQuarantinePath(port) {
91
+ const numericPort = Number(port);
92
+ if (!Number.isInteger(numericPort) || numericPort <= 0 || numericPort > 65535) {
93
+ throw new Error(`validationPortQuarantinePath: invalid cdp port: ${port}`);
94
+ }
95
+ return path.join(validationPortQuarantineRoot(), `port-${numericPort}.json`);
96
+ }
97
+
98
+ function readValidationPortQuarantine(port) {
99
+ const destination = validationPortQuarantinePath(port);
100
+ let descriptor;
101
+ try {
102
+ descriptor = fs.openSync(destination, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
103
+ const stat = fs.fstatSync(descriptor);
104
+ if (
105
+ !stat.isFile() ||
106
+ stat.uid !== process.getuid() ||
107
+ stat.size <= 0 ||
108
+ stat.size > 4_096 ||
109
+ (stat.mode & 0o777) !== 0o600
110
+ ) {
111
+ return { path: destination };
112
+ }
113
+ const value = JSON.parse(fs.readFileSync(descriptor, 'utf8'));
114
+ if (
115
+ value.schemaVersion !== 1 ||
116
+ value.port !== Number(port) ||
117
+ typeof value.profile !== 'string' ||
118
+ (value.lease !== undefined && !/^[a-f0-9]{64}$/u.test(value.lease))
119
+ ) {
120
+ return { path: destination };
121
+ }
122
+ return {
123
+ path: destination,
124
+ profile: value.profile,
125
+ ...(value.lease === undefined ? {} : { lease: value.lease }),
126
+ };
127
+ } catch (error) {
128
+ if (error.code === 'ENOENT') return null;
129
+ return { path: destination };
130
+ } finally {
131
+ if (descriptor !== undefined) fs.closeSync(descriptor);
132
+ }
133
+ }
134
+
135
+ function writeValidationPortQuarantine(port, profile, reason, allowExisting, lease) {
136
+ const destination = validationPortQuarantinePath(port);
137
+ const pending = path.join(
138
+ path.dirname(destination),
139
+ `.port-${Number(port)}.${process.pid}.${crypto.randomBytes(16).toString('hex')}.pending`,
140
+ );
141
+ let descriptor;
142
+ try {
143
+ descriptor = fs.openSync(
144
+ pending,
145
+ fs.constants.O_WRONLY |
146
+ fs.constants.O_CREAT |
147
+ fs.constants.O_EXCL |
148
+ fs.constants.O_NOFOLLOW,
149
+ 0o600,
150
+ );
151
+ fs.fchmodSync(descriptor, 0o600);
152
+ fs.writeFileSync(descriptor, `${JSON.stringify({
153
+ schemaVersion: 1,
154
+ port: Number(port),
155
+ profile: path.resolve(profile),
156
+ reason: String(reason).slice(0, 1_024),
157
+ ...(lease === undefined ? {} : { lease }),
158
+ })}\n`, 'utf8');
159
+ fs.fsyncSync(descriptor);
160
+ fs.closeSync(descriptor);
161
+ descriptor = undefined;
162
+ fs.linkSync(pending, destination);
163
+ return destination;
164
+ } catch (error) {
165
+ if (allowExisting && error.code === 'EEXIST') return destination;
166
+ throw error;
167
+ } finally {
168
+ if (descriptor !== undefined) {
169
+ try {
170
+ fs.closeSync(descriptor);
171
+ } catch {}
172
+ }
173
+ try {
174
+ fs.unlinkSync(pending);
175
+ } catch (error) {
176
+ if (error.code !== 'ENOENT') throw error;
177
+ }
178
+ }
179
+ }
180
+
181
+ function markValidationPortLaunchUnproven(port, profile, lease) {
182
+ if (lease !== undefined && !/^[a-f0-9]{64}$/u.test(lease)) {
183
+ throw new Error('markValidationPortLaunchUnproven: invalid lease');
184
+ }
185
+ return writeValidationPortQuarantine(
186
+ port,
187
+ profile,
188
+ 'A detached browser launch was submitted but ownership was not proven.',
189
+ false,
190
+ lease,
191
+ );
192
+ }
193
+
194
+ function quarantineValidationPort(port, profile, reason) {
195
+ return writeValidationPortQuarantine(port, profile, reason, true, undefined);
196
+ }
197
+
198
+ function clearValidationPortQuarantine(port, profile, lease) {
199
+ const quarantine = readValidationPortQuarantine(port);
200
+ if (!quarantine) return;
201
+ if (
202
+ quarantine.profile !== path.resolve(profile) ||
203
+ (quarantine.lease !== undefined && quarantine.lease !== lease)
204
+ ) {
205
+ throw new Error(`Extension validation port ${port} is quarantined for another or invalid profile.`);
206
+ }
207
+ fs.unlinkSync(quarantine.path);
208
+ }
209
+
210
+ function shellQuote(value) {
211
+ return `'${String(value).replaceAll("'", `'"'"'`)}'`;
212
+ }
213
+
214
+ function validationLaunchQuarantineError(port, ownerProfile, requestedProfile) {
215
+ const portMarker = validationPortQuarantinePath(port);
216
+ const profiles = [...new Set([ownerProfile, requestedProfile]
217
+ .filter((profile) => typeof profile === 'string')
218
+ .map((profile) => path.resolve(profile)))];
219
+ const markers = [
220
+ ...profiles.map(detachedLaunchUnprovenPath),
221
+ portMarker,
222
+ ];
223
+ const profileText = profiles.length > 0 ? ` or profile ${profiles.join(' or ')}` : '';
224
+ return new Error(
225
+ `Refusing to launch on CDP port ${port} because its detached browser state is quarantined. ` +
226
+ `Next: confirm no browser uses port ${port}${profileText}, then run: rm -- ${markers.map(shellQuote).join(' ')}`,
227
+ );
228
+ }
229
+
230
+ function writeDetachedLaunchMarker(profile, reason, allowExisting) {
231
+ const destination = detachedLaunchUnprovenPath(profile);
232
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
233
+ let descriptor;
234
+ try {
235
+ descriptor = fs.openSync(
236
+ destination,
237
+ fs.constants.O_WRONLY |
238
+ fs.constants.O_CREAT |
239
+ fs.constants.O_EXCL |
240
+ fs.constants.O_NOFOLLOW,
241
+ 0o600,
242
+ );
243
+ fs.fchmodSync(descriptor, 0o600);
244
+ fs.writeFileSync(descriptor, `${reason}\n`, 'utf8');
245
+ fs.fsyncSync(descriptor);
246
+ fs.closeSync(descriptor);
247
+ return destination;
248
+ } catch (error) {
249
+ if (descriptor !== undefined) {
250
+ try {
251
+ fs.closeSync(descriptor);
252
+ } catch {}
253
+ }
254
+ if (allowExisting && error.code === 'EEXIST') return destination;
255
+ throw error;
256
+ }
257
+ }
258
+
259
+ function markDetachedLaunchUnproven(profile) {
260
+ return writeDetachedLaunchMarker(
261
+ profile,
262
+ 'A detached browser launch was submitted but ownership was not proven.',
263
+ false,
264
+ );
265
+ }
266
+
267
+ function quarantineDetachedLaunch(profile, reason) {
268
+ return writeDetachedLaunchMarker(profile, reason, true);
269
+ }
270
+
271
+ function hasDetachedLaunchUnproven(profile) {
272
+ return fs.lstatSync(detachedLaunchUnprovenPath(profile), { throwIfNoEntry: false }) !== undefined;
273
+ }
274
+
275
+ function clearDetachedLaunchUnproven(profile) {
276
+ const destination = detachedLaunchUnprovenPath(profile);
277
+ const stat = fs.lstatSync(destination, { throwIfNoEntry: false });
278
+ if (!stat) return;
279
+ if (stat.isSymbolicLink() || !stat.isFile()) {
280
+ throw new Error(`Detached launch marker is not a regular file: ${destination}`);
281
+ }
282
+ fs.unlinkSync(destination);
283
+ }
284
+
285
+ function removeRuntimeIdentity(runtimeDir) {
286
+ fs.rmSync(runtimeIdentityPath(runtimeDir), { force: true });
287
+ }
288
+
289
+ function writeRuntimeIdentity(runtimeDir, identity) {
290
+ assertRuntimeIdentity(identity);
291
+ const destination = runtimeIdentityPath(runtimeDir);
292
+ const directory = path.dirname(destination);
293
+ fs.mkdirSync(directory, { recursive: true });
294
+ const temporary = path.join(
295
+ directory,
296
+ `.${RUNTIME_IDENTITY_FILENAME}.${process.pid}.${crypto.randomBytes(8).toString('hex')}.tmp`,
297
+ );
298
+ let descriptor;
299
+ try {
300
+ descriptor = fs.openSync(
301
+ temporary,
302
+ fs.constants.O_WRONLY |
303
+ fs.constants.O_CREAT |
304
+ fs.constants.O_EXCL |
305
+ fs.constants.O_NOFOLLOW,
306
+ 0o600,
307
+ );
308
+ fs.fchmodSync(descriptor, 0o600);
309
+ fs.writeFileSync(descriptor, `${JSON.stringify(identity)}\n`, 'utf8');
310
+ fs.fsyncSync(descriptor);
311
+ fs.closeSync(descriptor);
312
+ descriptor = undefined;
313
+ fs.renameSync(temporary, destination);
314
+ } catch (error) {
315
+ if (descriptor !== undefined) {
316
+ try {
317
+ fs.closeSync(descriptor);
318
+ } catch {}
319
+ }
320
+ fs.rmSync(temporary, { force: true });
321
+ throw error;
322
+ }
323
+ }
324
+
325
+ function assertRuntimeIdentity(identity) {
326
+ if (
327
+ !identity ||
328
+ !Number.isInteger(identity.port) ||
329
+ identity.port <= 0 ||
330
+ identity.port > 65535 ||
331
+ !Number.isInteger(identity.pid) ||
332
+ identity.pid <= 0 ||
333
+ !Number.isInteger(identity.startedAt) ||
334
+ identity.startedAt <= 0 ||
335
+ !/^[a-f0-9]{64}$/u.test(identity.nonce)
336
+ ) {
337
+ throw new Error('writeRuntimeIdentity: invalid runtime identity');
338
+ }
339
+ }
340
+
341
+ module.exports = {
342
+ DETACHED_LAUNCH_UNPROVEN_FILENAME,
343
+ RUNTIME_IDENTITY_FILENAME,
344
+ RUNTIME_NONCE_PREFIX,
345
+ automationRuntimeArgs,
346
+ clearDetachedLaunchUnproven,
347
+ clearValidationPortQuarantine,
348
+ createRuntimeIdentityNonce,
349
+ detachedLaunchUnprovenPath,
350
+ hasDetachedLaunchUnproven,
351
+ isolatedProfileArgs,
352
+ markDetachedLaunchUnproven,
353
+ markValidationPortLaunchUnproven,
354
+ quarantineDetachedLaunch,
355
+ quarantineValidationPort,
356
+ readValidationPortQuarantine,
357
+ remoteDebuggingArgs,
358
+ removeRuntimeIdentity,
359
+ runtimeIdentityArgs,
360
+ runtimeIdentityPath,
361
+ shellQuote,
362
+ validationLaunchQuarantineError,
363
+ validationPortQuarantinePath,
364
+ writeRuntimeIdentity,
365
+ };
@@ -0,0 +1,34 @@
1
+ 'use strict';
2
+
3
+ function buildEvaluationExpression(callbackOrExpression, argument) {
4
+ if (typeof callbackOrExpression === 'string') return callbackOrExpression;
5
+ if (typeof callbackOrExpression !== 'function') {
6
+ throw new TypeError('CDP evaluation requires a function or expression string.');
7
+ }
8
+ return argument === undefined
9
+ ? `(${callbackOrExpression.toString()})()`
10
+ : `(${callbackOrExpression.toString()})(${JSON.stringify(argument)})`;
11
+ }
12
+
13
+ async function evaluatePageViaCdp(page, callbackOrExpression, argument) {
14
+ const session = await page.context().newCDPSession(page);
15
+ try {
16
+ const response = await session.send('Runtime.evaluate', {
17
+ expression: buildEvaluationExpression(callbackOrExpression, argument),
18
+ awaitPromise: true,
19
+ returnByValue: true,
20
+ });
21
+ if (response.exceptionDetails) {
22
+ const detail =
23
+ response.exceptionDetails.exception?.description ??
24
+ response.exceptionDetails.text ??
25
+ 'unknown evaluation failure';
26
+ throw new Error(`CDP evaluation failed: ${detail}`);
27
+ }
28
+ return response.result?.value;
29
+ } finally {
30
+ await session.detach().catch(() => {});
31
+ }
32
+ }
33
+
34
+ module.exports = { buildEvaluationExpression, evaluatePageViaCdp };
@@ -39,12 +39,10 @@ function readSlotId(target, runtimeDir) {
39
39
  }
40
40
 
41
41
  /**
42
- * Playwright page.evaluate callback. Must stay a plain function (no closure)
43
- * so Playwright can serialize it into the page.
42
+ * Serialized page-evaluation callback. Must stay a plain function (no closure).
44
43
  */
45
44
  function applyPersistentSlotTitle(slot) {
46
- // Inline sanitize this function is serialized into the page for Playwright
47
- // and CDP, so it cannot call Node helpers.
45
+ // Inline sanitize because the serialized function cannot call Node helpers.
48
46
  const id =
49
47
  typeof slot === 'string' && /^[A-Za-z0-9._:-]{1,64}$/u.test(slot.trim())
50
48
  ? slot.trim()
@@ -0,0 +1,292 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const dgram = require('node:dgram');
5
+ const { execFileSync, spawn } = require('node:child_process');
6
+ const {
7
+ clearValidationPortQuarantine,
8
+ createRuntimeIdentityNonce,
9
+ hasDetachedLaunchUnproven,
10
+ markValidationPortLaunchUnproven,
11
+ quarantineDetachedLaunch,
12
+ quarantineValidationPort,
13
+ readValidationPortQuarantine,
14
+ validationLaunchQuarantineError,
15
+ } = require('./chrome-args.cjs');
16
+ const { stopProfileProcesses } = require('./validation-process-ownership.cjs');
17
+
18
+ const port = Number(process.argv[2]);
19
+ if (!Number.isInteger(port) || port <= 0 || port > 65535 || typeof process.send !== 'function') {
20
+ process.exit(2);
21
+ }
22
+
23
+ const socket = dgram.createSocket('udp4');
24
+ let launchChild;
25
+ let launchLease;
26
+ let ownsPortLease = false;
27
+ let launchRequest;
28
+ let launchFinished = false;
29
+ let finishStarted = false;
30
+ let finalizing = false;
31
+ let releaseRequest;
32
+ let profileCleaned = false;
33
+
34
+ process.stdout.on('error', () => {});
35
+ process.stderr.on('error', () => {});
36
+
37
+ function send(message, callback = () => {}) {
38
+ if (!process.connected) {
39
+ callback();
40
+ return;
41
+ }
42
+ process.send(message, callback);
43
+ }
44
+
45
+ function signalLaunchTree(signal) {
46
+ if (!launchChild?.pid) return;
47
+ if (process.platform === 'win32') launchChild.kill(signal);
48
+ else signalPids(launchTreePids(), signal);
49
+ }
50
+
51
+ function launchTreeExists() {
52
+ if (!launchChild?.pid) return false;
53
+ if (process.platform !== 'win32') return launchTreePids().length > 0;
54
+ try {
55
+ process.kill(launchChild.pid, 0);
56
+ return true;
57
+ } catch (error) {
58
+ return error.code !== 'ESRCH';
59
+ }
60
+ }
61
+
62
+ function launchTreePids() {
63
+ const output = execFileSync('ps', ['-axo', 'pid=,pgid='], {
64
+ encoding: 'utf8',
65
+ stdio: ['ignore', 'pipe', 'ignore'],
66
+ });
67
+ const pids = [];
68
+ for (const line of output.split('\n')) {
69
+ const match = line.match(/^\s*(\d+)\s+(\d+)\s*$/u);
70
+ if (match && Number(match[2]) === launchChild.pid) pids.push(Number(match[1]));
71
+ }
72
+ return pids.filter((pid) => pid !== process.pid);
73
+ }
74
+
75
+ function delay(milliseconds) {
76
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
77
+ }
78
+
79
+ async function stopRemainingLaunchTree() {
80
+ if (!launchTreeExists()) return;
81
+ signalLaunchTree('SIGTERM');
82
+ const termDeadline = Date.now() + 1_000;
83
+ while (launchTreeExists() && Date.now() < termDeadline) await delay(50);
84
+ if (!launchTreeExists()) return;
85
+ signalLaunchTree('SIGKILL');
86
+ const killDeadline = Date.now() + 1_000;
87
+ while (launchTreeExists() && Date.now() < killDeadline) await delay(50);
88
+ if (launchTreeExists()) throw new Error(`Extension validation launcher process group ${launchChild.pid} survived SIGKILL.`);
89
+ }
90
+
91
+ function signalPids(pids, signal) {
92
+ for (const pid of pids) {
93
+ try {
94
+ process.kill(pid, signal);
95
+ } catch (error) {
96
+ if (error.code !== 'ESRCH') throw error;
97
+ }
98
+ }
99
+ }
100
+
101
+ function closeSocket() {
102
+ return new Promise((resolve) => socket.close(resolve));
103
+ }
104
+
105
+ async function cleanProfile(quietMs) {
106
+ await stopProfileProcesses(launchRequest.profile, { quietMs });
107
+ profileCleaned = true;
108
+ }
109
+
110
+ function quarantineProfile(error) {
111
+ let quarantineError;
112
+ try {
113
+ quarantineDetachedLaunch(launchRequest.profile, error.message);
114
+ } catch (markerError) {
115
+ quarantineError = markerError;
116
+ }
117
+ try {
118
+ quarantineValidationPort(port, launchRequest.profile, error.message);
119
+ } catch (markerError) {
120
+ quarantineError ||= markerError;
121
+ }
122
+ return quarantineError
123
+ ? new Error(`${error.message} Extension validation could not persist quarantine: ${quarantineError.message}`)
124
+ : error;
125
+ }
126
+
127
+ async function finalize(preserveProfileOwner) {
128
+ if (finalizing) return;
129
+ finalizing = true;
130
+ let finalError;
131
+ if (launchRequest && !preserveProfileOwner && !profileCleaned) {
132
+ try {
133
+ await cleanProfile(2_000);
134
+ } catch (error) {
135
+ finalError = quarantineProfile(error);
136
+ }
137
+ }
138
+ if (launchRequest && ownsPortLease && !finalError) {
139
+ try {
140
+ const detachedLaunchUnproven = hasDetachedLaunchUnproven(launchRequest.profile);
141
+ if (preserveProfileOwner && detachedLaunchUnproven) {
142
+ throw new Error('Extension validation cannot release an unproven detached browser launch.');
143
+ }
144
+ if (!detachedLaunchUnproven) {
145
+ clearValidationPortQuarantine(port, launchRequest.profile, launchLease);
146
+ ownsPortLease = false;
147
+ }
148
+ } catch (error) {
149
+ finalError = quarantineProfile(error);
150
+ }
151
+ }
152
+ await closeSocket();
153
+ if (finalError) send({ type: 'release-error', message: finalError.message }, () => process.exit(1));
154
+ else process.exit(0);
155
+ }
156
+
157
+ async function finish(result) {
158
+ if (finishStarted) return;
159
+ finishStarted = true;
160
+ let finalResult = result;
161
+ let launchTreeError;
162
+ try {
163
+ await stopRemainingLaunchTree();
164
+ } catch (error) {
165
+ launchTreeError = quarantineProfile(error);
166
+ }
167
+ if (launchRequest && (launchTreeError || result.exitCode !== 0 || result.timedOut || result.error)) {
168
+ let detachedLaunchUnproven = false;
169
+ try {
170
+ detachedLaunchUnproven = hasDetachedLaunchUnproven(launchRequest.profile);
171
+ } catch (error) {
172
+ launchTreeError ||= quarantineProfile(error);
173
+ }
174
+ try {
175
+ await cleanProfile(detachedLaunchUnproven || result.timedOut || result.error || launchTreeError ? 2_000 : 0);
176
+ } catch (error) {
177
+ launchTreeError ||= quarantineProfile(error);
178
+ }
179
+ }
180
+ if (launchTreeError) {
181
+ finalResult = { type: 'result', exitCode: null, timedOut: result.timedOut, error: launchTreeError.message };
182
+ }
183
+ launchFinished = true;
184
+ send(finalResult);
185
+ if (releaseRequest !== undefined || !process.connected) void finalize(releaseRequest === true);
186
+ }
187
+
188
+ function startLaunch(message) {
189
+ if (
190
+ launchChild ||
191
+ !message ||
192
+ typeof message.command !== 'string' ||
193
+ !Array.isArray(message.args) ||
194
+ typeof message.profile !== 'string'
195
+ ) {
196
+ send({ type: 'error', message: 'Invalid extension validation supervisor launch request.' }, () => process.exit(2));
197
+ return;
198
+ }
199
+ const timeoutMs = Number(message.timeoutMs);
200
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
201
+ send({ type: 'error', message: 'Invalid extension validation supervisor timeout.' }, () => process.exit(2));
202
+ return;
203
+ }
204
+ launchRequest = message;
205
+ const lease = createRuntimeIdentityNonce();
206
+ launchLease = lease;
207
+ try {
208
+ markValidationPortLaunchUnproven(port, message.profile, lease);
209
+ ownsPortLease = true;
210
+ } catch (error) {
211
+ void finish({ type: 'result', exitCode: null, timedOut: false, error: error.message });
212
+ return;
213
+ }
214
+ launchChild = spawn(message.command, message.args, {
215
+ cwd: message.cwd,
216
+ env: {
217
+ ...message.env,
218
+ MM_HARNESS_VALIDATION_PORT_LEASE: lease,
219
+ },
220
+ detached: process.platform !== 'win32',
221
+ stdio: ['ignore', 'pipe', 'pipe'],
222
+ });
223
+ launchChild.stdout.pipe(process.stdout);
224
+ launchChild.stderr.pipe(process.stderr);
225
+ let timedOut = false;
226
+ let killTimer;
227
+ const timeout = setTimeout(() => {
228
+ timedOut = true;
229
+ try {
230
+ signalLaunchTree('SIGTERM');
231
+ } catch (error) {
232
+ void finish({ type: 'result', exitCode: null, timedOut, error: error.message });
233
+ return;
234
+ }
235
+ killTimer = setTimeout(() => {
236
+ try {
237
+ signalLaunchTree('SIGKILL');
238
+ } catch (error) {
239
+ void finish({ type: 'result', exitCode: null, timedOut, error: error.message });
240
+ }
241
+ }, 1_000);
242
+ killTimer.unref();
243
+ }, timeoutMs);
244
+ launchChild.once('error', (error) => {
245
+ clearTimeout(timeout);
246
+ if (killTimer) clearTimeout(killTimer);
247
+ void finish({ type: 'result', exitCode: null, timedOut, error: error.message });
248
+ });
249
+ launchChild.once('close', (exitCode) => {
250
+ clearTimeout(timeout);
251
+ if (killTimer) clearTimeout(killTimer);
252
+ void finish({ type: 'result', exitCode, timedOut });
253
+ });
254
+ }
255
+
256
+ process.on('message', (message) => {
257
+ if (message?.type === 'run') startLaunch(message);
258
+ if (message?.type === 'release') {
259
+ releaseRequest = message.preserveProfileOwner === true;
260
+ if (launchFinished) void finalize(releaseRequest);
261
+ }
262
+ if (message?.type === 'cancel' && !launchChild) {
263
+ releaseRequest = false;
264
+ void finish({ type: 'cancelled', timedOut: false });
265
+ }
266
+ });
267
+
268
+ process.on('disconnect', () => {
269
+ releaseRequest = false;
270
+ if (!launchChild) {
271
+ void finish({ type: 'cancelled', timedOut: false });
272
+ return;
273
+ }
274
+ launchChild.stdout.unpipe(process.stdout);
275
+ launchChild.stderr.unpipe(process.stderr);
276
+ launchChild.stdout.resume();
277
+ launchChild.stderr.resume();
278
+ if (launchFinished) void finalize(false);
279
+ });
280
+
281
+ socket.once('error', (error) => {
282
+ const message = error.code === 'EADDRINUSE'
283
+ ? `Extension validation runtime is already preparing on port ${port}.`
284
+ : error.message;
285
+ send({ type: 'error', message }, () => process.exit(1));
286
+ });
287
+ const existingQuarantine = readValidationPortQuarantine(port);
288
+ if (existingQuarantine) {
289
+ send({ type: 'error', message: validationLaunchQuarantineError(port, existingQuarantine.profile).message }, () => process.exit(1));
290
+ } else {
291
+ socket.bind({ address: '127.0.0.1', port, exclusive: true }, () => send({ type: 'ready' }));
292
+ }