@deeeed/metamask-harness 0.66.1 → 0.66.2

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 CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.66.2 - 2026-09-27
6
+
7
+ ### Fixed
8
+
9
+ - Resolve the latest iOS Runway build from named artifacts rather than the last 30 workflow runs, and fail instead of silently selecting an older build when GitHub artifact lookup fails.
10
+ - Discover Metro targets through its canonical loopback address and send the matching Origin from both the Mobile CDP bridge and console collector, fixing rejected inspector connections on current Expo clients.
11
+
5
12
  ## 0.66.1 - 2026-09-25
6
13
 
7
14
  ### Fixed
@@ -36,9 +36,8 @@ const {
36
36
  } = require('./lib/cdp-broker.cjs');
37
37
  const { createDevtoolsProxy } = require('./lib/devtools-proxy.cjs');
38
38
  const { resolvePort } = require('./lib/config.cjs');
39
+ const { createInspectorWebSocket } = require('./lib/ws-client.cjs');
39
40
 
40
- // Hermes uses Node's built-in WebSocket client. The local DevTools proxy uses
41
- // the package's pinned `ws` server implementation.
42
41
  const HANDSHAKE_TIMEOUT_MS = 3000;
43
42
  const RUNTIME_ENABLE_TIMEOUT_MS = 60_000;
44
43
 
@@ -288,7 +287,7 @@ function connect(target) {
288
287
  const name = deviceNameFromTitle(target.title);
289
288
  let ws;
290
289
  try {
291
- ws = new WebSocket(target.webSocketDebuggerUrl);
290
+ ws = createInspectorWebSocket(target.webSocketDebuggerUrl);
292
291
  } catch {
293
292
  return;
294
293
  }
@@ -302,8 +301,6 @@ function connect(target) {
302
301
  pending: new Map(),
303
302
  };
304
303
  sessions.set(deviceId, session);
305
- // The built-in WebSocket has no handshake timeout; a hung connect would hold
306
- // the session slot forever and block every future re-attach for this device.
307
304
  const handshakeTimer = setTimeout(() => {
308
305
  try {
309
306
  ws.close();
@@ -419,7 +416,7 @@ function discover() {
419
416
  return;
420
417
  }
421
418
  http
422
- .get({ host: 'localhost', port, path: '/json/list', timeout: 3000 }, (res) => {
419
+ .get({ host: '127.0.0.1', port, path: '/json/list', timeout: 3000 }, (res) => {
423
420
  let body = '';
424
421
  res.on('data', (c) => (body += c));
425
422
  res.on('end', () => {
@@ -139,7 +139,7 @@ function targetDeviceIdentity(target) {
139
139
  * - We probe candidates to find the one with __AGENTIC__ installed
140
140
  */
141
141
  async function discoverTarget(port, { probe = true } = {}) {
142
- const listUrl = `http://localhost:${port}/json/list`;
142
+ const listUrl = `http://127.0.0.1:${port}/json/list`;
143
143
  const androidTargetName = loadAndroidTargetDeviceName();
144
144
  const androidDevice = loadAndroidDevice();
145
145
  const adbSerial = process.env.ADB_SERIAL || process.env.ANDROID_SERIAL || '';
@@ -358,7 +358,7 @@ async function discoverTarget(port, { probe = true } = {}) {
358
358
  * Groups by deviceName so each device returns at most one target.
359
359
  */
360
360
  async function discoverAllTargets(port) {
361
- const listUrl = `http://localhost:${port}/json/list`;
361
+ const listUrl = `http://127.0.0.1:${port}/json/list`;
362
362
  let targets;
363
363
  try {
364
364
  targets = await fetchJSON(listUrl);
@@ -1,34 +1,19 @@
1
1
  'use strict';
2
2
 
3
- /* global globalThis */
4
-
5
3
  const { BRIDGE_ERROR_CODES, coded } = require('./bridge-errors.cjs');
4
+ const WebSocket = require('ws');
5
+
6
+ function createInspectorWebSocket(wsUrl) {
7
+ const origin = new URL(wsUrl).origin.replace(/^ws/, 'http');
8
+ return new WebSocket(wsUrl, { origin });
9
+ }
6
10
 
7
11
  /**
8
- * Minimal CDP client using the built-in ws-like interface over raw WebSocket.
9
- * Node 22+ has a built-in WebSocket; for older versions we use the ws package
10
- * that ships with React Native / Metro dev dependencies.
12
+ * Minimal CDP client that supplies the debugger Origin required by Metro.
11
13
  */
12
14
  function createWSClient(wsUrl, timeout) {
13
15
  return new Promise((resolve, reject) => {
14
- let WebSocketImpl;
15
- // Node 22+ has globalThis.WebSocket
16
- if (typeof globalThis.WebSocket === 'function') {
17
- WebSocketImpl = globalThis.WebSocket;
18
- } else {
19
- try {
20
- // Dynamic require avoids depcheck static analysis — ws is an optional
21
- // fallback for Node < 22 which has no built-in WebSocket.
22
- const wsModule = 'ws';
23
- WebSocketImpl = require(wsModule);
24
- } catch {
25
- throw new Error(
26
- 'WebSocket not available. Install "ws" package or use Node >= 22.',
27
- );
28
- }
29
- }
30
-
31
- const ws = new WebSocketImpl(wsUrl);
16
+ const ws = createInspectorWebSocket(wsUrl);
32
17
  let msgId = 0;
33
18
  const pending = new Map();
34
19
  const eventHandlers = new Map();
@@ -128,4 +113,4 @@ function createWSClient(wsUrl, timeout) {
128
113
  });
129
114
  }
130
115
 
131
- module.exports = { createWSClient };
116
+ module.exports = { createWSClient, createInspectorWebSocket };
@@ -101,7 +101,7 @@ function parseArgs(argv, env = process.env) {
101
101
  function fetchJson(port, pathname, timeoutMs) {
102
102
  return new Promise((resolve, reject) => {
103
103
  const request = http.get(
104
- { host: 'localhost', port, path: pathname },
104
+ { host: '127.0.0.1', port, path: pathname },
105
105
  (response) => {
106
106
  let body = '';
107
107
  response.setEncoding('utf8');
@@ -241,35 +241,33 @@ function resolveArtifactRun(repo, branch, defaultBranch, runOverride) {
241
241
  return resolvedArtifact(repo, branch, runOverride, revision, artifact);
242
242
  }
243
243
  const tried = [.../* @__PURE__ */ new Set([branch, defaultBranch])];
244
- for (const candidate of tried) {
245
- const run = latestRunWithArtifact(repo, candidate, RUNWAY_IOS_METADATA.artifactName);
246
- if (run) return resolvedArtifact(repo, candidate, run.runId, run.revision, run.artifact);
247
- }
244
+ const run = latestRunWithArtifact(repo, tried, RUNWAY_IOS_METADATA.artifactName);
245
+ if (run) return resolvedArtifact(repo, run.branch, run.runId, run.revision, run.artifact);
248
246
  throw new Error(`no Runway artifact found for ${RUNWAY_IOS_METADATA.artifactName} on ${tried.join(", ")}.`);
249
247
  }
250
- function latestRunWithArtifact(repo, branch, artifactName) {
251
- let runs;
252
- try {
253
- runs = execJson("gh", [
254
- "run",
255
- "list",
256
- "--repo",
257
- repo,
258
- `--workflow=${RUNWAY_IOS_METADATA.workflow}`,
259
- `--branch=${branch}`,
260
- "--status=success",
261
- "--limit=30",
262
- "--json",
263
- "databaseId,headSha"
264
- ]);
265
- } catch {
266
- return void 0;
267
- }
268
- for (const run of runs) {
269
- const runId = run.databaseId ? String(run.databaseId) : "";
270
- if (!runId) continue;
271
- const artifact = findRunArtifact(repo, runId, artifactName);
272
- if (artifact) return { runId, revision: run.headSha || runId, artifact };
248
+ function latestRunWithArtifact(repo, branches, artifactName) {
249
+ const output = execFileSync("gh", [
250
+ "api",
251
+ `repos/${repo}/actions/artifacts?name=${encodeURIComponent(artifactName)}&per_page=100`,
252
+ "--paginate",
253
+ "--jq",
254
+ ".artifacts[] | select(.expired == false)"
255
+ ], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
256
+ const artifacts = output.trim().split("\n").filter(Boolean).map((line) => JSON.parse(line)).filter((artifact) => artifact.name === artifactName).sort((left, right) => (right.created_at ?? "").localeCompare(left.created_at ?? ""));
257
+ for (const branch of branches) {
258
+ for (const artifact of artifacts.filter((candidate) => candidate.workflow_run?.head_branch === branch)) {
259
+ const runId = artifact.workflow_run?.id;
260
+ if (!runId) continue;
261
+ const run = execJson("gh", [
262
+ "api",
263
+ `repos/${repo}/actions/runs/${runId}`
264
+ ]);
265
+ if (run.status === "completed" && run.conclusion === "success" && run.path === `.github/workflows/${RUNWAY_IOS_METADATA.workflow}`) {
266
+ return { branch, runId: String(runId), revision: artifact.workflow_run?.head_sha || String(runId), artifact };
267
+ }
268
+ process.stderr.write(`runway: skipping artifact from run ${runId} (${run.status}/${run.conclusion}, ${run.path})
269
+ `);
270
+ }
273
271
  }
274
272
  return void 0;
275
273
  }
@@ -278,16 +276,13 @@ function assertValidRunId(runId) {
278
276
  }
279
277
  function findRunArtifact(repo, runId, artifactName) {
280
278
  assertValidRunId(runId);
281
- try {
282
- const data = execJson("gh", [
283
- "api",
284
- `repos/${repo}/actions/runs/${runId}/artifacts`,
285
- "--paginate"
286
- ]);
287
- return data.artifacts?.find((artifact) => artifact.expired === false && artifact.name === artifactName);
288
- } catch {
289
- return void 0;
290
- }
279
+ const pages = execJson("gh", [
280
+ "api",
281
+ `repos/${repo}/actions/runs/${runId}/artifacts`,
282
+ "--paginate",
283
+ "--slurp"
284
+ ]);
285
+ return pages.flatMap((page) => page.artifacts ?? []).find((artifact) => artifact.expired === false && artifact.name === artifactName);
291
286
  }
292
287
  function runRevision(repo, runId) {
293
288
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeeed/metamask-harness",
3
- "version": "0.66.1",
3
+ "version": "0.66.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mm-harness": "bin/mm-harness"